@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/CHANGELOG.md +94 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.js +461 -454
- package/dist/rest.d.ts +62 -1
- package/dist/rest.js +135 -87
- package/dist/{serverErrorBus-QQgyq6fV.js → serverErrorBus-BTnTpMqj.js} +17 -8
- package/package.json +3 -3
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
|
|
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 {
|
|
3
|
-
import { Effect as
|
|
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
|
|
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 =
|
|
9
|
-
r(
|
|
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
|
|
11
|
+
return b({
|
|
12
12
|
method: i,
|
|
13
13
|
path: a,
|
|
14
|
-
input:
|
|
14
|
+
input: s,
|
|
15
15
|
output: e.output,
|
|
16
|
-
handler:
|
|
17
|
-
...
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
}),
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
}),
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
89
|
-
query:
|
|
107
|
+
}, j = (e, t) => ({
|
|
108
|
+
query: k(e.query),
|
|
90
109
|
params: t,
|
|
91
|
-
body:
|
|
92
|
-
}),
|
|
110
|
+
body: O(e.rawBody)
|
|
111
|
+
}), M = /* @__PURE__ */ new Set([
|
|
93
112
|
"POST",
|
|
94
113
|
"PUT",
|
|
95
114
|
"PATCH",
|
|
96
115
|
"DELETE"
|
|
97
|
-
]),
|
|
98
|
-
let
|
|
99
|
-
return
|
|
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:
|
|
102
|
-
...
|
|
103
|
-
handle: async (
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
127
|
+
allow: l.method
|
|
108
128
|
}, {
|
|
109
|
-
...
|
|
110
|
-
Allow:
|
|
129
|
+
...p,
|
|
130
|
+
Allow: l.method
|
|
111
131
|
});
|
|
112
|
-
if (
|
|
113
|
-
let e = Date.parse(
|
|
114
|
-
if (!Number.isNaN(e) && Date.now() >= e) return
|
|
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
|
-
...
|
|
117
|
-
...
|
|
118
|
-
},
|
|
136
|
+
...l.version === void 0 ? {} : { version: l.version },
|
|
137
|
+
...l.deprecated === void 0 ? {} : { replacement: l.deprecated }
|
|
138
|
+
}, p);
|
|
119
139
|
}
|
|
120
|
-
let
|
|
121
|
-
if (
|
|
122
|
-
let 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
|
-
})),
|
|
145
|
+
})), s.catchAll((e) => s.succeed({
|
|
126
146
|
ok: !1,
|
|
127
147
|
cause: e
|
|
128
148
|
}))));
|
|
129
|
-
if (!t.ok) return
|
|
149
|
+
if (!t.ok) return D(400, {
|
|
130
150
|
error: "Invalid request",
|
|
131
151
|
detail: String(t.cause)
|
|
132
|
-
},
|
|
133
|
-
|
|
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
|
|
136
|
-
subject:
|
|
137
|
-
headers:
|
|
138
|
-
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 (
|
|
141
|
-
let t = await e(
|
|
142
|
-
if (t) return
|
|
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
|
|
145
|
-
if (
|
|
146
|
-
let e = await
|
|
147
|
-
if (e.kind === "replay") return
|
|
148
|
-
...
|
|
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
|
|
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
|
|
155
|
-
if (
|
|
156
|
-
status: 200,
|
|
157
|
-
headers:
|
|
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:
|
|
200
|
+
headers: p,
|
|
163
201
|
stream: e.stream
|
|
164
202
|
};
|
|
165
|
-
let t = await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
...
|
|
215
|
+
...p,
|
|
175
216
|
ETag: e
|
|
176
217
|
}
|
|
177
|
-
} :
|
|
178
|
-
...
|
|
218
|
+
} : D(t.status, r, {
|
|
219
|
+
...i,
|
|
179
220
|
ETag: e
|
|
180
221
|
});
|
|
181
222
|
}
|
|
182
|
-
return
|
|
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 (
|
|
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
|
|
234
|
+
return D(t.status, { error: t.message ?? "Error" }, p);
|
|
187
235
|
}
|
|
188
|
-
return
|
|
236
|
+
return o({
|
|
189
237
|
error: e,
|
|
190
238
|
source: "rest",
|
|
191
|
-
name: `${
|
|
239
|
+
name: `${l.method} ${l.path}`,
|
|
192
240
|
fields: { status: 500 }
|
|
193
|
-
}),
|
|
241
|
+
}), D(500, { error: "Internal Server Error" }, p);
|
|
194
242
|
}
|
|
195
243
|
}
|
|
196
244
|
};
|
|
197
|
-
},
|
|
245
|
+
}, P = (e, t = {}) => {
|
|
198
246
|
if (t.idempotency !== void 0) {
|
|
199
|
-
for (let t of e) if (t.streaming === !0 &&
|
|
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) =>
|
|
202
|
-
},
|
|
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,
|
|
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}
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
44
|
-
for (let t of
|
|
52
|
+
}, l = 1e3, u = /* @__PURE__ */ new Set(), d = (e) => {
|
|
53
|
+
for (let t of u) try {
|
|
45
54
|
t(e);
|
|
46
55
|
} catch {}
|
|
47
|
-
},
|
|
48
|
-
|
|
56
|
+
}, f = (e) => (u.add(e), () => {
|
|
57
|
+
u.delete(e);
|
|
49
58
|
});
|
|
50
59
|
//#endregion
|
|
51
|
-
export {
|
|
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.
|
|
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.
|
|
63
|
-
"@voltro/logger": "0.
|
|
62
|
+
"@voltro/database": "0.66.0",
|
|
63
|
+
"@voltro/logger": "0.66.0",
|
|
64
64
|
"jose": "^6.2.8"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|