@boon4681/giri 0.0.2-alpha-2 → 0.0.2-alpha-4

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.
@@ -22,6 +22,42 @@ interface ValidatedInput {
22
22
  body?: unknown;
23
23
  query?: unknown;
24
24
  }
25
+ /** Attributes for a `Set-Cookie` header. `path` defaults to `/`. */
26
+ interface CookieOptions {
27
+ domain?: string;
28
+ path?: string;
29
+ /** Lifetime in seconds. */
30
+ maxAge?: number;
31
+ expires?: Date;
32
+ httpOnly?: boolean;
33
+ secure?: boolean;
34
+ sameSite?: 'Strict' | 'Lax' | 'None' | 'strict' | 'lax' | 'none';
35
+ partitioned?: boolean;
36
+ priority?: 'Low' | 'Medium' | 'High' | 'low' | 'medium' | 'high';
37
+ }
38
+ /**
39
+ * Cookie read/write, implemented per adapter with its runtime's native helpers. giri core
40
+ * supplies the {@link CookieSink} (where to read from / write to); the adapter owns encoding.
41
+ */
42
+ interface CookieJar {
43
+ get(name: string): string | undefined;
44
+ all(): Record<string, string>;
45
+ set(name: string, value: string, options?: CookieOptions): void;
46
+ delete(name: string, options?: CookieOptions): void;
47
+ getSigned(name: string): Promise<string | false | undefined>;
48
+ setSigned(name: string, value: string, options?: CookieOptions): Promise<void>;
49
+ }
50
+ /** What core hands an adapter's cookie jar: the request to read from, the response sink to write to. */
51
+ interface CookieSink {
52
+ /** The incoming request, for reading the `Cookie` header. */
53
+ request: Request;
54
+ /** Append one already-serialized `Set-Cookie` header value to the response. */
55
+ append(setCookieHeader: string): void;
56
+ /** The configured `cookieSecret`, if any (for signed cookies). */
57
+ secret?: string;
58
+ }
59
+ /** Builds a {@link CookieJar} bound to one request's {@link CookieSink}. Each adapter provides one. */
60
+ type CookieJarFactory = (sink: CookieSink) => CookieJar;
25
61
  interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {
26
62
  raw: Request;
27
63
  url: URL;
@@ -32,6 +68,16 @@ interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {
32
68
  arrayBuffer(): Promise<ArrayBuffer>;
33
69
  formData(): Promise<FormData>;
34
70
  valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];
71
+ /** Read a request cookie by name, or `undefined` if absent. */
72
+ cookie(name: string): string | undefined;
73
+ /** All request cookies as a name→value map. */
74
+ cookies(): Record<string, string>;
75
+ /**
76
+ * Read and verify a signed cookie. Resolves to the original value, `false` if the
77
+ * signature was tampered with, or `undefined` if the cookie is absent. Requires
78
+ * `cookieSecret` in `giri.config`.
79
+ */
80
+ signedCookie(name: string): Promise<string | false | undefined>;
35
81
  }
36
82
  declare global {
37
83
  /**
@@ -88,6 +134,13 @@ interface Context<Params extends Record<string, string> = Record<string, string>
88
134
  }): void;
89
135
  /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */
90
136
  status(code: StatusCode): void;
137
+ /**
138
+ * Set a response cookie via `Set-Cookie`. Pass `value: null` to delete it (send the
139
+ * same `path`/`domain` you set it with). Stacks with other cookies set this request.
140
+ */
141
+ cookie(name: string, value: string | null, options?: CookieOptions): void;
142
+ /** Set an HMAC-signed cookie. Requires `cookieSecret` in `giri.config`. */
143
+ signedCookie(name: string, value: string, options?: CookieOptions): Promise<void>;
91
144
  }
92
145
  type Handle<Params extends Record<string, string> = Record<string, string>, Input extends ValidatedInput = ValidatedInput, Vars extends Record<string, unknown> = {}> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;
93
146
  type Next = () => Promise<HandlerResponse | void>;
@@ -210,6 +263,8 @@ interface GiriRouteRegistration {
210
263
  input?: RouteInput;
211
264
  /** App-wide services to seed onto `c.app` (same instance for every route). */
212
265
  services?: Services;
266
+ /** Secret for signing/verifying cookies (`c.signedCookie`), from `config.cookieSecret`. */
267
+ cookieSecret?: string;
213
268
  }
214
269
  type GiriFetchHandler = (req: Request) => Response | Promise<Response>;
215
270
  interface GiriServeOptions {
@@ -244,6 +299,8 @@ interface GiriConfig<App = unknown> {
244
299
  hostname?: string;
245
300
  };
246
301
  errorSchema?: unknown;
302
+ /** Secret used to sign/verify cookies via `c.signedCookie` / `c.req.signedCookie`. */
303
+ cookieSecret?: string;
247
304
  }
248
305
  interface GiriPaths {
249
306
  cwd: string;
@@ -251,4 +308,4 @@ interface GiriPaths {
251
308
  outDir: string;
252
309
  }
253
310
 
254
- export { type ValidQuery as A, type BodyContentType as B, type Context as C, type VarsOf as D, type GiriAdapter as G, type HandlerResponse as H, type Infer as I, type JsonSchema as J, type Middleware as M, type Next as N, type ResponseFormat as R, type Services as S, type TypedResponse as T, type ValidatedInput as V, type GiriBodySchema as a, type GiriInputSchema as b, type StatusCode as c, type MiddlewareOptions as d, type RouteInput as e, type HttpMethod as f, type GiriConfig as g, type GiriPaths as h, inputSchemaBrand as i, type SecurityRequirement as j, type GiriFetchHandler as k, type GiriRequest as l, type GiriRouteRegistration as m, type GiriServeOptions as n, type GiriServer as o, type GiriServerInfo as p, type Handle as q, type InferStackVars as r, type InputValidationResult as s, type MergeStack as t, type MiddlewareOpenApi as u, type MiddlewareVarsOf as v, type RouteInputOf as w, type RouteOpenApi as x, type RouteOpenApiConfig as y, type ValidBody as z };
311
+ export { type RouteInputOf as A, type BodyContentType as B, type Context as C, type RouteOpenApi as D, type RouteOpenApiConfig as E, type ValidBody as F, type GiriAdapter as G, type HandlerResponse as H, type Infer as I, type JsonSchema as J, type ValidQuery as K, type VarsOf as L, type Middleware as M, type Next as N, type ResponseFormat as R, type Services as S, type TypedResponse as T, type ValidatedInput as V, type GiriBodySchema as a, type GiriInputSchema as b, type CookieJarFactory as c, type StatusCode as d, type MiddlewareOptions as e, type RouteInput as f, type HttpMethod as g, type GiriConfig as h, inputSchemaBrand as i, type GiriPaths as j, type SecurityRequirement as k, type CookieJar as l, type CookieOptions as m, type CookieSink as n, type GiriFetchHandler as o, type GiriRequest as p, type GiriRouteRegistration as q, type GiriServeOptions as r, type GiriServer as s, type GiriServerInfo as t, type Handle as u, type InferStackVars as v, type InputValidationResult as w, type MergeStack as x, type MiddlewareOpenApi as y, type MiddlewareVarsOf as z };
@@ -1,5 +1,5 @@
1
1
  import * as v from 'valibot';
2
- import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-C4J1_iLZ.js';
2
+ import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-NVvedshc.js';
3
3
 
4
4
  type AnySchema = v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>;
5
5
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/validators/valibot.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { toJsonSchema } from '@valibot/to-json-schema';\nimport * as v from 'valibot';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\ntype AnySchema = v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>;\n\n/** Wrap a single Valibot schema as a giri input schema. */\nfunction wrap<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return defineInputSchema<v.InferOutput<Schema>>({\n validate(value) {\n const result = v.safeParse(schema, value);\n return result.success\n ? { ok: true, value: result.output }\n : { ok: false, issues: result.issues };\n },\n toJsonSchema() {\n return toJsonSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Valibot adapter. Peer-depends `valibot` and `@valibot/to-json-schema`.\n *\n * ```ts\n * import * as v from 'valibot';\n * import { valibot } from '@boon4681/giri/validators/valibot';\n *\n * export const body = valibot.body({ json: v.object({ name: v.pipe(v.string(), v.minLength(1)) }) });\n * export const query = valibot.query(v.object({ page: v.string() }));\n * ```\n */\nexport const valibot = {\n body<Map extends Partial<Record<BodyContentType, AnySchema>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never;\n }>;\n },\n query<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text' | 'html';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n /** An HTML response (`text/html`). Like `text`, the body is a string. */\n html<S extends StatusCode = 200>(\n html: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'html'>;\n /** A raw-body response - string, stream, buffer, FormData, … (not documented in OpenAPI). */\n body(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** Alias of `body`, mirroring Hono's `c.newResponse`. */\n newResponse(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** A redirect (defaults to 302) with the `Location` header set. */\n redirect(location: string, status?: StatusCode): Response;\n /** A 404 Not Found response. */\n notFound(): Response;\n /**\n * Set a response header applied to whatever this handler returns. Pass `{ append: true }` to add\n * another value (e.g. `Set-Cookie`); omit `value` to delete. Mirrors Hono's `c.header`.\n */\n header(name: string, value?: string, options?: { append?: boolean }): void;\n /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */\n status(code: StatusCode): void;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\n ? MergeStack<T>\n : T extends Middleware<Record<string, string>, ValidatedInput, any>\n ? VarsOf<T>\n : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAA6B;AAC7B,QAAmB;;;AC0LZ,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;ACpLpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF3CA,SAAS,KAA+B,QAAwD;AAC5F,SAAO,kBAAyC;AAAA,IAC5C,SAAS,OAAO;AACZ,YAAM,SAAW,YAAU,QAAQ,KAAK;AACxC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,OAAO,IACjC,EAAE,IAAI,OAAO,QAAQ,OAAO,OAAO;AAAA,IAC7C;AAAA,IACA,eAAe;AACX,iBAAO,oCAAa,MAAM;AAAA,IAC9B;AAAA,EACJ,CAAC;AACL;AAaO,IAAM,UAAU;AAAA,EACnB,KACI,KAC8F;AAC9F,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAwD;AACpF,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/validators/valibot.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { toJsonSchema } from '@valibot/to-json-schema';\nimport * as v from 'valibot';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\ntype AnySchema = v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>;\n\n/** Wrap a single Valibot schema as a giri input schema. */\nfunction wrap<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return defineInputSchema<v.InferOutput<Schema>>({\n validate(value) {\n const result = v.safeParse(schema, value);\n return result.success\n ? { ok: true, value: result.output }\n : { ok: false, issues: result.issues };\n },\n toJsonSchema() {\n return toJsonSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Valibot adapter. Peer-depends `valibot` and `@valibot/to-json-schema`.\n *\n * ```ts\n * import * as v from 'valibot';\n * import { valibot } from '@boon4681/giri/validators/valibot';\n *\n * export const body = valibot.body({ json: v.object({ name: v.pipe(v.string(), v.minLength(1)) }) });\n * export const query = valibot.query(v.object({ page: v.string() }));\n * ```\n */\nexport const valibot = {\n body<Map extends Partial<Record<BodyContentType, AnySchema>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends AnySchema ? v.InferOutput<Map[K]> : never;\n }>;\n },\n query<Schema extends AnySchema>(schema: Schema): GiriInputSchema<v.InferOutput<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text' | 'html';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\n/** Attributes for a `Set-Cookie` header. `path` defaults to `/`. */\nexport interface CookieOptions {\n domain?: string;\n path?: string;\n /** Lifetime in seconds. */\n maxAge?: number;\n expires?: Date;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'Strict' | 'Lax' | 'None' | 'strict' | 'lax' | 'none';\n partitioned?: boolean;\n priority?: 'Low' | 'Medium' | 'High' | 'low' | 'medium' | 'high';\n}\n\n/**\n * Cookie read/write, implemented per adapter with its runtime's native helpers. giri core\n * supplies the {@link CookieSink} (where to read from / write to); the adapter owns encoding.\n */\nexport interface CookieJar {\n get(name: string): string | undefined;\n all(): Record<string, string>;\n set(name: string, value: string, options?: CookieOptions): void;\n delete(name: string, options?: CookieOptions): void;\n getSigned(name: string): Promise<string | false | undefined>;\n setSigned(name: string, value: string, options?: CookieOptions): Promise<void>;\n}\n\n/** What core hands an adapter's cookie jar: the request to read from, the response sink to write to. */\nexport interface CookieSink {\n /** The incoming request, for reading the `Cookie` header. */\n request: Request;\n /** Append one already-serialized `Set-Cookie` header value to the response. */\n append(setCookieHeader: string): void;\n /** The configured `cookieSecret`, if any (for signed cookies). */\n secret?: string;\n}\n\n/** Builds a {@link CookieJar} bound to one request's {@link CookieSink}. Each adapter provides one. */\nexport type CookieJarFactory = (sink: CookieSink) => CookieJar;\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n /** Read a request cookie by name, or `undefined` if absent. */\n cookie(name: string): string | undefined;\n /** All request cookies as a name→value map. */\n cookies(): Record<string, string>;\n /**\n * Read and verify a signed cookie. Resolves to the original value, `false` if the\n * signature was tampered with, or `undefined` if the cookie is absent. Requires\n * `cookieSecret` in `giri.config`.\n */\n signedCookie(name: string): Promise<string | false | undefined>;\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n /** An HTML response (`text/html`). Like `text`, the body is a string. */\n html<S extends StatusCode = 200>(\n html: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'html'>;\n /** A raw-body response - string, stream, buffer, FormData, … (not documented in OpenAPI). */\n body(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** Alias of `body`, mirroring Hono's `c.newResponse`. */\n newResponse(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** A redirect (defaults to 302) with the `Location` header set. */\n redirect(location: string, status?: StatusCode): Response;\n /** A 404 Not Found response. */\n notFound(): Response;\n /**\n * Set a response header applied to whatever this handler returns. Pass `{ append: true }` to add\n * another value (e.g. `Set-Cookie`); omit `value` to delete. Mirrors Hono's `c.header`.\n */\n header(name: string, value?: string, options?: { append?: boolean }): void;\n /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */\n status(code: StatusCode): void;\n /**\n * Set a response cookie via `Set-Cookie`. Pass `value: null` to delete it (send the\n * same `path`/`domain` you set it with). Stacks with other cookies set this request.\n */\n cookie(name: string, value: string | null, options?: CookieOptions): void;\n /** Set an HMAC-signed cookie. Requires `cookieSecret` in `giri.config`. */\n signedCookie(name: string, value: string, options?: CookieOptions): Promise<void>;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\n ? MergeStack<T>\n : T extends Middleware<Record<string, string>, ValidatedInput, any>\n ? VarsOf<T>\n : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n /** Secret for signing/verifying cookies (`c.signedCookie`), from `config.cookieSecret`. */\n cookieSecret?: string;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n /** Secret used to sign/verify cookies via `c.signedCookie` / `c.req.signedCookie`. */\n cookieSecret?: string;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAA6B;AAC7B,QAAmB;;;ACmPZ,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;AC7OpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF3CA,SAAS,KAA+B,QAAwD;AAC5F,SAAO,kBAAyC;AAAA,IAC5C,SAAS,OAAO;AACZ,YAAM,SAAW,YAAU,QAAQ,KAAK;AACxC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,OAAO,IACjC,EAAE,IAAI,OAAO,QAAQ,OAAO,OAAO;AAAA,IAC7C;AAAA,IACA,eAAe;AACX,iBAAO,oCAAa,MAAM;AAAA,IAC9B;AAAA,EACJ,CAAC;AACL;AAaO,IAAM,UAAU;AAAA,EACnB,KACI,KAC8F;AAC9F,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAwD;AACpF,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-C4J1_iLZ.js';
2
+ import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-NVvedshc.js';
3
3
 
4
4
  /**
5
5
  * Zod adapter. Peer-depends `zod`.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/validators/zod.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { z } from 'zod';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\n/** Wrap a single Zod schema as a giri input schema (validate via `safeParse`, JSON Schema via Zod 4). */\nfunction wrap<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return defineInputSchema<z.infer<Schema>>({\n validate(value) {\n const result = schema.safeParse(value);\n return result.success\n ? { ok: true, value: result.data }\n : { ok: false, issues: result.error };\n },\n toJsonSchema() {\n return z.toJSONSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Zod adapter. Peer-depends `zod`.\n *\n * ```ts\n * import { z } from 'zod';\n * import { zod } from '@boon4681/giri/validators/zod';\n *\n * // JSON body\n * export const body = zod.body({ json: z.object({ name: z.string().min(1) }) });\n * // JSON *or* multipart dispatched on Content-Type at runtime\n * export const body = zod.body({\n * json: z.object({ name: z.string() }),\n * form: z.object({ name: z.string(), avatar: z.instanceof(File) }),\n * });\n * export const query = zod.query(z.object({ page: z.coerce.number() }));\n * ```\n */\nexport const zod = {\n body<Map extends Partial<Record<BodyContentType, z.ZodType>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never;\n }>;\n },\n query<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text' | 'html';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n /** An HTML response (`text/html`). Like `text`, the body is a string. */\n html<S extends StatusCode = 200>(\n html: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'html'>;\n /** A raw-body response - string, stream, buffer, FormData, … (not documented in OpenAPI). */\n body(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** Alias of `body`, mirroring Hono's `c.newResponse`. */\n newResponse(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** A redirect (defaults to 302) with the `Location` header set. */\n redirect(location: string, status?: StatusCode): Response;\n /** A 404 Not Found response. */\n notFound(): Response;\n /**\n * Set a response header applied to whatever this handler returns. Pass `{ append: true }` to add\n * another value (e.g. `Set-Cookie`); omit `value` to delete. Mirrors Hono's `c.header`.\n */\n header(name: string, value?: string, options?: { append?: boolean }): void;\n /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */\n status(code: StatusCode): void;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\n ? MergeStack<T>\n : T extends Middleware<Record<string, string>, ValidatedInput, any>\n ? VarsOf<T>\n : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;;;AC2LX,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;ACpLpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF9CA,SAAS,KAA+B,QAAkD;AACtF,SAAO,kBAAmC;AAAA,IACtC,SAAS,OAAO;AACZ,YAAM,SAAS,OAAO,UAAU,KAAK;AACrC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,KAAK,IAC/B,EAAE,IAAI,OAAO,QAAQ,OAAO,MAAM;AAAA,IAC5C;AAAA,IACA,eAAe;AACX,aAAO,aAAE,aAAa,MAAM;AAAA,IAChC;AAAA,EACJ,CAAC;AACL;AAmBO,IAAM,MAAM;AAAA,EACf,KACI,KACwF;AACxF,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAkD;AAC9E,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/validators/zod.ts","../../src/types.ts","../../src/validation.ts"],"sourcesContent":["import { z } from 'zod';\nimport { defineBodySchema, defineInputSchema } from '../validation';\nimport type { BodyContentType, GiriBodySchema, GiriInputSchema } from '../types';\n\n/** Wrap a single Zod schema as a giri input schema (validate via `safeParse`, JSON Schema via Zod 4). */\nfunction wrap<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return defineInputSchema<z.infer<Schema>>({\n validate(value) {\n const result = schema.safeParse(value);\n return result.success\n ? { ok: true, value: result.data }\n : { ok: false, issues: result.error };\n },\n toJsonSchema() {\n return z.toJSONSchema(schema) as Record<string, unknown>;\n },\n });\n}\n\n/**\n * Zod adapter. Peer-depends `zod`.\n *\n * ```ts\n * import { z } from 'zod';\n * import { zod } from '@boon4681/giri/validators/zod';\n *\n * // JSON body\n * export const body = zod.body({ json: z.object({ name: z.string().min(1) }) });\n * // JSON *or* multipart dispatched on Content-Type at runtime\n * export const body = zod.body({\n * json: z.object({ name: z.string() }),\n * form: z.object({ name: z.string(), avatar: z.instanceof(File) }),\n * });\n * export const query = zod.query(z.object({ page: z.coerce.number() }));\n * ```\n */\nexport const zod = {\n body<Map extends Partial<Record<BodyContentType, z.ZodType>>>(\n map: Map,\n ): GiriBodySchema<{ [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never }> {\n const contents = {} as Record<BodyContentType, GiriInputSchema>;\n for (const [contentType, schema] of Object.entries(map)) {\n if (schema) {\n contents[contentType as BodyContentType] = wrap(schema);\n }\n }\n return defineBodySchema(contents) as unknown as GiriBodySchema<{\n [K in keyof Map]: Map[K] extends z.ZodType ? z.infer<Map[K]> : never;\n }>;\n },\n query<Schema extends z.ZodType>(schema: Schema): GiriInputSchema<z.infer<Schema>> {\n return wrap(schema);\n },\n};\n","export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';\n\nexport type StatusCode = number;\n\nexport type ResponseFormat = 'json' | 'text' | 'html';\n\nexport const typedResponseBrand: unique symbol = Symbol.for('giri.typed-response') as never;\nexport const nativeContextBrand: unique symbol = Symbol.for('giri.native-context') as never;\n\nexport interface TypedResponse<\n T,\n S extends StatusCode = StatusCode,\n F extends ResponseFormat = ResponseFormat,\n> {\n readonly [typedResponseBrand]: {\n data: T;\n status: S;\n format: F;\n };\n readonly data: T;\n readonly status: S;\n readonly format: F;\n readonly headers?: HeadersInit;\n}\n\nexport type HandlerResponse = Response | TypedResponse<unknown, StatusCode, ResponseFormat>;\n\nexport interface ValidatedInput {\n /**\n * The validated request body. For a single declared content-type it's that schema's\n * output; for several it's a discriminated union `{ type; data }` (see `ValidBody`).\n */\n body?: unknown;\n query?: unknown;\n}\n\n/** Attributes for a `Set-Cookie` header. `path` defaults to `/`. */\nexport interface CookieOptions {\n domain?: string;\n path?: string;\n /** Lifetime in seconds. */\n maxAge?: number;\n expires?: Date;\n httpOnly?: boolean;\n secure?: boolean;\n sameSite?: 'Strict' | 'Lax' | 'None' | 'strict' | 'lax' | 'none';\n partitioned?: boolean;\n priority?: 'Low' | 'Medium' | 'High' | 'low' | 'medium' | 'high';\n}\n\n/**\n * Cookie read/write, implemented per adapter with its runtime's native helpers. giri core\n * supplies the {@link CookieSink} (where to read from / write to); the adapter owns encoding.\n */\nexport interface CookieJar {\n get(name: string): string | undefined;\n all(): Record<string, string>;\n set(name: string, value: string, options?: CookieOptions): void;\n delete(name: string, options?: CookieOptions): void;\n getSigned(name: string): Promise<string | false | undefined>;\n setSigned(name: string, value: string, options?: CookieOptions): Promise<void>;\n}\n\n/** What core hands an adapter's cookie jar: the request to read from, the response sink to write to. */\nexport interface CookieSink {\n /** The incoming request, for reading the `Cookie` header. */\n request: Request;\n /** Append one already-serialized `Set-Cookie` header value to the response. */\n append(setCookieHeader: string): void;\n /** The configured `cookieSecret`, if any (for signed cookies). */\n secret?: string;\n}\n\n/** Builds a {@link CookieJar} bound to one request's {@link CookieSink}. Each adapter provides one. */\nexport type CookieJarFactory = (sink: CookieSink) => CookieJar;\n\nexport interface GiriRequest<Input extends ValidatedInput = ValidatedInput> {\n raw: Request;\n url: URL;\n method: string;\n header(name: string): string | null;\n json<T = unknown>(): Promise<T>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n formData(): Promise<FormData>;\n valid<K extends keyof Input & ('body' | 'query')>(key: K): Input[K];\n /** Read a request cookie by name, or `undefined` if absent. */\n cookie(name: string): string | undefined;\n /** All request cookies as a name→value map. */\n cookies(): Record<string, string>;\n /**\n * Read and verify a signed cookie. Resolves to the original value, `false` if the\n * signature was tampered with, or `undefined` if the cookie is absent. Requires\n * `cookieSecret` in `giri.config`.\n */\n signedCookie(name: string): Promise<string | false | undefined>;\n}\n\ndeclare global {\n /**\n * Global registration surface for app-wide types. `giri sync` augments\n * `Giri.Register[\"app\"]` from `src/main.ts` `init()` return type so `c.app` is\n * typed without per-route generics (the registration pattern).\n */\n namespace Giri {\n interface Register {}\n }\n}\n\n/**\n * The app-wide services container, the type of `c.app`. `giri sync` infers it from\n * `src/main.ts`'s `init()` return type (via the global `Giri.Register` augmentation);\n * until then it falls back to an open record. Leave `init` unannotated (its return is\n * the source of truth) and annotate `teardown`'s parameter with this:\n *\n * ```ts\n * export const init = () => ({ db }); // inferred\n * export const teardown = (services: Services) => services.db.close();\n * ```\n */\nexport type Services = Giri.Register extends { app: infer A }\n ? A\n : Record<string, unknown>;\n\nexport interface Context<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n params: Params;\n /** App-wide services from `src/main.ts`'s `init()`, seeded into every request. */\n app: Services;\n req: GiriRequest<Input>;\n // Context vars (`c.set`/`c.get`). Keys declared by middleware (`Vars`) are typed;\n // any other key stays open (`unknown`) so untracked keys still work.\n set<K extends keyof Vars & string>(key: K, value: Vars[K]): void;\n set<K extends string>(key: K, value: unknown): void;\n get<K extends keyof Vars & string>(key: K): Vars[K];\n get<V = unknown>(key: string): V;\n json<T, S extends StatusCode = 200>(\n data: T,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<T, S, 'json'>;\n text<S extends StatusCode = 200>(\n text: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'text'>;\n /** An HTML response (`text/html`). Like `text`, the body is a string. */\n html<S extends StatusCode = 200>(\n html: string,\n status?: S,\n headers?: HeadersInit,\n ): TypedResponse<string, S, 'html'>;\n /** A raw-body response - string, stream, buffer, FormData, … (not documented in OpenAPI). */\n body(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** Alias of `body`, mirroring Hono's `c.newResponse`. */\n newResponse(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;\n /** A redirect (defaults to 302) with the `Location` header set. */\n redirect(location: string, status?: StatusCode): Response;\n /** A 404 Not Found response. */\n notFound(): Response;\n /**\n * Set a response header applied to whatever this handler returns. Pass `{ append: true }` to add\n * another value (e.g. `Set-Cookie`); omit `value` to delete. Mirrors Hono's `c.header`.\n */\n header(name: string, value?: string, options?: { append?: boolean }): void;\n /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */\n status(code: StatusCode): void;\n /**\n * Set a response cookie via `Set-Cookie`. Pass `value: null` to delete it (send the\n * same `path`/`domain` you set it with). Stacks with other cookies set this request.\n */\n cookie(name: string, value: string | null, options?: CookieOptions): void;\n /** Set an HMAC-signed cookie. Requires `cookieSecret` in `giri.config`. */\n signedCookie(name: string, value: string, options?: CookieOptions): Promise<void>;\n}\n\nexport type Handle<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> = (c: Context<Params, Input, Vars>) => HandlerResponse | Promise<HandlerResponse>;\n\nexport type Next = () => Promise<HandlerResponse | void>;\n\n/** An OpenAPI security requirement, e.g. `{ bearerAuth: [] }`. */\nexport type SecurityRequirement = Record<string, string[]>;\n\nexport interface MiddlewareOpenApi {\n /** Security requirements this middleware enforces */\n security?: SecurityRequirement[];\n /** Optional scheme definitions, merged into `components.securitySchemes` so the doc is self-contained. */\n securitySchemes?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface MiddlewareOptions {\n openapi?: MiddlewareOpenApi;\n}\n\nexport interface Middleware<\n Params extends Record<string, string> = Record<string, string>,\n Input extends ValidatedInput = ValidatedInput,\n Vars extends Record<string, unknown> = {},\n> {\n (c: Context<Params, Input, Vars>, next: Next): HandlerResponse | void | Promise<HandlerResponse | void>;\n openapi?: MiddlewareOpenApi;\n}\n\n/** The context vars a middleware injects (its `Vars` type parameter). */\nexport type VarsOf<M> = M extends Middleware<Record<string, string>, ValidatedInput, infer V>\n ? V\n : {};\n\n/** Intersect the injected vars of a tuple of middleware (built with `stack(...)`). */\nexport type MergeStack<T> = T extends readonly [infer Head, ...infer Rest]\n ? VarsOf<Head> & MergeStack<Rest>\n : {};\n\n/**\n * Merge the injected vars of a `middleware` export. A `stack(...)` tuple is merged element-wise;\n * a single bare middleware (`export const middleware = fromHono(...)`) contributes its own vars; a\n * plain `Middleware[]` (not a `stack(...)` tuple) contributes nothing - its element types are lost.\n */\nexport type InferStackVars<T> = T extends readonly [unknown, ...unknown[]]\n ? MergeStack<T>\n : T extends Middleware<Record<string, string>, ValidatedInput, any>\n ? VarsOf<T>\n : {};\n\n/**\n * The vars injected by a module own `middleware` export (a `stack(...)`). Used by the\n * generated per-method handle so a verb file's own `export const middleware` types\n * `c.get`/`c.set`, on top of the folder's `+shared.ts` chain.\n */\nexport type MiddlewareVarsOf<M> = M extends { middleware: infer Stack }\n ? InferStackVars<Stack>\n : {};\n\n/** A JSON Schema object (JSON Schema 2020-12 / OpenAPI 3.1 dialect). */\nexport type JsonSchema = Record<string, unknown>;\n\nexport const inputSchemaBrand: unique symbol = Symbol.for('giri.input-schema') as never;\n\nexport type InputValidationResult<Output = unknown> =\n | { ok: true; value: Output }\n | { ok: false; issues: unknown };\n\n/**\n * A input schema every wrapper form (`body`/`query`) export takes. A vendor\n * adapter (`@boon4681/giri/validators/zod`, `@boon4681/giri/validators/valibot`, …) returns one; build a\n * custom one with `defineInputSchema`. giri core depends only on this interface, never\n * on a validator library. `validate` is the runtime check; `toJsonSchema` feeds OpenAPI.\n */\nexport interface GiriInputSchema<Output = unknown> {\n readonly [inputSchemaBrand]: true;\n validate(value: unknown): InputValidationResult<Output> | Promise<InputValidationResult<Output>>;\n toJsonSchema(): JsonSchema;\n}\n\n/** Extract the validated output type of a giri input schema: `Infer<typeof body>`. */\nexport type Infer<T> = T extends GiriInputSchema<infer Output> ? Output : never;\n\nexport type BodyContentType = 'json' | 'form' | 'urlencoded' | 'text';\n\nexport const bodySchemaBrand: unique symbol = Symbol.for('giri.body-schema') as never;\n\n/**\n * A request body declared as a set of accepted content-types wrapped form `body`\n * takes (`zod.body({ json, form })`). One key means that encoding only; several mean the\n * endpoint accepts any of them, dispatched at runtime on the request `Content-Type`.\n * Each entry is a plain `GiriInputSchema`, so `validate`/`toJsonSchema` work per content-type.\n */\nexport interface GiriBodySchema<\n Outputs extends Partial<Record<BodyContentType, unknown>> = Partial<Record<BodyContentType, unknown>>,\n> {\n readonly [bodySchemaBrand]: true;\n readonly contents: { [K in keyof Outputs & BodyContentType]: GiriInputSchema<Outputs[K]> };\n}\n\n/** True when `T` is a union of more than one member. */\ntype IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The validated body a handler receives. A single declared content-type yields that\n * schema's output directly; several yield a discriminated union keyed by content-type.\n */\nexport type ValidBody<B> = B extends GiriBodySchema<infer Outputs>\n ? IsUnion<keyof Outputs> extends true\n ? { [K in keyof Outputs]: { type: K; data: Outputs[K] } }[keyof Outputs]\n : Outputs[keyof Outputs]\n : never;\n\n/** The validated query a handler receives. */\nexport type ValidQuery<Q> = Q extends GiriInputSchema<infer Output> ? Output : never;\n\n/** Drop keys whose value resolved to `never` (an input the route didn't declare). */\ntype PruneNever<T> = { [K in keyof T as [T[K]] extends [never] ? never : K]: T[K] };\n\n/**\n * Derive a route's `ValidatedInput` from a module's `body`/`query` exports. The generated\n * per-method `$types` handle (`POST`, `GET`, …) uses this so handlers infer `c.req.valid`\n * with no manual generic.\n */\nexport type RouteInputOf<M> = PruneNever<{\n body: M extends { body: infer B } ? ValidBody<B> : never;\n query: M extends { query: infer Q } ? ValidQuery<Q> : never;\n}>;\n\nexport interface RouteInput {\n body?: GiriBodySchema;\n query?: GiriInputSchema;\n}\n\nexport interface RouteOpenApi {\n /** Omit this route from the generated `openapi.json` (it still serves normally). */\n hidden?: boolean;\n // Room to grow: summary, description, tags, deprecated, operationId, …\n}\n\nexport type RouteOpenApiConfig = RouteOpenApi | boolean;\n\nexport interface GiriRouteRegistration {\n method: HttpMethod;\n path: string;\n handle: Handle;\n middleware: Middleware[];\n input?: RouteInput;\n /** App-wide services to seed onto `c.app` (same instance for every route). */\n services?: Services;\n /** Secret for signing/verifying cookies (`c.signedCookie`), from `config.cookieSecret`. */\n cookieSecret?: string;\n}\n\nexport type GiriFetchHandler = (req: Request) => Response | Promise<Response>;\n\nexport interface GiriServeOptions {\n port: number;\n hostname?: string;\n}\n\nexport interface GiriServerInfo {\n address: string;\n port: number;\n}\n\nexport interface GiriServer {\n close(): void | Promise<void>;\n}\n\nexport interface GiriAdapter<App> {\n name?: string;\n createApp(): App;\n register(app: App, route: GiriRouteRegistration): void;\n fetch(app: App, req: Request): Promise<Response>;\n /**\n * Bind the configured backend's runtime to a port and start serving.\n * giri core stays runtime-agnostic: it hands the adapter a request handler\n * (so hot-reload keeps working) and the adapter owns the actual server.\n */\n serve(\n handler: GiriFetchHandler,\n options: GiriServeOptions,\n onListen?: (info: GiriServerInfo) => void,\n ): GiriServer;\n}\n\nexport interface GiriConfig<App = unknown> {\n adapter: GiriAdapter<App>;\n alias?: Record<string, string | string[]>;\n outDir?: string;\n server?: {\n port?: number;\n hostname?: string;\n };\n errorSchema?: unknown;\n /** Secret used to sign/verify cookies via `c.signedCookie` / `c.req.signedCookie`. */\n cookieSecret?: string;\n}\n\nexport interface GiriPaths {\n cwd: string;\n routesDir: string;\n outDir: string;\n}\n","import {\n type BodyContentType,\n type GiriBodySchema,\n type GiriInputSchema,\n type InputValidationResult,\n type RouteInput,\n type TypedResponse,\n type ValidatedInput,\n bodySchemaBrand,\n inputSchemaBrand,\n} from './types';\nimport { createTypedResponse } from './context';\n\ninterface PreparedInput {\n ok: true;\n validated: ValidatedInput;\n}\n\ninterface FailedInput {\n ok: false;\n response: TypedResponse<{ message: string; issues: unknown }, 400 | 415, 'json'>;\n}\n\nexport type PreparedRequestInput = PreparedInput | FailedInput;\n\n/**\n * Build a giri input schema from a `validate` + `toJsonSchema` pair. Vendor adapters use\n * this; you can call it directly to make a custom validator. The brand is a global symbol,\n * so a hand-rolled `{ [Symbol.for(\"giri.input-schema\")]: true, validate, toJsonSchema }` works too.\n */\nexport function defineInputSchema<Output>(\n schema: Omit<GiriInputSchema<Output>, typeof inputSchemaBrand>,\n): GiriInputSchema<Output> {\n return { [inputSchemaBrand]: true, ...schema };\n}\n\nexport function isGiriInputSchema(value: unknown): value is GiriInputSchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[inputSchemaBrand] === true,\n );\n}\n\n/**\n * Build a giri body schema from per-content-type input schemas. Validator adapters use this `zod.body({ json, form })`\n */\nexport function defineBodySchema<Outputs extends Partial<Record<BodyContentType, unknown>>>(\n contents: GiriBodySchema<Outputs>['contents'],\n): GiriBodySchema<Outputs> {\n return { [bodySchemaBrand]: true, contents };\n}\n\nexport function isGiriBodySchema(value: unknown): value is GiriBodySchema {\n return Boolean(\n value &&\n typeof value === 'object' &&\n (value as Record<symbol, unknown>)[bodySchemaBrand] === true,\n );\n}\n\nconst MIME_TO_CONTENT_TYPE: Record<string, BodyContentType> = {\n 'application/json': 'json',\n 'multipart/form-data': 'form',\n 'application/x-www-form-urlencoded': 'urlencoded',\n 'text/plain': 'text',\n};\n\nfunction contentTypeFromHeader(header: string | null): BodyContentType | undefined {\n if (!header) {\n return undefined;\n }\n const mime = header.split(';', 1)[0].trim().toLowerCase();\n return MIME_TO_CONTENT_TYPE[mime];\n}\n\n/** Flatten a `FormData` into a plain object, collapsing repeated fields into arrays. */\nfunction formDataObject(form: FormData): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n form.forEach((value, key) => {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n });\n return result;\n}\n\nasync function readRawBody(request: Request, contentType: BodyContentType): Promise<unknown> {\n const cloned = request.clone();\n if (contentType === 'json') {\n return cloned.json();\n }\n if (contentType === 'text') {\n return cloned.text();\n }\n return formDataObject(await cloned.formData());\n}\n\nfunction queryObject(url: URL): Record<string, string | string[]> {\n const result: Record<string, string | string[]> = {};\n for (const [key, value] of url.searchParams) {\n const current = result[key];\n if (current === undefined) {\n result[key] = value;\n } else if (Array.isArray(current)) {\n current.push(value);\n } else {\n result[key] = [current, value];\n }\n }\n return result;\n}\n\nasync function runValidation(\n schema: GiriInputSchema,\n value: unknown,\n label: string,\n): Promise<InputValidationResult> {\n if (!isGiriInputSchema(schema)) {\n throw new Error(\n `giri: ${label} schema must be wrapped with a validator, e.g. \\`export const ${label} = zod(...)\\` from @boon4681/giri/validators/zod.`,\n );\n }\n return schema.validate(value);\n}\n\nexport async function prepareRequestInput(request: Request, input?: RouteInput): Promise<PreparedRequestInput> {\n const validated: ValidatedInput = {};\n\n if (input?.query) {\n const query = queryObject(new URL(request.url));\n const result = await runValidation(input.query, query, 'query');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid query parameters.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n validated.query = result.value;\n }\n\n if (input?.body) {\n const contents = input.body.contents as Record<BodyContentType, GiriInputSchema>;\n const declared = Object.keys(contents) as BodyContentType[];\n const requested = contentTypeFromHeader(request.headers.get('content-type'));\n // Pick the schema matching the request's content-type; fall back to JSON when the\n // header is missing/unrecognized but JSON is on offer (so header-less posts still work).\n const chosen: BodyContentType | undefined =\n requested && contents[requested] ? requested : contents.json ? 'json' : undefined;\n\n if (!chosen) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Unsupported media type.', issues: { accepted: declared } },\n 415,\n 'json',\n ),\n };\n }\n\n let rawBody: unknown;\n try {\n rawBody = await readRawBody(request, chosen);\n } catch (error) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: error },\n 400,\n 'json',\n ),\n };\n }\n\n const result = await runValidation(contents[chosen], rawBody, 'body');\n if (!result.ok) {\n return {\n ok: false,\n response: createTypedResponse(\n { message: 'Invalid request body.', issues: result.issues },\n 400,\n 'json',\n ),\n };\n }\n\n validated.body = declared.length > 1 ? { type: chosen, data: result.value } : result.value;\n }\n\n return { ok: true, validated };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAkB;;;ACoPX,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;AC7OpE,SAAS,kBACZ,QACuB;AACvB,SAAO,EAAE,CAAC,gBAAgB,GAAG,MAAM,GAAG,OAAO;AACjD;AAaO,SAAS,iBACZ,UACuB;AACvB,SAAO,EAAE,CAAC,eAAe,GAAG,MAAM,SAAS;AAC/C;;;AF9CA,SAAS,KAA+B,QAAkD;AACtF,SAAO,kBAAmC;AAAA,IACtC,SAAS,OAAO;AACZ,YAAM,SAAS,OAAO,UAAU,KAAK;AACrC,aAAO,OAAO,UACR,EAAE,IAAI,MAAM,OAAO,OAAO,KAAK,IAC/B,EAAE,IAAI,OAAO,QAAQ,OAAO,MAAM;AAAA,IAC5C;AAAA,IACA,eAAe;AACX,aAAO,aAAE,aAAa,MAAM;AAAA,IAChC;AAAA,EACJ,CAAC;AACL;AAmBO,IAAM,MAAM;AAAA,EACf,KACI,KACwF;AACxF,UAAM,WAAW,CAAC;AAClB,eAAW,CAAC,aAAa,MAAM,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,UAAI,QAAQ;AACR,iBAAS,WAA8B,IAAI,KAAK,MAAM;AAAA,MAC1D;AAAA,IACJ;AACA,WAAO,iBAAiB,QAAQ;AAAA,EAGpC;AAAA,EACA,MAAgC,QAAkD;AAC9E,WAAO,KAAK,MAAM;AAAA,EACtB;AACJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boon4681/giri",
3
- "version": "0.0.2-alpha-2",
3
+ "version": "0.0.2-alpha-4",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",