@voltro/protocol 0.64.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
@@ -71,13 +71,37 @@ declare interface AnyApprovalPolicy {
71
71
  readonly reason?: string;
72
72
  }
73
73
 
74
- /** A guard is either a scope check or a relationship check. */
75
- declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
74
+ /** A guard is a scope check, a relationship check, or an identity check. */
75
+ declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input> | AuthenticatedGuardSpec;
76
+
77
+ /**
78
+ * "The caller must have an identity" — and nothing narrower.
79
+ *
80
+ * The third case the vocabulary was missing, and the gap was not theoretical.
81
+ * An app whose authority lives per-team in its executors holds NO raw scopes on
82
+ * the identity, so `{ scope }` refuses every real user; a relationship guard
83
+ * cannot help when the resource does not exist yet (the first message of a new
84
+ * thread); and `openAccess` would be a lie whenever the auth fallback is a
85
+ * deliberate anonymous subject. The one thing left to decide — is anybody
86
+ * there — had no spelling, so apps wrote a resource policy whose entire content
87
+ * was "you are logged in".
88
+ *
89
+ * `checkGuards` has always distinguished this internally: its return type is
90
+ * `ScopeError | Unauthenticated | null`. This makes the distinction sayable.
91
+ *
92
+ * It is a real DECISION, so it satisfies `security.defaultDeny` — unlike
93
+ * `openAccess`, it also enforces something.
94
+ */
95
+ declare interface AuthenticatedGuardSpec {
96
+ readonly authenticated: true;
97
+ }
76
98
 
77
99
  export declare const bytes: (stream: ReadableStream<Uint8Array> | (() => ReadableStream<Uint8Array>), options?: {
78
100
  readonly contentType?: string;
79
101
  readonly contentLength?: number;
80
102
  readonly contentDisposition?: string;
103
+ readonly status?: number;
104
+ readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
81
105
  }) => RestByteResponse;
82
106
 
83
107
  /**
@@ -258,6 +282,8 @@ declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<str
258
282
 
259
283
  export declare const isRestByteResponse: (v: unknown) => v is RestByteResponse;
260
284
 
285
+ export declare const isRestResponse: (value: unknown) => value is RestResponse<unknown>;
286
+
261
287
  export declare const isRestStreamResponse: (v: unknown) => v is RestStreamResponse;
262
288
 
263
289
  /** Extract `:name` path params by aligning the route PATTERN with the request
@@ -267,12 +293,20 @@ export declare const isRestStreamResponse: (v: unknown) => v is RestStreamRespon
267
293
  * empty and every `:param` route 400s on decode. */
268
294
  export declare const matchPathParams: (pattern: string, path: string) => Record<string, string>;
269
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
+
270
300
  declare interface MutationProcedureDescriptor<Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All> {
271
301
  readonly kind: 'mutation';
272
302
  readonly name: Name;
273
303
  readonly input: Input;
274
304
  readonly output: Output;
275
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;
276
310
  /** Declarative target(s) — drives auto-optimistic on the client AND
277
311
  * surfaces which tables this mutation touches (debug, future
278
312
  * query-invalidation analytics). Mutations without a target run
@@ -771,6 +805,11 @@ declare interface QueryProcedureDescriptor<Name extends string, Input extends Sc
771
805
  */
772
806
  export declare const requireAnyScope: (scopes: ReadonlyArray<string>) => RestGuard;
773
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
+
774
813
  /**
775
814
  * Guard that requires the resolved subject to carry `scope`. Anonymous /
776
815
  * scope-less subjects are rejected with `403`. The blanket admin scope
@@ -780,6 +819,11 @@ export declare const requireAnyScope: (scopes: ReadonlyArray<string>) => RestGua
780
819
  */
781
820
  export declare const requireScope: (scope: string) => RestGuard;
782
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
+
783
827
  /** A BINARY streaming response (download/export). The serve layer pipes the
784
828
  * ReadableStream without buffering — a body larger than the heap is fine.
785
829
  * The LAZY thunk form defers opening the source until the response streams. */
@@ -791,8 +835,12 @@ export declare interface RestByteResponse {
791
835
  readonly contentDisposition?: string;
792
836
  };
793
837
  readonly contentType?: string;
838
+ readonly status?: number;
839
+ readonly headers?: Readonly<Record<string, string | ReadonlyArray<string>>>;
794
840
  }
795
841
 
842
+ export declare const restError: (status: number, body: unknown, headers?: Readonly<Record<string, string | ReadonlyArray<string>>>) => RestRouteError;
843
+
796
844
  export declare type RestGuard = (ctx: RestRouteContext) => RestGuardRejection | undefined | Promise<RestGuardRejection | undefined>;
797
845
 
798
846
  /**
@@ -807,6 +855,17 @@ export declare interface RestGuardRejection {
807
855
 
808
856
  export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
809
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
+
810
869
  /**
811
870
  * Per-call context handed to a REST route handler. Resolved by the serve
812
871
  * pipeline: `subject` from the framework's auth resolver, `headers`
@@ -817,10 +876,22 @@ export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | '
817
876
  export declare interface RestRouteContext {
818
877
  readonly subject: Subject;
819
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;
820
887
  /** Framework `DataStore`. Untyped in protocol (same discipline as
821
888
  * `PluginHttpRoute` / `bindDataStore`); cast to the concrete store at
822
889
  * the call site. `undefined` only in unit tests that don't inject one. */
823
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;
824
895
  }
825
896
 
826
897
  export declare interface RestRouteDescriptor<I, O> {
@@ -836,7 +907,7 @@ export declare interface RestRouteDescriptor<I, O> {
836
907
  /** Output schema. The handler returns the decoded `Type`; the desugar
837
908
  * encodes it to JSON for the `200` response. */
838
909
  readonly output: Schema.Schema<O, any, never>;
839
- readonly handler: (input: I, ctx: RestRouteContext) => Promise<O> | O;
910
+ readonly handler: (input: I, ctx: RestRouteContext) => Promise<O | RestResponse<O>> | O | RestResponse<O>;
840
911
  readonly summary?: string;
841
912
  readonly description?: string;
842
913
  readonly example?: RestRouteExample;
@@ -884,6 +955,16 @@ export declare interface RestRouteDescriptor<I, O> {
884
955
  readonly etag?: boolean;
885
956
  }
886
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
+
887
968
  export declare interface RestRouteExample {
888
969
  readonly request?: unknown;
889
970
  readonly response?: unknown;
@@ -907,6 +988,8 @@ export declare interface RestServeBindings {
907
988
  readonly resolveSubject?: (headers: Readonly<Record<string, string>>) => Subject | Promise<Subject>;
908
989
  /** The bound framework DataStore. */
909
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>;
910
993
  /**
911
994
  * HTTP idempotency — when set, a mutating request (POST/PUT/PATCH/DELETE)
912
995
  * carrying the `header` is deduplicated: the first call runs + caches its
package/dist/rest.js CHANGED
@@ -1,39 +1,56 @@
1
- import { P as e } from "./scopes-D9t3tYc7.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 };