@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/CHANGELOG.md +80 -0
- package/dist/index.d.ts +320 -11
- package/dist/index.js +204 -196
- package/dist/rest.d.ts +355 -17
- package/dist/rest.js +302 -157
- package/dist/restFormat-Bnngx-zI.js +232 -0
- package/dist/session.d.ts +8 -8
- package/package.json +3 -3
- package/dist/serverErrorBus-BTnTpMqj.js +0 -60
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,86 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.70.0] — 2026-09-11
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/cli** — The DevTools dashboard has a lock of its own: `VOLTRO_DASHBOARD_TOKEN`. A deployed dashboard that holds other apps' inspect tokens in `VOLTRO_DASHBOARD_APPS` refuses to boot in production without it, `/api/dashboard/config` no longer serves token values, and the proxy attaches a configured target's credentials itself.
|
|
47
|
+
|
|
48
|
+
Before this the dashboard had no door: whoever reached its URL got every configured app's inspect read AND write token from `/api/dashboard/config` in one request, and the proxy forwarded whatever bearer that browser then presented. The URL was the credential.
|
|
49
|
+
|
|
50
|
+
Three rules now, on both boot paths: the browser proves the dashboard token once (`POST /api/dashboard/login`) and gets an HttpOnly, SameSite=Strict session cookie derived from it by HMAC (12 h; a rotated lock invalidates every cookie); every other `/api/dashboard/*` route answers 401 without a session; the config route says where the targets are and whether the pod holds credentials (`hasToken`, `hasWriteToken`), never which. A dashboard with nothing to guard (no configured targets, a loopback dev registry) boots open as before — setting the token locks it anyway. `voltro secret generate dashboard` prints the variable paste-ready. `codemod: none`: no user code changes; the operator sets one variable, and the chart's `dashboardToken` renders it.
|
|
51
|
+
|
|
52
|
+
**No codemod** — this break touches no user-authored code.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **@voltro/protocol, @voltro/plugin-openapi, @voltro/cli, @voltro/testing** — The `publicApi` surface speaks a REST profile, chosen once in the api's `app.config.ts` (`publicApi: { profile }`) and overridable per descriptor (`publicApi.format`): `'rpc'` — the default, today's wire — `'standard'`, `'jsonapi'`, or a `RestFormat` object of the app's own, with building blocks (`dates`, `errors`, `envelope`, `pagination`, `naming`, `etag`) that override the preset. `'standard'` is the Zalando / Google AIP / Microsoft intersection: `timestampMs` as RFC 3339 (accepted inbound too), every failure as RFC 9457 Problem Details — the transport's refusals included — no envelope, a `nextCursor` as a `Link: <…>; rel="next"` header, a weak `ETag` on GET with `If-None-Match` → 304. `'jsonapi'` answers `{ data, links }` as `application/vnd.api+json`. The spec follows the profile (`format: date-time`, the `ProblemDetails` component, `Link`/`ETag` headers, snake-case names). `makeTestApp({ publicApiProfile })` measures the same wire.
|
|
57
|
+
|
|
58
|
+
A path with `{teamId}` or `:teamId` binds that segment to the input field of the same name — always, under every profile — with the remaining fields from the query string (GET) or the body; the brace form used to be no route, the colon form a `400`, so `GET /v1/teams/{teamId}` was unreachable for a projection.
|
|
59
|
+
- **@voltro/testing, @voltro/database** — `makeTestContext({ fieldEncryptionKey })` registers the field cipher for `.encrypted()` columns under test. Setting `VOLTRO_FIELD_ENCRYPTION_KEY` — in `process.env` or the context's `env` — did nothing here, because the harness runs no boot gate and the gate is what turns the variable into a cipher; the only working route was `setFieldCipher(makeFieldCipher(key))` from another package, which nothing pointed at. The refusal now names the option.
|
|
60
|
+
- **@voltro/database, @voltro/runtime** — `insertIgnoreWithOutcome` on the framework store (and on `yield* EffectStore` plus the `tx` view), with the typed helper `insertIgnoreRowOutcome`: `{ row, outcome: 'inserted' | 'ignored' }`.
|
|
61
|
+
|
|
62
|
+
`insertIgnore` returns the existing row on the ignore path, and that row is indistinguishable from one it just wrote — so a loop that plants missing rows reported every row it visited as planted unless it read the destination first, per row. The answer is decided the way `upsertWithOutcome` decides it: the id the store minted before the statement is not the id the ignore path hands back; where the id cannot decide (caller-supplied, or database-assigned) the destination is read first and the answer stays exact. Plain `insertIgnore` never pays for that read.
|
|
63
|
+
- **@voltro/protocol, @voltro/client, @voltro/web** — `defineMutation({ serialize: { key, maxPending? } })` — calls of one mutation to the SAME resource run in invocation order on the client, for every caller of the tag. A row lock serialises transactions, not the order a browser invoked them in: two quick saves of one snapshot could take the lock in reverse order and the older snapshot then overwrote the newer one; server-side idempotency does not see it, and `.version()` detects the conflict and replays the older write. `key` names the resource from the input; calls sharing it run one at a time, FIFO, per subject, the permit spanning connection setup, the request, replay and its acknowledgement — optimistic previews stay immediate, different keys run independently. An unknown outcome (a transport failure after the request started) blocks the key for that subject rather than let a later write pass it; a subject change drops the queue; admission beyond `maxPending` (default 32) rejects with `MutationSerialExecutionError`. `useMutation`'s `pending` now counts every concurrent call, not only the one that settled last.
|
|
64
|
+
|
|
65
|
+
Declared on the descriptor — where `concurrency` lives for workflows — rather than as a hook builder, because a hook-scoped queue orders one component's calls and lets a second component writing the same resource interleave. Not a durable outbox, a distributed lock or a cross-tab merge.
|
|
66
|
+
- **@voltro/cli** — API `fonts:` declarations now package validated native-readable fonts and relocatable Fontconfig artifacts during build. Dev, serve and direct production entries activate them before startups and handlers; missing or corrupt artifacts refuse boot. Native renderers no longer depend on undeclared host fonts or original workspace font paths.
|
|
67
|
+
- **@voltro/plugin-openapi** — Every mounted API version has an OpenAPI document of its own — `/openapi/v2.json` beside `/openapi.json`, or `?version=v2` — the same spec filtered to that version, every operation fact included; the unversioned URL keeps serving everything. With more than one version mounted the docs page shows Swagger UI's "Select a definition" dropdown: one entry per version, newest first and preselected, an expiring version named with its earliest `sunset` (`v1 — sunset 2027-03-01`), the whole document last.
|
|
68
|
+
|
|
69
|
+
Before this `v1` and `v2` shared one document and one viewer: a consumer integrating `v2` scrolled past every `v1` path, and a generator built both into one client.
|
|
70
|
+
- **@voltro/protocol, @voltro/cli, @voltro/plugin-openapi** — `onProcedureSurface` — a plugin hook told the app's wire-reachable rpc procedures (every query, mutation, action and stream discovery found, minus `internal`) once discovery has run, on both boot paths; the procedure counterpart of `onRestSurface`. `openapiPlugin({ includeAppProcedures: true })` documents them as `POST /rpc/<name>` operations without the app listing them: opt-in, so the spec stays REST-only unless asked, and a procedure also passed in `procedures` is documented once.
|
|
71
|
+
|
|
72
|
+
Before this `procedures` was the only way in, and filling it meant importing every descriptor module into `app.config.ts` — evaluated before discovery on every load of the config, the pattern the `onRestSurface` docs name as a boot that hung — while `voltro serve` loads no TypeScript at runtime, so a plugin could not read them later either.
|
|
73
|
+
- **@voltro/protocol, @voltro/plugin-openapi** — A `publicApi` projection documents itself completely: `tags` from the rpc tag's prefix (`teams.list` → `teams`; `publicApi.tags` overrides) with a top-level `tags[]` and `tagDescriptions` on the OpenAPI plugin; an `operationId` from the tag (`teamsList`; `publicApi.operationId` overrides); `security` on every operation — required, or optional under `openAccess` (`[{ bearerAuth: [] }, {}]`), `publicApi.security: false` for none — with `securitySchemes.bearerAuth` published once any projection exists; one response per `errorStatus` entry carrying the error's own schema, `422` for the union's other tags, `429` for a mounted rate limiter; `publicApi.example: { request?, response? }`; path parameters `in: path` and query parameters typed from the input.
|
|
74
|
+
|
|
75
|
+
Before this the plugin wrote `tags` only for an explicit `version`, `security` only for a `guards:` route — so after "Authorize" Swagger UI sent the token to those alone and "Try it out" on every other route ran without a key — no `operationId`, no `errorStatus` response, no example, and no query parameter at all for a GET projection.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- **@voltro/protocol** — A `publicApi.path` segment that names no input field refuses the boot, naming the descriptor and the segment (`descriptor 'bookings.get' — path '/v1/bookings/:uuid' binds ':uuid', and the input declares no field of that name (fields: bookingId)`). A field the path binds may repeat in the body or query string with the same value — a PATCH carrying the whole resource — but another value answers `400` naming the field.
|
|
80
|
+
|
|
81
|
+
Before this the segment was bound silently: the route mounted, the handler never saw the value, and a body that carried the same field with another value was overwritten by the path without a word.
|
|
82
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/plugin-openapi, @voltro/plugin-prometheus** — A plugin route answers the path it declared, byte for byte: the dispatcher refuses a request whose path is not the route's own — case-folded twins and a bare trailing slash included — with `404`, before the body is read. `PluginHttpRoute.match: 'exact'` narrows a route to its path alone, no sub-path; the default stays `'prefix'`, the contract every route that branches on its tail was written against. A REST descriptor is an exact route; the OpenAPI spec and docs routes and the Prometheus scrape route declare `'exact'`.
|
|
83
|
+
|
|
84
|
+
Before this the router matched without case and ignored a trailing slash, and every route owned its sub-paths: `/OPENAPI.JSON`, `/openapi.json/`, `/Docs` and `/Metrics` all answered, so a path guard at the ingress that named the one documented spelling covered none of the others. The docs of both plugins now say the guard can name the path as written.
|
|
85
|
+
- **@voltro/workflow, @voltro/cli** — A run that ended outside the engine no longer leaves its unanswered requests in the cluster journal. Cancelling a run (`timeouts.finish`, `cancelOn`, the operator's button, the bulk action) discards its unprocessed messages at once, and the staleness tick discards those of every ended run (`succeeded`, `failed`, `cancelled`) that a runner last read more than a minute ago — logged as `workflow.cluster: discarded unprocessed messages of runs that have ended`.
|
|
86
|
+
|
|
87
|
+
Before this, the `activity` request a killed executor was inside stayed unprocessed after the run was cancelled: every runner that acquired the shard re-read it every ten minutes for the rest of time, its handler waited on a latch that never opens, and the `unprocessed messages` warning was printed again on every engine start. An unprocessed `run` request of a cancelled run would have gone further and executed it. The engine's own reset deletes one request; this deletes the ended run's, and `ClusterDiagnostics.discardMessages` is the one write on that service.
|
|
88
|
+
- **@voltro/cli** — `voltro db encrypt-column <table>.<column> .` accepts the app directory every other `db` command takes as its last argument — accepted and not needed, the connection comes from env. It used to parse `.` as a column reference and refuse. A positional that is neither a `<table>.<column>` nor a directory is named in the refusal.
|
|
89
|
+
- **@voltro/runtime, @voltro/cli, @voltro/database** — Every API key has its `actors` row after the first boot on this version. Both boot paths plant, after the schema is applied, one row per `_voltro_api_keys.id` that has none — through the same `ensureActor` a new key goes through (the built-in row, or the app's `apiKeys.ensureActor` handed the issue input rebuilt from the row). Idempotent; two queries when nothing is missing; `api keys: planted actors rows for keys that had none` with the count, and a key whose row cannot be written is named in a warning rather than failing the boot.
|
|
90
|
+
|
|
91
|
+
Two release notes had asked the operator to run a loop once. The first was printed to the wrong group; the second printed `store.select(...)` for a `*.startup.ts`, and `StartupContext.store` is a `DataStore` with no fluent builder — both API pods refused to boot on it. The 0.69.1 note now says what the boot does and which line to look for. The framework tables `@voltro/database` exports (`_voltroApiKeysTable` and the others) are typed `SchemaTable` instead of `TableLike`, so `queryFor(_voltroApiKeysTable)` compiles where a hand-written read is still wanted.
|
|
92
|
+
- **@voltro/runtime** — Idempotent WebSocket mutations acquire and finalize their claims safely across request interruption. A closed socket cannot orphan a claim acquired by a still pending store Promise; synchronous executor failures release fresh claims too. Already-started Promise executors finish their actual write and record the result before interruption completes, preventing a replay from duplicating the write. Effect executors remain interruptible and release only after cleanup. A late interrupt never deletes the completion receipt of a successful write.
|
|
93
|
+
|
|
94
|
+
Codemod: none — no application source or configuration changes. Claim TTL, replay limits, transport guards and process-crash recovery guarantees are unchanged.
|
|
95
|
+
- **@voltro/cli** — Docker deployments retain optional native platform bindings until runtime tracing, and the pruner traces both conditional import and require entries so relocated API and web images can still execute native image processing.
|
|
96
|
+
- **@voltro/workflow, @voltro/cli** — `own` on a shard-lock holder in the stall report and the `workflow.cluster` warning is now the engine's own account (`Sharding.hasShardId`), and a holder that is this runner reads `this runner (pid N)`.
|
|
97
|
+
|
|
98
|
+
The first version compared the holder's pid with `pg_backend_pid()` of the query that asked. The advisory lock lives on the runner's reserved connection, and the pool hands every other query a different backend — so a runner's own 300 locks read as a stranger's, always, and behind a connection pooler nothing in `pg_stat_activity` could have told them apart either. The report's stated lever for a stranger, ending the holder's session, would have ended the runner. The hint now distinguishes the three cases (another runner, a session that outlived its runner, this runner re-reading a request nobody answers), and the docs say to read `own` before `pg_terminate_backend`.
|
|
99
|
+
- **@voltro/cli** — Production HTTP proxies release upstream resources when the browser abandons an unfinished response, including SSE, and propagate truncated upstream responses as transport failures instead of hanging. Complete responses retain payloads and keep-alive; no application configuration change is needed.
|
|
100
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/testing, @voltro/plugin-ratelimit** — `publicApi.rateLimit` on a descriptor is that endpoint's own rule for calls through the `/v1` projection — ahead of the plugin's `rules` and `default`, one bucket per caller per endpoint, under `enabled`. A REST call now carries `surface: 'rest'` and the descriptor's `publicApi` spec on the interceptor context, so a plugin can tell a projected call from an rpc one.
|
|
101
|
+
|
|
102
|
+
The field said "rides @voltro/plugin-ratelimit when mounted" and nothing read it: three calls at `limit: 1` answered `200 200 200`, no bucket, no `onLimited`. The plugin never learned which calls came through the projection, let alone what it declared. Without the plugin mounted the spec is inert, as the comment always implied.
|
|
103
|
+
- **@voltro/cli** — The retention sweep arms its policies on the `_voltro_workflow_*` tables only where the workflow engine that creates those tables is composed. An api with no `*.workflow.tsx` has no engine and no such tables, and the five policies still armed on them were five failing `DELETE`s per tick, each an `ERROR` line in the database log, for the life of the deployment — not switchable off from the app. The boot line `retention: N policy(ies) armed` now lists exactly what is swept.
|
|
104
|
+
- **@voltro/plugin-mail** — SMTP replies are classified by SMTP rules instead of HTTP rules: 4xx replies remain retryable, permanent 5xx rejections do not trigger automatic retries (RFC 5321 §4.2.1). HTTP providers keep their existing policy. Tests exercise actual loopback connections, envelope, MIME and attachment encoding without sending external mail.
|
|
105
|
+
|
|
106
|
+
Codemod: none — corrected SMTP reply semantics require no application source or configuration migration.
|
|
107
|
+
- **@voltro/workflow** — The workflow engine's boot check for its cluster tables asks whether a table exists with a query that succeeds either way (`to_regclass` on postgres, `information_schema` on mysql, `OBJECT_ID` on mssql, `sqlite_master` on sqlite) instead of selecting from the table and reading the failure as "no" — which the database server logged as an `ERROR` on every first boot of a database. The probes `@effect/cluster` itself makes for its legacy tables are upstream and still fail-as-answer.
|
|
108
|
+
- **@voltro/cli** — `voltro dev` acquires the workflow runtime as a boot step, as `voltro serve` always has — `workflow runtime ready — N entity type(s) registered` on both paths, through one shared helper with a parity test.
|
|
109
|
+
|
|
110
|
+
A `ManagedRuntime` builds its layer on first use, and `voltro dev` never used it at boot. A fleet of dev pods therefore had NO cluster runner after a restart — no runner row, no shard locks, no re-delivery — until something asked the runtime for a service. The first thing that asked, in a deployment, was the once-per-fleet staleness tick, five minutes after the boot, in the one pod that ran it: a run orphaned by the restart waited exactly that long and was then adopted by that pod alone.
|
|
111
|
+
- **@voltro/cli** — `voltro baseline sync` writes its `docker/Caddyfile` with tabs, the way `caddy fmt` writes it. Caddy warned on every proxy start that the file "is not formatted", and a project that checks its generated files for drift could not fix it on its side — the next sync undid it.
|
|
112
|
+
- **@voltro/plugin-ratelimit** — `redisStore({ url })` binds its client on the first `consume` when nobody activated it — from `url` or the env it reads at activation — so a limit holds in the test harness, where `makeTestContext` runs no lifecycle hooks. A store with neither stays fail-open (`failOpenReason: 'unbound'`), as at activation.
|
|
113
|
+
|
|
114
|
+
Before this a suite that "checked the redis store" ran every call through an unbound store: `200 200 200` at `limit: 2`, no key in Redis, no warning — and nothing a test could read told "the limit held" from "the store was never there".
|
|
115
|
+
- **@voltro/cli, @voltro/i18n** — A `src/pages/[locale]/…` route renders in the locale it matched, on both web boot paths: `voltro start` and `voltro dev` hand the matched `[locale]` parameter to the same i18n resolver (also for the ISR key and for an app without catalogs), and `resolveLocale` takes it as `routeLocale` — ahead of the `voltro:locale` cookie, `Accept-Language` and the default, and only on an exact supported spelling. The build's static prerender already did this; the runtime render fell back to cookie or browser language, so `/en` answered with a German head and `html lang="de"`. In the same change `voltro dev`'s buffered SPA and ISR responses merge a page's `meta.title` into the shell the way production does — there was a second `<title>` before.
|
|
116
|
+
- **@voltro/protocol, @voltro/runtime, @voltro/cli, @voltro/testing, @voltro/plugin-ratelimit** — A rate-limited call on the `/v1` projection answers `429` with `Retry-After` and the `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` headers, the tag and fields as the body — on both boot paths and in `makeTestApp`. A `PluginErrorSchema` may declare `rest: { status, headers }`; the projection encodes a plugin's cross-cutting error under that status (`422` when it names none; the descriptor's own `errorStatus` entry wins), and the plugin declares `429` for `RateLimited`.
|
|
117
|
+
|
|
118
|
+
The plugin merged `RateLimited` into every rpc's wire union at boot and the client decoded it typed, but the REST projection encoded through the descriptor's own `error:` union only — the one surface with no typed channel to fall back on answered `500 Internal Server Error` with no `Retry-After`. The only way to a `429` was to import a plugin error into every descriptor file. A `500`'s cause now rides on the response value off the wire, so the `GET /v1/… → 500` log line names the reason on `voltro serve` too.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
42
122
|
## [0.69.1] — 2026-09-11
|
|
43
123
|
|
|
44
124
|
### Added
|
package/dist/index.d.ts
CHANGED
|
@@ -761,6 +761,8 @@ export declare interface ClientDescriptor {
|
|
|
761
761
|
readonly kind: 'query' | 'mutation' | 'action' | 'stream' | 'workflow';
|
|
762
762
|
/** Mutation only: connection setup hooks require an explicit connection scope. */
|
|
763
763
|
readonly idempotencyScope?: MutationIdempotencyScope | undefined;
|
|
764
|
+
/** Mutation only: same-key calls run in invocation order on the client. */
|
|
765
|
+
readonly serialize?: MutationSerialization | undefined;
|
|
764
766
|
/** Query only: table(s) the cache should match this subscription against. */
|
|
765
767
|
readonly source?: string | ReadonlyArray<string> | undefined;
|
|
766
768
|
/** Mutation only: tables this mutation writes, with op. */
|
|
@@ -1549,6 +1551,26 @@ export declare const defineMutation: <const Name extends string, Input extends S
|
|
|
1549
1551
|
* never ordinary business writes. Requires a live WebSocket even when dedup
|
|
1550
1552
|
* is disabled; internal/public REST/agent-tool projections are refused. */
|
|
1551
1553
|
readonly idempotencyScope?: MutationIdempotencyScope;
|
|
1554
|
+
/**
|
|
1555
|
+
* Run this mutation's calls to the SAME resource in invocation order, on the
|
|
1556
|
+
* client — for every caller of the tag, not one hook.
|
|
1557
|
+
*
|
|
1558
|
+
* A row lock serialises transactions, not the order a browser invoked them
|
|
1559
|
+
* in: two quick saves of one snapshot can take the lock in reverse order, and
|
|
1560
|
+
* the older snapshot then overwrites the newer one; server-side idempotency
|
|
1561
|
+
* does not see it, and `.version()` detects the conflict and replays the
|
|
1562
|
+
* OLDER write. `key` names the resource from the input; calls sharing a key
|
|
1563
|
+
* run one at a time, FIFO, per subject, the permit spanning connection
|
|
1564
|
+
* setup, the request, replay and its acknowledgement — optimistic previews
|
|
1565
|
+
* stay immediate. Different keys run independently. An unknown outcome (a
|
|
1566
|
+
* transport failure after the request started) blocks the key for that
|
|
1567
|
+
* subject rather than let a later write pass it; a subject change drops the
|
|
1568
|
+
* queue. `maxPending` bounds active plus waiting calls per subject (default
|
|
1569
|
+
* 32); admission beyond it rejects with `MutationSerialExecutionError`.
|
|
1570
|
+
* Declared here, on the mutation, so every `useMutation` of it orders the
|
|
1571
|
+
* same way. Not a durable outbox, a distributed lock or a cross-tab merge.
|
|
1572
|
+
*/
|
|
1573
|
+
readonly serialize?: MutationSerialization<Schema.Schema.Type<Input>>;
|
|
1552
1574
|
/** Optional declarative target(s) — drives auto-optimistic. */
|
|
1553
1575
|
readonly target?: Target<Schema.Schema.Type<Input>, Schema.Schema.Type<Output>>;
|
|
1554
1576
|
/** Declarative authorization guard(s) — the caller must hold the named
|
|
@@ -1858,6 +1880,17 @@ export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string
|
|
|
1858
1880
|
*/
|
|
1859
1881
|
export declare const encodeEventKey: (key: unknown) => string;
|
|
1860
1882
|
|
|
1883
|
+
/** Encode a failure under `format`: the tagged body the desugar always wrote
|
|
1884
|
+
* (`{ error, …fields }`), Problem Details, or the format's own encoder. */
|
|
1885
|
+
export declare const encodeRestError: (format: RestFormat, error: RestFormatError, ctx: RestFormatContext) => RestFormatResponse;
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Encode a success value under `format`: the bare value, `{ data, next? }`, or
|
|
1889
|
+
* a JSON:API document; a cursor becomes a `Link` header under `link-header`.
|
|
1890
|
+
* `value` is already in wire form (dates, naming applied by `restViewSchema`).
|
|
1891
|
+
*/
|
|
1892
|
+
export declare const encodeRestSuccess: (format: RestFormat, value: unknown, ctx: RestFormatContext) => RestFormatResponse;
|
|
1893
|
+
|
|
1861
1894
|
/**
|
|
1862
1895
|
* Read the `_tag` off a thrown error value, or `undefined` when it has none.
|
|
1863
1896
|
*
|
|
@@ -2607,6 +2640,8 @@ export declare const isWireReachable: (descriptor: {
|
|
|
2607
2640
|
readonly internal?: boolean | undefined;
|
|
2608
2641
|
}) => boolean;
|
|
2609
2642
|
|
|
2643
|
+
export declare const JSONAPI_MEDIA_TYPE = "application/vnd.api+json; charset=utf-8";
|
|
2644
|
+
|
|
2610
2645
|
/**
|
|
2611
2646
|
* Build a scope cache. Hand it to `composeAuthStrategies({ scopeCache })` and
|
|
2612
2647
|
* keep the handle: `invalidate(scopeCacheKey(subject))` from the mutation that
|
|
@@ -2686,6 +2721,8 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
|
|
|
2686
2721
|
* re-executes on a new server-owned WebSocket identity and rejects HTTP RPC.
|
|
2687
2722
|
* This does not grant access or restore application membership by itself. */
|
|
2688
2723
|
readonly idempotencyScope?: MutationIdempotencyScope | undefined;
|
|
2724
|
+
/** Same-key calls run in invocation order on the client — see `defineMutation`. */
|
|
2725
|
+
readonly serialize?: MutationSerialization<Schema.Schema.Type<Input>> | undefined;
|
|
2689
2726
|
/** Declarative target(s) — drives auto-optimistic on the client AND
|
|
2690
2727
|
* surfaces which tables this mutation touches (debug, future
|
|
2691
2728
|
* query-invalidation analytics). Mutations without a target run
|
|
@@ -2734,6 +2771,14 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
|
|
|
2734
2771
|
readonly internal: boolean | undefined;
|
|
2735
2772
|
}
|
|
2736
2773
|
|
|
2774
|
+
/** Same-resource ordering for a mutation's calls on the client — see `defineMutation`'s `serialize`. */
|
|
2775
|
+
export declare interface MutationSerialization<Input = unknown> {
|
|
2776
|
+
/** The resource a call writes, from its input; calls sharing it run in order. */
|
|
2777
|
+
readonly key: (input: Input) => string;
|
|
2778
|
+
/** Bound for active plus waiting calls per subject. Default 32. */
|
|
2779
|
+
readonly maxPending?: number;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2737
2782
|
export declare const mutationToRpc: <Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Err extends Schema.Schema.All>(descriptor: MutationProcedureDescriptor<Name, Input, Output, Err>, extraErrors?: ExtraErrors) => Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Output, Schema.Schema.All, never>;
|
|
2738
2783
|
|
|
2739
2784
|
export declare interface NestedTargetFields<Input = unknown> {
|
|
@@ -2751,6 +2796,10 @@ export declare interface NestedTargetFields<Input = unknown> {
|
|
|
2751
2796
|
readonly match?: ((value: unknown, input: Input) => boolean) | undefined;
|
|
2752
2797
|
}
|
|
2753
2798
|
|
|
2799
|
+
/** `Link: <path?cursor=…>; rel="next"` — the current query with the cursor
|
|
2800
|
+
* parameter replaced. */
|
|
2801
|
+
export declare const nextLink: (ctx: RestFormatContext, cursor: string, param: string) => string;
|
|
2802
|
+
|
|
2754
2803
|
/**
|
|
2755
2804
|
* Project a runtime descriptor down to the subset the client needs.
|
|
2756
2805
|
* Functions (`shape`, `identify`) are passed through by reference —
|
|
@@ -3310,6 +3359,20 @@ export declare interface PluginErrorSchema {
|
|
|
3310
3359
|
readonly module: string;
|
|
3311
3360
|
readonly name: string;
|
|
3312
3361
|
};
|
|
3362
|
+
/**
|
|
3363
|
+
* How the `publicApi` REST projection answers this error. The projection
|
|
3364
|
+
* encodes a plugin's cross-cutting error the way it encodes a descriptor's
|
|
3365
|
+
* own declared error — tag and fields as the body — under this status
|
|
3366
|
+
* (`422` when absent, the status an undeclared-status descriptor error
|
|
3367
|
+
* gets), with these headers. A rate limiter names `429` and the
|
|
3368
|
+
* `Retry-After` / `RateLimit-*` headers here; a descriptor's own
|
|
3369
|
+
* `errorStatus` entry for the tag still wins. Without this the same
|
|
3370
|
+
* rejection that decoded typed on the rpc wire was a `500` on `/v1`.
|
|
3371
|
+
*/
|
|
3372
|
+
readonly rest?: {
|
|
3373
|
+
readonly status: number;
|
|
3374
|
+
readonly headers?: (error: unknown) => Readonly<Record<string, string>>;
|
|
3375
|
+
};
|
|
3313
3376
|
}
|
|
3314
3377
|
|
|
3315
3378
|
/** A public raw-HTTP route a plugin serves on the framework listener. */
|
|
@@ -3319,9 +3382,22 @@ export declare interface PluginHttpRoute {
|
|
|
3319
3382
|
* this union lacked PATCH; that reason is gone (the `'*'` mount remains
|
|
3320
3383
|
* for its other job: one dispatcher per shared path + a precise 405). */
|
|
3321
3384
|
readonly method: '*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
3322
|
-
/** Absolute path
|
|
3323
|
-
*
|
|
3385
|
+
/** Absolute path, e.g. `/_voltro/storage`. Matched byte for byte: the
|
|
3386
|
+
* router folds case and a trailing slash to FIND the route, the dispatcher
|
|
3387
|
+
* then asks whether the request's path is one the route declared — so
|
|
3388
|
+
* `/Metrics` and `/metrics/` are not `/metrics`. A `:name` segment takes one
|
|
3389
|
+
* non-empty segment. Whether the route also owns its sub-paths is
|
|
3390
|
+
* `match`. */
|
|
3324
3391
|
readonly path: string;
|
|
3392
|
+
/**
|
|
3393
|
+
* `'prefix'` (default): the route answers its path AND every sub-path
|
|
3394
|
+
* (`/_voltro/storage/abc123`) — a plugin that branches on the tail mounts
|
|
3395
|
+
* once. `'exact'`: the path alone — no sub-path, no trailing slash — which is
|
|
3396
|
+
* what a single document or a scrape endpoint is (`/openapi.json`,
|
|
3397
|
+
* `/metrics`), and what a path guard at the ingress can cover, because
|
|
3398
|
+
* exactly one spelling answers. A path ending in `/*` is a prefix either way.
|
|
3399
|
+
*/
|
|
3400
|
+
readonly match?: 'prefix' | 'exact';
|
|
3325
3401
|
readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
|
|
3326
3402
|
/** Per-route body cap override (bytes) — wins over the listener's shared
|
|
3327
3403
|
* `maxBodyBytes`. NOTE: routes SHARING a path share one body read, so the
|
|
@@ -3518,6 +3594,13 @@ export declare interface PluginHttpRouteResult {
|
|
|
3518
3594
|
/** Stream the response (SSE) instead of sending `body`. See
|
|
3519
3595
|
* {@link PluginHttpRouteStream}. */
|
|
3520
3596
|
readonly stream?: PluginHttpRouteStream;
|
|
3597
|
+
/**
|
|
3598
|
+
* The error behind a `5xx` — for the server log line that names the
|
|
3599
|
+
* status, never for the wire. A route that maps a throw into a response
|
|
3600
|
+
* value has swallowed the reason by the time the transport logs the value;
|
|
3601
|
+
* this carries it alongside.
|
|
3602
|
+
*/
|
|
3603
|
+
readonly cause?: unknown;
|
|
3521
3604
|
/** Stream a BINARY response (a download, an export) instead of sending
|
|
3522
3605
|
* `body` — see {@link PluginHttpRouteByteStream}. Never buffered by the
|
|
3523
3606
|
* serve layer; never compressed (flush timing + Content-Length are the
|
|
@@ -4052,6 +4135,16 @@ export declare interface PolicyGuardSpec<Input = unknown> {
|
|
|
4052
4135
|
readonly resource: (input: Input) => string | undefined;
|
|
4053
4136
|
}
|
|
4054
4137
|
|
|
4138
|
+
export declare const PROBLEM_JSON = "application/problem+json; charset=utf-8";
|
|
4139
|
+
|
|
4140
|
+
/** RFC 9457 Problem Details for one failure: `type` from the tag, `title` from
|
|
4141
|
+
* the tag's words, `status`, `detail` from the message, the error's own fields
|
|
4142
|
+
* as extension members. */
|
|
4143
|
+
export declare const problemDetails: (error: RestFormatError) => Record<string, unknown>;
|
|
4144
|
+
|
|
4145
|
+
/** A stable, non-resolving URI for an error type: the tag as a URN. */
|
|
4146
|
+
export declare const problemTypeOf: (tag: string) => string;
|
|
4147
|
+
|
|
4055
4148
|
export declare type ProcedureDescriptor = 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> | StreamProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All>;
|
|
4056
4149
|
|
|
4057
4150
|
/** The shape of one procedure, as the pair of peers must agree on it. */
|
|
@@ -4074,6 +4167,23 @@ export declare const PROTOCOL_VERSION: 1;
|
|
|
4074
4167
|
|
|
4075
4168
|
export declare type ProtocolVersion = typeof PROTOCOL_VERSION;
|
|
4076
4169
|
|
|
4170
|
+
/** The `publicApi:` block of an api `app.config.ts`, and the shape of a
|
|
4171
|
+
* descriptor's `publicApi.format` override (every field optional there). */
|
|
4172
|
+
export declare interface PublicApiProfileConfig {
|
|
4173
|
+
/** The profile every projection speaks. Default `'rpc'` — today's wire. */
|
|
4174
|
+
readonly profile?: PublicApiProfileName | RestFormat;
|
|
4175
|
+
readonly dates?: RestDates;
|
|
4176
|
+
readonly errors?: RestErrors;
|
|
4177
|
+
readonly envelope?: RestEnvelope;
|
|
4178
|
+
readonly pagination?: RestPagination;
|
|
4179
|
+
readonly cursorField?: string;
|
|
4180
|
+
readonly cursorParam?: string;
|
|
4181
|
+
readonly naming?: RestNaming;
|
|
4182
|
+
readonly etag?: boolean;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
export declare type PublicApiProfileName = 'rpc' | 'standard' | 'jsonapi';
|
|
4186
|
+
|
|
4077
4187
|
export declare interface PublicApiSpec {
|
|
4078
4188
|
/** Derived by kind when omitted: query→GET, mutation/action→POST. */
|
|
4079
4189
|
readonly method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
@@ -4110,6 +4220,37 @@ export declare interface PublicApiSpec {
|
|
|
4110
4220
|
* publicApi: { errorStatus: { AccessDeniedError: 403, NotFoundError: 404 } }
|
|
4111
4221
|
*/
|
|
4112
4222
|
readonly errorStatus?: Readonly<Record<string, number>>;
|
|
4223
|
+
/**
|
|
4224
|
+
* The spec's tag group(s) for this operation. Default: the rpc tag's prefix
|
|
4225
|
+
* (`teams.list` → `teams`) — the first path segment, what a consumer looks
|
|
4226
|
+
* for, and what an SDK generator turns into a client class.
|
|
4227
|
+
*/
|
|
4228
|
+
readonly tags?: ReadonlyArray<string>;
|
|
4229
|
+
/**
|
|
4230
|
+
* The spec's `operationId`. Default: the rpc tag in camelCase (`teams.list`
|
|
4231
|
+
* → `teamsList`) — the stable identifier a generator names methods from.
|
|
4232
|
+
*/
|
|
4233
|
+
readonly operationId?: string;
|
|
4234
|
+
/**
|
|
4235
|
+
* Whether the operation documents a bearer requirement. Default: `'required'`
|
|
4236
|
+
* unless the descriptor declares `openAccess` — then `'optional'` (the key
|
|
4237
|
+
* strategy authenticates a bearer when one is sent, and an anonymous caller
|
|
4238
|
+
* is allowed through), which Swagger UI and generated clients read as
|
|
4239
|
+
* "send the token when you have one". `false` documents no scheme at all.
|
|
4240
|
+
*/
|
|
4241
|
+
readonly security?: 'required' | 'optional' | false;
|
|
4242
|
+
/** A request/response example for the spec — the same semantics as a
|
|
4243
|
+
* `defineRestRoute` example. */
|
|
4244
|
+
readonly example?: {
|
|
4245
|
+
readonly request?: unknown;
|
|
4246
|
+
readonly response?: unknown;
|
|
4247
|
+
};
|
|
4248
|
+
/**
|
|
4249
|
+
* This projection's wire format, overriding the app's `publicApi` profile
|
|
4250
|
+
* (`app.config.ts`) for the one endpoint that must differ — every field
|
|
4251
|
+
* optional, the rest inherited. See `PublicApiProfileConfig`.
|
|
4252
|
+
*/
|
|
4253
|
+
readonly format?: PublicApiProfileConfig;
|
|
4113
4254
|
}
|
|
4114
4255
|
|
|
4115
4256
|
/**
|
|
@@ -4377,6 +4518,14 @@ export declare const requireScope: (subject: Subject, scope: string) => Effect.E
|
|
|
4377
4518
|
/** Drop every registration. Tests only — production registers once at module load. */
|
|
4378
4519
|
export declare const resetPluginAliases: () => void;
|
|
4379
4520
|
|
|
4521
|
+
/**
|
|
4522
|
+
* The format one projection speaks: the app profile, then its building-block
|
|
4523
|
+
* overrides, then the descriptor's own `format`. A named profile is a
|
|
4524
|
+
* `RestFormat` with a name; an object is taken as-is and its fields overridden
|
|
4525
|
+
* the same way.
|
|
4526
|
+
*/
|
|
4527
|
+
export declare const resolveRestFormat: (config: PublicApiProfileConfig | undefined, override?: Partial<PublicApiProfileConfig> | undefined) => RestFormat;
|
|
4528
|
+
|
|
4380
4529
|
/**
|
|
4381
4530
|
* Validate an author's access options and normalise them into the array every
|
|
4382
4531
|
* enforcement path reads. Throws at DEFINITION time — module load, which is
|
|
@@ -4404,6 +4553,85 @@ export declare interface ResourceScopeRequest {
|
|
|
4404
4553
|
*/
|
|
4405
4554
|
export declare type ResourceScopeResolver = (req: ResourceScopeRequest) => Effect.Effect<boolean, unknown>;
|
|
4406
4555
|
|
|
4556
|
+
export declare const REST_PROFILE_NAMES: ReadonlyArray<PublicApiProfileName>;
|
|
4557
|
+
|
|
4558
|
+
export declare type RestDates = 'iso' | 'epoch-ms';
|
|
4559
|
+
|
|
4560
|
+
export declare type RestEnvelope = 'none' | 'data' | 'jsonapi';
|
|
4561
|
+
|
|
4562
|
+
export declare type RestErrors = 'problem-details' | 'tagged';
|
|
4563
|
+
|
|
4564
|
+
/**
|
|
4565
|
+
* The wire format of one `publicApi` surface. `'standard'` and `'jsonapi'` are
|
|
4566
|
+
* named instances of this; an app's own object is the custom case.
|
|
4567
|
+
*/
|
|
4568
|
+
export declare interface RestFormat {
|
|
4569
|
+
readonly name: string;
|
|
4570
|
+
/** `timestampMs` fields: RFC 3339 strings, or the epoch milliseconds the rpc wire carries. */
|
|
4571
|
+
readonly dates: RestDates;
|
|
4572
|
+
/** Failures: RFC 9457 `application/problem+json`, or the tagged object rpc carries. */
|
|
4573
|
+
readonly errors: RestErrors;
|
|
4574
|
+
/** Success bodies: the bare value, `{ data }`, or the JSON:API document. */
|
|
4575
|
+
readonly envelope: RestEnvelope;
|
|
4576
|
+
/** A cursor the handler returns in `cursorField`: as a `Link: <…>; rel="next"`
|
|
4577
|
+
* header (and `next` beside `data`), or left where the handler put it. */
|
|
4578
|
+
readonly pagination: RestPagination;
|
|
4579
|
+
/** The cursor field a paginated output carries. Default `nextCursor`. */
|
|
4580
|
+
readonly cursorField: string;
|
|
4581
|
+
/** The query parameter a cursor travels back in. Default `cursor`. */
|
|
4582
|
+
readonly cursorParam: string;
|
|
4583
|
+
/** Property names on the wire. */
|
|
4584
|
+
readonly naming: RestNaming;
|
|
4585
|
+
/** Weak `ETag` on GET answers, `If-None-Match` → 304. */
|
|
4586
|
+
readonly etag: boolean;
|
|
4587
|
+
/** Custom success encoding — receives the value already in wire form (dates,
|
|
4588
|
+
* naming applied). Replaces the envelope/pagination steps. */
|
|
4589
|
+
readonly encodeSuccess?: (value: unknown, ctx: RestFormatContext) => RestFormatResponse;
|
|
4590
|
+
/** Custom error encoding. Replaces `errors`. */
|
|
4591
|
+
readonly encodeError?: (error: RestFormatError, ctx: RestFormatContext) => RestFormatResponse;
|
|
4592
|
+
/** A hook over the OpenAPI operation the plugin emits for a projection. */
|
|
4593
|
+
readonly describe?: (operation: Record<string, unknown>, ctx: {
|
|
4594
|
+
readonly tag: string;
|
|
4595
|
+
readonly method: string;
|
|
4596
|
+
readonly path: string;
|
|
4597
|
+
}) => Record<string, unknown>;
|
|
4598
|
+
}
|
|
4599
|
+
|
|
4600
|
+
/** What a format sees when it encodes one response. */
|
|
4601
|
+
export declare interface RestFormatContext {
|
|
4602
|
+
/** The rpc tag, e.g. `teams.list`. */
|
|
4603
|
+
readonly tag: string;
|
|
4604
|
+
readonly method: string;
|
|
4605
|
+
/** The request path, for `Link` headers. */
|
|
4606
|
+
readonly path: string;
|
|
4607
|
+
/** The request's query string (without `?`). */
|
|
4608
|
+
readonly query: string;
|
|
4609
|
+
/** The first `source` table of the procedure, when it declares one — the
|
|
4610
|
+
* JSON:API resource `type`. */
|
|
4611
|
+
readonly source: string | undefined;
|
|
4612
|
+
readonly status: number;
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
/** A failure the desugar or the projection is about to answer with. */
|
|
4616
|
+
export declare interface RestFormatError {
|
|
4617
|
+
readonly status: number;
|
|
4618
|
+
/** The typed error's `_tag` (`AccessDeniedError`), or a stable name for a
|
|
4619
|
+
* transport failure (`InvalidRequest`, `Unauthorized`, `Forbidden`, …). */
|
|
4620
|
+
readonly tag: string;
|
|
4621
|
+
/** A human-readable line. */
|
|
4622
|
+
readonly message: string;
|
|
4623
|
+
/** The error's own fields (a typed error), without `_tag`. */
|
|
4624
|
+
readonly fields: Readonly<Record<string, unknown>>;
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
/** A response a format produced: body (already wire-shaped), headers, and the
|
|
4628
|
+
* media type when it is not `application/json`. */
|
|
4629
|
+
export declare interface RestFormatResponse {
|
|
4630
|
+
readonly body: unknown;
|
|
4631
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
4632
|
+
readonly contentType?: string;
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4407
4635
|
declare type RestGuard = (ctx: RestRouteContext) => RestGuardRejection | undefined | Promise<RestGuardRejection | undefined>;
|
|
4408
4636
|
|
|
4409
4637
|
/**
|
|
@@ -4418,6 +4646,10 @@ declare interface RestGuardRejection {
|
|
|
4418
4646
|
|
|
4419
4647
|
declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
4420
4648
|
|
|
4649
|
+
export declare type RestNaming = 'camelCase' | 'snake_case';
|
|
4650
|
+
|
|
4651
|
+
export declare type RestPagination = 'link-header' | 'body-cursor';
|
|
4652
|
+
|
|
4421
4653
|
/** A schema-encoded REST body with an application-selected HTTP status and
|
|
4422
4654
|
* response headers. The marker keeps ordinary domain objects unambiguous.
|
|
4423
4655
|
* `output` still validates/encodes `body`; use `Schema.Void` with a 204 when
|
|
@@ -4427,6 +4659,8 @@ declare interface RestResponse<O> {
|
|
|
4427
4659
|
readonly status: number;
|
|
4428
4660
|
readonly body: O;
|
|
4429
4661
|
readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
|
|
4662
|
+
/** A media type other than `application/json` (a JSON:API document). */
|
|
4663
|
+
readonly contentType?: string;
|
|
4430
4664
|
}
|
|
4431
4665
|
|
|
4432
4666
|
/**
|
|
@@ -4474,6 +4708,17 @@ declare interface RestRouteDescriptor<I, O> {
|
|
|
4474
4708
|
readonly summary?: string;
|
|
4475
4709
|
readonly description?: string;
|
|
4476
4710
|
readonly example?: RestRouteExample;
|
|
4711
|
+
/** The spec's `operationId` — set by a `publicApi` projection from its tag. */
|
|
4712
|
+
readonly operationId?: string;
|
|
4713
|
+
/** The spec's tag group(s) — set by a projection from its tag prefix. */
|
|
4714
|
+
readonly tags?: ReadonlyArray<string>;
|
|
4715
|
+
/**
|
|
4716
|
+
* Present on a `publicApi` projection: what the route projects, in the wire
|
|
4717
|
+
* form its profile speaks. The OpenAPI generator documents parameters,
|
|
4718
|
+
* bodies, responses and security from it; the desugar answers every refusal
|
|
4719
|
+
* through its format.
|
|
4720
|
+
*/
|
|
4721
|
+
readonly projection?: RestRouteProjection;
|
|
4477
4722
|
/** Replacement hint. Sets a `Deprecation: true` response header. */
|
|
4478
4723
|
readonly deprecated?: string;
|
|
4479
4724
|
/** ISO date. Sets a `Sunset:` header; past the date the route returns
|
|
@@ -4523,6 +4768,48 @@ declare interface RestRouteExample {
|
|
|
4523
4768
|
readonly response?: unknown;
|
|
4524
4769
|
}
|
|
4525
4770
|
|
|
4771
|
+
/**
|
|
4772
|
+
* What a `publicApi` projection is, for the two readers that need more than
|
|
4773
|
+
* the route: the OpenAPI generator and the desugar's refusals. `input` and
|
|
4774
|
+
* `output` are the WIRE schemas — the descriptor's, rewritten by the profile
|
|
4775
|
+
* (`restViewSchema`): a `timestampMs` is a `date-time` string under
|
|
4776
|
+
* `dates: 'iso'`, keys are snake_case under that naming. `input` carries the
|
|
4777
|
+
* path parameters as fields; `pathParams` says which.
|
|
4778
|
+
*/
|
|
4779
|
+
declare interface RestRouteProjection {
|
|
4780
|
+
readonly tag: string;
|
|
4781
|
+
readonly kind: 'query' | 'mutation' | 'action';
|
|
4782
|
+
readonly version: string;
|
|
4783
|
+
readonly security: 'required' | 'optional' | 'none';
|
|
4784
|
+
readonly input: Schema.Schema.All;
|
|
4785
|
+
/** The wire names of the input fields bound from the path. */
|
|
4786
|
+
readonly pathParams: ReadonlyArray<string>;
|
|
4787
|
+
readonly output: Schema.Schema.All;
|
|
4788
|
+
/** The first `source` table, when the procedure declares one. */
|
|
4789
|
+
readonly source: string | undefined;
|
|
4790
|
+
/** Every failure the operation answers with a typed body, and its status:
|
|
4791
|
+
* the descriptor's `errorStatus` entries, `422` for the union's other tags,
|
|
4792
|
+
* and each plugin error that names a REST status. */
|
|
4793
|
+
readonly errorResponses: ReadonlyArray<{
|
|
4794
|
+
readonly status: number;
|
|
4795
|
+
readonly tag: string;
|
|
4796
|
+
readonly schema: Schema.Schema.All;
|
|
4797
|
+
}>;
|
|
4798
|
+
readonly format: RestFormat;
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
/**
|
|
4802
|
+
* Rewrite a descriptor's schema into the shape the profile puts on the wire:
|
|
4803
|
+
* `timestampMs` (`DateFromNumber`) becomes an RFC 3339 string under
|
|
4804
|
+
* `dates: 'iso'`, and every struct's property names become `snake_case` under
|
|
4805
|
+
* `naming: 'snake_case'` — as a transformation, so decoding a wire value yields
|
|
4806
|
+
* the descriptor's own typed value and encoding the typed value yields the
|
|
4807
|
+
* wire form. `JSONSchema.make` of the result documents the wire (a
|
|
4808
|
+
* `format: date-time` string, the snake keys). Classes and other opaque
|
|
4809
|
+
* transformations pass through unchanged; a struct inside one keeps its keys.
|
|
4810
|
+
*/
|
|
4811
|
+
export declare const restViewSchema: (schema: Schema.Schema.All, format: RestFormat) => Schema.Schema.All;
|
|
4812
|
+
|
|
4526
4813
|
/** The id of a row, addressed in op paths as `/<id>`. */
|
|
4527
4814
|
export declare type RowId = string | number;
|
|
4528
4815
|
|
|
@@ -4693,6 +4980,14 @@ export declare interface RpcInterceptorContext {
|
|
|
4693
4980
|
* plugins (Sentry/Datadog) attach an error to the exact span, not just
|
|
4694
4981
|
* the trace. Absent when no tracer is installed. */
|
|
4695
4982
|
readonly spanId?: string;
|
|
4983
|
+
/**
|
|
4984
|
+
* `'rest'` when the call came through the `publicApi` projection (`/v1`);
|
|
4985
|
+
* absent on the rpc wire. A per-endpoint policy on the projection — the
|
|
4986
|
+
* descriptor's `publicApi.rateLimit` — applies to these calls only.
|
|
4987
|
+
*/
|
|
4988
|
+
readonly surface?: 'rest';
|
|
4989
|
+
/** The descriptor's `publicApi` spec, on a projected call. */
|
|
4990
|
+
readonly publicApi?: PublicApiSpec;
|
|
4696
4991
|
}
|
|
4697
4992
|
|
|
4698
4993
|
/**
|
|
@@ -5089,18 +5384,18 @@ export declare const Subject: Schema.Union<[Schema.Struct<{
|
|
|
5089
5384
|
export declare type Subject = typeof Subject.Type;
|
|
5090
5385
|
|
|
5091
5386
|
export declare const SubjectIdentity: Schema.Union<[Schema.Struct<{
|
|
5092
|
-
id: typeof Schema.String;
|
|
5093
5387
|
type: Schema.Literal<["user"]>;
|
|
5388
|
+
id: typeof Schema.String;
|
|
5094
5389
|
tenantId: typeof Schema.String;
|
|
5095
5390
|
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
5096
5391
|
}>, Schema.Struct<{
|
|
5097
|
-
id: typeof Schema.String;
|
|
5098
5392
|
type: Schema.Literal<["apiKey"]>;
|
|
5393
|
+
id: typeof Schema.String;
|
|
5099
5394
|
tenantId: typeof Schema.String;
|
|
5100
5395
|
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
5101
5396
|
}>, Schema.Struct<{
|
|
5102
|
-
id: typeof Schema.String;
|
|
5103
5397
|
type: Schema.Literal<["serviceAccount"]>;
|
|
5398
|
+
id: typeof Schema.String;
|
|
5104
5399
|
tenantId: typeof Schema.String;
|
|
5105
5400
|
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
5106
5401
|
}>, Schema.Struct<{
|
|
@@ -5125,8 +5420,8 @@ export declare const SubjectIdentity: Schema.Union<[Schema.Struct<{
|
|
|
5125
5420
|
*/
|
|
5126
5421
|
credentialRejected: Schema.optional<typeof Schema.String>;
|
|
5127
5422
|
}>, Schema.Struct<{
|
|
5128
|
-
id: typeof Schema.String;
|
|
5129
5423
|
type: Schema.Literal<["system"]>;
|
|
5424
|
+
id: typeof Schema.String;
|
|
5130
5425
|
tenantId: typeof Schema.Null;
|
|
5131
5426
|
metadata: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.Unknown>>;
|
|
5132
5427
|
}>]>;
|
|
@@ -5164,37 +5459,37 @@ export declare class SubjectService extends SubjectService_base {
|
|
|
5164
5459
|
}
|
|
5165
5460
|
|
|
5166
5461
|
declare const SubjectService_base: Context.TagClass<SubjectService, "@voltro/Subject", {
|
|
5167
|
-
readonly id: string;
|
|
5168
5462
|
readonly type: "user";
|
|
5463
|
+
readonly id: string;
|
|
5169
5464
|
readonly tenantId: string;
|
|
5170
5465
|
readonly scopes?: readonly string[] | undefined;
|
|
5171
5466
|
readonly metadata?: {
|
|
5172
5467
|
readonly [x: string]: unknown;
|
|
5173
5468
|
} | undefined;
|
|
5174
5469
|
} | {
|
|
5175
|
-
readonly id: string;
|
|
5176
5470
|
readonly type: "apiKey";
|
|
5471
|
+
readonly id: string;
|
|
5177
5472
|
readonly tenantId: string;
|
|
5178
5473
|
readonly scopes?: readonly string[] | undefined;
|
|
5179
5474
|
readonly metadata?: {
|
|
5180
5475
|
readonly [x: string]: unknown;
|
|
5181
5476
|
} | undefined;
|
|
5182
5477
|
} | {
|
|
5183
|
-
readonly id: string;
|
|
5184
5478
|
readonly type: "serviceAccount";
|
|
5479
|
+
readonly id: string;
|
|
5185
5480
|
readonly tenantId: string;
|
|
5186
5481
|
readonly scopes?: readonly string[] | undefined;
|
|
5187
5482
|
readonly metadata?: {
|
|
5188
5483
|
readonly [x: string]: unknown;
|
|
5189
5484
|
} | undefined;
|
|
5190
5485
|
} | {
|
|
5191
|
-
readonly id: null;
|
|
5192
5486
|
readonly type: "anonymous";
|
|
5487
|
+
readonly id: null;
|
|
5193
5488
|
readonly tenantId: string | null;
|
|
5194
5489
|
readonly credentialRejected?: string | undefined;
|
|
5195
5490
|
} | {
|
|
5196
|
-
readonly id: string;
|
|
5197
5491
|
readonly type: "system";
|
|
5492
|
+
readonly id: string;
|
|
5198
5493
|
readonly tenantId: null;
|
|
5199
5494
|
readonly scopes?: readonly string[] | undefined;
|
|
5200
5495
|
readonly metadata?: {
|
|
@@ -5534,6 +5829,8 @@ export declare const toRpc: <Name extends string, Input extends Schema.Schema.An
|
|
|
5534
5829
|
revision: Schema.optional<typeof Schema.Number>;
|
|
5535
5830
|
}>]>, Schema.Schema.All>, typeof Schema.Never, never> | Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Output, Schema.Schema.All, never> | Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Stream<Output, Schema.Schema.All>, typeof Schema.Never, never>;
|
|
5536
5831
|
|
|
5832
|
+
export declare const toSnakeCase: (key: string) => string;
|
|
5833
|
+
|
|
5537
5834
|
/**
|
|
5538
5835
|
* Coerce a timestamp read (`Date` / epoch-ms number / ISO string) to
|
|
5539
5836
|
* epoch milliseconds. Unrecognised inputs collapse to `0`.
|
|
@@ -6131,6 +6428,18 @@ export declare interface VoltroPlugin {
|
|
|
6131
6428
|
* mounted when this runs, so the hook only records.
|
|
6132
6429
|
*/
|
|
6133
6430
|
readonly onRestSurface?: (routes: ReadonlyArray<RestRouteDescriptor<any, any>>) => void;
|
|
6431
|
+
/**
|
|
6432
|
+
* Told the app's wire-reachable rpc procedures — every query, mutation,
|
|
6433
|
+
* action and stream discovery found, minus `internal` ones — once discovery
|
|
6434
|
+
* has run, on both boot paths. The procedure counterpart of
|
|
6435
|
+
* `onRestSurface`, for the same reason: a documentation plugin that wants
|
|
6436
|
+
* the descriptors otherwise asks the app to import them into
|
|
6437
|
+
* `app.config.ts`, which evaluates every descriptor module before discovery
|
|
6438
|
+
* on every load of the config — and `voltro serve` loads no TypeScript at
|
|
6439
|
+
* runtime, so a plugin cannot read them later either. Synchronous: records
|
|
6440
|
+
* only.
|
|
6441
|
+
*/
|
|
6442
|
+
readonly onProcedureSurface?: (procedures: ReadonlyArray<ProcedureDescriptor>) => void;
|
|
6134
6443
|
}
|
|
6135
6444
|
|
|
6136
6445
|
/**
|