@voltro/protocol 0.69.1 → 0.71.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
@@ -159,11 +159,22 @@ declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input
159
159
  }
160
160
 
161
161
  /** query→GET, mutation/action→POST, unless the spec overrides. */
162
- export declare const derivePublicMethod: (kind: PublicApiDescriptor["kind"], spec: PublicApiSpec) => RestMethod;
162
+ export declare const derivePublicMethod: (kind: PublicApiDescriptor["kind"], spec: PublicApiSpec, descriptor?: ResourceDescriptorFacts) => RestMethod;
163
163
 
164
164
  /** `/<version>/<tag-with-dots-as-slashes>` unless the spec gives an explicit
165
- * path. `orders.create` @ v1 → `/v1/orders/create`. */
166
- export declare const derivePublicPath: (tag: string, spec: PublicApiSpec) => string;
165
+ * path, or `resource` derives one. `orders.create` @ v1 → `/v1/orders/create`. */
166
+ export declare const derivePublicPath: (tag: string, spec: PublicApiSpec, descriptor?: ResourceDescriptorFacts) => string;
167
+
168
+ /**
169
+ * `publicApi.resource`: method and path from the resource, the descriptor's
170
+ * kind and target, and whether the input identifies one row — the convention
171
+ * `crud.*` already follows, so a surface of hundreds of projections is not
172
+ * hundreds of hand-written paths.
173
+ */
174
+ export declare const deriveResource: (tag: string, spec: PublicApiSpec, descriptor: ResourceDescriptorFacts) => {
175
+ readonly method: RestMethod;
176
+ readonly path: string;
177
+ };
167
178
 
168
179
  /**
169
180
  * Dispatch a request across REST routes that SHARE a path. Every REST route
@@ -182,6 +193,21 @@ export declare const derivePublicPath: (tag: string, spec: PublicApiSpec) => str
182
193
  */
183
194
  export declare const dispatchSharedPath: (group: ReadonlyArray<PluginHttpRoute>, req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
184
195
 
196
+ /** Encode a failure under `format`: the tagged body the desugar always wrote
197
+ * (`{ error, …fields }`), Problem Details, or the format's own encoder. */
198
+ export declare const encodeRestError: (format: RestFormat, error: RestFormatError, ctx: RestFormatContext) => RestFormatResponse;
199
+
200
+ /**
201
+ * Encode a success value under `format`: the bare value, `{ data, next? }`, or
202
+ * a JSON:API document; a cursor becomes a `Link` header under `link-header`.
203
+ * `value` is already in wire form (dates, naming applied by `restViewSchema`).
204
+ */
205
+ export declare const encodeRestSuccess: (format: RestFormat, value: unknown, ctx: RestFormatContext) => RestFormatResponse;
206
+
207
+ /** The member of an error union that declares `tag`, as a schema of its own —
208
+ * for the spec's per-status responses. */
209
+ export declare const errorMemberSchema: (error: Schema.Schema.All | undefined, tag: string) => Schema.Schema.All | undefined;
210
+
185
211
  /** Normalize the `exposeAsTool` shorthand. `true` is only valid when the
186
212
  * descriptor carries a top-level `description`; callers pass that in. */
187
213
  declare type ExposeAsTool = boolean | ExposeAsToolSpec;
@@ -306,6 +332,8 @@ export declare const isRestResponse: (value: unknown) => value is RestResponse<u
306
332
 
307
333
  export declare const isRestStreamResponse: (v: unknown) => v is RestStreamResponse;
308
334
 
335
+ export declare const JSONAPI_MEDIA_TYPE = "application/vnd.api+json; charset=utf-8";
336
+
309
337
  /** Extract `:name` path params by aligning the route PATTERN with the request
310
338
  * path segment-by-segment (`/projects/:project/deploy` + `/projects/acme/deploy`
311
339
  * → `{ project: 'acme' }`). `PluginHttpRouteRequest` carries no matched params,
@@ -327,6 +355,8 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
327
355
  * re-executes on a new server-owned WebSocket identity and rejects HTTP RPC.
328
356
  * This does not grant access or restore application membership by itself. */
329
357
  readonly idempotencyScope?: MutationIdempotencyScope | undefined;
358
+ /** Same-key calls run in invocation order on the client — see `defineMutation`. */
359
+ readonly serialize?: MutationSerialization<Schema.Schema.Type<Input>> | undefined;
330
360
  /** Declarative target(s) — drives auto-optimistic on the client AND
331
361
  * surfaces which tables this mutation touches (debug, future
332
362
  * query-invalidation analytics). Mutations without a target run
@@ -375,6 +405,14 @@ declare interface MutationProcedureDescriptor<Name extends string, Input extends
375
405
  readonly internal: boolean | undefined;
376
406
  }
377
407
 
408
+ /** Same-resource ordering for a mutation's calls on the client — see `defineMutation`'s `serialize`. */
409
+ declare interface MutationSerialization<Input = unknown> {
410
+ /** The resource a call writes, from its input; calls sharing it run in order. */
411
+ readonly key: (input: Input) => string;
412
+ /** Bound for active plus waiting calls per subject. Default 32. */
413
+ readonly maxPending?: number;
414
+ }
415
+
378
416
  declare interface NestedTargetFields<Input = unknown> {
379
417
  /** Dot-path to the nested array within the query VALUE to patch (e.g.
380
418
  * `'snapshot.projects'`). Absent → patch the top-level row array (default). */
@@ -390,6 +428,15 @@ declare interface NestedTargetFields<Input = unknown> {
390
428
  readonly match?: ((value: unknown, input: Input) => boolean) | undefined;
391
429
  }
392
430
 
431
+ /** `Link: <path?cursor=…>; rel="next"` — the current query with the cursor
432
+ * parameter replaced. */
433
+ export declare const nextLink: (ctx: RestFormatContext, cursor: string, param: string) => string;
434
+
435
+ /** `{teamId}` — the form every guideline writes first — is the same route as
436
+ * `:teamId`; the desugar matches the colon form, so the path is normalised
437
+ * once here. */
438
+ export declare const normalizePathParams: (path: string) => string;
439
+
393
440
  /**
394
441
  * The runtime-erased form of a procedure's `openAccess:` — a DECLARED decision
395
442
  * that this procedure needs no authorization check, and the reason.
@@ -411,6 +458,50 @@ declare interface OpenAccessSpec {
411
458
  readonly open: string;
412
459
  }
413
460
 
461
+ /** The rpc tag as an `operationId`: `teams.list` → `teamsList`. */
462
+ export declare const operationIdOf: (tag: string) => string;
463
+
464
+ /** The parameter names a route pattern binds, in order. */
465
+ export declare const pathParamNames: (path: string) => ReadonlyArray<string>;
466
+
467
+ /**
468
+ * A cross-cutting error schema a plugin merges into EVERY procedure's
469
+ * wire error union. Use when a plugin interceptor can fail with a typed
470
+ * error (e.g. a rate-limit `RateLimited`) that must decode TYPED on the
471
+ * client rather than crossing as an untyped defect.
472
+ *
473
+ * Two halves because the error must be applied in two places that can't
474
+ * share a value:
475
+ * - `schema`: the live `effect/Schema` — used at runtime when the cli
476
+ * assembles the SERVER rpc group.
477
+ * - `import`: where to import the same schema from — used by codegen to
478
+ * emit the import into the generated CLIENT rpc group, so both ends
479
+ * agree on the wire shape. `name` must be the exported identifier.
480
+ *
481
+ * Requires the plugin to ship the schema as a NAMED export from `module`.
482
+ */
483
+ declare interface PluginErrorSchema {
484
+ readonly schema: Schema.Schema.All;
485
+ readonly import: {
486
+ readonly module: string;
487
+ readonly name: string;
488
+ };
489
+ /**
490
+ * How the `publicApi` REST projection answers this error. The projection
491
+ * encodes a plugin's cross-cutting error the way it encodes a descriptor's
492
+ * own declared error — tag and fields as the body — under this status
493
+ * (`422` when absent, the status an undeclared-status descriptor error
494
+ * gets), with these headers. A rate limiter names `429` and the
495
+ * `Retry-After` / `RateLimit-*` headers here; a descriptor's own
496
+ * `errorStatus` entry for the tag still wins. Without this the same
497
+ * rejection that decoded typed on the rpc wire was a `500` on `/v1`.
498
+ */
499
+ readonly rest?: {
500
+ readonly status: number;
501
+ readonly headers?: (error: unknown) => Readonly<Record<string, string>>;
502
+ };
503
+ }
504
+
414
505
  /** A public raw-HTTP route a plugin serves on the framework listener. */
415
506
  declare interface PluginHttpRoute {
416
507
  /** HTTP method, or `'*'` for any (the handler decides). PATCH/HEAD/OPTIONS
@@ -418,9 +509,22 @@ declare interface PluginHttpRoute {
418
509
  * this union lacked PATCH; that reason is gone (the `'*'` mount remains
419
510
  * for its other job: one dispatcher per shared path + a precise 405). */
420
511
  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`). */
512
+ /** Absolute path, e.g. `/_voltro/storage`. Matched byte for byte: the
513
+ * router folds case and a trailing slash to FIND the route, the dispatcher
514
+ * then asks whether the request's path is one the route declared — so
515
+ * `/Metrics` and `/metrics/` are not `/metrics`. A `:name` segment takes one
516
+ * non-empty segment. Whether the route also owns its sub-paths is
517
+ * `match`. */
423
518
  readonly path: string;
519
+ /**
520
+ * `'prefix'` (default): the route answers its path AND every sub-path
521
+ * (`/_voltro/storage/abc123`) — a plugin that branches on the tail mounts
522
+ * once. `'exact'`: the path alone — no sub-path, no trailing slash — which is
523
+ * what a single document or a scrape endpoint is (`/openapi.json`,
524
+ * `/metrics`), and what a path guard at the ingress can cover, because
525
+ * exactly one spelling answers. A path ending in `/*` is a prefix either way.
526
+ */
527
+ readonly match?: 'prefix' | 'exact';
424
528
  readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
425
529
  /** Per-route body cap override (bytes) — wins over the listener's shared
426
530
  * `maxBodyBytes`. NOTE: routes SHARING a path share one body read, so the
@@ -593,6 +697,13 @@ declare interface PluginHttpRouteResult {
593
697
  /** Stream the response (SSE) instead of sending `body`. See
594
698
  * {@link PluginHttpRouteStream}. */
595
699
  readonly stream?: PluginHttpRouteStream;
700
+ /**
701
+ * The error behind a `5xx` — for the server log line that names the
702
+ * status, never for the wire. A route that maps a throw into a response
703
+ * value has swallowed the reason by the time the transport logs the value;
704
+ * this carries it alongside.
705
+ */
706
+ readonly cause?: unknown;
596
707
  /** Stream a BINARY response (a download, an export) instead of sending
597
708
  * `body` — see {@link PluginHttpRouteByteStream}. Never buffered by the
598
709
  * serve layer; never compressed (flush timing + Content-Length are the
@@ -641,7 +752,7 @@ declare interface PluginHttpRouteStream {
641
752
  */
642
753
  declare interface PolicyGuardSpec<Input = unknown> {
643
754
  /** The action to authorize, as named in the resource policy's `actions`. */
644
- readonly action: string;
755
+ readonly action: string | ((input: Input) => string);
645
756
  /** Which registered resource policy governs the check. */
646
757
  readonly resourceType: string;
647
758
  /** PURE `input → resource id`. Returning undefined DENIES — an unidentifiable
@@ -649,15 +760,47 @@ declare interface PolicyGuardSpec<Input = unknown> {
649
760
  readonly resource: (input: Input) => string | undefined;
650
761
  }
651
762
 
763
+ export declare const PROBLEM_JSON = "application/problem+json; charset=utf-8";
764
+
765
+ /** RFC 9457 Problem Details for one failure: `type` from the tag, `title` from
766
+ * the tag's words, `status`, `detail` from the message, the error's own fields
767
+ * as extension members. */
768
+ export declare const problemDetails: (error: RestFormatError) => Record<string, unknown>;
769
+
770
+ /** A stable, non-resolving URI for an error type: the tag as a URN. */
771
+ export declare const problemTypeOf: (tag: string) => string;
772
+
652
773
  /**
653
774
  * Translate a handler's failure into the response the projection promised:
654
775
  * a DECLARED typed error becomes a `RestRouteError` with the tag, the fields
655
776
  * 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).
777
+ * OpenAPI projection already documents for a typed error); a plugin's
778
+ * cross-cutting error answers under the status the plugin names. Anything
779
+ * else is rethrown for the desugar's shared mapping (`ScopeError` → 403/401, a
780
+ * `status`-carrying error → itself, the rest → 500). The body is the tagged
781
+ * `{ error, …fields }` form; `projectedFailureFor` speaks the profile.
659
782
  */
660
- export declare const projectedFailure: (err: unknown, descriptor: PublicApiDescriptor, spec: PublicApiSpec) => unknown;
783
+ export declare const projectedFailure: (err: unknown, descriptor: PublicApiDescriptor, spec: PublicApiSpec, pluginErrors?: ReadonlyArray<PluginErrorSchema>) => unknown;
784
+
785
+ /** The generated TypeScript client, as the app declares it. */
786
+ export declare interface PublicApiClientConfig {
787
+ /** The npm package name. Default `@app/api-client`. */
788
+ readonly package?: string;
789
+ /** Where the package is written, relative to the app root. Default `sdk/typescript`. */
790
+ readonly out?: string;
791
+ /** `'plain'` — the Promise client, no Effect anywhere; `'effect'` — the
792
+ * Effect service; `'both'` — `.` plain and `./effect` Effect, with `effect`
793
+ * an optional peer. Default `'both'`. */
794
+ readonly flavor?: 'plain' | 'effect' | 'both';
795
+ /** The package's `license` field (an SPDX id). */
796
+ readonly license?: string;
797
+ /** The package's version. Default `0.1.0`. */
798
+ readonly packageVersion?: string;
799
+ /** Generate for ONE API version (`'v2'`). */
800
+ readonly apiVersion?: string;
801
+ /** The client's default base URL. */
802
+ readonly baseUrl?: string;
803
+ }
661
804
 
662
805
  /** A descriptor kind that can be projected to REST (streams are not, in v1). */
663
806
  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,23 +820,87 @@ export declare interface PublicApiEntry {
677
820
  * handler through the SAME AuthMiddleware/RBAC/tenant/audit as the rpc path. */
678
821
  export declare type PublicApiInvoke = (input: unknown, ctx: RestRouteContext) => Promise<unknown> | unknown;
679
822
 
823
+ /** The `publicApi:` block of an api `app.config.ts`, and the shape of a
824
+ * descriptor's `publicApi.format` override (every field optional there). */
825
+ export declare interface PublicApiProfileConfig {
826
+ /** The profile every projection speaks. Default `'rpc'` — today's wire. */
827
+ readonly profile?: PublicApiProfileName | RestFormat;
828
+ readonly dates?: RestDates;
829
+ readonly errors?: RestErrors;
830
+ readonly envelope?: RestEnvelope;
831
+ readonly pagination?: RestPagination;
832
+ readonly cursorField?: string;
833
+ readonly cursorParam?: string;
834
+ readonly naming?: RestNaming;
835
+ readonly etag?: boolean;
836
+ /** `'required'`: `voltro doctor` reports a projection without a
837
+ * `description` and an input/output field without a `description`
838
+ * annotation — so a new projection does not reach the surface without text.
839
+ * Default `'optional'`. */
840
+ readonly docs?: 'required' | 'optional';
841
+ /**
842
+ * Files `voltro dev` writes beside `rpcGroup.generated.ts` on every boot, from
843
+ * the mounted surface: `'openapi'` (`openapi.generated.json`) and the
844
+ * collections — `'postman'`, `'bruno'`, `'insomnia'`, `'hoppscotch'` — as
845
+ * committed, generated files, so a review diff shows the surface change and
846
+ * a client imported once cannot drift; `'typescript'` keeps the generated
847
+ * client package (`client`) written the same way. Off by default.
848
+ */
849
+ readonly artifacts?: ReadonlyArray<'openapi' | 'postman' | 'bruno' | 'insomnia' | 'hoppscotch' | 'typescript'>;
850
+ /**
851
+ * The app's decision about its generated TypeScript client — what
852
+ * `voltro build api --target typescript` (and `voltro api-client build`)
853
+ * produces without flags, and what `voltro dev` keeps written when
854
+ * `artifacts` names `'typescript'`. `voltro api-client set …` writes this
855
+ * block; a flag on the command line overrides a field for one run.
856
+ */
857
+ readonly client?: PublicApiClientConfig;
858
+ }
859
+
860
+ export declare type PublicApiProfileName = 'rpc' | 'standard' | 'jsonapi';
861
+
862
+ /** What a projection needs beyond its descriptor: the errors every mounted
863
+ * plugin may fail a call with, and the app's `publicApi` profile. */
864
+ export declare interface PublicApiProjectionOptions {
865
+ readonly pluginErrors?: ReadonlyArray<PluginErrorSchema>;
866
+ /** The `publicApi:` block of the api's `app.config.ts`. Absent → `'rpc'`. */
867
+ readonly profile?: PublicApiProfileConfig;
868
+ }
869
+
680
870
  /**
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.
871
+ * Synthesize the public REST route for ONE descriptor.
872
+ *
873
+ * The input arrives as `{ query, params, body }` from the desugar and is
874
+ * assembled into ONE object — the query (GET) or body (else), plus every
875
+ * path parameter bound to the input field of the same name — coerced toward
876
+ * the input's encoded shape (a query string and a path carry strings), then
877
+ * decoded by the input schema in the profile's wire form (`restViewSchema`).
878
+ * The handler receives the descriptor's typed input; its output is encoded to
879
+ * the wire form and enveloped by the profile. A query is drained to its first
880
+ * snapshot by the serve layer — same as POST /rpc — unless `stream: 'sse'`
881
+ * streams it. The guards are `requireAuthenticated()` unless the descriptor
882
+ * declares `openAccess`, then one `requireScope` per `scopes` entry, on top of
883
+ * the handler's own RBAC.
689
884
  */
690
- export declare const publicApiRoute: (descriptor: PublicApiDescriptor, invoke: PublicApiInvoke, subscribe?: PublicApiSubscribe) => RestRouteDescriptor<Record<string, unknown>, unknown>;
885
+ export declare const publicApiRoute: (descriptor: PublicApiDescriptor, invoke: PublicApiInvoke, subscribe?: PublicApiSubscribe, options?: PublicApiProjectionOptions) => RestRouteDescriptor<Record<string, unknown>, unknown>;
691
886
 
692
887
  declare interface PublicApiSpec {
693
888
  /** Derived by kind when omitted: query→GET, mutation/action→POST. */
694
889
  readonly method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
695
890
  /** Derived from the tag + version when omitted: `/<version>/<tag-as-path>`. */
696
891
  readonly path?: string;
892
+ /**
893
+ * Derive method and path from the resource instead of writing them — the
894
+ * convention `crud.*` already follows, opt-in per descriptor. `'teams'` on a
895
+ * query whose input carries `teamId` or `id` → `GET /v1/teams/{teamId}`,
896
+ * without one → `GET /v1/teams`; on a mutation whose `target.op` is
897
+ * `insert` → `POST /v1/teams`, `update` → `PATCH /v1/teams/{teamId}`,
898
+ * `delete` → `DELETE /v1/teams/{teamId}`; an action, or a mutation without
899
+ * a target, → `POST /v1/teams/{teamId}/<verb>` (the tag's last segment) when
900
+ * the input carries the id, else `POST /v1/teams/<verb>`. An explicit
901
+ * `method` or `path` wins over the derivation.
902
+ */
903
+ readonly resource?: string;
697
904
  /** Path version segment; multiple versions coexist as separate descriptors. */
698
905
  readonly version?: string;
699
906
  /** Additional API-key scopes required ON TOP of the handler's RBAC. */
@@ -725,6 +932,37 @@ declare interface PublicApiSpec {
725
932
  * publicApi: { errorStatus: { AccessDeniedError: 403, NotFoundError: 404 } }
726
933
  */
727
934
  readonly errorStatus?: Readonly<Record<string, number>>;
935
+ /**
936
+ * The spec's tag group(s) for this operation. Default: the rpc tag's prefix
937
+ * (`teams.list` → `teams`) — the first path segment, what a consumer looks
938
+ * for, and what an SDK generator turns into a client class.
939
+ */
940
+ readonly tags?: ReadonlyArray<string>;
941
+ /**
942
+ * The spec's `operationId`. Default: the rpc tag in camelCase (`teams.list`
943
+ * → `teamsList`) — the stable identifier a generator names methods from.
944
+ */
945
+ readonly operationId?: string;
946
+ /**
947
+ * Whether the operation documents a bearer requirement. Default: `'required'`
948
+ * unless the descriptor declares `openAccess` — then `'optional'` (the key
949
+ * strategy authenticates a bearer when one is sent, and an anonymous caller
950
+ * is allowed through), which Swagger UI and generated clients read as
951
+ * "send the token when you have one". `false` documents no scheme at all.
952
+ */
953
+ readonly security?: 'required' | 'optional' | false;
954
+ /** A request/response example for the spec — the same semantics as a
955
+ * `defineRestRoute` example. */
956
+ readonly example?: {
957
+ readonly request?: unknown;
958
+ readonly response?: unknown;
959
+ };
960
+ /**
961
+ * This projection's wire format, overriding the app's `publicApi` profile
962
+ * (`app.config.ts`) for the one endpoint that must differ — every field
963
+ * optional, the rest inherited. See `PublicApiProfileConfig`.
964
+ */
965
+ readonly format?: PublicApiProfileConfig;
728
966
  }
729
967
 
730
968
  /**
@@ -739,6 +977,16 @@ export declare type PublicApiSubscribe = (input: unknown, ctx: RestRouteContext,
739
977
  readonly [k: string]: unknown;
740
978
  }) => void) => () => void;
741
979
 
980
+ /**
981
+ * The digest of a public surface: every projection's facts (tag, method, path,
982
+ * parameters, version, security, the typed errors and their statuses) and the
983
+ * SHAPE of its wire schemas, in tag order. The OpenAPI document publishes it as
984
+ * `x-voltro-surface-digest`, a generated client carries the one it was built
985
+ * from, and `voltro check --public-api --against` compares the two — so one
986
+ * number says whether a consumer's client still describes the API it calls.
987
+ */
988
+ export declare const publicSurfaceDigest: (routes: ReadonlyArray<RestRouteDescriptor<any, any>>) => string;
989
+
742
990
  /**
743
991
  * Server-side snapshot caching for a query (see `defineQuery`). When set,
744
992
  * the dispatcher caches the initial snapshot result and auto-invalidates it
@@ -861,11 +1109,35 @@ export declare const requireAuthenticated: () => RestGuard;
861
1109
  */
862
1110
  export declare const requireScope: (scope: string) => RestGuard;
863
1111
 
1112
+ /**
1113
+ * The format one projection speaks: the app profile, then its building-block
1114
+ * overrides, then the descriptor's own `format`. A named profile is a
1115
+ * `RestFormat` with a name; an object is taken as-is and its fields overridden
1116
+ * the same way.
1117
+ */
1118
+ export declare const resolveRestFormat: (config: PublicApiProfileConfig | undefined, override?: Partial<PublicApiProfileConfig> | undefined) => RestFormat;
1119
+
1120
+ /** What `resource` derivation reads off a descriptor. */
1121
+ export declare interface ResourceDescriptorFacts {
1122
+ readonly kind: 'query' | 'mutation' | 'action' | 'stream';
1123
+ readonly input?: unknown;
1124
+ readonly target?: {
1125
+ readonly op?: string;
1126
+ } | undefined;
1127
+ }
1128
+
1129
+ /** The input field that identifies one row of `resource`: `<singular>Id`,
1130
+ * then `id`; `undefined` when the input names neither (a list, a create). */
1131
+ export declare const resourceIdField: (resource: string, input: unknown) => string | undefined;
1132
+
864
1133
  export declare const response: <O>(body: O, options?: {
865
1134
  readonly status?: number;
866
1135
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
1136
+ readonly contentType?: string;
867
1137
  }) => RestResponse<O>;
868
1138
 
1139
+ export declare const REST_PROFILE_NAMES: ReadonlyArray<PublicApiProfileName>;
1140
+
869
1141
  /** A BINARY streaming response (download/export). The serve layer pipes the
870
1142
  * ReadableStream without buffering — a body larger than the heap is fine.
871
1143
  * The LAZY thunk form defers opening the source until the response streams. */
@@ -881,8 +1153,85 @@ export declare interface RestByteResponse {
881
1153
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
882
1154
  }
883
1155
 
1156
+ export declare type RestDates = 'iso' | 'epoch-ms';
1157
+
1158
+ export declare type RestEnvelope = 'none' | 'data' | 'jsonapi';
1159
+
884
1160
  export declare const restError: (status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>) => RestRouteError;
885
1161
 
1162
+ export declare type RestErrors = 'problem-details' | 'tagged';
1163
+
1164
+ /**
1165
+ * The wire format of one `publicApi` surface. `'standard'` and `'jsonapi'` are
1166
+ * named instances of this; an app's own object is the custom case.
1167
+ */
1168
+ export declare interface RestFormat {
1169
+ readonly name: string;
1170
+ /** `timestampMs` fields: RFC 3339 strings, or the epoch milliseconds the rpc wire carries. */
1171
+ readonly dates: RestDates;
1172
+ /** Failures: RFC 9457 `application/problem+json`, or the tagged object rpc carries. */
1173
+ readonly errors: RestErrors;
1174
+ /** Success bodies: the bare value, `{ data }`, or the JSON:API document. */
1175
+ readonly envelope: RestEnvelope;
1176
+ /** A cursor the handler returns in `cursorField`: as a `Link: <…>; rel="next"`
1177
+ * header (and `next` beside `data`), or left where the handler put it. */
1178
+ readonly pagination: RestPagination;
1179
+ /** The cursor field a paginated output carries. Default `nextCursor`. */
1180
+ readonly cursorField: string;
1181
+ /** The query parameter a cursor travels back in. Default `cursor`. */
1182
+ readonly cursorParam: string;
1183
+ /** Property names on the wire. */
1184
+ readonly naming: RestNaming;
1185
+ /** Weak `ETag` on GET answers, `If-None-Match` → 304. */
1186
+ readonly etag: boolean;
1187
+ /** Custom success encoding — receives the value already in wire form (dates,
1188
+ * naming applied). Replaces the envelope/pagination steps. */
1189
+ readonly encodeSuccess?: (value: unknown, ctx: RestFormatContext) => RestFormatResponse;
1190
+ /** Custom error encoding. Replaces `errors`. */
1191
+ readonly encodeError?: (error: RestFormatError, ctx: RestFormatContext) => RestFormatResponse;
1192
+ /** A hook over the OpenAPI operation the plugin emits for a projection. */
1193
+ readonly describe?: (operation: Record<string, unknown>, ctx: {
1194
+ readonly tag: string;
1195
+ readonly method: string;
1196
+ readonly path: string;
1197
+ }) => Record<string, unknown>;
1198
+ }
1199
+
1200
+ /** What a format sees when it encodes one response. */
1201
+ export declare interface RestFormatContext {
1202
+ /** The rpc tag, e.g. `teams.list`. */
1203
+ readonly tag: string;
1204
+ readonly method: string;
1205
+ /** The request path, for `Link` headers. */
1206
+ readonly path: string;
1207
+ /** The request's query string (without `?`). */
1208
+ readonly query: string;
1209
+ /** The first `source` table of the procedure, when it declares one — the
1210
+ * JSON:API resource `type`. */
1211
+ readonly source: string | undefined;
1212
+ readonly status: number;
1213
+ }
1214
+
1215
+ /** A failure the desugar or the projection is about to answer with. */
1216
+ export declare interface RestFormatError {
1217
+ readonly status: number;
1218
+ /** The typed error's `_tag` (`AccessDeniedError`), or a stable name for a
1219
+ * transport failure (`InvalidRequest`, `Unauthorized`, `Forbidden`, …). */
1220
+ readonly tag: string;
1221
+ /** A human-readable line. */
1222
+ readonly message: string;
1223
+ /** The error's own fields (a typed error), without `_tag`. */
1224
+ readonly fields: Readonly<Record<string, unknown>>;
1225
+ }
1226
+
1227
+ /** A response a format produced: body (already wire-shaped), headers, and the
1228
+ * media type when it is not `application/json`. */
1229
+ export declare interface RestFormatResponse {
1230
+ readonly body: unknown;
1231
+ readonly headers?: Readonly<Record<string, string>>;
1232
+ readonly contentType?: string;
1233
+ }
1234
+
886
1235
  export declare type RestGuard = (ctx: RestRouteContext) => RestGuardRejection | undefined | Promise<RestGuardRejection | undefined>;
887
1236
 
888
1237
  /**
@@ -897,6 +1246,10 @@ export declare interface RestGuardRejection {
897
1246
 
898
1247
  export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
899
1248
 
1249
+ export declare type RestNaming = 'camelCase' | 'snake_case';
1250
+
1251
+ export declare type RestPagination = 'link-header' | 'body-cursor';
1252
+
900
1253
  /** A schema-encoded REST body with an application-selected HTTP status and
901
1254
  * response headers. The marker keeps ordinary domain objects unambiguous.
902
1255
  * `output` still validates/encodes `body`; use `Schema.Void` with a 204 when
@@ -906,6 +1259,8 @@ export declare interface RestResponse<O> {
906
1259
  readonly status: number;
907
1260
  readonly body: O;
908
1261
  readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
1262
+ /** A media type other than `application/json` (a JSON:API document). */
1263
+ readonly contentType?: string;
909
1264
  }
910
1265
 
911
1266
  /**
@@ -953,6 +1308,17 @@ export declare interface RestRouteDescriptor<I, O> {
953
1308
  readonly summary?: string;
954
1309
  readonly description?: string;
955
1310
  readonly example?: RestRouteExample;
1311
+ /** The spec's `operationId` — set by a `publicApi` projection from its tag. */
1312
+ readonly operationId?: string;
1313
+ /** The spec's tag group(s) — set by a projection from its tag prefix. */
1314
+ readonly tags?: ReadonlyArray<string>;
1315
+ /**
1316
+ * Present on a `publicApi` projection: what the route projects, in the wire
1317
+ * form its profile speaks. The OpenAPI generator documents parameters,
1318
+ * bodies, responses and security from it; the desugar answers every refusal
1319
+ * through its format.
1320
+ */
1321
+ readonly projection?: RestRouteProjection;
956
1322
  /** Replacement hint. Sets a `Deprecation: true` response header. */
957
1323
  readonly deprecated?: string;
958
1324
  /** ISO date. Sets a `Sunset:` header; past the date the route returns
@@ -1004,7 +1370,9 @@ export declare class RestRouteError extends Error {
1004
1370
  readonly status: number;
1005
1371
  readonly body: unknown;
1006
1372
  readonly headers: Readonly<Record<string, string | ReadonlyArray<string>>> | undefined;
1007
- constructor(status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>);
1373
+ /** A media type other than `application/json` (`application/problem+json`). */
1374
+ readonly contentType: string | undefined;
1375
+ constructor(status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>, contentType?: string);
1008
1376
  }
1009
1377
 
1010
1378
  export declare interface RestRouteExample {
@@ -1012,6 +1380,39 @@ export declare interface RestRouteExample {
1012
1380
  readonly response?: unknown;
1013
1381
  }
1014
1382
 
1383
+ /**
1384
+ * What a `publicApi` projection is, for the two readers that need more than
1385
+ * the route: the OpenAPI generator and the desugar's refusals. `input` and
1386
+ * `output` are the WIRE schemas — the descriptor's, rewritten by the profile
1387
+ * (`restViewSchema`): a `timestampMs` is a `date-time` string under
1388
+ * `dates: 'iso'`, keys are snake_case under that naming. `input` carries the
1389
+ * path parameters as fields; `pathParams` says which.
1390
+ */
1391
+ export declare interface RestRouteProjection {
1392
+ readonly tag: string;
1393
+ readonly kind: 'query' | 'mutation' | 'action';
1394
+ readonly version: string;
1395
+ readonly security: 'required' | 'optional' | 'none';
1396
+ /** `publicApi.idempotent` — the route honours `Idempotency-Key` (under the
1397
+ * app's idempotency binding); a client sends one per attempt. */
1398
+ readonly idempotent: boolean;
1399
+ readonly input: Schema.Schema.All;
1400
+ /** The wire names of the input fields bound from the path. */
1401
+ readonly pathParams: ReadonlyArray<string>;
1402
+ readonly output: Schema.Schema.All;
1403
+ /** The first `source` table, when the procedure declares one. */
1404
+ readonly source: string | undefined;
1405
+ /** Every failure the operation answers with a typed body, and its status:
1406
+ * the descriptor's `errorStatus` entries, `422` for the union's other tags,
1407
+ * and each plugin error that names a REST status. */
1408
+ readonly errorResponses: ReadonlyArray<{
1409
+ readonly status: number;
1410
+ readonly tag: string;
1411
+ readonly schema: Schema.Schema.All;
1412
+ }>;
1413
+ readonly format: RestFormat;
1414
+ }
1415
+
1015
1416
  /**
1016
1417
  * Desugar a list of REST descriptors into `PluginHttpRoute[]` ready to
1017
1418
  * spread into a plugin/app's `httpRoutes`. `bindings` is supplied by the
@@ -1059,6 +1460,33 @@ export declare interface RestStreamResponse {
1059
1460
  readonly stream: PluginHttpRouteStream;
1060
1461
  }
1061
1462
 
1463
+ /**
1464
+ * Rewrite a descriptor's schema into the shape the profile puts on the wire:
1465
+ * `timestampMs` (`DateFromNumber`) becomes an RFC 3339 string under
1466
+ * `dates: 'iso'`, and every struct's property names become `snake_case` under
1467
+ * `naming: 'snake_case'` — as a transformation, so decoding a wire value yields
1468
+ * the descriptor's own typed value and encoding the typed value yields the
1469
+ * wire form. `JSONSchema.make` of the result documents the wire (a
1470
+ * `format: date-time` string, the snake keys). Classes and other opaque
1471
+ * transformations pass through unchanged; a struct inside one keeps its keys.
1472
+ */
1473
+ export declare const restViewSchema: (schema: Schema.Schema.All, format: RestFormat) => Schema.Schema.All;
1474
+
1475
+ /**
1476
+ * Whether `path` is one the route declared. The underlying router folds case
1477
+ * and trailing slashes when selecting candidates; this dispatcher deliberately
1478
+ * rejects those undeclared aliases. It checks byte for byte: a literal
1479
+ * segment must be equal, a `:name` segment takes one non-empty segment, and
1480
+ * beyond the declared segments a `'prefix'` route (the default) owns any
1481
+ * sub-path while an `'exact'` route owns nothing. A trailing slash alone is
1482
+ * not a sub-path — `/metrics/` is nobody's, whatever the mode. An explicit
1483
+ * `/*` tail is a prefix by itself.
1484
+ */
1485
+ export declare const routeAcceptsPath: (route: {
1486
+ readonly path: string;
1487
+ readonly match?: "prefix" | "exact" | undefined;
1488
+ }, path: string) => boolean;
1489
+
1062
1490
  /**
1063
1491
  * Build a streaming (Server-Sent Events) handler result. `subscribe` gets an
1064
1492
  * `emit` for one already-framed SSE chunk and MUST return an unsubscribe, which
@@ -1093,6 +1521,11 @@ declare const Subject: Schema.Union<[Schema.Struct<{
1093
1521
  type: Schema.Literal<["serviceAccount"]>;
1094
1522
  id: typeof Schema.String;
1095
1523
  tenantId: typeof Schema.String;
1524
+ /** Application actor used for audit columns; null means machine work with
1525
+ * no application actor. Identity only, never a permission grant. */
1526
+ auditActorId: Schema.optionalWith<Schema.NullOr<typeof Schema.String>, {
1527
+ exact: true;
1528
+ }>;
1096
1529
  scopes: Schema.optional<Schema.Array$<typeof Schema.String>>;
1097
1530
  metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
1098
1531
  }>, Schema.Struct<{
@@ -1177,6 +1610,8 @@ declare type TargetRelations = Readonly<Record<string, TargetRelation>>;
1177
1610
 
1178
1611
  declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
1179
1612
 
1613
+ export declare const toSnakeCase: (key: string) => string;
1614
+
1180
1615
  declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
1181
1616
  readonly table: string;
1182
1617
  readonly op: 'update';
@@ -1194,4 +1629,12 @@ declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<str
1194
1629
  readonly shapeItem?: ((input: Input, current: Item) => Item) | undefined;
1195
1630
  }
1196
1631
 
1632
+ export declare const wireExample: (example: {
1633
+ readonly request?: unknown;
1634
+ readonly response?: unknown;
1635
+ }, wireInput: Schema.Schema.All, wireOutput: Schema.Schema.All) => {
1636
+ readonly request?: unknown;
1637
+ readonly response?: unknown;
1638
+ };
1639
+
1197
1640
  export { }