@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/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<GuardSpec<Input>>;
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-0wjvKwGO.js";
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 `NODE_ENV` is
6
- * `'production'` and `VOLTRO_SESSION_SECRET` is missing, the dev fallback, or
7
- * too short. No-op otherwise. Idempotent — safe to call from every prod entry.
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 production session secret. 32 bytes of
49
- * entropy is the floor for HMAC-SHA256; shorter values are almost always
50
- * hand-typed placeholders, not real secrets. */
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
- /** Resolve the HMAC session secret with the dev fallback applied. */
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` (dev fallback
66
- * applied), keyed `VOLTRO_SESSION_KID` (defaults to `'k0'`).
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 = "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD", c = () => process.env.VOLTRO_SESSION_SECRET ?? "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD", l = () => {
11
- let e = {
12
- kid: process.env.VOLTRO_SESSION_KID ?? "k0",
13
- secret: process.env.VOLTRO_SESSION_SECRET ?? "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD"
14
- }, t = process.env.VOLTRO_SESSION_SECRET_PREVIOUS;
15
- return t ? {
16
- current: e,
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: t
26
+ secret: n
20
27
  }
21
- } : { current: e };
22
- }, u = 32, d = (e = process.env) => {
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. In production the framework refuses to boot rather than sign session cookies with the PUBLIC dev fallback secret (which would let anyone forge any user's session). Set VOLTRO_SESSION_SECRET to a high-entropy value (>= 32 chars). Generate one with `voltro secret generate session`.");
26
- if (t === "voltro-dev-session-secret-32b-DO-NOT-USE-IN-PROD") throw Error("VOLTRO_SESSION_SECRET is set to the built-in dev fallback secret, which is PUBLIC in the framework source. Set a real, high-entropy secret in production. Generate one with `voltro secret generate session`.");
27
- if (t.length < 32) throw Error(`VOLTRO_SESSION_SECRET is only ${t.length} chars too short for a production session secret (need >= 32). This looks like a placeholder. Generate a real one with \`voltro secret generate session\`.`);
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
- }, g = 3600 * 24 * 7, _ = .7, v = (e, t) => m(n("sha256", t).update(e).digest()), y = (e) => typeof e == "string" ? {
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, b = (e, t, n = {}) => {
35
- let r = y(t);
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 ?? g, a = Math.floor((n.now ?? Date.now)() / 1e3), o = {
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 = m(JSON.stringify(f(o)));
43
- return `${s}.${v(s, r.secret)}`;
44
- }, x = (e, t, i, a) => {
45
- let o = n("sha256", i).update(e).digest(), s = h(t);
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 = h(e).toString("utf8"), n = p(JSON.parse(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
- }, S = (e, t) => {
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 = x(e.slice(0, n), e.slice(n + 1), t, Date.now);
62
+ let r = C(e.slice(0, n), e.slice(n + 1), t, Date.now);
58
63
  return r ? r.subject : null;
59
- }, C = (e, t, n = {}) => {
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 = x(i, a, e.secret, s);
71
+ let t = C(i, a, e.secret, s);
67
72
  if (!t) continue;
68
- let r = n.renewFraction ?? _, o = Math.floor(s() / 1e3), c = t.exp - t.iat, l = t.iat + c * r;
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
- }, w = (e, t) => {
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
- }, T = (e, t) => {
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 : w(n.slice(7).trim(), t);
85
- }, E = (e, t) => {
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
- }, D = (e, t, n = {}) => {
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, u as MIN_SESSION_SECRET_LENGTH, s as VOLTRO_DEV_SESSION_SECRET, d as assertProductionSessionSecret, D as buildSetCookie, T as checkBearer, E as readCookie, c as resolveSessionSecret, l as resolveSessionSecrets, b as signSession, w as timingSafeStringEqual, S as verifySession, C as verifySessionKeyed };
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.4.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.4.0",
56
+ "@voltro/database": "0.6.0",
57
57
  "jose": "^6.2.3"
58
58
  },
59
59
  "peerDependencies": {
Binary file