@voltro/protocol 0.4.0 → 0.6.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 +68 -0
- package/dist/apikey.d.ts +6 -0
- package/dist/apikey.js +4 -1
- package/dist/index.d.ts +280 -3
- package/dist/index.js +278 -230
- package/dist/rest.d.ts +34 -1
- package/dist/rest.js +1 -1
- package/dist/serverErrorBus-B3hDwGoL.js +0 -0
- package/dist/session.d.ts +22 -11
- package/dist/session.js +40 -35
- package/package.json +2 -2
- package/dist/serverErrorBus-0wjvKwGO.js +0 -0
package/dist/rest.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ declare interface ActionProcedureDescriptor<Name extends string, Input extends S
|
|
|
15
15
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** A guard is either a scope check or a relationship check. */
|
|
19
|
+
declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* Project every descriptor carrying a `publicApi` annotation into a REST
|
|
20
23
|
* route, in stable (path) order. The boot layer feeds these straight into
|
|
@@ -77,7 +80,7 @@ declare interface ExposeAsToolSpec {
|
|
|
77
80
|
|
|
78
81
|
/** One or more declarative guards on a descriptor. All must pass (AND across
|
|
79
82
|
* entries; `mode` controls AND/OR WITHIN one entry's scope array). */
|
|
80
|
-
declare type Guards<Input = unknown> = ReadonlyArray<
|
|
83
|
+
declare type Guards<Input = unknown> = ReadonlyArray<AnyGuardSpec<Input>>;
|
|
81
84
|
|
|
82
85
|
declare interface GuardSpec<Input = unknown> {
|
|
83
86
|
/** Required permission scope(s). A single string, or an array combined by
|
|
@@ -253,6 +256,36 @@ declare interface PluginHttpRouteResult {
|
|
|
253
256
|
readonly headers?: Record<string, string>;
|
|
254
257
|
}
|
|
255
258
|
|
|
259
|
+
/**
|
|
260
|
+
* A relationship (ReBAC) guard — "may this subject perform ACTION on THIS row?"
|
|
261
|
+
*
|
|
262
|
+
* The scope guard above answers "what may this subject do at all"; this answers
|
|
263
|
+
* "on which row". Both live in the same `guards:` array on purpose, because the
|
|
264
|
+
* alternative is what apps actually built: a hand-maintained map from rpc tag →
|
|
265
|
+
* policy rule, installed as an interceptor. That map is FAIL-OPEN BY OMISSION —
|
|
266
|
+
* add an rpc, forget the entry, and it is silently unguarded. A declaration on
|
|
267
|
+
* the descriptor cannot be forgotten for an rpc that exists, because it IS the
|
|
268
|
+
* rpc.
|
|
269
|
+
*
|
|
270
|
+
* Browser-safe by the same construction as `GuardSpec`: strings plus a PURE
|
|
271
|
+
* `resource` extractor. Resolution — reading relationship tuples, applying the
|
|
272
|
+
* policy's `implies` closure — happens server-side through the registered
|
|
273
|
+
* policy resolver. With no resolver registered the check FAILS CLOSED: an
|
|
274
|
+
* unanswerable authorization question is a denial, never a pass.
|
|
275
|
+
*
|
|
276
|
+
* Because it is data, the same declaration compiles into the capability
|
|
277
|
+
* manifest the client reads, so a UI gate and the server check cannot drift.
|
|
278
|
+
*/
|
|
279
|
+
declare interface PolicyGuardSpec<Input = unknown> {
|
|
280
|
+
/** The action to authorize, as named in the resource policy's `actions`. */
|
|
281
|
+
readonly action: string;
|
|
282
|
+
/** Which registered resource policy governs the check. */
|
|
283
|
+
readonly resourceType: string;
|
|
284
|
+
/** PURE `input → resource id`. Returning undefined DENIES — an unidentifiable
|
|
285
|
+
* resource is not a reason to skip the check. */
|
|
286
|
+
readonly resource: (input: Input) => string | undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
256
289
|
/** A descriptor kind that can be projected to REST (streams are not, in v1). */
|
|
257
290
|
export declare type PublicApiDescriptor = QueryProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | MutationProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All> | ActionProcedureDescriptor<string, Schema.Schema.Any, Schema.Schema.Any, Schema.Schema.All>;
|
|
258
291
|
|
package/dist/rest.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-
|
|
1
|
+
import { a as e, i as t, o as n, r, t as i } from "./serverErrorBus-B3hDwGoL.js";
|
|
2
2
|
import { s as a } from "./auth-DCE6m7Bo.js";
|
|
3
3
|
import { Effect as o, Schema as s } from "effect";
|
|
4
4
|
//#region src/publicApi.ts
|
|
Binary file
|
package/dist/session.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { Schema } from 'effect';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Fail-closed check that a real session secret is configured. Throws a plain
|
|
5
|
-
* `Error` (a fatal boot condition, not a wire error) when
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
5
|
+
* `Error` (a fatal boot condition, not a wire error) when
|
|
6
|
+
* `VOLTRO_SESSION_SECRET` is missing or too short. Idempotent — safe to call
|
|
7
|
+
* from every serve entry.
|
|
8
8
|
*/
|
|
9
9
|
export declare const assertProductionSessionSecret: (env?: NodeJS.ProcessEnv) => void;
|
|
10
10
|
|
|
@@ -45,9 +45,9 @@ export declare interface KeyedSecret {
|
|
|
45
45
|
readonly secret: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/** Minimum length we accept for a
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
/** Minimum length we accept for a session secret. 32 bytes of entropy is the
|
|
49
|
+
* floor for HMAC-SHA256; shorter values are almost always hand-typed
|
|
50
|
+
* placeholders, not real secrets. */
|
|
51
51
|
export declare const MIN_SESSION_SECRET_LENGTH: 32;
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -56,14 +56,27 @@ export declare const MIN_SESSION_SECRET_LENGTH: 32;
|
|
|
56
56
|
*/
|
|
57
57
|
export declare const readCookie: (header: string | undefined, name: string) => string | undefined;
|
|
58
58
|
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* The same resolution as `resolveSessionSecrets`, but yielding `undefined`
|
|
61
|
+
* instead of throwing when `VOLTRO_SESSION_SECRET` is unset.
|
|
62
|
+
*
|
|
63
|
+
* This exists for callers that were GIVEN a secret explicitly and consult the
|
|
64
|
+
* environment only for adornment — the `kid` to stamp, and any `previous` key
|
|
65
|
+
* to also accept during a rotation. For them an unconfigured environment is
|
|
66
|
+
* simply "no rotation set up", not an error: they already hold everything
|
|
67
|
+
* needed to sign and verify. Making them go through the throwing resolver
|
|
68
|
+
* would turn a working explicit-secret setup into a boot failure.
|
|
69
|
+
*/
|
|
70
|
+
export declare const resolveOptionalSessionSecrets: () => SessionSecrets | undefined;
|
|
71
|
+
|
|
72
|
+
/** Resolve the HMAC session secret. Throws when unset — there is no fallback. */
|
|
60
73
|
export declare const resolveSessionSecret: () => string;
|
|
61
74
|
|
|
62
75
|
/**
|
|
63
76
|
* Resolve the keyed session-secret set for multi-key rotation.
|
|
64
77
|
*
|
|
65
|
-
* - `current` is built from `VOLTRO_SESSION_SECRET` (
|
|
66
|
-
*
|
|
78
|
+
* - `current` is built from `VOLTRO_SESSION_SECRET` (required — throws
|
|
79
|
+
* when unset), keyed `VOLTRO_SESSION_KID` (defaults to `'k0'`).
|
|
67
80
|
* - `previous` is built from `VOLTRO_SESSION_SECRET_PREVIOUS`, keyed
|
|
68
81
|
* `VOLTRO_SESSION_KID_PREVIOUS` (defaults to `'k-previous'` — the kid
|
|
69
82
|
* is a non-secret label, so rotation needs only the secret var).
|
|
@@ -257,6 +270,4 @@ export declare const verifySession: (cookieValue: string, secret: string) => Sub
|
|
|
257
270
|
*/
|
|
258
271
|
export declare const verifySessionKeyed: (cookieValue: string, secrets: SessionSecrets, options?: VerifyOptions) => VerifyResult | null;
|
|
259
272
|
|
|
260
|
-
export declare const VOLTRO_DEV_SESSION_SECRET: "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD";
|
|
261
|
-
|
|
262
273
|
export { }
|
package/dist/session.js
CHANGED
|
@@ -7,65 +7,70 @@ var i = t.Struct({
|
|
|
7
7
|
exp: t.Number,
|
|
8
8
|
iat: t.Number,
|
|
9
9
|
kid: t.optional(t.String)
|
|
10
|
-
}), a = "k0", o = "k-previous", s =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
}), a = "k0", o = "k-previous", s = () => {
|
|
11
|
+
throw Error("VOLTRO_SESSION_SECRET is not set. It signs and verifies session cookies, and the framework refuses to invent one at this point: a fixed fallback would be readable by anyone with the source, making every session forgeable. `voltro dev` mints a project-local value into .env.local on boot; for a deployment, generate one with `voltro secret generate session`.");
|
|
12
|
+
}, c = () => process.env.VOLTRO_SESSION_SECRET ?? s(), l = () => d() ?? { current: {
|
|
13
|
+
kid: u(),
|
|
14
|
+
secret: c()
|
|
15
|
+
} }, u = () => process.env.VOLTRO_SESSION_KID ?? "k0", d = () => {
|
|
16
|
+
let e = process.env.VOLTRO_SESSION_SECRET;
|
|
17
|
+
if (e === void 0 || e === "") return;
|
|
18
|
+
let t = {
|
|
19
|
+
kid: u(),
|
|
20
|
+
secret: e
|
|
21
|
+
}, n = process.env.VOLTRO_SESSION_SECRET_PREVIOUS;
|
|
22
|
+
return n ? {
|
|
23
|
+
current: t,
|
|
17
24
|
previous: {
|
|
18
25
|
kid: process.env.VOLTRO_SESSION_KID_PREVIOUS ?? "k-previous",
|
|
19
|
-
secret:
|
|
26
|
+
secret: n
|
|
20
27
|
}
|
|
21
|
-
} : { current:
|
|
22
|
-
},
|
|
23
|
-
if (e.NODE_ENV !== "production") return;
|
|
28
|
+
} : { current: t };
|
|
29
|
+
}, f = 32, p = (e = process.env) => {
|
|
24
30
|
let t = e.VOLTRO_SESSION_SECRET;
|
|
25
|
-
if (t === void 0 || t === "") throw Error("VOLTRO_SESSION_SECRET is not set
|
|
26
|
-
if (t
|
|
27
|
-
|
|
28
|
-
}, f = t.encodeSync(i), p = t.decodeUnknownSync(i), m = (e) => (typeof e == "string" ? Buffer.from(e, "utf8") : e).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), h = (e) => {
|
|
31
|
+
if (t === void 0 || t === "") throw Error("VOLTRO_SESSION_SECRET is not set — refusing to serve. It signs session cookies, and there is no fallback: a built-in one would be readable by anyone with the source. Generate one with `voltro secret generate session`. (`voltro dev` mints a project-local value automatically; a deployment needs its own.)");
|
|
32
|
+
if (t.length < 32) throw Error(`VOLTRO_SESSION_SECRET is only ${t.length} chars — too short for a session secret (need >= 32). This looks like a placeholder. Generate a real one with \`voltro secret generate session\`.`);
|
|
33
|
+
}, m = t.encodeSync(i), h = t.decodeUnknownSync(i), g = (e) => (typeof e == "string" ? Buffer.from(e, "utf8") : e).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), _ = (e) => {
|
|
29
34
|
let t = e.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - e.length % 4) % 4);
|
|
30
35
|
return Buffer.from(t, "base64");
|
|
31
|
-
},
|
|
36
|
+
}, v = 3600 * 24 * 7, y = .7, b = (e, t) => g(n("sha256", t).update(e).digest()), x = (e) => typeof e == "string" ? {
|
|
32
37
|
kid: "k0",
|
|
33
38
|
secret: e
|
|
34
|
-
} : e,
|
|
35
|
-
let r =
|
|
39
|
+
} : e, S = (e, t, n = {}) => {
|
|
40
|
+
let r = x(t);
|
|
36
41
|
if (!r.secret) throw Error("signSession: secret must be a non-empty string");
|
|
37
|
-
let i = n.ttlSeconds ??
|
|
42
|
+
let i = n.ttlSeconds ?? v, a = Math.floor((n.now ?? Date.now)() / 1e3), o = {
|
|
38
43
|
subject: e,
|
|
39
44
|
exp: a + i,
|
|
40
45
|
iat: a,
|
|
41
46
|
...typeof t == "string" ? {} : { kid: r.kid }
|
|
42
|
-
}, s =
|
|
43
|
-
return `${s}.${
|
|
44
|
-
},
|
|
45
|
-
let o = n("sha256", i).update(e).digest(), s =
|
|
47
|
+
}, s = g(JSON.stringify(m(o)));
|
|
48
|
+
return `${s}.${b(s, r.secret)}`;
|
|
49
|
+
}, C = (e, t, i, a) => {
|
|
50
|
+
let o = n("sha256", i).update(e).digest(), s = _(t);
|
|
46
51
|
if (s.length !== o.length || !r(s, o)) return null;
|
|
47
52
|
try {
|
|
48
|
-
let t =
|
|
53
|
+
let t = _(e).toString("utf8"), n = h(JSON.parse(t));
|
|
49
54
|
return n.exp < Math.floor(a() / 1e3) ? null : n;
|
|
50
55
|
} catch {
|
|
51
56
|
return null;
|
|
52
57
|
}
|
|
53
|
-
},
|
|
58
|
+
}, w = (e, t) => {
|
|
54
59
|
if (!e || !t) return null;
|
|
55
60
|
let n = e.indexOf(".");
|
|
56
61
|
if (n <= 0 || n === e.length - 1) return null;
|
|
57
|
-
let r =
|
|
62
|
+
let r = C(e.slice(0, n), e.slice(n + 1), t, Date.now);
|
|
58
63
|
return r ? r.subject : null;
|
|
59
|
-
},
|
|
64
|
+
}, T = (e, t, n = {}) => {
|
|
60
65
|
if (!e) return null;
|
|
61
66
|
let r = e.indexOf(".");
|
|
62
67
|
if (r <= 0 || r === e.length - 1) return null;
|
|
63
68
|
let i = e.slice(0, r), a = e.slice(r + 1), o = t.previous ? [t.current, t.previous] : [t.current], s = n.now ?? Date.now;
|
|
64
69
|
for (let e of o) {
|
|
65
70
|
if (!e.secret) continue;
|
|
66
|
-
let t =
|
|
71
|
+
let t = C(i, a, e.secret, s);
|
|
67
72
|
if (!t) continue;
|
|
68
|
-
let r = n.renewFraction ??
|
|
73
|
+
let r = n.renewFraction ?? y, o = Math.floor(s() / 1e3), c = t.exp - t.iat, l = t.iat + c * r;
|
|
69
74
|
return {
|
|
70
75
|
subject: t.subject,
|
|
71
76
|
renew: o >= l,
|
|
@@ -75,14 +80,14 @@ var i = t.Struct({
|
|
|
75
80
|
};
|
|
76
81
|
}
|
|
77
82
|
return null;
|
|
78
|
-
},
|
|
83
|
+
}, E = (e, t) => {
|
|
79
84
|
let n = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
|
|
80
85
|
return n.length === i.length && r(n, i);
|
|
81
|
-
},
|
|
86
|
+
}, D = (e, t) => {
|
|
82
87
|
if (!t || t.length === 0) return !0;
|
|
83
88
|
let n = e.authorization ?? e.Authorization;
|
|
84
|
-
return !n || !n.startsWith("Bearer ") ? !1 :
|
|
85
|
-
},
|
|
89
|
+
return !n || !n.startsWith("Bearer ") ? !1 : E(n.slice(7).trim(), t);
|
|
90
|
+
}, O = (e, t) => {
|
|
86
91
|
if (e) for (let n of e.split(";")) {
|
|
87
92
|
let e = n.indexOf("=");
|
|
88
93
|
if (e < 0 || n.slice(0, e).trim() !== t) continue;
|
|
@@ -94,7 +99,7 @@ var i = t.Struct({
|
|
|
94
99
|
return r;
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
|
-
},
|
|
102
|
+
}, k = (e, t, n = {}) => {
|
|
98
103
|
let r = [`${e}=${encodeURIComponent(t)}`];
|
|
99
104
|
if (r.push(`Path=${n.path ?? "/"}`), n.maxAgeSeconds !== void 0 && r.push(`Max-Age=${n.maxAgeSeconds}`), r.push("HttpOnly"), n.httpOnly === !1) {
|
|
100
105
|
let e = r.indexOf("HttpOnly");
|
|
@@ -103,4 +108,4 @@ var i = t.Struct({
|
|
|
103
108
|
return r.push(`SameSite=${n.sameSite ?? "Lax"}`), (n.secure ?? !0) && r.push("Secure"), n.domain && r.push(`Domain=${n.domain}`), r.join("; ");
|
|
104
109
|
};
|
|
105
110
|
//#endregion
|
|
106
|
-
export { o as DEFAULT_PREVIOUS_SESSION_KID, a as DEFAULT_SESSION_KID,
|
|
111
|
+
export { o as DEFAULT_PREVIOUS_SESSION_KID, a as DEFAULT_SESSION_KID, f as MIN_SESSION_SECRET_LENGTH, p as assertProductionSessionSecret, k as buildSetCookie, D as checkBearer, O as readCookie, d as resolveOptionalSessionSecrets, c as resolveSessionSecret, l as resolveSessionSecrets, S as signSession, E as timingSafeStringEqual, w as verifySession, T as verifySessionKeyed };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.6.0",
|
|
57
57
|
"jose": "^6.2.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
Binary file
|