@voltro/protocol 0.65.0 → 0.66.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
@@ -100,6 +100,8 @@ export declare const bytes: (stream: ReadableStream<Uint8Array> | (() => Readabl
100
100
  readonly contentType?: string;
101
101
  readonly contentLength?: number;
102
102
  readonly contentDisposition?: string;
103
+ readonly status?: number;
104
+ readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
103
105
  }) => RestByteResponse;
104
106
 
105
107
  /**
@@ -280,6 +282,8 @@ declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<str
280
282
 
281
283
  export declare const isRestByteResponse: (v: unknown) => v is RestByteResponse;
282
284
 
285
+ export declare const isRestResponse: (value: unknown) => value is RestResponse<unknown>;
286
+
283
287
  export declare const isRestStreamResponse: (v: unknown) => v is RestStreamResponse;
284
288
 
285
289
  /** Extract `:name` path params by aligning the route PATTERN with the request
@@ -289,12 +293,20 @@ export declare const isRestStreamResponse: (v: unknown) => v is RestStreamRespon
289
293
  * empty and every `:param` route 400s on decode. */
290
294
  export declare const matchPathParams: (pattern: string, path: string) => Record<string, string>;
291
295
 
296
+ /** Ordinary writes deduplicate across reconnects. A connection-side effect
297
+ * (for example joining a room) must execute once for EACH native connection. */
298
+ declare type MutationIdempotencyScope = 'subject' | 'connection';
299
+
292
300
  declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
293
301
  readonly kind: 'mutation';
294
302
  readonly name: Name;
295
303
  readonly input: Input;
296
304
  readonly output: Output;
297
305
  readonly error: Error;
306
+ /** Default 'subject': retries deduplicate across connections. 'connection'
307
+ * re-executes on a new server-owned WebSocket identity and rejects HTTP RPC.
308
+ * This does not grant access or restore application membership by itself. */
309
+ readonly idempotencyScope?: MutationIdempotencyScope | undefined;
298
310
  /** Declarative target(s) — drives auto-optimistic on the client AND
299
311
  * surfaces which tables this mutation touches (debug, future
300
312
  * query-invalidation analytics). Mutations without a target run
@@ -793,6 +805,11 @@ declare interface QueryProcedureDescriptor<Name extends string, Input extends Sc
793
805
  */
794
806
  export declare const requireAnyScope: (scopes: ReadonlyArray<string>) => RestGuard;
795
807
 
808
+ /** Require a non-anonymous subject on a REST route. Rejected credentials and
809
+ * absent credentials are both authentication failures (401), while scope
810
+ * guards remain authorization failures (403). */
811
+ export declare const requireAuthenticated: () => RestGuard;
812
+
796
813
  /**
797
814
  * Guard that requires the resolved subject to carry `scope`. Anonymous /
798
815
  * scope-less subjects are rejected with `403`. The blanket admin scope
@@ -802,6 +819,11 @@ export declare const requireAnyScope: (scopes: ReadonlyArray<string>) => RestGua
802
819
  */
803
820
  export declare const requireScope: (scope: string) => RestGuard;
804
821
 
822
+ export declare const response: <O>(body: O, options?: {
823
+ readonly status?: number;
824
+ readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
825
+ }) => RestResponse<O>;
826
+
805
827
  /** A BINARY streaming response (download/export). The serve layer pipes the
806
828
  * ReadableStream without buffering — a body larger than the heap is fine.
807
829
  * The LAZY thunk form defers opening the source until the response streams. */
@@ -813,8 +835,12 @@ export declare interface RestByteResponse {
813
835
  readonly contentDisposition?: string;
814
836
  };
815
837
  readonly contentType?: string;
838
+ readonly status?: number;
839
+ readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
816
840
  }
817
841
 
842
+ export declare const restError: (status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>) => RestRouteError;
843
+
818
844
  export declare type RestGuard = (ctx: RestRouteContext) => RestGuardRejection | undefined | Promise<RestGuardRejection | undefined>;
819
845
 
820
846
  /**
@@ -829,6 +855,17 @@ export declare interface RestGuardRejection {
829
855
 
830
856
  export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
831
857
 
858
+ /** A schema-encoded REST body with an application-selected HTTP status and
859
+ * response headers. The marker keeps ordinary domain objects unambiguous.
860
+ * `output` still validates/encodes `body`; use `Schema.Void` with a 204 when
861
+ * the response intentionally has no representation. */
862
+ export declare interface RestResponse<O> {
863
+ readonly __voltroRestResponse: true;
864
+ readonly status: number;
865
+ readonly body: O;
866
+ readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
867
+ }
868
+
832
869
  /**
833
870
  * Per-call context handed to a REST route handler. Resolved by the serve
834
871
  * pipeline: `subject` from the framework's auth resolver, `headers`
@@ -839,10 +876,22 @@ export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | '
839
876
  export declare interface RestRouteContext {
840
877
  readonly subject: Subject;
841
878
  readonly headers: Readonly<Record<string, string>>;
879
+ /** Request metadata and the byte-exact body as received by the HTTP
880
+ * transport. JSON routes normally use decoded `input.body`; `rawBody` is
881
+ * for signature verification, multipart parsing, and other protocols where
882
+ * parsing first would destroy information the handler must authenticate. */
883
+ readonly method: RestMethod;
884
+ readonly path: string;
885
+ readonly query: string;
886
+ readonly rawBody: Uint8Array;
842
887
  /** Framework `DataStore`. Untyped in protocol (same discipline as
843
888
  * `PluginHttpRoute` / `bindDataStore`); cast to the concrete store at
844
889
  * the call site. `undefined` only in unit tests that don't inject one. */
845
890
  readonly store: unknown;
891
+ /** Full framework context for this authenticated request. This keeps custom
892
+ * REST handlers on the same capability surface as query/mutation/action
893
+ * handlers without creating a protocol-to-runtime package dependency. */
894
+ readonly app?: unknown;
846
895
  }
847
896
 
848
897
  export declare interface RestRouteDescriptor<I, O> {
@@ -858,7 +907,7 @@ export declare interface RestRouteDescriptor<I, O> {
858
907
  /** Output schema. The handler returns the decoded `Type`; the desugar
859
908
  * encodes it to JSON for the `200` response. */
860
909
  readonly output: Schema.Schema<O, any, never>;
861
- readonly handler: (input: I, ctx: RestRouteContext) => Promise<O> | O;
910
+ readonly handler: (input: I, ctx: RestRouteContext) => Promise<O | RestResponse<O>> | O | RestResponse<O>;
862
911
  readonly summary?: string;
863
912
  readonly description?: string;
864
913
  readonly example?: RestRouteExample;
@@ -906,6 +955,16 @@ export declare interface RestRouteDescriptor<I, O> {
906
955
  readonly etag?: boolean;
907
956
  }
908
957
 
958
+ /** Intentional non-2xx REST control flow with an application-defined JSON
959
+ * error contract. Unlike an arbitrary thrown object this is never reported as
960
+ * a server fault, and its body is not rewritten to `{ error: message }`. */
961
+ export declare class RestRouteError extends Error {
962
+ readonly status: number;
963
+ readonly body: unknown;
964
+ readonly headers: Readonly<Record<string, string | ReadonlyArray<string>>> | undefined;
965
+ constructor(status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>);
966
+ }
967
+
909
968
  export declare interface RestRouteExample {
910
969
  readonly request?: unknown;
911
970
  readonly response?: unknown;
@@ -929,6 +988,8 @@ export declare interface RestServeBindings {
929
988
  readonly resolveSubject?: (headers: Readonly<Record<string, string>>) => Subject | Promise<Subject>;
930
989
  /** The bound framework DataStore. */
931
990
  readonly store?: unknown;
991
+ /** Build the full per-request app context after authentication. */
992
+ readonly buildContext?: (subject: Subject, headers: Readonly<Record<string, string>>) => unknown | Promise<unknown>;
932
993
  /**
933
994
  * HTTP idempotency — when set, a mutating request (POST/PUT/PATCH/DELETE)
934
995
  * carrying the `header` is deduplicated: the first call runs + caches its
package/dist/rest.js CHANGED
@@ -1,39 +1,56 @@
1
- import { F as e } from "./scopes-C86BDyNJ.js";
2
- import { a as t, c as n, o as r, s as i, t as a } from "./serverErrorBus-QQgyq6fV.js";
3
- import { Effect as o, Schema as s } from "effect";
1
+ import { F as e, P as t } from "./scopes-C86BDyNJ.js";
2
+ import { c as n, l as r, o as i, s as a, t as o } from "./serverErrorBus-BTnTpMqj.js";
3
+ import { Effect as s, Schema as c } from "effect";
4
4
  //#region src/publicApi.ts
5
- var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.path ?? `/${t.version ?? "v1"}/${e.replace(/\./g, "/")}`, u = (e, t, n) => {
5
+ var l = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), u = (e, t) => t.path ?? `/${t.version ?? "v1"}/${e.replace(/\./g, "/")}`, d = (e, t, n) => {
6
6
  let r = e.publicApi;
7
7
  if (r === void 0) throw Error(`publicApiRoute: descriptor '${e.name}' has no publicApi annotation`);
8
- let i = c(e.kind, r), a = l(e.name, r), o = i === "GET" ? "query" : "body", u = s.Struct({ [o]: e.input }), d = (r.scopes ?? []).map((e) => f(e)), p = r.stream === "sse" && e.kind === "query" && n !== void 0, _ = p ? (e, t) => h((r) => n(e[o], t, (e) => {
9
- r(g(String(e._tag ?? "message"), e));
8
+ let i = l(e.kind, r), a = u(e.name, r), o = i === "GET" ? "query" : "body", s = c.Struct({ [o]: e.input }), d = (r.scopes ?? []).map((e) => m(e)), f = r.stream === "sse" && e.kind === "query" && n !== void 0, p = f ? (e, t) => x((r) => n(e[o], t, (e) => {
9
+ r(S(String(e._tag ?? "message"), e));
10
10
  })) : (e, n) => t(e[o], n);
11
- return m({
11
+ return b({
12
12
  method: i,
13
13
  path: a,
14
- input: u,
14
+ input: s,
15
15
  output: e.output,
16
- handler: _,
17
- ...p ? { streaming: !0 } : {},
16
+ handler: p,
17
+ ...f ? { streaming: !0 } : {},
18
18
  ...d.length > 0 ? { guards: d } : {},
19
19
  ...r.summary === void 0 ? {} : { summary: r.summary },
20
20
  ...r.description === void 0 ? {} : { description: r.description },
21
21
  ...r.deprecated === void 0 ? {} : { deprecated: r.deprecated },
22
22
  ...r.sunset === void 0 ? {} : { sunset: r.sunset }
23
23
  });
24
- }, d = (e) => e.filter((e) => e.descriptor.publicApi !== void 0).map((e) => u(e.descriptor, e.invoke, e.subscribe)).sort((e, t) => e.path.localeCompare(t.path)), f = (e) => (t) => {
24
+ }, f = (e) => e.filter((e) => e.descriptor.publicApi !== void 0).map((e) => d(e.descriptor, e.invoke, e.subscribe)).sort((e, t) => e.path.localeCompare(t.path)), p = () => (e) => {
25
+ if (e.subject.type === "anonymous") return {
26
+ status: 401,
27
+ message: e.subject.credentialRejected ?? "Authentication required"
28
+ };
29
+ }, m = (e) => (t) => {
25
30
  let n = t.subject.scopes;
26
31
  if (!(n && (n.includes(e) || n.includes("admin:full")))) return {
27
32
  status: 403,
28
33
  message: `Missing required scope: ${e}`
29
34
  };
30
- }, p = (e) => (t) => {
35
+ }, h = (e) => (t) => {
31
36
  let n = t.subject.scopes;
32
37
  if (!(n && (n.includes("admin:full") || e.some((e) => n.includes(e))))) return {
33
38
  status: 403,
34
39
  message: `Missing required scope: one of ${e.join(", ")}`
35
40
  };
36
- }, m = (e) => {
41
+ }, g = (e, t = {}) => ({
42
+ __voltroRestResponse: !0,
43
+ status: t.status ?? 200,
44
+ body: e,
45
+ ...t.headers === void 0 ? {} : { headers: t.headers }
46
+ }), _ = (e) => typeof e == "object" && !!e && e.__voltroRestResponse === !0, v = class extends Error {
47
+ status;
48
+ body;
49
+ headers;
50
+ constructor(e, t, n) {
51
+ super(`REST ${e}`), this.name = "RestRouteError", this.status = e, this.body = t, this.headers = n;
52
+ }
53
+ }, y = (e, t, n) => new v(e, t, n), b = (e) => {
37
54
  if (e.version === void 0) return e;
38
55
  if (!/^v\d+$/.test(e.version)) throw Error(`defineRestRoute(${e.method} ${e.path}): version "${e.version}" must be 'v' followed by digits — 'v1', 'v2', …`);
39
56
  if ((/* @__PURE__ */ RegExp("^/v\\d+(/|$)")).test(e.path)) throw Error(`defineRestRoute(${e.method} ${e.path}): the path already starts with a version segment AND declares version: '${e.version}'. Pick one spelling — either bake the version into the path, or declare version: and keep the path bare ('/customers'). Both would mount '/${e.version}${e.path}', which is never what the author meant.`);
@@ -41,37 +58,39 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
41
58
  ...e,
42
59
  path: `/${e.version}${e.path}`
43
60
  };
44
- }, h = (e, t = {}) => ({
61
+ }, x = (e, t = {}) => ({
45
62
  __voltroRestStream: !0,
46
63
  stream: {
47
64
  subscribe: e,
48
65
  ...t.keepAliveMs === void 0 ? {} : { keepAliveMs: t.keepAliveMs }
49
66
  }
50
- }), g = (e, t) => `event: ${e}\n${(typeof t == "string" ? t : JSON.stringify(t)).split("\n").map((e) => `data: ${e}`).join("\n")}\n\n`, _ = (e) => typeof e == "object" && !!e && e.__voltroRestStream === !0, v = (e, t = {}) => ({
67
+ }), S = (e, t) => `event: ${e}\n${(typeof t == "string" ? t : JSON.stringify(t)).split("\n").map((e) => `data: ${e}`).join("\n")}\n\n`, C = (e) => typeof e == "object" && !!e && e.__voltroRestStream === !0, w = (e, t = {}) => ({
51
68
  __voltroRestBytes: !0,
52
69
  byteStream: {
53
70
  stream: e,
54
71
  ...t.contentLength === void 0 ? {} : { contentLength: t.contentLength },
55
72
  ...t.contentDisposition === void 0 ? {} : { contentDisposition: t.contentDisposition }
56
73
  },
57
- ...t.contentType === void 0 ? {} : { contentType: t.contentType }
58
- }), y = (e) => typeof e == "object" && !!e && e.__voltroRestBytes === !0, b = async (e) => {
74
+ ...t.contentType === void 0 ? {} : { contentType: t.contentType },
75
+ ...t.status === void 0 ? {} : { status: t.status },
76
+ ...t.headers === void 0 ? {} : { headers: t.headers }
77
+ }), T = (e) => typeof e == "object" && !!e && e.__voltroRestBytes === !0, E = async (e) => {
59
78
  let t = await globalThis.crypto.subtle.digest("SHA-1", new TextEncoder().encode(e));
60
79
  return [...new Uint8Array(t)].map((e) => e.toString(16).padStart(2, "0")).join("");
61
- }, x = (e, t, n) => ({
80
+ }, D = (e, t, n) => ({
62
81
  status: e,
63
82
  contentType: "application/json; charset=utf-8",
64
83
  body: JSON.stringify(t),
65
84
  ...n ? { headers: n } : {}
66
- }), S = (e) => {
85
+ }), O = (e) => {
67
86
  if (e.length === 0) return;
68
87
  let t = new TextDecoder().decode(e);
69
88
  if (t.trim() !== "") return JSON.parse(t);
70
- }, C = (e) => {
89
+ }, k = (e) => {
71
90
  let t = {};
72
91
  for (let [n, r] of new URLSearchParams(e)) t[n] = r;
73
92
  return t;
74
- }, w = (e, t) => {
93
+ }, A = (e, t) => {
75
94
  let n = e.split("/").filter((e) => e.length > 0), r = t.split("/").filter((e) => e.length > 0), i = {};
76
95
  for (let e = 0; e < n.length; e++) {
77
96
  let t = n[e];
@@ -85,124 +104,153 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
85
104
  }
86
105
  }
87
106
  return i;
88
- }, T = (e, t) => ({
89
- query: C(e.query),
107
+ }, j = (e, t) => ({
108
+ query: k(e.query),
90
109
  params: t,
91
- body: S(e.rawBody)
92
- }), E = /* @__PURE__ */ new Set([
110
+ body: O(e.rawBody)
111
+ }), M = /* @__PURE__ */ new Set([
93
112
  "POST",
94
113
  "PUT",
95
114
  "PATCH",
96
115
  "DELETE"
97
- ]), D = (c, l) => {
98
- let u = c.input ? s.decodeUnknown(c.input) : void 0, d = s.encode(c.output), f = {};
99
- return c.deprecated !== void 0 && (f.Deprecation = "true"), c.sunset !== void 0 && (f.Sunset = c.sunset), {
116
+ ]), N = (l, u) => {
117
+ let d = l.input ? c.decodeUnknown(l.input) : void 0, f = c.encode(l.output), p = {}, m = (e) => e.replace(/\/$/, "").split("/").length, h = m(l.path);
118
+ return l.deprecated !== void 0 && (p.Deprecation = "true"), l.sunset !== void 0 && (p.Sunset = l.sunset), {
100
119
  method: "*",
101
- path: c.path,
102
- ...c.maxBodyBytes === void 0 ? {} : { maxBodyBytes: c.maxBodyBytes },
103
- handle: async (s) => {
104
- let p = s.method.toUpperCase();
105
- if (!(p === c.method || p === "HEAD" && c.method === "GET")) return x(405, {
120
+ path: l.path,
121
+ ...l.maxBodyBytes === void 0 ? {} : { maxBodyBytes: l.maxBodyBytes },
122
+ handle: async (c) => {
123
+ if (!l.path.endsWith("/*") && m(c.path) !== h) return D(404, { error: "Not Found" }, p);
124
+ let y = c.method.toUpperCase();
125
+ if (!(y === l.method || y === "HEAD" && l.method === "GET")) return D(405, {
106
126
  error: "Method Not Allowed",
107
- allow: c.method
127
+ allow: l.method
108
128
  }, {
109
- ...f,
110
- Allow: c.method
129
+ ...p,
130
+ Allow: l.method
111
131
  });
112
- if (c.sunset !== void 0) {
113
- let e = Date.parse(c.sunset);
114
- if (!Number.isNaN(e) && Date.now() >= e) return x(410, {
132
+ if (l.sunset !== void 0) {
133
+ let e = Date.parse(l.sunset);
134
+ if (!Number.isNaN(e) && Date.now() >= e) return D(410, {
115
135
  error: "Gone",
116
- ...c.version === void 0 ? {} : { version: c.version },
117
- ...c.deprecated === void 0 ? {} : { replacement: c.deprecated }
118
- }, f);
136
+ ...l.version === void 0 ? {} : { version: l.version },
137
+ ...l.deprecated === void 0 ? {} : { replacement: l.deprecated }
138
+ }, p);
119
139
  }
120
- let m;
121
- if (u) {
122
- let e = T(s, w(c.path, s.path)), t = await o.runPromise(u(e).pipe(o.map((e) => ({
140
+ let b;
141
+ if (d) {
142
+ let e = j(c, A(l.path, c.path)), t = await s.runPromise(d(e).pipe(s.map((e) => ({
123
143
  ok: !0,
124
144
  value: e
125
- })), o.catchAll((e) => o.succeed({
145
+ })), s.catchAll((e) => s.succeed({
126
146
  ok: !1,
127
147
  cause: e
128
148
  }))));
129
- if (!t.ok) return x(400, {
149
+ if (!t.ok) return D(400, {
130
150
  error: "Invalid request",
131
151
  detail: String(t.cause)
132
- }, f);
133
- m = t.value;
152
+ }, p);
153
+ b = t.value;
154
+ }
155
+ let x;
156
+ try {
157
+ x = u.resolveSubject ? await u.resolveSubject(c.headers) : e(c.headers["x-tenant"] ?? null);
158
+ } catch (e) {
159
+ if (e instanceof t || typeof e == "object" && e && e._tag === "Unauthenticated") {
160
+ let t = e.reason;
161
+ return D(401, { error: t ?? "Authentication required" }, p);
162
+ }
163
+ throw e;
134
164
  }
135
- let h = l.resolveSubject ? await l.resolveSubject(s.headers) : e(s.headers["x-tenant"] ?? null), g = {
136
- subject: h,
137
- headers: s.headers,
138
- store: l.store
165
+ let S = u.buildContext ? await u.buildContext(x, c.headers) : void 0, w = typeof S == "object" && S && "store" in S ? S.store : u.store, O = {
166
+ subject: x,
167
+ headers: c.headers,
168
+ store: w,
169
+ method: y,
170
+ path: c.path,
171
+ query: c.query,
172
+ rawBody: c.rawBody,
173
+ ...S === void 0 ? {} : { app: S }
139
174
  };
140
- if (c.guards) for (let e of c.guards) {
141
- let t = await e(g);
142
- if (t) return x(t.status, { error: t.message }, f);
175
+ if (l.guards) for (let e of l.guards) {
176
+ let t = await e(O);
177
+ if (t) return D(t.status, { error: t.message }, p);
143
178
  }
144
- let v = l.idempotency, S = v && E.has(c.method) ? s.headers[v.header.toLowerCase()] : void 0, C = v && S ? n(h.tenantId, c.method, c.path) : "";
145
- if (v && S) {
146
- let e = await t(v.store, C, S, v.ttlMs, Date.now());
147
- if (e.kind === "replay") return x(e.response.status, e.response.body, {
148
- ...f,
179
+ let k = u.idempotency, N = k && M.has(l.method) ? c.headers[k.header.toLowerCase()] : void 0, P = k && N ? r(x.tenantId, l.method, l.path) : "";
180
+ if (k && N) {
181
+ let e = await i(k.store, P, N, k.ttlMs, Date.now());
182
+ if (e.kind === "replay") return D(e.response.status, e.response.body, {
183
+ ...p,
149
184
  "Idempotency-Replayed": "true"
150
185
  });
151
- if (e.kind === "conflict") return x(409, { error: "A request with this Idempotency-Key is already being processed" }, f);
186
+ if (e.kind === "conflict") return D(409, { error: "A request with this Idempotency-Key is already being processed" }, p);
152
187
  }
153
188
  try {
154
- let e = await c.handler(m, g);
155
- if (_(e) || y(e)) return v && S && await r(v.store, C, S), y(e) ? {
156
- status: 200,
157
- headers: f,
189
+ let e = await l.handler(b, O);
190
+ if (C(e) || T(e)) return k && N && await a(k.store, P, N), T(e) ? {
191
+ status: e.status ?? 200,
192
+ headers: {
193
+ ...p,
194
+ ...e.headers
195
+ },
158
196
  byteStream: e.byteStream,
159
197
  ...e.contentType === void 0 ? {} : { contentType: e.contentType }
160
198
  } : {
161
199
  status: 200,
162
- headers: f,
200
+ headers: p,
163
201
  stream: e.stream
164
202
  };
165
- let t = await o.runPromise(d(e));
166
- if (v && S && await i(v.store, C, S, {
167
- status: 200,
168
- body: t
169
- }, Date.now()), c.etag === !0 && c.method === "GET") {
170
- let e = `W/"${await b(JSON.stringify(t))}"`, n = s.headers["if-none-match"];
203
+ let t = _(e) ? e : g(e), r = await s.runPromise(f(t.body)), i = {
204
+ ...p,
205
+ ...t.headers
206
+ };
207
+ if (k && N && await n(k.store, P, N, {
208
+ status: t.status,
209
+ body: r
210
+ }, Date.now()), l.etag === !0 && l.method === "GET") {
211
+ let e = `W/"${await E(JSON.stringify(r))}"`, n = c.headers["if-none-match"];
171
212
  return n !== void 0 && n.split(",").some((t) => t.trim() === e) ? {
172
213
  status: 304,
173
214
  headers: {
174
- ...f,
215
+ ...p,
175
216
  ETag: e
176
217
  }
177
- } : x(200, t, {
178
- ...f,
218
+ } : D(t.status, r, {
219
+ ...i,
179
220
  ETag: e
180
221
  });
181
222
  }
182
- return x(200, t, f);
223
+ return t.status === 204 || t.status === 304 ? {
224
+ status: t.status,
225
+ headers: i
226
+ } : D(t.status, r, i);
183
227
  } catch (e) {
184
- if (v && S && await r(v.store, C, S), typeof e == "object" && e && typeof e.status == "number") {
228
+ if (k && N && await a(k.store, P, N), e instanceof v) return D(e.status, e.body, {
229
+ ...p,
230
+ ...e.headers
231
+ });
232
+ if (typeof e == "object" && e && typeof e.status == "number") {
185
233
  let t = e;
186
- return x(t.status, { error: t.message ?? "Error" }, f);
234
+ return D(t.status, { error: t.message ?? "Error" }, p);
187
235
  }
188
- return a({
236
+ return o({
189
237
  error: e,
190
238
  source: "rest",
191
- name: `${c.method} ${c.path}`,
239
+ name: `${l.method} ${l.path}`,
192
240
  fields: { status: 500 }
193
- }), x(500, { error: "Internal Server Error" }, f);
241
+ }), D(500, { error: "Internal Server Error" }, p);
194
242
  }
195
243
  }
196
244
  };
197
- }, O = (e, t = {}) => {
245
+ }, P = (e, t = {}) => {
198
246
  if (t.idempotency !== void 0) {
199
- for (let t of e) if (t.streaming === !0 && E.has(t.method)) throw Error(`REST route ${t.method} ${t.path} declares \`streaming: true\` on a method the idempotency binding claims (${[...E].join("/")}). A stream cannot complete an idempotency claim — there is no replayable body to cache — so a retried request would 409 until the claim's TTL. Either serve the stream on GET, or scope the idempotency binding away from this app's streaming routes.`);
247
+ for (let t of e) if (t.streaming === !0 && M.has(t.method)) throw Error(`REST route ${t.method} ${t.path} declares \`streaming: true\` on a method the idempotency binding claims (${[...M].join("/")}). A stream cannot complete an idempotency claim — there is no replayable body to cache — so a retried request would 409 until the claim's TTL. Either serve the stream on GET, or scope the idempotency binding away from this app's streaming routes.`);
200
248
  }
201
- return e.map((e) => D(e, t));
202
- }, k = async (e, t) => {
249
+ return e.map((e) => N(e, t));
250
+ }, F = async (e, t) => {
203
251
  let n = await e[0].handle(t);
204
252
  for (let r = 1; r < e.length && n.status === 405; r++) n = await e[r].handle(t);
205
253
  return n;
206
254
  };
207
255
  //#endregion
208
- export { v as bytes, d as collectPublicApiRoutes, m as defineRestRoute, c as derivePublicMethod, l as derivePublicPath, k as dispatchSharedPath, y as isRestByteResponse, _ as isRestStreamResponse, w as matchPathParams, u as publicApiRoute, p as requireAnyScope, f as requireScope, O as restRoutesToHttpRoutes, h as sse, g as sseFrame };
256
+ export { v as RestRouteError, w as bytes, f as collectPublicApiRoutes, b as defineRestRoute, l as derivePublicMethod, u as derivePublicPath, F as dispatchSharedPath, T as isRestByteResponse, _ as isRestResponse, C as isRestStreamResponse, A as matchPathParams, d as publicApiRoute, h as requireAnyScope, p as requireAuthenticated, m as requireScope, g as response, y as restError, P as restRoutesToHttpRoutes, x as sse, S as sseFrame };
@@ -1,12 +1,21 @@
1
1
  import { Schema as e } from "effect";
2
2
  //#region src/idempotency.ts
3
- var t = (e, t, n) => `${e ?? "(global)"}|${t.toUpperCase()}|${n}`, n = (e, t, n) => `${e ?? "(global)"}|${t ?? "(anon)"}|WS|${n}`, r = async (e, t, n, r, i) => {
3
+ var t = (e, t, n) => `${e ?? "(global)"}|${t.toUpperCase()}|${n}`, n = (e, t, n, r) => r === void 0 ? `${e ?? "(global)"}|${t ?? "(anon)"}|WS|${n}` : `WS-connection:${JSON.stringify([
4
+ e ?? null,
5
+ t ?? null,
6
+ n,
7
+ r
8
+ ])}`, r = class extends e.TaggedError()("MutationConnectionRequired", { traceId: e.String }) {
9
+ get message() {
10
+ return "this mutation requires a live native WebSocket connection";
11
+ }
12
+ }, i = async (e, t, n, r, i) => {
4
13
  let a = await e.claim(t, n, i, r);
5
14
  return a === "claimed" ? { kind: "fresh" } : a.status === "completed" && a.response ? {
6
15
  kind: "replay",
7
16
  response: a.response
8
17
  } : { kind: "conflict" };
9
- }, i = (e, t, n, r, i) => e.complete(t, n, r, i), a = (e, t, n) => e.release(t, n), o = () => {
18
+ }, a = (e, t, n, r, i) => e.complete(t, n, r, i), o = (e, t, n) => e.release(t, n), s = () => {
10
19
  let e = /* @__PURE__ */ new Map(), t = (e, t) => `${e}\u0000${t}`;
11
20
  return {
12
21
  get: async (n, r) => e.get(t(n, r)) ?? null,
@@ -33,19 +42,19 @@ var t = (e, t, n) => `${e ?? "(global)"}|${t.toUpperCase()}|${n}`, n = (e, t, n)
33
42
  e.delete(t(n, r));
34
43
  }
35
44
  };
36
- }, s = class extends e.TaggedError()("MutationInFlight", {
45
+ }, c = class extends e.TaggedError()("MutationInFlight", {
37
46
  traceId: e.String,
38
47
  retryAfterMs: e.Number
39
48
  }) {
40
49
  get message() {
41
50
  return `a request with this idempotency key is still in flight — replay the same key after ${this.retryAfterMs}ms`;
42
51
  }
43
- }, c = 1e3, l = /* @__PURE__ */ new Set(), u = (e) => {
44
- for (let t of l) try {
52
+ }, l = 1e3, u = /* @__PURE__ */ new Set(), d = (e) => {
53
+ for (let t of u) try {
45
54
  t(e);
46
55
  } catch {}
47
- }, d = (e) => (l.add(e), () => {
48
- l.delete(e);
56
+ }, f = (e) => (u.add(e), () => {
57
+ u.delete(e);
49
58
  });
50
59
  //#endregion
51
- export { r as a, t as c, s as i, o as l, d as n, a as o, c as r, i as s, u as t, n as u };
60
+ export { c as a, a as c, n as d, r as i, t as l, f as n, i as o, l as r, o as s, d as t, s as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/protocol",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "The Voltro wire + plugin contract — defineQuery/Mutation/Action/Stream, definePlugin, sessions / JWT / API-keys, and the RPC protocol.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -59,8 +59,8 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@effect/sql": "^0.52.0",
62
- "@voltro/database": "0.65.0",
63
- "@voltro/logger": "0.65.0",
62
+ "@voltro/database": "0.66.0",
63
+ "@voltro/logger": "0.66.0",
64
64
  "jose": "^6.2.8"
65
65
  },
66
66
  "peerDependencies": {