@voltro/protocol 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/rest.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as AST from 'effect/SchemaAST';
1
2
  import { DataStore } from '@voltro/database';
2
3
  import { Schema } from 'effect';
3
4
 
@@ -104,12 +105,28 @@ export declare const bytes: (stream: ReadableStream<Uint8Array> | (() => Readabl
104
105
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
105
106
  }) => RestByteResponse;
106
107
 
108
+ /**
109
+ * Coerce a decoded query-string object toward the ENCODED shape of `input`.
110
+ * Field by field for a struct; the object is returned unchanged for any other
111
+ * shape (the schema then decides). Unknown keys pass through untouched.
112
+ */
113
+ export declare const coerceQueryObject: (query: unknown, input: Schema.Schema.All) => unknown;
114
+
115
+ /**
116
+ * Coerce ONE query-string value toward what `ast` (an ENCODED shape) expects.
117
+ * Only a value that reads unambiguously as the target is converted; anything
118
+ * else is left for the schema to refuse with its own message. Refinements are
119
+ * looked through to their base; a union tries each arm and takes the first
120
+ * that changed the value.
121
+ */
122
+ export declare const coerceQueryValue: (value: unknown, ast: AST.AST) => unknown;
123
+
107
124
  /**
108
125
  * Project every descriptor carrying a `publicApi` annotation into a REST
109
126
  * route, in stable (path) order. The boot layer feeds these straight into
110
127
  * `restRoutesToHttpRoutes` alongside any hand-authored `restRoutes`.
111
128
  */
112
- export declare const collectPublicApiRoutes: (entries: ReadonlyArray<PublicApiEntry>) => ReadonlyArray<RestRouteDescriptor<Record<string, unknown>, unknown>>;
129
+ export declare const collectPublicApiRoutes: (entries: ReadonlyArray<PublicApiEntry>, options?: PublicApiProjectionOptions) => ReadonlyArray<RestRouteDescriptor<Record<string, unknown>, unknown>>;
113
130
 
114
131
  /**
115
132
  * What a descriptor CARRIES: the author's guards, or the erased form of their
@@ -123,6 +140,9 @@ export declare const collectPublicApiRoutes: (entries: ReadonlyArray<PublicApiEn
123
140
  */
124
141
  declare type DeclaredAccess<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input> | OpenAccessSpec>;
125
142
 
143
+ /** The `_tag` literals a descriptor's `error:` union declares (empty for none). */
144
+ export declare const declaredErrorTags: (error: Schema.Schema.All | undefined) => ReadonlySet<string>;
145
+
126
146
  /**
127
147
  * Identity helper that fixes the descriptor's types (like `defineAction` /
128
148
  * `definePlugin`). Returns its argument unchanged at runtime; exists for
@@ -162,6 +182,21 @@ export declare const derivePublicPath: (tag: string, spec: PublicApiSpec) => str
162
182
  */
163
183
  export declare const dispatchSharedPath: (group: ReadonlyArray<PluginHttpRoute>, req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
164
184
 
185
+ /** Encode a failure under `format`: the tagged body the desugar always wrote
186
+ * (`{ error, …fields }`), Problem Details, or the format's own encoder. */
187
+ export declare const encodeRestError: (format: RestFormat, error: RestFormatError, ctx: RestFormatContext) => RestFormatResponse;
188
+
189
+ /**
190
+ * Encode a success value under `format`: the bare value, `{ data, next? }`, or
191
+ * a JSON:API document; a cursor becomes a `Link` header under `link-header`.
192
+ * `value` is already in wire form (dates, naming applied by `restViewSchema`).
193
+ */
194
+ export declare const encodeRestSuccess: (format: RestFormat, value: unknown, ctx: RestFormatContext) => RestFormatResponse;
195
+
196
+ /** The member of an error union that declares `tag`, as a schema of its own —
197
+ * for the spec's per-status responses. */
198
+ export declare const errorMemberSchema: (error: Schema.Schema.All | undefined, tag: string) => Schema.Schema.All | undefined;
199
+
165
200
  /** Normalize the `exposeAsTool` shorthand. `true` is only valid when the
166
201
  * descriptor carries a top-level `description`; callers pass that in. */
167
202
  declare type ExposeAsTool = boolean | ExposeAsToolSpec;
@@ -286,6 +321,8 @@ export declare const isRestResponse: (value: unknown) => value is RestResponse<u
286
321
 
287
322
  export declare const isRestStreamResponse: (v: unknown) => v is RestStreamResponse;
288
323
 
324
+ export declare const JSONAPI_MEDIA_TYPE = "application/vnd.api+json; charset=utf-8";
325
+
289
326
  /** Extract `:name` path params by aligning the route PATTERN with the request
290
327
  * path segment-by-segment (`/projects/:project/deploy` + `/projects/acme/deploy`
291
328
  * → `{ project: 'acme' }`). `PluginHttpRouteRequest` carries no matched params,
@@ -307,6 +344,8 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
307
344
  * re-executes on a new server-owned WebSocket identity and rejects HTTP RPC.
308
345
  * This does not grant access or restore application membership by itself. */
309
346
  readonly idempotencyScope?: MutationIdempotencyScope | undefined;
347
+ /** Same-key calls run in invocation order on the client — see `defineMutation`. */
348
+ readonly serialize?: MutationSerialization<Schema.Schema.Type<Input>> | undefined;
310
349
  /** Declarative target(s) — drives auto-optimistic on the client AND
311
350
  * surfaces which tables this mutation touches (debug, future
312
351
  * query-invalidation analytics). Mutations without a target run
@@ -355,6 +394,14 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
355
394
  readonly internal: boolean | undefined;
356
395
  }
357
396
 
397
+ /** Same-resource ordering for a mutation's calls on the client — see `defineMutation`'s `serialize`. */
398
+ declare interface MutationSerialization<Input = unknown> {
399
+ /** The resource a call writes, from its input; calls sharing it run in order. */
400
+ readonly key: (input: Input) => string;
401
+ /** Bound for active plus waiting calls per subject. Default 32. */
402
+ readonly maxPending?: number;
403
+ }
404
+
358
405
  declare interface NestedTargetFields<Input = unknown> {
359
406
  /** Dot-path to the nested array within the query VALUE to patch (e.g.
360
407
  * `'snapshot.projects'`). Absent → patch the top-level row array (default). */
@@ -370,6 +417,15 @@ declare interface NestedTargetFields<Input = unknown> {
370
417
  readonly match?: ((value: unknown, input: Input) => boolean) | undefined;
371
418
  }
372
419
 
420
+ /** `Link: <path?cursor=…>; rel="next"` — the current query with the cursor
421
+ * parameter replaced. */
422
+ export declare const nextLink: (ctx: RestFormatContext, cursor: string, param: string) => string;
423
+
424
+ /** `{teamId}` — the form every guideline writes first — is the same route as
425
+ * `:teamId`; the desugar matches the colon form, so the path is normalised
426
+ * once here. */
427
+ export declare const normalizePathParams: (path: string) => string;
428
+
373
429
  /**
374
430
  * The runtime-erased form of a procedure's `openAccess:` — a DECLARED decision
375
431
  * that this procedure needs no authorization check, and the reason.
@@ -391,6 +447,50 @@ declare interface OpenAccessSpec {
391
447
  readonly open: string;
392
448
  }
393
449
 
450
+ /** The rpc tag as an `operationId`: `teams.list` → `teamsList`. */
451
+ export declare const operationIdOf: (tag: string) => string;
452
+
453
+ /** The parameter names a route pattern binds, in order. */
454
+ export declare const pathParamNames: (path: string) => ReadonlyArray<string>;
455
+
456
+ /**
457
+ * A cross-cutting error schema a plugin merges into EVERY procedure's
458
+ * wire error union. Use when a plugin interceptor can fail with a typed
459
+ * error (e.g. a rate-limit `RateLimited`) that must decode TYPED on the
460
+ * client rather than crossing as an untyped defect.
461
+ *
462
+ * Two halves because the error must be applied in two places that can't
463
+ * share a value:
464
+ * - `schema`: the live `effect/Schema` — used at runtime when the cli
465
+ * assembles the SERVER rpc group.
466
+ * - `import`: where to import the same schema from — used by codegen to
467
+ * emit the import into the generated CLIENT rpc group, so both ends
468
+ * agree on the wire shape. `name` must be the exported identifier.
469
+ *
470
+ * Requires the plugin to ship the schema as a NAMED export from `module`.
471
+ */
472
+ declare interface PluginErrorSchema {
473
+ readonly schema: Schema.Schema.All;
474
+ readonly import: {
475
+ readonly module: string;
476
+ readonly name: string;
477
+ };
478
+ /**
479
+ * How the `publicApi` REST projection answers this error. The projection
480
+ * encodes a plugin's cross-cutting error the way it encodes a descriptor's
481
+ * own declared error — tag and fields as the body — under this status
482
+ * (`422` when absent, the status an undeclared-status descriptor error
483
+ * gets), with these headers. A rate limiter names `429` and the
484
+ * `Retry-After` / `RateLimit-*` headers here; a descriptor's own
485
+ * `errorStatus` entry for the tag still wins. Without this the same
486
+ * rejection that decoded typed on the rpc wire was a `500` on `/v1`.
487
+ */
488
+ readonly rest?: {
489
+ readonly status: number;
490
+ readonly headers?: (error: unknown) => Readonly<Record<string, string>>;
491
+ };
492
+ }
493
+
394
494
  /** A public raw-HTTP route a plugin serves on the framework listener. */
395
495
  declare interface PluginHttpRoute {
396
496
  /** HTTP method, or `'*'` for any (the handler decides). PATCH/HEAD/OPTIONS
@@ -398,9 +498,22 @@ declare interface PluginHttpRoute {
398
498
  * this union lacked PATCH; that reason is gone (the `'*'` mount remains
399
499
  * for its other job: one dispatcher per shared path + a precise 405). */
400
500
  readonly method: '*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
401
- /** Absolute path prefix, e.g. `/_voltro/storage`. Matches the path AND
402
- * any sub-path (`/_voltro/storage/abc123`). */
501
+ /** Absolute path, e.g. `/_voltro/storage`. Matched byte for byte: the
502
+ * router folds case and a trailing slash to FIND the route, the dispatcher
503
+ * then asks whether the request's path is one the route declared — so
504
+ * `/Metrics` and `/metrics/` are not `/metrics`. A `:name` segment takes one
505
+ * non-empty segment. Whether the route also owns its sub-paths is
506
+ * `match`. */
403
507
  readonly path: string;
508
+ /**
509
+ * `'prefix'` (default): the route answers its path AND every sub-path
510
+ * (`/_voltro/storage/abc123`) — a plugin that branches on the tail mounts
511
+ * once. `'exact'`: the path alone — no sub-path, no trailing slash — which is
512
+ * what a single document or a scrape endpoint is (`/openapi.json`,
513
+ * `/metrics`), and what a path guard at the ingress can cover, because
514
+ * exactly one spelling answers. A path ending in `/*` is a prefix either way.
515
+ */
516
+ readonly match?: 'prefix' | 'exact';
404
517
  readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
405
518
  /** Per-route body cap override (bytes) — wins over the listener's shared
406
519
  * `maxBodyBytes`. NOTE: routes SHARING a path share one body read, so the
@@ -573,6 +686,13 @@ declare interface PluginHttpRouteResult {
573
686
  /** Stream the response (SSE) instead of sending `body`. See
574
687
  * {@link PluginHttpRouteStream}. */
575
688
  readonly stream?: PluginHttpRouteStream;
689
+ /**
690
+ * The error behind a `5xx` — for the server log line that names the
691
+ * status, never for the wire. A route that maps a throw into a response
692
+ * value has swallowed the reason by the time the transport logs the value;
693
+ * this carries it alongside.
694
+ */
695
+ readonly cause?: unknown;
576
696
  /** Stream a BINARY response (a download, an export) instead of sending
577
697
  * `body` — see {@link PluginHttpRouteByteStream}. Never buffered by the
578
698
  * serve layer; never compressed (flush timing + Content-Length are the
@@ -629,6 +749,28 @@ declare interface PolicyGuardSpec<Input = unknown> {
629
749
  readonly resource: (input: Input) => string | undefined;
630
750
  }
631
751
 
752
+ export declare const PROBLEM_JSON = "application/problem+json; charset=utf-8";
753
+
754
+ /** RFC 9457 Problem Details for one failure: `type` from the tag, `title` from
755
+ * the tag's words, `status`, `detail` from the message, the error's own fields
756
+ * as extension members. */
757
+ export declare const problemDetails: (error: RestFormatError) => Record<string, unknown>;
758
+
759
+ /** A stable, non-resolving URI for an error type: the tag as a URN. */
760
+ export declare const problemTypeOf: (tag: string) => string;
761
+
762
+ /**
763
+ * Translate a handler's failure into the response the projection promised:
764
+ * a DECLARED typed error becomes a `RestRouteError` with the tag, the fields
765
+ * and the status `errorStatus` assigns (`422` by default, the status the
766
+ * OpenAPI projection already documents for a typed error); a plugin's
767
+ * cross-cutting error answers under the status the plugin names. Anything
768
+ * else is rethrown for the desugar's shared mapping (`ScopeError` → 403/401, a
769
+ * `status`-carrying error → itself, the rest → 500). The body is the tagged
770
+ * `{ error, …fields }` form; `projectedFailureFor` speaks the profile.
771
+ */
772
+ export declare const projectedFailure: (err: unknown, descriptor: PublicApiDescriptor, spec: PublicApiSpec, pluginErrors?: ReadonlyArray<PluginErrorSchema>) => unknown;
773
+
632
774
  /** A descriptor kind that can be projected to REST (streams are not, in v1). */
633
775
  export declare type PublicApiDescriptor = QueryProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | MutationProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | ActionProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All>;
634
776
 
@@ -647,16 +789,47 @@ export declare interface PublicApiEntry {
647
789
  * handler through the SAME AuthMiddleware/RBAC/tenant/audit as the rpc path. */
648
790
  export declare type PublicApiInvoke = (input: unknown, ctx: RestRouteContext) => Promise<unknown> | unknown;
649
791
 
792
+ /** The `publicApi:` block of an api `app.config.ts`, and the shape of a
793
+ * descriptor's `publicApi.format` override (every field optional there). */
794
+ export declare interface PublicApiProfileConfig {
795
+ /** The profile every projection speaks. Default `'rpc'` — today's wire. */
796
+ readonly profile?: PublicApiProfileName | RestFormat;
797
+ readonly dates?: RestDates;
798
+ readonly errors?: RestErrors;
799
+ readonly envelope?: RestEnvelope;
800
+ readonly pagination?: RestPagination;
801
+ readonly cursorField?: string;
802
+ readonly cursorParam?: string;
803
+ readonly naming?: RestNaming;
804
+ readonly etag?: boolean;
805
+ }
806
+
807
+ export declare type PublicApiProfileName = 'rpc' | 'standard' | 'jsonapi';
808
+
809
+ /** What a projection needs beyond its descriptor: the errors every mounted
810
+ * plugin may fail a call with, and the app's `publicApi` profile. */
811
+ export declare interface PublicApiProjectionOptions {
812
+ readonly pluginErrors?: ReadonlyArray<PluginErrorSchema>;
813
+ /** The `publicApi:` block of the api's `app.config.ts`. Absent → `'rpc'`. */
814
+ readonly profile?: PublicApiProfileConfig;
815
+ }
816
+
650
817
  /**
651
- * Synthesize the public REST route for ONE descriptor. The descriptor's input
652
- * schema is wrapped into the REST `{ query }` (GET) / `{ body }` (other) shape
653
- * the desugar decodes, then handed UNWRAPPED to `invoke`. Output is the
654
- * descriptor's output schema verbatim (a query is drained to its first snapshot
655
- * by the serve layer — same as POST /rpc — unless `stream: 'sse'` streams it).
656
- * `scopes` become
657
- * `requireScope` guards on top of the handler's own RBAC.
818
+ * Synthesize the public REST route for ONE descriptor.
819
+ *
820
+ * The input arrives as `{ query, params, body }` from the desugar and is
821
+ * assembled into ONE object — the query (GET) or body (else), plus every
822
+ * path parameter bound to the input field of the same name — coerced toward
823
+ * the input's encoded shape (a query string and a path carry strings), then
824
+ * decoded by the input schema in the profile's wire form (`restViewSchema`).
825
+ * The handler receives the descriptor's typed input; its output is encoded to
826
+ * the wire form and enveloped by the profile. A query is drained to its first
827
+ * snapshot by the serve layer — same as POST /rpc — unless `stream: 'sse'`
828
+ * streams it. The guards are `requireAuthenticated()` unless the descriptor
829
+ * declares `openAccess`, then one `requireScope` per `scopes` entry, on top of
830
+ * the handler's own RBAC.
658
831
  */
659
- export declare const publicApiRoute: (descriptor: PublicApiDescriptor, invoke: PublicApiInvoke, subscribe?: PublicApiSubscribe) => RestRouteDescriptor<Record<string, unknown>, unknown>;
832
+ export declare const publicApiRoute: (descriptor: PublicApiDescriptor, invoke: PublicApiInvoke, subscribe?: PublicApiSubscribe, options?: PublicApiProjectionOptions) => RestRouteDescriptor<Record<string, unknown>, unknown>;
660
833
 
661
834
  declare interface PublicApiSpec {
662
835
  /** Derived by kind when omitted: query→GET, mutation/action→POST. */
@@ -683,6 +856,48 @@ declare interface PublicApiSpec {
683
856
  /** Streaming queries over request/response: 'snapshot' (default — first
684
857
  * snapshot, like POST /rpc) | 'sse' (Server-Sent Events of snapshot+deltas). */
685
858
  readonly stream?: 'snapshot' | 'sse';
859
+ /**
860
+ * HTTP status per DECLARED typed error tag, for the projection. A typed
861
+ * error the handler fails with — one of the descriptor's `error:` union —
862
+ * answers with its tag and fields as the body, and this status; a tag not
863
+ * listed here answers `422`, the status the OpenAPI projection documents
864
+ * for a typed error. The framework's own `ScopeError` is mapped
865
+ * before this (`403`, or `401` for an anonymous caller) and needs no entry.
866
+ *
867
+ * publicApi: { errorStatus: { AccessDeniedError: 403, NotFoundError: 404 } }
868
+ */
869
+ readonly errorStatus?: Readonly<Record<string, number>>;
870
+ /**
871
+ * The spec's tag group(s) for this operation. Default: the rpc tag's prefix
872
+ * (`teams.list` → `teams`) — the first path segment, what a consumer looks
873
+ * for, and what an SDK generator turns into a client class.
874
+ */
875
+ readonly tags?: ReadonlyArray<string>;
876
+ /**
877
+ * The spec's `operationId`. Default: the rpc tag in camelCase (`teams.list`
878
+ * → `teamsList`) — the stable identifier a generator names methods from.
879
+ */
880
+ readonly operationId?: string;
881
+ /**
882
+ * Whether the operation documents a bearer requirement. Default: `'required'`
883
+ * unless the descriptor declares `openAccess` — then `'optional'` (the key
884
+ * strategy authenticates a bearer when one is sent, and an anonymous caller
885
+ * is allowed through), which Swagger UI and generated clients read as
886
+ * "send the token when you have one". `false` documents no scheme at all.
887
+ */
888
+ readonly security?: 'required' | 'optional' | false;
889
+ /** A request/response example for the spec — the same semantics as a
890
+ * `defineRestRoute` example. */
891
+ readonly example?: {
892
+ readonly request?: unknown;
893
+ readonly response?: unknown;
894
+ };
895
+ /**
896
+ * This projection's wire format, overriding the app's `publicApi` profile
897
+ * (`app.config.ts`) for the one endpoint that must differ — every field
898
+ * optional, the rest inherited. See `PublicApiProfileConfig`.
899
+ */
900
+ readonly format?: PublicApiProfileConfig;
686
901
  }
687
902
 
688
903
  /**
@@ -819,11 +1034,22 @@ export declare const requireAuthenticated: () => RestGuard;
819
1034
  */
820
1035
  export declare const requireScope: (scope: string) => RestGuard;
821
1036
 
1037
+ /**
1038
+ * The format one projection speaks: the app profile, then its building-block
1039
+ * overrides, then the descriptor's own `format`. A named profile is a
1040
+ * `RestFormat` with a name; an object is taken as-is and its fields overridden
1041
+ * the same way.
1042
+ */
1043
+ export declare const resolveRestFormat: (config: PublicApiProfileConfig | undefined, override?: Partial<PublicApiProfileConfig> | undefined) => RestFormat;
1044
+
822
1045
  export declare const response: <O>(body: O, options?: {
823
1046
  readonly status?: number;
824
1047
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
1048
+ readonly contentType?: string;
825
1049
  }) => RestResponse<O>;
826
1050
 
1051
+ export declare const REST_PROFILE_NAMES: ReadonlyArray<PublicApiProfileName>;
1052
+
827
1053
  /** A BINARY streaming response (download/export). The serve layer pipes the
828
1054
  * ReadableStream without buffering — a body larger than the heap is fine.
829
1055
  * The LAZY thunk form defers opening the source until the response streams. */
@@ -839,8 +1065,85 @@ export declare interface RestByteResponse {
839
1065
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
840
1066
  }
841
1067
 
1068
+ export declare type RestDates = 'iso' | 'epoch-ms';
1069
+
1070
+ export declare type RestEnvelope = 'none' | 'data' | 'jsonapi';
1071
+
842
1072
  export declare const restError: (status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>) => RestRouteError;
843
1073
 
1074
+ export declare type RestErrors = 'problem-details' | 'tagged';
1075
+
1076
+ /**
1077
+ * The wire format of one `publicApi` surface. `'standard'` and `'jsonapi'` are
1078
+ * named instances of this; an app's own object is the custom case.
1079
+ */
1080
+ export declare interface RestFormat {
1081
+ readonly name: string;
1082
+ /** `timestampMs` fields: RFC 3339 strings, or the epoch milliseconds the rpc wire carries. */
1083
+ readonly dates: RestDates;
1084
+ /** Failures: RFC 9457 `application/problem+json`, or the tagged object rpc carries. */
1085
+ readonly errors: RestErrors;
1086
+ /** Success bodies: the bare value, `{ data }`, or the JSON:API document. */
1087
+ readonly envelope: RestEnvelope;
1088
+ /** A cursor the handler returns in `cursorField`: as a `Link: <…>; rel="next"`
1089
+ * header (and `next` beside `data`), or left where the handler put it. */
1090
+ readonly pagination: RestPagination;
1091
+ /** The cursor field a paginated output carries. Default `nextCursor`. */
1092
+ readonly cursorField: string;
1093
+ /** The query parameter a cursor travels back in. Default `cursor`. */
1094
+ readonly cursorParam: string;
1095
+ /** Property names on the wire. */
1096
+ readonly naming: RestNaming;
1097
+ /** Weak `ETag` on GET answers, `If-None-Match` → 304. */
1098
+ readonly etag: boolean;
1099
+ /** Custom success encoding — receives the value already in wire form (dates,
1100
+ * naming applied). Replaces the envelope/pagination steps. */
1101
+ readonly encodeSuccess?: (value: unknown, ctx: RestFormatContext) => RestFormatResponse;
1102
+ /** Custom error encoding. Replaces `errors`. */
1103
+ readonly encodeError?: (error: RestFormatError, ctx: RestFormatContext) => RestFormatResponse;
1104
+ /** A hook over the OpenAPI operation the plugin emits for a projection. */
1105
+ readonly describe?: (operation: Record<string, unknown>, ctx: {
1106
+ readonly tag: string;
1107
+ readonly method: string;
1108
+ readonly path: string;
1109
+ }) => Record<string, unknown>;
1110
+ }
1111
+
1112
+ /** What a format sees when it encodes one response. */
1113
+ export declare interface RestFormatContext {
1114
+ /** The rpc tag, e.g. `teams.list`. */
1115
+ readonly tag: string;
1116
+ readonly method: string;
1117
+ /** The request path, for `Link` headers. */
1118
+ readonly path: string;
1119
+ /** The request's query string (without `?`). */
1120
+ readonly query: string;
1121
+ /** The first `source` table of the procedure, when it declares one — the
1122
+ * JSON:API resource `type`. */
1123
+ readonly source: string | undefined;
1124
+ readonly status: number;
1125
+ }
1126
+
1127
+ /** A failure the desugar or the projection is about to answer with. */
1128
+ export declare interface RestFormatError {
1129
+ readonly status: number;
1130
+ /** The typed error's `_tag` (`AccessDeniedError`), or a stable name for a
1131
+ * transport failure (`InvalidRequest`, `Unauthorized`, `Forbidden`, …). */
1132
+ readonly tag: string;
1133
+ /** A human-readable line. */
1134
+ readonly message: string;
1135
+ /** The error's own fields (a typed error), without `_tag`. */
1136
+ readonly fields: Readonly<Record<string, unknown>>;
1137
+ }
1138
+
1139
+ /** A response a format produced: body (already wire-shaped), headers, and the
1140
+ * media type when it is not `application/json`. */
1141
+ export declare interface RestFormatResponse {
1142
+ readonly body: unknown;
1143
+ readonly headers?: Readonly<Record<string, string>>;
1144
+ readonly contentType?: string;
1145
+ }
1146
+
844
1147
  export declare type RestGuard = (ctx: RestRouteContext) => RestGuardRejection | undefined | Promise<RestGuardRejection | undefined>;
845
1148
 
846
1149
  /**
@@ -855,6 +1158,10 @@ export declare interface RestGuardRejection {
855
1158
 
856
1159
  export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
857
1160
 
1161
+ export declare type RestNaming = 'camelCase' | 'snake_case';
1162
+
1163
+ export declare type RestPagination = 'link-header' | 'body-cursor';
1164
+
858
1165
  /** A schema-encoded REST body with an application-selected HTTP status and
859
1166
  * response headers. The marker keeps ordinary domain objects unambiguous.
860
1167
  * `output` still validates/encodes `body`; use `Schema.Void` with a 204 when
@@ -864,6 +1171,8 @@ export declare interface RestResponse<O> {
864
1171
  readonly status: number;
865
1172
  readonly body: O;
866
1173
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
1174
+ /** A media type other than `application/json` (a JSON:API document). */
1175
+ readonly contentType?: string;
867
1176
  }
868
1177
 
869
1178
  /**
@@ -911,6 +1220,17 @@ export declare interface RestRouteDescriptor<I, O> {
911
1220
  readonly summary?: string;
912
1221
  readonly description?: string;
913
1222
  readonly example?: RestRouteExample;
1223
+ /** The spec's `operationId` — set by a `publicApi` projection from its tag. */
1224
+ readonly operationId?: string;
1225
+ /** The spec's tag group(s) — set by a projection from its tag prefix. */
1226
+ readonly tags?: ReadonlyArray<string>;
1227
+ /**
1228
+ * Present on a `publicApi` projection: what the route projects, in the wire
1229
+ * form its profile speaks. The OpenAPI generator documents parameters,
1230
+ * bodies, responses and security from it; the desugar answers every refusal
1231
+ * through its format.
1232
+ */
1233
+ readonly projection?: RestRouteProjection;
914
1234
  /** Replacement hint. Sets a `Deprecation: true` response header. */
915
1235
  readonly deprecated?: string;
916
1236
  /** ISO date. Sets a `Sunset:` header; past the date the route returns
@@ -962,7 +1282,9 @@ export declare class RestRouteError extends Error {
962
1282
  readonly status: number;
963
1283
  readonly body: unknown;
964
1284
  readonly headers: Readonly<Record<string, string | ReadonlyArray<string>>> | undefined;
965
- constructor(status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>);
1285
+ /** A media type other than `application/json` (`application/problem+json`). */
1286
+ readonly contentType: string | undefined;
1287
+ constructor(status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>, contentType?: string);
966
1288
  }
967
1289
 
968
1290
  export declare interface RestRouteExample {
@@ -970,6 +1292,36 @@ export declare interface RestRouteExample {
970
1292
  readonly response?: unknown;
971
1293
  }
972
1294
 
1295
+ /**
1296
+ * What a `publicApi` projection is, for the two readers that need more than
1297
+ * the route: the OpenAPI generator and the desugar's refusals. `input` and
1298
+ * `output` are the WIRE schemas — the descriptor's, rewritten by the profile
1299
+ * (`restViewSchema`): a `timestampMs` is a `date-time` string under
1300
+ * `dates: 'iso'`, keys are snake_case under that naming. `input` carries the
1301
+ * path parameters as fields; `pathParams` says which.
1302
+ */
1303
+ export declare interface RestRouteProjection {
1304
+ readonly tag: string;
1305
+ readonly kind: 'query' | 'mutation' | 'action';
1306
+ readonly version: string;
1307
+ readonly security: 'required' | 'optional' | 'none';
1308
+ readonly input: Schema.Schema.All;
1309
+ /** The wire names of the input fields bound from the path. */
1310
+ readonly pathParams: ReadonlyArray<string>;
1311
+ readonly output: Schema.Schema.All;
1312
+ /** The first `source` table, when the procedure declares one. */
1313
+ readonly source: string | undefined;
1314
+ /** Every failure the operation answers with a typed body, and its status:
1315
+ * the descriptor's `errorStatus` entries, `422` for the union's other tags,
1316
+ * and each plugin error that names a REST status. */
1317
+ readonly errorResponses: ReadonlyArray<{
1318
+ readonly status: number;
1319
+ readonly tag: string;
1320
+ readonly schema: Schema.Schema.All;
1321
+ }>;
1322
+ readonly format: RestFormat;
1323
+ }
1324
+
973
1325
  /**
974
1326
  * Desugar a list of REST descriptors into `PluginHttpRoute[]` ready to
975
1327
  * spread into a plugin/app's `httpRoutes`. `bindings` is supplied by the
@@ -1017,6 +1369,32 @@ export declare interface RestStreamResponse {
1017
1369
  readonly stream: PluginHttpRouteStream;
1018
1370
  }
1019
1371
 
1372
+ /**
1373
+ * Rewrite a descriptor's schema into the shape the profile puts on the wire:
1374
+ * `timestampMs` (`DateFromNumber`) becomes an RFC 3339 string under
1375
+ * `dates: 'iso'`, and every struct's property names become `snake_case` under
1376
+ * `naming: 'snake_case'` — as a transformation, so decoding a wire value yields
1377
+ * the descriptor's own typed value and encoding the typed value yields the
1378
+ * wire form. `JSONSchema.make` of the result documents the wire (a
1379
+ * `format: date-time` string, the snake keys). Classes and other opaque
1380
+ * transformations pass through unchanged; a struct inside one keeps its keys.
1381
+ */
1382
+ export declare const restViewSchema: (schema: Schema.Schema.All, format: RestFormat) => Schema.Schema.All;
1383
+
1384
+ /**
1385
+ * Whether `path` is one the route declared. The router matches case-folded and
1386
+ * ignores a trailing slash; the dispatcher asks this, byte for byte: a literal
1387
+ * segment must be equal, a `:name` segment takes one non-empty segment, and
1388
+ * beyond the declared segments a `'prefix'` route (the default) owns any
1389
+ * sub-path while an `'exact'` route owns nothing. A trailing slash alone is
1390
+ * not a sub-path — `/metrics/` is nobody's, whatever the mode. An explicit
1391
+ * `/*` tail is a prefix by itself.
1392
+ */
1393
+ export declare const routeAcceptsPath: (route: {
1394
+ readonly path: string;
1395
+ readonly match?: "prefix" | "exact" | undefined;
1396
+ }, path: string) => boolean;
1397
+
1020
1398
  /**
1021
1399
  * Build a streaming (Server-Sent Events) handler result. `subscribe` gets an
1022
1400
  * `emit` for one already-framed SSE chunk and MUST return an unsubscribe, which
@@ -1135,6 +1513,8 @@ declare type TargetRelations = Readonly<Record<string, TargetRelation>>;
1135
1513
 
1136
1514
  declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
1137
1515
 
1516
+ export declare const toSnakeCase: (key: string) => string;
1517
+
1138
1518
  declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
1139
1519
  readonly table: string;
1140
1520
  readonly op: 'update';