@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.
@@ -56,7 +56,7 @@ var i = n.Record({
56
56
  scopes: n.scopes ?? ["admin:full"],
57
57
  ...n.metadata === void 0 ? {} : { metadata: n.metadata }
58
58
  };
59
- }, g = (e) => e.type === "system", _ = "impersonation", v = (e) => e.metadata?.impersonation ?? null, y = class extends e.Tag("@voltro/Subject")() {}, b = class extends e.Tag("@voltro/ConnectionInfo")() {}, x = class extends r.Tag()("@voltro/AuthMiddleware", { provides: y }) {}, ne = class extends r.Tag()("@voltro/ConnectionInfoMiddleware", { provides: b }) {}, S = (e) => typeof e.mountRoutes == "function", C = (e) => Array.isArray(e) ? {
59
+ }, g = (e) => e.type === "system", _ = "impersonation", v = (e) => e.metadata?.impersonation ?? null, y = class extends e.Tag("@voltro/Subject")() {}, b = class extends e.Tag("@voltro/ConnectionInfo")() {}, x = class extends r.Tag()("@voltro/AuthMiddleware", { provides: y }) {}, S = class extends r.Tag()("@voltro/ConnectionInfoMiddleware", { provides: b }) {}, ne = (e) => typeof e.mountRoutes == "function", C = (e) => Array.isArray(e) ? {
60
60
  kind: "grant",
61
61
  scopes: e
62
62
  } : e, w = (e, t) => {
@@ -88,14 +88,14 @@ var i = n.Record({
88
88
  },
89
89
  removed: i
90
90
  };
91
- }, re = 3e4, ie = 1e4, T = (e) => `${e.type}\u0000${e.tenantId ?? ""}\u0000${e.id ?? ""}`, ae = () => {
91
+ }, T = 3e4, re = 1e4, E = (e) => `${e.type}\u0000${e.tenantId ?? ""}\u0000${e.id ?? ""}`, ie = () => {
92
92
  if (typeof process > "u") return;
93
93
  let e = process.env.VOLTRO_AUTH_SCOPE_CACHE_TTL_MS;
94
94
  if (e === void 0 || e === "") return;
95
95
  let t = Number(e);
96
96
  return Number.isFinite(t) && t >= 0 ? t : void 0;
97
- }, E = (e = {}) => {
98
- let t = e.ttlMs ?? ae() ?? 3e4, n = e.maxEntries ?? 1e4, r = e.now ?? Date.now, i = /* @__PURE__ */ new Map(), a = () => {
97
+ }, D = (e = {}) => {
98
+ let t = e.ttlMs ?? ie() ?? 3e4, n = e.maxEntries ?? 1e4, r = e.now ?? Date.now, i = /* @__PURE__ */ new Map(), a = () => {
99
99
  if (i.size <= n) return;
100
100
  let e = r();
101
101
  for (let [t, n] of i) n.freshUntil <= e && i.delete(t);
@@ -131,8 +131,8 @@ var i = n.Record({
131
131
  };
132
132
  }
133
133
  };
134
- }, D = (e) => typeof e.resolve == "function", O = (e, t) => {
135
- let n = t?.resolveScopes, r = t?.subjectGuards ?? [], i = n !== void 0 || r.length > 0, a = !i || t?.scopeCache === !1 ? null : t?.scopeCache !== void 0 && D(t.scopeCache) ? t.scopeCache : E(t?.scopeCache ?? {});
134
+ }, ae = (e) => typeof e.resolve == "function", O = (e, t) => {
135
+ let n = t?.resolveScopes, r = t?.subjectGuards ?? [], i = n !== void 0 || r.length > 0, a = !i || t?.scopeCache === !1 ? null : t?.scopeCache !== void 0 && ae(t.scopeCache) ? t.scopeCache : D(t?.scopeCache ?? {});
136
136
  return async (o) => {
137
137
  let s = t?.getStore?.(), c = s === void 0 ? o : {
138
138
  ...o,
@@ -167,7 +167,7 @@ var i = n.Record({
167
167
  }, { decision: l, fresh: u } = a === null ? {
168
168
  decision: await i(),
169
169
  fresh: !0
170
- } : await a.resolve(T(e.subject), i);
170
+ } : await a.resolve(E(e.subject), i);
171
171
  if (l.kind === "vetoed") {
172
172
  let e = `${l.code}: ${l.message}`;
173
173
  throw t?.onStrategyFailed?.({
@@ -235,9 +235,9 @@ var i = n.Record({
235
235
  }, H = (e, n) => B(e, n) ? t.void : t.fail(new F({
236
236
  required: n,
237
237
  message: `missing required scope '${n}'`
238
- })), U = (e) => typeof e.action == "string" && typeof e.resourceType == "string", W = (e) => typeof e.open == "string", G = (e) => ({ open: e }), K = (e) => typeof e == "string" ? [e] : e, q, J = (e) => {
239
- q = e;
240
- }, oe = (e) => q === void 0 ? e.filter((e) => (e.guards ?? []).some((e) => !U(e) && e.resource !== void 0)).map((e) => e.tag) : [], se = (e) => [
238
+ })), U = (e) => typeof e.action == "string" && typeof e.resourceType == "string", W = (e) => typeof e.open == "string", G = (e) => e.authenticated === !0, K = (e) => ({ open: e }), q = (e) => typeof e == "string" ? [e] : e, J, oe = (e) => {
239
+ J = e;
240
+ }, se = (e) => J === void 0 ? e.filter((e) => (e.guards ?? []).some((e) => !U(e) && e.resource !== void 0)).map((e) => e.tag) : [], ce = (e) => [
241
241
  `${e.length} procedure${e.length === 1 ? "" : "s"} declare a per-resource guard that nothing enforces:`,
242
242
  ...e.slice(0, 10).map((e) => ` ${e}`),
243
243
  ...e.length > 10 ? [` … and ${e.length - 10} more`] : [],
@@ -251,9 +251,9 @@ var i = n.Record({
251
251
  " • setResourceScopeResolver((req) => Effect<boolean>) — answers \"does this",
252
252
  " subject hold `req.scope` on `req.resource`\" from your own tables.",
253
253
  " • Drop the `resource` extractor if a global scope check is what you meant."
254
- ].join("\n"), ce = () => q, Y, le = (e) => {
254
+ ].join("\n"), le = () => J, Y, X = (e) => {
255
255
  Y = e;
256
- }, X = () => Y, Z = (e) => new F({
256
+ }, ue = () => Y, Z = (e) => new F({
257
257
  required: "access-decision",
258
258
  message: `${e ?? "this procedure"} declares no access decision: it has neither \`guards:\` nor \`openAccess:\`, and the app runs with \`security.defaultDeny\`. Add the scope(s) a caller must hold (\`guards: [{ scope: '…' }]\`), or declare it open on purpose with \`openAccess: '<why anyone may call this>'\`.`
259
259
  }), Q = (e, t) => {
@@ -263,6 +263,13 @@ var i = n.Record({
263
263
  if (!t || t.length === 0) return n?.defaultDeny === !0 ? Z(n.procedure) : null;
264
264
  for (let n of t) {
265
265
  if (W(n)) continue;
266
+ if (G(n)) {
267
+ if (e.id !== null && e.id !== void 0) continue;
268
+ return Q(e, new F({
269
+ required: "authenticated",
270
+ message: "this procedure requires an authenticated caller"
271
+ }));
272
+ }
266
273
  if (U(n)) {
267
274
  if (B(e, "admin:full")) continue;
268
275
  return Q(e, new F({
@@ -270,7 +277,7 @@ var i = n.Record({
270
277
  message: `relationship guard '${n.action}' on '${n.resourceType}' cannot be checked synchronously`
271
278
  }));
272
279
  }
273
- let t = K(n.scope);
280
+ let t = q(n.scope);
274
281
  if (t.length === 0) continue;
275
282
  let r = t.filter((t) => B(e, t));
276
283
  if (!((n.mode ?? "all") === "any" ? r.length > 0 : r.length === t.length)) {
@@ -282,7 +289,7 @@ var i = n.Record({
282
289
  }
283
290
  }
284
291
  return null;
285
- }, ue = (e, n, r) => {
292
+ }, de = (e, n, r) => {
286
293
  let i = (e) => new F({
287
294
  required: `${n.resourceType}:${n.action}`,
288
295
  message: e
@@ -297,18 +304,25 @@ var i = n.Record({
297
304
  resourceType: n.resourceType,
298
305
  resourceId: o
299
306
  })).pipe(t.catchAllCause(() => t.succeed(!1)), t.map((e) => e ? null : i(`no '${n.action}' relation on ${n.resourceType}:${o}`)));
300
- }, de = (e, n, r, i) => {
307
+ }, fe = (e, n, r, i) => {
301
308
  if (!n || n.length === 0) return t.succeed($(e, n, i));
302
- let a = q;
309
+ let a = J;
303
310
  return !n.some(U) && (!a || !n.some((e) => e.resource !== void 0)) ? t.succeed($(e, n, i)) : t.gen(function* () {
304
311
  for (let i of n) {
305
312
  if (W(i)) continue;
313
+ if (G(i)) {
314
+ if (e.id !== null && e.id !== void 0) continue;
315
+ return Q(e, new F({
316
+ required: "authenticated",
317
+ message: "this procedure requires an authenticated caller"
318
+ }));
319
+ }
306
320
  if (U(i)) {
307
- let t = yield* ue(e, i, r);
321
+ let t = yield* de(e, i, r);
308
322
  if (t) return t;
309
323
  continue;
310
324
  }
311
- let n = K(i.scope);
325
+ let n = q(i.scope);
312
326
  if (n.length === 0) continue;
313
327
  let o = i.resource ? i.resource(r) : void 0, s = [];
314
328
  for (let r of n) {
@@ -338,4 +352,4 @@ var i = n.Record({
338
352
  });
339
353
  };
340
354
  //#endregion
341
- export { d as A, E as B, I as C, ie as D, ne as E, w as F, te as G, T as H, A as I, O as L, y as M, k as N, re as O, h as P, S as R, J as S, b as T, p as U, v as V, ee as W, Z as _, F as a, R as b, de as c, X as d, ce as f, U as g, W as h, M as i, f as j, _ as k, z as l, V as m, N as n, se as o, B as p, P as r, $ as s, j as t, oe as u, G as v, x as w, le as x, H as y, g as z };
355
+ export { _ as A, g as B, oe as C, S as D, b as E, h as F, ee as G, v as H, w as I, te as K, A as L, f as M, y as N, re as O, k as P, O as R, X as S, x as T, E as U, D as V, p as W, U as _, F as a, H as b, fe as c, ue as d, le as f, W as g, G as h, M as i, d as j, T as k, z as l, V as m, N as n, ce as o, B as p, P as r, $ as s, j as t, se as u, Z as v, I as w, R as x, K as y, ne as z };
@@ -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}`, r = async (e, t, n, r, i) => {
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
- }, i = (e, t, n, r, i) => e.complete(t, n, r, i), a = (e, t, n) => e.release(t, n), o = () => {
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
- }, s = class extends e.TaggedError()("MutationInFlight", {
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
- }, c = 1e3, l = /* @__PURE__ */ new Set(), u = (e) => {
44
- for (let t of l) try {
52
+ }, l = 1e3, u = /* @__PURE__ */ new Set(), d = (e) => {
53
+ for (let t of u) try {
45
54
  t(e);
46
55
  } catch {}
47
- }, d = (e) => (l.add(e), () => {
48
- l.delete(e);
56
+ }, f = (e) => (u.add(e), () => {
57
+ u.delete(e);
49
58
  });
50
59
  //#endregion
51
- export { r as a, t as c, s as i, o as l, d as n, a as o, c as r, i as s, u as t, n as u };
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/dist/session.js CHANGED
@@ -1,11 +1,11 @@
1
- import { C as e, U as t, j as n } from "./scopes-D9t3tYc7.js";
1
+ import { M as e, W as t, w as n } from "./scopes-C86BDyNJ.js";
2
2
  import { Schema as r } from "effect";
3
3
  import { createHmac as i, timingSafeEqual as a } from "node:crypto";
4
4
  import { createLogger as o } from "@voltro/logger";
5
5
  //#region src/session.ts
6
6
  var s = o({ scope: "@voltro/protocol:session" }), c = 2, l = r.Struct({
7
7
  v: r.Literal(2),
8
- subject: n,
8
+ subject: e,
9
9
  exp: r.Number,
10
10
  iat: r.Number,
11
11
  kid: r.optional(r.String)
@@ -38,14 +38,14 @@ var s = o({ scope: "@voltro/protocol:session" }), c = 2, l = r.Struct({
38
38
  }, C = 604800, w = .7, T = (e, t) => x(i("sha256", t).update(e).digest()), E = (e) => typeof e == "string" ? {
39
39
  kid: "k0",
40
40
  secret: e
41
- } : e, D = (n, r, i = {}) => {
41
+ } : e, D = (e, r, i = {}) => {
42
42
  let a = E(r);
43
43
  if (!a.secret) throw Error("signSession: secret must be a non-empty string");
44
- let o = e(n);
45
- if (o.length > 0) throw Error(`signSession: refusing to mint a session cookie for a ${n.type} subject carrying ${o.length} scope(s) (${o.slice(0, 5).join(", ")}). A session cookie carries IDENTITY only — authority is resolved per request, so that removing a role takes effect on the session that already exists instead of in up to 7 days. Mint the session from the identity and move the scopes into the auth.resolveScopes resolver, which runs on every request and can be invalidated the moment a role changes.`);
44
+ let o = n(e);
45
+ if (o.length > 0) throw Error(`signSession: refusing to mint a session cookie for a ${e.type} subject carrying ${o.length} scope(s) (${o.slice(0, 5).join(", ")}). A session cookie carries IDENTITY only — authority is resolved per request, so that removing a role takes effect on the session that already exists instead of in up to 7 days. Mint the session from the identity and move the scopes into the auth.resolveScopes resolver, which runs on every request and can be invalidated the moment a role changes.`);
46
46
  let s = i.ttlSeconds ?? C, c = Math.floor((i.now ?? Date.now)() / 1e3), l = {
47
47
  v: 2,
48
- subject: t(n),
48
+ subject: t(e),
49
49
  exp: c + s,
50
50
  iat: c,
51
51
  ...typeof r == "string" ? {} : { kid: a.kid }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/protocol",
3
- "version": "0.64.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.64.0",
63
- "@voltro/logger": "0.64.0",
62
+ "@voltro/database": "0.66.0",
63
+ "@voltro/logger": "0.66.0",
64
64
  "jose": "^6.2.8"
65
65
  },
66
66
  "peerDependencies": {