@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.
- package/CHANGELOG.md +161 -0
- package/dist/index.d.ts +109 -7
- package/dist/index.js +492 -478
- package/dist/rest.d.ts +86 -3
- package/dist/rest.js +135 -87
- package/dist/{scopes-D9t3tYc7.js → scopes-C86BDyNJ.js} +33 -19
- package/dist/{serverErrorBus-QQgyq6fV.js → serverErrorBus-BTnTpMqj.js} +17 -8
- package/dist/session.js +6 -6
- package/package.json +3 -3
|
@@ -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 }) {},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
98
|
-
let t = e.ttlMs ??
|
|
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
|
-
},
|
|
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 &&
|
|
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(
|
|
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 }),
|
|
239
|
-
|
|
240
|
-
},
|
|
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"),
|
|
254
|
+
].join("\n"), le = () => J, Y, X = (e) => {
|
|
255
255
|
Y = e;
|
|
256
|
-
},
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
},
|
|
307
|
+
}, fe = (e, n, r, i) => {
|
|
301
308
|
if (!n || n.length === 0) return t.succeed($(e, n, i));
|
|
302
|
-
let a =
|
|
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*
|
|
321
|
+
let t = yield* de(e, i, r);
|
|
308
322
|
if (t) return t;
|
|
309
323
|
continue;
|
|
310
324
|
}
|
|
311
|
-
let n =
|
|
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 {
|
|
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}
|
|
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/dist/session.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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 = (
|
|
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
|
|
45
|
-
if (o.length > 0) throw Error(`signSession: refusing to mint a session cookie for a ${
|
|
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(
|
|
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.
|
|
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": {
|