@voltro/protocol 0.69.1 → 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
@@ -126,7 +126,7 @@ export declare const coerceQueryValue: (value: unknown, ast: AST.AST) => unknown
126
126
  * route, in stable (path) order. The boot layer feeds these straight into
127
127
  * `restRoutesToHttpRoutes` alongside any hand-authored `restRoutes`.
128
128
  */
129
- 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>>;
130
130
 
131
131
  /**
132
132
  * What a descriptor CARRIES: the author's guards, or the erased form of their
@@ -182,6 +182,21 @@ export declare const derivePublicPath: (tag: string, spec: PublicApiSpec) => str
182
182
  */
183
183
  export declare const dispatchSharedPath: (group: ReadonlyArray<PluginHttpRoute>, req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
184
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
+
185
200
  /** Normalize the `exposeAsTool` shorthand. `true` is only valid when the
186
201
  * descriptor carries a top-level `description`; callers pass that in. */
187
202
  declare type ExposeAsTool = boolean | ExposeAsToolSpec;
@@ -306,6 +321,8 @@ export declare const isRestResponse: (value: unknown) => value is RestResponse<u
306
321
 
307
322
  export declare const isRestStreamResponse: (v: unknown) => v is RestStreamResponse;
308
323
 
324
+ export declare const JSONAPI_MEDIA_TYPE = "application/vnd.api+json; charset=utf-8";
325
+
309
326
  /** Extract `:name` path params by aligning the route PATTERN with the request
310
327
  * path segment-by-segment (`/projects/:project/deploy` + `/projects/acme/deploy`
311
328
  * → `{ project: 'acme' }`). `PluginHttpRouteRequest` carries no matched params,
@@ -327,6 +344,8 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
327
344
  * re-executes on a new server-owned WebSocket identity and rejects HTTP RPC.
328
345
  * This does not grant access or restore application membership by itself. */
329
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;
330
349
  /** Declarative target(s) — drives auto-optimistic on the client AND
331
350
  * surfaces which tables this mutation touches (debug, future
332
351
  * query-invalidation analytics). Mutations without a target run
@@ -375,6 +394,14 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
375
394
  readonly internal: boolean | undefined;
376
395
  }
377
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
+
378
405
  declare interface NestedTargetFields<Input = unknown> {
379
406
  /** Dot-path to the nested array within the query VALUE to patch (e.g.
380
407
  * `'snapshot.projects'`). Absent → patch the top-level row array (default). */
@@ -390,6 +417,15 @@ declare interface NestedTargetFields<Input = unknown> {
390
417
  readonly match?: ((value: unknown, input: Input) => boolean) | undefined;
391
418
  }
392
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
+
393
429
  /**
394
430
  * The runtime-erased form of a procedure's `openAccess:` — a DECLARED decision
395
431
  * that this procedure needs no authorization check, and the reason.
@@ -411,6 +447,50 @@ declare interface OpenAccessSpec {
411
447
  readonly open: string;
412
448
  }
413
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
+
414
494
  /** A public raw-HTTP route a plugin serves on the framework listener. */
415
495
  declare interface PluginHttpRoute {
416
496
  /** HTTP method, or `'*'` for any (the handler decides). PATCH/HEAD/OPTIONS
@@ -418,9 +498,22 @@ declare interface PluginHttpRoute {
418
498
  * this union lacked PATCH; that reason is gone (the `'*'` mount remains
419
499
  * for its other job: one dispatcher per shared path + a precise 405). */
420
500
  readonly method: '*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
421
- /** Absolute path prefix, e.g. `/_voltro/storage`. Matches the path AND
422
- * 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`. */
423
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';
424
517
  readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
425
518
  /** Per-route body cap override (bytes) — wins over the listener's shared
426
519
  * `maxBodyBytes`. NOTE: routes SHARING a path share one body read, so the
@@ -593,6 +686,13 @@ declare interface PluginHttpRouteResult {
593
686
  /** Stream the response (SSE) instead of sending `body`. See
594
687
  * {@link PluginHttpRouteStream}. */
595
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;
596
696
  /** Stream a BINARY response (a download, an export) instead of sending
597
697
  * `body` — see {@link PluginHttpRouteByteStream}. Never buffered by the
598
698
  * serve layer; never compressed (flush timing + Content-Length are the
@@ -649,15 +749,27 @@ declare interface PolicyGuardSpec<Input = unknown> {
649
749
  readonly resource: (input: Input) => string | undefined;
650
750
  }
651
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
+
652
762
  /**
653
763
  * Translate a handler's failure into the response the projection promised:
654
764
  * a DECLARED typed error becomes a `RestRouteError` with the tag, the fields
655
765
  * and the status `errorStatus` assigns (`422` by default, the status the
656
- * OpenAPI projection already documents for a typed error). Anything else is
657
- * rethrown for the desugar's shared mapping (`ScopeError` → 403/401, a
658
- * `status`-carrying error → itself, the rest → 500).
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.
659
771
  */
660
- export declare const projectedFailure: (err: unknown, descriptor: PublicApiDescriptor, spec: PublicApiSpec) => unknown;
772
+ export declare const projectedFailure: (err: unknown, descriptor: PublicApiDescriptor, spec: PublicApiSpec, pluginErrors?: ReadonlyArray<PluginErrorSchema>) => unknown;
661
773
 
662
774
  /** A descriptor kind that can be projected to REST (streams are not, in v1). */
663
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>;
@@ -677,17 +789,47 @@ export declare interface PublicApiEntry {
677
789
  * handler through the SAME AuthMiddleware/RBAC/tenant/audit as the rpc path. */
678
790
  export declare type PublicApiInvoke = (input: unknown, ctx: RestRouteContext) => Promise<unknown> | unknown;
679
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
+
680
817
  /**
681
- * Synthesize the public REST route for ONE descriptor. The descriptor's input
682
- * schema is wrapped into the REST `{ query }` (GET) / `{ body }` (other) shape
683
- * the desugar decodes — for GET behind a query-string coercion — then handed
684
- * UNWRAPPED to `invoke`. Output is the descriptor's output schema verbatim (a
685
- * query is drained to its first snapshot by the serve layer — same as POST
686
- * /rpc — unless `stream: 'sse'` streams it). The guards are
687
- * `requireAuthenticated()` unless the descriptor declares `openAccess`, then
688
- * one `requireScope` per `scopes` entry, 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.
689
831
  */
690
- 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>;
691
833
 
692
834
  declare interface PublicApiSpec {
693
835
  /** Derived by kind when omitted: query→GET, mutation/action→POST. */
@@ -725,6 +867,37 @@ declare interface PublicApiSpec {
725
867
  * publicApi: { errorStatus: { AccessDeniedError: 403, NotFoundError: 404 } }
726
868
  */
727
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;
728
901
  }
729
902
 
730
903
  /**
@@ -861,11 +1034,22 @@ export declare const requireAuthenticated: () => RestGuard;
861
1034
  */
862
1035
  export declare const requireScope: (scope: string) => RestGuard;
863
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
+
864
1045
  export declare const response: <O>(body: O, options?: {
865
1046
  readonly status?: number;
866
1047
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
1048
+ readonly contentType?: string;
867
1049
  }) => RestResponse<O>;
868
1050
 
1051
+ export declare const REST_PROFILE_NAMES: ReadonlyArray<PublicApiProfileName>;
1052
+
869
1053
  /** A BINARY streaming response (download/export). The serve layer pipes the
870
1054
  * ReadableStream without buffering — a body larger than the heap is fine.
871
1055
  * The LAZY thunk form defers opening the source until the response streams. */
@@ -881,8 +1065,85 @@ export declare interface RestByteResponse {
881
1065
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
882
1066
  }
883
1067
 
1068
+ export declare type RestDates = 'iso' | 'epoch-ms';
1069
+
1070
+ export declare type RestEnvelope = 'none' | 'data' | 'jsonapi';
1071
+
884
1072
  export declare const restError: (status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>) => RestRouteError;
885
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
+
886
1147
  export declare type RestGuard = (ctx: RestRouteContext) => RestGuardRejection | undefined | Promise<RestGuardRejection | undefined>;
887
1148
 
888
1149
  /**
@@ -897,6 +1158,10 @@ export declare interface RestGuardRejection {
897
1158
 
898
1159
  export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
899
1160
 
1161
+ export declare type RestNaming = 'camelCase' | 'snake_case';
1162
+
1163
+ export declare type RestPagination = 'link-header' | 'body-cursor';
1164
+
900
1165
  /** A schema-encoded REST body with an application-selected HTTP status and
901
1166
  * response headers. The marker keeps ordinary domain objects unambiguous.
902
1167
  * `output` still validates/encodes `body`; use `Schema.Void` with a 204 when
@@ -906,6 +1171,8 @@ export declare interface RestResponse<O> {
906
1171
  readonly status: number;
907
1172
  readonly body: O;
908
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;
909
1176
  }
910
1177
 
911
1178
  /**
@@ -953,6 +1220,17 @@ export declare interface RestRouteDescriptor<I, O> {
953
1220
  readonly summary?: string;
954
1221
  readonly description?: string;
955
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;
956
1234
  /** Replacement hint. Sets a `Deprecation: true` response header. */
957
1235
  readonly deprecated?: string;
958
1236
  /** ISO date. Sets a `Sunset:` header; past the date the route returns
@@ -1004,7 +1282,9 @@ export declare class RestRouteError extends Error {
1004
1282
  readonly status: number;
1005
1283
  readonly body: unknown;
1006
1284
  readonly headers: Readonly<Record<string, string | ReadonlyArray<string>>> | undefined;
1007
- 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);
1008
1288
  }
1009
1289
 
1010
1290
  export declare interface RestRouteExample {
@@ -1012,6 +1292,36 @@ export declare interface RestRouteExample {
1012
1292
  readonly response?: unknown;
1013
1293
  }
1014
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
+
1015
1325
  /**
1016
1326
  * Desugar a list of REST descriptors into `PluginHttpRoute[]` ready to
1017
1327
  * spread into a plugin/app's `httpRoutes`. `bindings` is supplied by the
@@ -1059,6 +1369,32 @@ export declare interface RestStreamResponse {
1059
1369
  readonly stream: PluginHttpRouteStream;
1060
1370
  }
1061
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
+
1062
1398
  /**
1063
1399
  * Build a streaming (Server-Sent Events) handler result. `subscribe` gets an
1064
1400
  * `emit` for one already-framed SSE chunk and MUST return an unsubscribe, which
@@ -1177,6 +1513,8 @@ declare type TargetRelations = Readonly<Record<string, TargetRelation>>;
1177
1513
 
1178
1514
  declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
1179
1515
 
1516
+ export declare const toSnakeCase: (key: string) => string;
1517
+
1180
1518
  declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
1181
1519
  readonly table: string;
1182
1520
  readonly op: 'update';