@voltro/protocol 0.11.4 → 0.13.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 +356 -0
- package/dist/apikey.d.ts +41 -5
- package/dist/apikey.js +25 -19
- package/dist/{auth-DCE6m7Bo.js → auth-DVrHg739.js} +22 -6
- package/dist/index.d.ts +122 -12
- package/dist/index.js +109 -109
- package/dist/jwt.d.ts +18 -0
- package/dist/rest.d.ts +12 -0
- package/dist/rest.js +40 -34
- package/dist/serverErrorBus-DhIVDkCi.js +0 -0
- package/dist/session.d.ts +21 -3
- package/dist/session.js +5 -5
- package/package.json +2 -2
- package/dist/serverErrorBus-B3hDwGoL.js +0 -0
package/dist/rest.d.ts
CHANGED
|
@@ -425,6 +425,18 @@ declare interface QueryProcedureDescriptor<Name extends string, Input extends Sc
|
|
|
425
425
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Passes when the subject holds ANY of `scopes` (or `admin:full`).
|
|
430
|
+
*
|
|
431
|
+
* For a route whose permitted callers differ by REQUEST SHAPE rather than by
|
|
432
|
+
* route — issuing an api key for yourself, for the org, or on behalf of someone
|
|
433
|
+
* else are three different rights on one endpoint. The guard establishes "may
|
|
434
|
+
* mint something"; the handler decides which, because only the body says which
|
|
435
|
+
* was asked for. Splitting them into three routes instead would move a decision
|
|
436
|
+
* that depends on the body into the URL, where a client could pick.
|
|
437
|
+
*/
|
|
438
|
+
export declare const requireAnyScope: (scopes: ReadonlyArray<string>) => RestGuard;
|
|
439
|
+
|
|
428
440
|
/**
|
|
429
441
|
* Guard that requires the resolved subject to carry `scope`. Anonymous /
|
|
430
442
|
* scope-less subjects are rejected with `403`. The blanket admin scope
|
package/dist/rest.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-
|
|
2
|
-
import { s as a } from "./auth-
|
|
1
|
+
import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-DhIVDkCi.js";
|
|
2
|
+
import { s as a } from "./auth-DVrHg739.js";
|
|
3
3
|
import { Effect as o, Schema as s } from "effect";
|
|
4
4
|
//#region src/publicApi.ts
|
|
5
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) => {
|
|
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)),
|
|
9
|
-
r(
|
|
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));
|
|
10
10
|
})) : (e, n) => t(e[o], n);
|
|
11
|
-
return
|
|
11
|
+
return m({
|
|
12
12
|
method: i,
|
|
13
13
|
path: a,
|
|
14
14
|
input: u,
|
|
15
15
|
output: e.output,
|
|
16
16
|
handler: _,
|
|
17
|
-
...
|
|
17
|
+
...p ? { 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 },
|
|
@@ -27,26 +27,32 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
|
|
|
27
27
|
status: 403,
|
|
28
28
|
message: `Missing required scope: ${e}`
|
|
29
29
|
};
|
|
30
|
-
}, p = (e) =>
|
|
30
|
+
}, p = (e) => (t) => {
|
|
31
|
+
let n = t.subject.scopes;
|
|
32
|
+
if (!(n && (n.includes("admin:full") || e.some((e) => n.includes(e))))) return {
|
|
33
|
+
status: 403,
|
|
34
|
+
message: `Missing required scope: one of ${e.join(", ")}`
|
|
35
|
+
};
|
|
36
|
+
}, m = (e) => e, h = (e, t = {}) => ({
|
|
31
37
|
__voltroRestStream: !0,
|
|
32
38
|
stream: {
|
|
33
39
|
subscribe: e,
|
|
34
40
|
...t.keepAliveMs === void 0 ? {} : { keepAliveMs: t.keepAliveMs }
|
|
35
41
|
}
|
|
36
|
-
}),
|
|
42
|
+
}), 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, n) => ({
|
|
37
43
|
status: e,
|
|
38
44
|
contentType: "application/json; charset=utf-8",
|
|
39
45
|
body: JSON.stringify(t),
|
|
40
46
|
...n ? { headers: n } : {}
|
|
41
|
-
}),
|
|
47
|
+
}), y = (e) => {
|
|
42
48
|
if (e.length === 0) return;
|
|
43
49
|
let t = new TextDecoder().decode(e);
|
|
44
50
|
if (t.trim() !== "") return JSON.parse(t);
|
|
45
|
-
},
|
|
51
|
+
}, b = (e) => {
|
|
46
52
|
let t = {};
|
|
47
53
|
for (let [n, r] of new URLSearchParams(e)) t[n] = r;
|
|
48
54
|
return t;
|
|
49
|
-
},
|
|
55
|
+
}, x = (e, t) => {
|
|
50
56
|
let n = e.split("/").filter((e) => e.length > 0), r = t.split("/").filter((e) => e.length > 0), i = {};
|
|
51
57
|
for (let e = 0; e < n.length; e++) {
|
|
52
58
|
let t = n[e];
|
|
@@ -60,22 +66,22 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
|
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
return i;
|
|
63
|
-
},
|
|
64
|
-
query:
|
|
69
|
+
}, S = (e, t) => ({
|
|
70
|
+
query: b(e.query),
|
|
65
71
|
params: t,
|
|
66
|
-
body:
|
|
67
|
-
}),
|
|
72
|
+
body: y(e.rawBody)
|
|
73
|
+
}), C = /* @__PURE__ */ new Set([
|
|
68
74
|
"POST",
|
|
69
75
|
"PUT",
|
|
70
76
|
"PATCH",
|
|
71
77
|
"DELETE"
|
|
72
|
-
]),
|
|
78
|
+
]), w = (c, l) => {
|
|
73
79
|
let u = c.input ? s.decodeUnknown(c.input) : void 0, d = s.encode(c.output), f = {};
|
|
74
80
|
return c.deprecated !== void 0 && (f.Deprecation = "true"), c.sunset !== void 0 && (f.Sunset = c.sunset), {
|
|
75
81
|
method: "*",
|
|
76
82
|
path: c.path,
|
|
77
83
|
handle: async (s) => {
|
|
78
|
-
if (s.method.toUpperCase() !== c.method) return
|
|
84
|
+
if (s.method.toUpperCase() !== c.method) return v(405, {
|
|
79
85
|
error: "Method Not Allowed",
|
|
80
86
|
allow: c.method
|
|
81
87
|
}, {
|
|
@@ -84,21 +90,21 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
|
|
|
84
90
|
});
|
|
85
91
|
if (c.sunset !== void 0) {
|
|
86
92
|
let e = Date.parse(c.sunset);
|
|
87
|
-
if (!Number.isNaN(e) && Date.now() >= e) return
|
|
93
|
+
if (!Number.isNaN(e) && Date.now() >= e) return v(410, {
|
|
88
94
|
error: "Gone",
|
|
89
95
|
...c.deprecated === void 0 ? {} : { replacement: c.deprecated }
|
|
90
96
|
}, f);
|
|
91
97
|
}
|
|
92
98
|
let p;
|
|
93
99
|
if (u) {
|
|
94
|
-
let e =
|
|
100
|
+
let e = S(s, x(c.path, s.path)), t = await o.runPromise(u(e).pipe(o.map((e) => ({
|
|
95
101
|
ok: !0,
|
|
96
102
|
value: e
|
|
97
103
|
})), o.catchAll((e) => o.succeed({
|
|
98
104
|
ok: !1,
|
|
99
105
|
cause: e
|
|
100
106
|
}))));
|
|
101
|
-
if (!t.ok) return
|
|
107
|
+
if (!t.ok) return v(400, {
|
|
102
108
|
error: "Invalid request",
|
|
103
109
|
detail: String(t.cause)
|
|
104
110
|
}, f);
|
|
@@ -111,47 +117,47 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
|
|
|
111
117
|
};
|
|
112
118
|
if (c.guards) for (let e of c.guards) {
|
|
113
119
|
let t = await e(h);
|
|
114
|
-
if (t) return
|
|
120
|
+
if (t) return v(t.status, { error: t.message }, f);
|
|
115
121
|
}
|
|
116
|
-
let
|
|
117
|
-
if (
|
|
118
|
-
let e = await r(
|
|
119
|
-
if (e.kind === "replay") return
|
|
122
|
+
let g = l.idempotency, y = g && C.has(c.method) ? s.headers[g.header.toLowerCase()] : void 0, b = g && y ? n(m.tenantId, c.method, c.path) : "";
|
|
123
|
+
if (g && y) {
|
|
124
|
+
let e = await r(g.store, b, y, g.ttlMs, Date.now());
|
|
125
|
+
if (e.kind === "replay") return v(e.response.status, e.response.body, {
|
|
120
126
|
...f,
|
|
121
127
|
"Idempotency-Replayed": "true"
|
|
122
128
|
});
|
|
123
|
-
if (e.kind === "conflict") return
|
|
129
|
+
if (e.kind === "conflict") return v(409, { error: "A request with this Idempotency-Key is already being processed" }, f);
|
|
124
130
|
}
|
|
125
131
|
try {
|
|
126
132
|
let t = await c.handler(p, h);
|
|
127
|
-
if (
|
|
133
|
+
if (_(t)) return {
|
|
128
134
|
status: 200,
|
|
129
135
|
headers: f,
|
|
130
136
|
stream: t.stream
|
|
131
137
|
};
|
|
132
138
|
let n = await o.runPromise(d(t));
|
|
133
|
-
return
|
|
139
|
+
return g && y && await e(g.store, b, y, {
|
|
134
140
|
status: 200,
|
|
135
141
|
body: n
|
|
136
|
-
}, Date.now()),
|
|
142
|
+
}, Date.now()), v(200, n, f);
|
|
137
143
|
} catch (e) {
|
|
138
|
-
if (
|
|
144
|
+
if (g && y && await t(g.store, b, y), typeof e == "object" && e && typeof e.status == "number") {
|
|
139
145
|
let t = e;
|
|
140
|
-
return
|
|
146
|
+
return v(t.status, { error: t.message ?? "Error" }, f);
|
|
141
147
|
}
|
|
142
148
|
return i({
|
|
143
149
|
error: e,
|
|
144
150
|
source: "rest",
|
|
145
151
|
name: `${c.method} ${c.path}`,
|
|
146
152
|
fields: { status: 500 }
|
|
147
|
-
}),
|
|
153
|
+
}), v(500, { error: "Internal Server Error" }, f);
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
};
|
|
151
|
-
},
|
|
157
|
+
}, T = (e, t = {}) => e.map((e) => w(e, t)), E = async (e, t) => {
|
|
152
158
|
let n = await e[0].handle(t);
|
|
153
159
|
for (let r = 1; r < e.length && n.status === 405; r++) n = await e[r].handle(t);
|
|
154
160
|
return n;
|
|
155
161
|
};
|
|
156
162
|
//#endregion
|
|
157
|
-
export { d as collectPublicApiRoutes,
|
|
163
|
+
export { d as collectPublicApiRoutes, m as defineRestRoute, c as derivePublicMethod, l as derivePublicPath, E as dispatchSharedPath, _ as isRestStreamResponse, x as matchPathParams, u as publicApiRoute, p as requireAnyScope, f as requireScope, T as restRoutesToHttpRoutes, h as sse, g as sseFrame };
|
|
Binary file
|
package/dist/session.d.ts
CHANGED
|
@@ -19,14 +19,32 @@ export declare const buildSetCookie: (name: string, value: string, options?: Set
|
|
|
19
19
|
/**
|
|
20
20
|
* Check an `Authorization: Bearer <token>` header against an expected token.
|
|
21
21
|
*
|
|
22
|
-
* - `expected` unset/empty → `
|
|
23
|
-
*
|
|
22
|
+
* - `expected` unset/empty → `false`, UNLESS the caller passes
|
|
23
|
+
* `{ openWhenUnset: true }`. See below.
|
|
24
24
|
* - header absent, or not using the `Bearer ` scheme → `false`.
|
|
25
25
|
* - presented token compared to `expected` in CONSTANT TIME.
|
|
26
26
|
*
|
|
27
27
|
* `headers` keys may be any casing (`authorization` / `Authorization`).
|
|
28
|
+
*
|
|
29
|
+
* **Why the default is closed.** It used to be open: an unset or empty
|
|
30
|
+
* `expected` returned `true`, on the reading that "no token configured" meant
|
|
31
|
+
* the caller chose not to gate the surface. That reading is only available to
|
|
32
|
+
* the caller, and one of them declared the opposite — `scimPlugin`'s `token` is
|
|
33
|
+
* a REQUIRED `string`, and `token: process.env.SCIM_TOKEN ?? ''` (the shape
|
|
34
|
+
* anyone writes) silently turned the gate off. The result was SCIM 2.0 Users
|
|
35
|
+
* and Groups readable with no credentials: a full directory dump plus the
|
|
36
|
+
* provisioning surface that can deactivate accounts. The failure was silent and
|
|
37
|
+
* likeliest exactly where it hurts — an env var set in production and unset in
|
|
38
|
+
* a preview environment or a fresh cluster.
|
|
39
|
+
*
|
|
40
|
+
* A two-argument helper cannot know its caller's intent, so it must not assume
|
|
41
|
+
* the permissive one. `openWhenUnset` makes the choice appear at the call site,
|
|
42
|
+
* where the caller who genuinely wants an ungated endpoint says so and the
|
|
43
|
+
* caller who forgot gets a 401 instead of an open door.
|
|
28
44
|
*/
|
|
29
|
-
export declare const checkBearer: (headers: Readonly<Record<string, string | undefined>>, expected: string | undefined
|
|
45
|
+
export declare const checkBearer: (headers: Readonly<Record<string, string | undefined>>, expected: string | undefined, options?: {
|
|
46
|
+
readonly openWhenUnset?: boolean;
|
|
47
|
+
}) => boolean;
|
|
30
48
|
|
|
31
49
|
/** Key id assigned to `VOLTRO_SESSION_SECRET_PREVIOUS` when no explicit
|
|
32
50
|
* `VOLTRO_SESSION_KID_PREVIOUS` is set. The kid is a non-secret label —
|
package/dist/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as e } from "./auth-
|
|
1
|
+
import { i as e } from "./auth-DVrHg739.js";
|
|
2
2
|
import { Schema as t } from "effect";
|
|
3
3
|
import { createHmac as n, timingSafeEqual as r } from "node:crypto";
|
|
4
4
|
//#region src/session.ts
|
|
@@ -83,10 +83,10 @@ var i = t.Struct({
|
|
|
83
83
|
}, E = (e, t) => {
|
|
84
84
|
let n = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
|
|
85
85
|
return n.length === i.length && r(n, i);
|
|
86
|
-
}, D = (e, t) => {
|
|
87
|
-
if (!t || t.length === 0) return !0;
|
|
88
|
-
let
|
|
89
|
-
return !
|
|
86
|
+
}, D = (e, t, n) => {
|
|
87
|
+
if (!t || t.length === 0) return n?.openWhenUnset === !0;
|
|
88
|
+
let r = e.authorization ?? e.Authorization;
|
|
89
|
+
return !r || !r.startsWith("Bearer ") ? !1 : E(r.slice(7).trim(), t);
|
|
90
90
|
}, O = (e, t) => {
|
|
91
91
|
if (e) for (let n of e.split(";")) {
|
|
92
92
|
let e = n.indexOf("=");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@effect/sql": "^0.51.1",
|
|
56
|
-
"@voltro/database": "0.
|
|
56
|
+
"@voltro/database": "0.13.0",
|
|
57
57
|
"jose": "^6.2.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
Binary file
|