@boon4681/giri 0.0.2 → 0.0.3-alpha-2

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.
@@ -1,6 +1,6 @@
1
1
  type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD';
2
2
  type StatusCode = number;
3
- type ResponseFormat = 'json' | 'text';
3
+ type ResponseFormat = 'json' | 'text' | 'html';
4
4
  declare const typedResponseBrand: unique symbol;
5
5
  interface TypedResponse<T, S extends StatusCode = StatusCode, F extends ResponseFormat = ResponseFormat> {
6
6
  readonly [typedResponseBrand]: {
@@ -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
  /**
@@ -69,6 +115,32 @@ interface Context<Params extends Record<string, string> = Record<string, string>
69
115
  get<V = unknown>(key: string): V;
70
116
  json<T, S extends StatusCode = 200>(data: T, status?: S, headers?: HeadersInit): TypedResponse<T, S, 'json'>;
71
117
  text<S extends StatusCode = 200>(text: string, status?: S, headers?: HeadersInit): TypedResponse<string, S, 'text'>;
118
+ /** An HTML response (`text/html`). Like `text`, the body is a string. */
119
+ html<S extends StatusCode = 200>(html: string, status?: S, headers?: HeadersInit): TypedResponse<string, S, 'html'>;
120
+ /** A raw-body response - string, stream, buffer, FormData, … (not documented in OpenAPI). */
121
+ body(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;
122
+ /** Alias of `body`, mirroring Hono's `c.newResponse`. */
123
+ newResponse(data: BodyInit | null, status?: StatusCode, headers?: HeadersInit): Response;
124
+ /** A redirect (defaults to 302) with the `Location` header set. */
125
+ redirect(location: string, status?: StatusCode): Response;
126
+ /** A 404 Not Found response. */
127
+ notFound(): Response;
128
+ /**
129
+ * Set a response header applied to whatever this handler returns. Pass `{ append: true }` to add
130
+ * another value (e.g. `Set-Cookie`); omit `value` to delete. Mirrors Hono's `c.header`.
131
+ */
132
+ header(name: string, value?: string, options?: {
133
+ append?: boolean;
134
+ }): void;
135
+ /** Default status for `body`/`redirect`, and for `json`/`text`/`html` when no status arg is given. */
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>;
72
144
  }
73
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>;
74
146
  type Next = () => Promise<HandlerResponse | void>;
@@ -181,6 +253,20 @@ interface RouteInput {
181
253
  interface RouteOpenApi {
182
254
  /** Omit this route from the generated `openapi.json` (it still serves normally). */
183
255
  hidden?: boolean;
256
+ /**
257
+ * OpenAPI tags - the grouping in doc viewers. On a `+shared.ts` they apply to every route in the
258
+ * folder; the chain is merged and de-duplicated, so a route's tags add to
259
+ * its folders'.
260
+ */
261
+ tags?: string[];
262
+ /** Short operation summary. Cascades down the chain (a verb file overrides its folders). */
263
+ summary?: string;
264
+ /** Longer operation description. Cascades down the chain (a verb file overrides its folders). */
265
+ description?: string;
266
+ /** Marks the operation(s) deprecated. On a `+shared.ts` it deprecates the whole folder. */
267
+ deprecated?: boolean;
268
+ /** Unique operationId. Verb-file only - it is never inherited from a `+shared.ts`. */
269
+ operationId?: string;
184
270
  }
185
271
  type RouteOpenApiConfig = RouteOpenApi | boolean;
186
272
  interface GiriRouteRegistration {
@@ -191,6 +277,8 @@ interface GiriRouteRegistration {
191
277
  input?: RouteInput;
192
278
  /** App-wide services to seed onto `c.app` (same instance for every route). */
193
279
  services?: Services;
280
+ /** Secret for signing/verifying cookies (`c.signedCookie`), from `config.cookieSecret`. */
281
+ cookieSecret?: string;
194
282
  }
195
283
  type GiriFetchHandler = (req: Request) => Response | Promise<Response>;
196
284
  interface GiriServeOptions {
@@ -225,6 +313,8 @@ interface GiriConfig<App = unknown> {
225
313
  hostname?: string;
226
314
  };
227
315
  errorSchema?: unknown;
316
+ /** Secret used to sign/verify cookies via `c.signedCookie` / `c.req.signedCookie`. */
317
+ cookieSecret?: string;
228
318
  }
229
319
  interface GiriPaths {
230
320
  cwd: string;
@@ -232,4 +322,4 @@ interface GiriPaths {
232
322
  outDir: string;
233
323
  }
234
324
 
235
- 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 };
325
+ 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-DkrKD1S4.js';
2
+ import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-BvRph0mx.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';\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}\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;;;ACqKZ,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;AC/JpE,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 /**\n * OpenAPI tags - the grouping in doc viewers. On a `+shared.ts` they apply to every route in the\n * folder; the chain is merged and de-duplicated, so a route's tags add to\n * its folders'.\n */\n tags?: string[];\n /** Short operation summary. Cascades down the chain (a verb file overrides its folders). */\n summary?: string;\n /** Longer operation description. Cascades down the chain (a verb file overrides its folders). */\n description?: string;\n /** Marks the operation(s) deprecated. On a `+shared.ts` it deprecates the whole folder. */\n deprecated?: boolean;\n /** Unique operationId. Verb-file only - it is never inherited from a `+shared.ts`. */\n operationId?: string;\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-DkrKD1S4.js';
2
+ import { B as BodyContentType, a as GiriBodySchema, b as GiriInputSchema } from '../types-BvRph0mx.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';\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}\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;;;ACsKX,IAAM,mBAAkC,uBAAO,IAAI,mBAAmB;AAuBtE,IAAM,kBAAiC,uBAAO,IAAI,kBAAkB;;;AC/JpE,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 /**\n * OpenAPI tags - the grouping in doc viewers. On a `+shared.ts` they apply to every route in the\n * folder; the chain is merged and de-duplicated, so a route's tags add to\n * its folders'.\n */\n tags?: string[];\n /** Short operation summary. Cascades down the chain (a verb file overrides its folders). */\n summary?: string;\n /** Longer operation description. Cascades down the chain (a verb file overrides its folders). */\n description?: string;\n /** Marks the operation(s) deprecated. On a `+shared.ts` it deprecates the whole folder. */\n deprecated?: boolean;\n /** Unique operationId. Verb-file only - it is never inherited from a `+shared.ts`. */\n operationId?: string;\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",
3
+ "version": "0.0.3-alpha-2",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -38,7 +38,9 @@
38
38
  "dist"
39
39
  ],
40
40
  "scripts": {
41
+ "bench": "yarn --cwd .bench bench",
41
42
  "build": "tsx ./build.ts",
43
+ "speed": "yarn bench",
42
44
  "test": "vitest --run",
43
45
  "typecheck": "tsc --noEmit"
44
46
  },
@@ -46,6 +48,7 @@
46
48
  "@clack/prompts": "^1.5.1",
47
49
  "@hono/standard-validator": "^0.2.1",
48
50
  "@sinclair/typebox": "^0.34.49",
51
+ "chokidar": "^5.0.0",
49
52
  "esbuild": "^0.27.7",
50
53
  "esbuild-register": "^3.6.0",
51
54
  "openapi-types": "^12.1.3",