@voltro/protocol 0.64.0 → 0.65.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 +67 -0
- package/dist/index.d.ts +75 -6
- package/dist/index.js +483 -476
- package/dist/rest.d.ts +24 -2
- package/dist/rest.js +1 -1
- package/dist/{scopes-D9t3tYc7.js → scopes-C86BDyNJ.js} +33 -19
- package/dist/session.js +6 -6
- package/package.json +3 -3
package/dist/rest.d.ts
CHANGED
|
@@ -71,8 +71,30 @@ declare interface AnyApprovalPolicy {
|
|
|
71
71
|
readonly reason?: string;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/** A guard is
|
|
75
|
-
declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input
|
|
74
|
+
/** A guard is a scope check, a relationship check, or an identity check. */
|
|
75
|
+
declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input> | AuthenticatedGuardSpec;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* "The caller must have an identity" — and nothing narrower.
|
|
79
|
+
*
|
|
80
|
+
* The third case the vocabulary was missing, and the gap was not theoretical.
|
|
81
|
+
* An app whose authority lives per-team in its executors holds NO raw scopes on
|
|
82
|
+
* the identity, so `{ scope }` refuses every real user; a relationship guard
|
|
83
|
+
* cannot help when the resource does not exist yet (the first message of a new
|
|
84
|
+
* thread); and `openAccess` would be a lie whenever the auth fallback is a
|
|
85
|
+
* deliberate anonymous subject. The one thing left to decide — is anybody
|
|
86
|
+
* there — had no spelling, so apps wrote a resource policy whose entire content
|
|
87
|
+
* was "you are logged in".
|
|
88
|
+
*
|
|
89
|
+
* `checkGuards` has always distinguished this internally: its return type is
|
|
90
|
+
* `ScopeError | Unauthenticated | null`. This makes the distinction sayable.
|
|
91
|
+
*
|
|
92
|
+
* It is a real DECISION, so it satisfies `security.defaultDeny` — unlike
|
|
93
|
+
* `openAccess`, it also enforces something.
|
|
94
|
+
*/
|
|
95
|
+
declare interface AuthenticatedGuardSpec {
|
|
96
|
+
readonly authenticated: true;
|
|
97
|
+
}
|
|
76
98
|
|
|
77
99
|
export declare const bytes: (stream: ReadableStream<Uint8Array> | (() => ReadableStream<Uint8Array>), options?: {
|
|
78
100
|
readonly contentType?: string;
|
package/dist/rest.js
CHANGED
|
@@ -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 };
|
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.65.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.65.0",
|
|
63
|
+
"@voltro/logger": "0.65.0",
|
|
64
64
|
"jose": "^6.2.8"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|