@voltro/protocol 0.39.0 → 0.40.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 +210 -0
- package/dist/apikey.d.ts +17 -0
- package/dist/index.d.ts +78 -13
- package/dist/index.js +397 -393
- package/dist/jwt.d.ts +17 -0
- package/dist/rest.d.ts +17 -0
- package/dist/rest.js +1 -1
- package/dist/{auth-CKS3UNnf.js → scopes-B0kGvAnI.js} +169 -160
- package/dist/session.d.ts +35 -0
- package/dist/session.js +1 -1
- package/package.json +3 -3
package/dist/jwt.d.ts
CHANGED
|
@@ -282,6 +282,23 @@ declare const Subject: Schema.Union<[Schema.Struct<{
|
|
|
282
282
|
type: Schema.Literal<["anonymous"]>;
|
|
283
283
|
id: typeof Schema.Null;
|
|
284
284
|
tenantId: Schema.NullOr<typeof Schema.String>;
|
|
285
|
+
/**
|
|
286
|
+
* Set when this caller PRESENTED a credential and it was rejected — an
|
|
287
|
+
* expired token above all. Absent when they presented none.
|
|
288
|
+
*
|
|
289
|
+
* The two are the same Subject and must not be the same ANSWER. A consumer
|
|
290
|
+
* measured the cost: a user's tab outlived their IdP's token lifetime, the
|
|
291
|
+
* strategy logged `supabase jwt expired`, the caller fell through to
|
|
292
|
+
* anonymous, and the guard then refused with `missing required scope
|
|
293
|
+
* 'task:u:o'`. Technically true — an anonymous caller holds no scopes — and
|
|
294
|
+
* it sent everyone who read it into the permissions system while the problem
|
|
295
|
+
* was an expired session. They did that round.
|
|
296
|
+
*
|
|
297
|
+
* It stays a FALLBACK rather than a hard failure on purpose: a stale cookie
|
|
298
|
+
* must not break an `openAccess` procedure that needs no session at all. The
|
|
299
|
+
* fact travels, and only a guard that actually refuses spends it.
|
|
300
|
+
*/
|
|
301
|
+
credentialRejected: Schema.optional<typeof Schema.String>;
|
|
285
302
|
}>, Schema.Struct<{
|
|
286
303
|
type: Schema.Literal<["system"]>;
|
|
287
304
|
id: typeof Schema.String;
|
package/dist/rest.d.ts
CHANGED
|
@@ -893,6 +893,23 @@ declare const Subject: Schema.Union<[Schema.Struct<{
|
|
|
893
893
|
type: Schema.Literal<["anonymous"]>;
|
|
894
894
|
id: typeof Schema.Null;
|
|
895
895
|
tenantId: Schema.NullOr<typeof Schema.String>;
|
|
896
|
+
/**
|
|
897
|
+
* Set when this caller PRESENTED a credential and it was rejected — an
|
|
898
|
+
* expired token above all. Absent when they presented none.
|
|
899
|
+
*
|
|
900
|
+
* The two are the same Subject and must not be the same ANSWER. A consumer
|
|
901
|
+
* measured the cost: a user's tab outlived their IdP's token lifetime, the
|
|
902
|
+
* strategy logged `supabase jwt expired`, the caller fell through to
|
|
903
|
+
* anonymous, and the guard then refused with `missing required scope
|
|
904
|
+
* 'task:u:o'`. Technically true — an anonymous caller holds no scopes — and
|
|
905
|
+
* it sent everyone who read it into the permissions system while the problem
|
|
906
|
+
* was an expired session. They did that round.
|
|
907
|
+
*
|
|
908
|
+
* It stays a FALLBACK rather than a hard failure on purpose: a stale cookie
|
|
909
|
+
* must not break an `openAccess` procedure that needs no session at all. The
|
|
910
|
+
* fact travels, and only a guard that actually refuses spends it.
|
|
911
|
+
*/
|
|
912
|
+
credentialRejected: Schema.optional<typeof Schema.String>;
|
|
896
913
|
}>, Schema.Struct<{
|
|
897
914
|
type: Schema.Literal<["system"]>;
|
|
898
915
|
id: typeof Schema.String;
|
package/dist/rest.js
CHANGED
|
@@ -1,169 +1,53 @@
|
|
|
1
1
|
import { Context as e, Effect as t, Schema as n } from "effect";
|
|
2
2
|
import { RpcMiddleware as r } from "@effect/rpc";
|
|
3
|
-
//#region src/
|
|
4
|
-
var i =
|
|
5
|
-
required: n.String,
|
|
6
|
-
message: n.String
|
|
7
|
-
}) {}, l = (e) => "scopes" in e && e.scopes ? e.scopes : [], u = /* @__PURE__ */ new WeakMap(), d = (e, t) => {
|
|
8
|
-
typeof e == "object" && e && u.set(e, t);
|
|
9
|
-
}, f = (e) => {
|
|
10
|
-
if (typeof e == "object" && e) {
|
|
11
|
-
let t = u.get(e);
|
|
12
|
-
if (t) return t;
|
|
13
|
-
}
|
|
14
|
-
return l(e);
|
|
15
|
-
}, p = (e, t) => {
|
|
16
|
-
let n = f(e);
|
|
17
|
-
return n.includes("admin:full") || n.includes(t);
|
|
18
|
-
}, ee = (e, t) => {
|
|
19
|
-
let n = l(e);
|
|
20
|
-
return n.includes("admin:full") || n.includes(t);
|
|
21
|
-
}, te = (e, n) => p(e, n) ? t.void : t.fail(new c({
|
|
22
|
-
required: n,
|
|
23
|
-
message: `missing required scope '${n}'`
|
|
24
|
-
})), m = (e) => typeof e.action == "string" && typeof e.resourceType == "string", h = (e) => typeof e.open == "string", ne = (e) => ({ open: e }), g = (e) => typeof e == "string" ? [e] : e, _, v = (e) => {
|
|
25
|
-
_ = e;
|
|
26
|
-
}, y = (e) => _ === void 0 ? e.filter((e) => (e.guards ?? []).some((e) => !m(e) && e.resource !== void 0)).map((e) => e.tag) : [], b = (e) => [
|
|
27
|
-
`${e.length} procedure${e.length === 1 ? "" : "s"} declare a per-resource guard that nothing enforces:`,
|
|
28
|
-
...e.slice(0, 10).map((e) => ` ${e}`),
|
|
29
|
-
...e.length > 10 ? [` … and ${e.length - 10} more`] : [],
|
|
30
|
-
"",
|
|
31
|
-
" `guards: [{ scope, resource }]` reads as \"may you do this to THIS resource\",",
|
|
32
|
-
" but with no resource-scope resolver registered the extractor is advisory and",
|
|
33
|
-
" the check runs against the caller's GLOBAL scopes. For an app whose",
|
|
34
|
-
" authorization is per-resource, that guard passes callers it should refuse.",
|
|
35
|
-
"",
|
|
36
|
-
" Fix it one of two ways:",
|
|
37
|
-
" • setResourceScopeResolver((req) => Effect<boolean>) — answers \"does this",
|
|
38
|
-
" subject hold `req.scope` on `req.resource`\" from your own tables.",
|
|
39
|
-
" • Drop the `resource` extractor if a global scope check is what you meant."
|
|
40
|
-
].join("\n"), x = () => _, S, re = (e) => {
|
|
41
|
-
S = e;
|
|
42
|
-
}, C = () => S, w = (e) => new c({
|
|
43
|
-
required: "access-decision",
|
|
44
|
-
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>'\`.`
|
|
45
|
-
}), T = (e, t, n) => {
|
|
46
|
-
if (!t || t.length === 0) return n?.defaultDeny === !0 ? w(n.procedure) : null;
|
|
47
|
-
for (let n of t) {
|
|
48
|
-
if (h(n)) continue;
|
|
49
|
-
if (m(n)) {
|
|
50
|
-
if (p(e, "admin:full")) continue;
|
|
51
|
-
return new c({
|
|
52
|
-
required: `${n.resourceType}:${n.action}`,
|
|
53
|
-
message: `relationship guard '${n.action}' on '${n.resourceType}' cannot be checked synchronously`
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
let t = g(n.scope);
|
|
57
|
-
if (t.length === 0) continue;
|
|
58
|
-
let r = t.filter((t) => p(e, t));
|
|
59
|
-
if (!((n.mode ?? "all") === "any" ? r.length > 0 : r.length === t.length)) {
|
|
60
|
-
let r = (n.mode ?? "all") === "any" ? t[0] : t.find((t) => !p(e, t));
|
|
61
|
-
return new c({
|
|
62
|
-
required: r,
|
|
63
|
-
message: (n.mode ?? "all") === "any" ? `missing all of required scopes [${t.join(", ")}]` : `missing required scope '${r}'`
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
}, E = (e, n, r) => {
|
|
69
|
-
let i = (e) => new c({
|
|
70
|
-
required: `${n.resourceType}:${n.action}`,
|
|
71
|
-
message: e
|
|
72
|
-
});
|
|
73
|
-
if (p(e, "admin:full")) return t.succeed(null);
|
|
74
|
-
let a = S;
|
|
75
|
-
if (a === void 0) return t.succeed(i(`no policy resolver registered — cannot authorize '${n.action}' on '${n.resourceType}'`));
|
|
76
|
-
let o = n.resource(r);
|
|
77
|
-
return o === void 0 || o === "" ? t.succeed(i(`input does not identify a '${n.resourceType}' to authorize`)) : t.suspend(() => a({
|
|
78
|
-
subject: e,
|
|
79
|
-
action: n.action,
|
|
80
|
-
resourceType: n.resourceType,
|
|
81
|
-
resourceId: o
|
|
82
|
-
})).pipe(t.catchAllCause(() => t.succeed(!1)), t.map((e) => e ? null : i(`no '${n.action}' relation on ${n.resourceType}:${o}`)));
|
|
83
|
-
}, D = (e, n, r, i) => {
|
|
84
|
-
if (!n || n.length === 0) return t.succeed(T(e, n, i));
|
|
85
|
-
let a = _;
|
|
86
|
-
return !n.some(m) && (!a || !n.some((e) => e.resource !== void 0)) ? t.succeed(T(e, n, i)) : t.gen(function* () {
|
|
87
|
-
for (let i of n) {
|
|
88
|
-
if (h(i)) continue;
|
|
89
|
-
if (m(i)) {
|
|
90
|
-
let t = yield* E(e, i, r);
|
|
91
|
-
if (t) return t;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
let n = g(i.scope);
|
|
95
|
-
if (n.length === 0) continue;
|
|
96
|
-
let o = i.resource ? i.resource(r) : void 0, s = [];
|
|
97
|
-
for (let r of n) {
|
|
98
|
-
if (p(e, r)) {
|
|
99
|
-
s.push(!0);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (o !== void 0 && a !== void 0) {
|
|
103
|
-
let n = yield* t.suspend(() => a({
|
|
104
|
-
subject: e,
|
|
105
|
-
scope: r,
|
|
106
|
-
resource: o
|
|
107
|
-
})).pipe(t.catchAllCause(() => t.succeed(!1)));
|
|
108
|
-
s.push(n);
|
|
109
|
-
} else s.push(!1);
|
|
110
|
-
}
|
|
111
|
-
let l = i.mode ?? "all";
|
|
112
|
-
if (!(l === "any" ? s.some(Boolean) : s.every(Boolean))) {
|
|
113
|
-
let e = l === "any" ? n[0] : n[n.findIndex((e, t) => !s[t])];
|
|
114
|
-
return new c({
|
|
115
|
-
required: e,
|
|
116
|
-
message: l === "any" ? `missing all of required scopes [${n.join(", ")}]` : `missing required scope '${e}'` + (o === void 0 ? "" : ` on resource '${o}'`)
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
});
|
|
122
|
-
}, O = n.Record({
|
|
3
|
+
//#region src/auth.ts
|
|
4
|
+
var i = n.Record({
|
|
123
5
|
key: n.String,
|
|
124
6
|
value: n.Unknown
|
|
125
|
-
}),
|
|
7
|
+
}), a = n.optional(n.Array(n.String)), o = n.Struct({
|
|
126
8
|
type: n.Literal("user"),
|
|
127
9
|
id: n.String,
|
|
128
10
|
tenantId: n.String,
|
|
129
|
-
scopes:
|
|
130
|
-
metadata: n.optional(
|
|
131
|
-
}),
|
|
11
|
+
scopes: a,
|
|
12
|
+
metadata: n.optional(i)
|
|
13
|
+
}), s = n.Struct({
|
|
132
14
|
type: n.Literal("apiKey"),
|
|
133
15
|
id: n.String,
|
|
134
16
|
tenantId: n.String,
|
|
135
|
-
scopes:
|
|
136
|
-
metadata: n.optional(
|
|
137
|
-
}),
|
|
17
|
+
scopes: a,
|
|
18
|
+
metadata: n.optional(i)
|
|
19
|
+
}), c = n.Struct({
|
|
138
20
|
type: n.Literal("serviceAccount"),
|
|
139
21
|
id: n.String,
|
|
140
22
|
tenantId: n.String,
|
|
141
|
-
scopes:
|
|
142
|
-
metadata: n.optional(
|
|
143
|
-
}),
|
|
23
|
+
scopes: a,
|
|
24
|
+
metadata: n.optional(i)
|
|
25
|
+
}), l = n.Struct({
|
|
144
26
|
type: n.Literal("anonymous"),
|
|
145
27
|
id: n.Null,
|
|
146
|
-
tenantId: n.NullOr(n.String)
|
|
147
|
-
|
|
28
|
+
tenantId: n.NullOr(n.String),
|
|
29
|
+
credentialRejected: n.optional(n.String)
|
|
30
|
+
}), u = n.Struct({
|
|
148
31
|
type: n.Literal("system"),
|
|
149
32
|
id: n.String,
|
|
150
33
|
tenantId: n.Null,
|
|
151
|
-
scopes:
|
|
152
|
-
metadata: n.optional(
|
|
153
|
-
}),
|
|
34
|
+
scopes: a,
|
|
35
|
+
metadata: n.optional(i)
|
|
36
|
+
}), ee = n.Union(o, s, c, l, u), d = n.Union(o.omit("scopes"), s.omit("scopes"), c.omit("scopes"), l, u.omit("scopes")), f = (e) => {
|
|
154
37
|
if (e.type === "anonymous") return e;
|
|
155
38
|
let { scopes: t, ...n } = e;
|
|
156
39
|
return n;
|
|
157
|
-
},
|
|
40
|
+
}, te = (e) => e.scopes ?? [], p = (e, t) => ({
|
|
158
41
|
type: "anonymous",
|
|
159
42
|
id: null,
|
|
160
|
-
tenantId: e
|
|
161
|
-
|
|
43
|
+
tenantId: e,
|
|
44
|
+
...t === void 0 ? {} : { credentialRejected: t }
|
|
45
|
+
}), m = (e, t = ["admin:full"]) => ({
|
|
162
46
|
type: "system",
|
|
163
47
|
id: e,
|
|
164
48
|
tenantId: null,
|
|
165
49
|
scopes: t
|
|
166
|
-
}),
|
|
50
|
+
}), h = (e, t) => {
|
|
167
51
|
let n = e;
|
|
168
52
|
return {
|
|
169
53
|
type: "serviceAccount",
|
|
@@ -172,15 +56,15 @@ var i = "admin:full", a = "apikeys:issue:self", o = "apikeys:issue:org", s = "ap
|
|
|
172
56
|
scopes: n.scopes ?? ["admin:full"],
|
|
173
57
|
...n.metadata === void 0 ? {} : { metadata: n.metadata }
|
|
174
58
|
};
|
|
175
|
-
},
|
|
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 }) {}, C = (e) => typeof e.mountRoutes == "function", w = (e) => Array.isArray(e) ? {
|
|
176
60
|
kind: "grant",
|
|
177
61
|
scopes: e
|
|
178
|
-
} : e,
|
|
62
|
+
} : e, T = (e, t) => {
|
|
179
63
|
if (e.type === "anonymous") return {
|
|
180
64
|
subject: e,
|
|
181
65
|
removed: []
|
|
182
66
|
};
|
|
183
|
-
let n =
|
|
67
|
+
let n = te(e);
|
|
184
68
|
if (t.kind === "grant") {
|
|
185
69
|
if (t.scopes.length === 0) return {
|
|
186
70
|
subject: e,
|
|
@@ -203,14 +87,14 @@ var i = "admin:full", a = "apikeys:issue:self", o = "apikeys:issue:org", s = "ap
|
|
|
203
87
|
},
|
|
204
88
|
removed: i
|
|
205
89
|
};
|
|
206
|
-
},
|
|
90
|
+
}, ne = 3e4, re = 1e4, E = (e) => `${e.type}\u0000${e.tenantId ?? ""}\u0000${e.id ?? ""}`, ie = () => {
|
|
207
91
|
if (typeof process > "u") return;
|
|
208
92
|
let e = process.env.VOLTRO_AUTH_SCOPE_CACHE_TTL_MS;
|
|
209
93
|
if (e === void 0 || e === "") return;
|
|
210
94
|
let t = Number(e);
|
|
211
95
|
return Number.isFinite(t) && t >= 0 ? t : void 0;
|
|
212
|
-
},
|
|
213
|
-
let t = e.ttlMs ??
|
|
96
|
+
}, D = (e = {}) => {
|
|
97
|
+
let t = e.ttlMs ?? ie() ?? 3e4, n = e.maxEntries ?? 1e4, r = e.now ?? Date.now, i = /* @__PURE__ */ new Map(), a = () => {
|
|
214
98
|
if (i.size <= n) return;
|
|
215
99
|
let e = r();
|
|
216
100
|
for (let [t, n] of i) n.freshUntil <= e && i.delete(t);
|
|
@@ -246,30 +130,30 @@ var i = "admin:full", a = "apikeys:issue:self", o = "apikeys:issue:org", s = "ap
|
|
|
246
130
|
};
|
|
247
131
|
}
|
|
248
132
|
};
|
|
249
|
-
},
|
|
250
|
-
let n = t?.resolveScopes, r = n === void 0 || t?.scopeCache === !1 ? null : t?.scopeCache !== void 0 &&
|
|
133
|
+
}, O = (e) => typeof e.resolve == "function", k = (e, t) => {
|
|
134
|
+
let n = t?.resolveScopes, r = n === void 0 || t?.scopeCache === !1 ? null : t?.scopeCache !== void 0 && O(t.scopeCache) ? t.scopeCache : D(t?.scopeCache ?? {});
|
|
251
135
|
return async (i) => {
|
|
252
136
|
let a = t?.getStore?.(), o = a === void 0 ? i : {
|
|
253
137
|
...i,
|
|
254
138
|
store: a
|
|
255
|
-
};
|
|
139
|
+
}, s;
|
|
256
140
|
for (let i of e) {
|
|
257
141
|
let e = await i.resolve(o);
|
|
258
142
|
if (e.kind === "matched") {
|
|
259
143
|
let a = e.subject;
|
|
260
144
|
if (n !== void 0) {
|
|
261
|
-
let s = async () =>
|
|
145
|
+
let s = async () => w(await n(e.subject, o)), { decision: c, fresh: l } = r === null ? {
|
|
262
146
|
decision: await s(),
|
|
263
147
|
fresh: !0
|
|
264
|
-
} : await r.resolve(
|
|
148
|
+
} : await r.resolve(E(e.subject), s);
|
|
265
149
|
if (c.kind === "unavailable") {
|
|
266
150
|
let e = `scope resolution unavailable: ${c.reason}`;
|
|
267
151
|
throw t?.onStrategyFailed?.({
|
|
268
152
|
strategyId: i.id,
|
|
269
153
|
reason: e
|
|
270
|
-
}), new
|
|
154
|
+
}), new A({ reason: e });
|
|
271
155
|
}
|
|
272
|
-
let u =
|
|
156
|
+
let u = T(e.subject, c);
|
|
273
157
|
l && u.removed.length > 0 && t?.onScopesNarrowed?.({
|
|
274
158
|
strategyId: i.id,
|
|
275
159
|
subjectType: e.subject.type,
|
|
@@ -287,17 +171,142 @@ var i = "admin:full", a = "apikeys:issue:self", o = "apikeys:issue:org", s = "ap
|
|
|
287
171
|
t?.onStrategyFailed?.({
|
|
288
172
|
strategyId: i.id,
|
|
289
173
|
reason: e.reason
|
|
290
|
-
});
|
|
174
|
+
}), s = e.reason;
|
|
291
175
|
break;
|
|
292
176
|
}
|
|
293
177
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
178
|
+
let c = (e) => s !== void 0 && e.type === "anonymous" ? {
|
|
179
|
+
...e,
|
|
180
|
+
credentialRejected: s
|
|
181
|
+
} : e;
|
|
182
|
+
if (t?.fallback) return { subject: c(await t.fallback(o)) };
|
|
183
|
+
let l = o.headers["x-tenant"] ?? null;
|
|
184
|
+
if (l === null && t?.anonymousTenantRequired === !0) throw new A({ reason: "tenant required (x-tenant header missing)" });
|
|
185
|
+
return { subject: c(p(l)) };
|
|
298
186
|
};
|
|
299
|
-
},
|
|
300
|
-
if (e.type === "anonymous") throw new
|
|
187
|
+
}, A = class extends n.TaggedError()("Unauthenticated", { reason: n.optional(n.String) }) {}, j = (e, t) => {
|
|
188
|
+
if (e.type === "anonymous") throw new A(t === void 0 ? {} : { reason: t });
|
|
189
|
+
}, M = "admin:full", N = "apikeys:issue:self", P = "apikeys:issue:org", F = "apikeys:issue:other", I = class extends n.TaggedError()("ScopeError", {
|
|
190
|
+
required: n.String,
|
|
191
|
+
message: n.String
|
|
192
|
+
}) {}, L = (e) => "scopes" in e && e.scopes ? e.scopes : [], R = /* @__PURE__ */ new WeakMap(), z = (e, t) => {
|
|
193
|
+
typeof e == "object" && e && R.set(e, t);
|
|
194
|
+
}, B = (e) => {
|
|
195
|
+
if (typeof e == "object" && e) {
|
|
196
|
+
let t = R.get(e);
|
|
197
|
+
if (t) return t;
|
|
198
|
+
}
|
|
199
|
+
return L(e);
|
|
200
|
+
}, V = (e, t) => {
|
|
201
|
+
let n = B(e);
|
|
202
|
+
return n.includes("admin:full") || n.includes(t);
|
|
203
|
+
}, H = (e, t) => {
|
|
204
|
+
let n = L(e);
|
|
205
|
+
return n.includes("admin:full") || n.includes(t);
|
|
206
|
+
}, ae = (e, n) => V(e, n) ? t.void : t.fail(new I({
|
|
207
|
+
required: n,
|
|
208
|
+
message: `missing required scope '${n}'`
|
|
209
|
+
})), 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) => {
|
|
210
|
+
q = e;
|
|
211
|
+
}, oe = (e) => q === void 0 ? e.filter((e) => (e.guards ?? []).some((e) => !U(e) && e.resource !== void 0)).map((e) => e.tag) : [], se = (e) => [
|
|
212
|
+
`${e.length} procedure${e.length === 1 ? "" : "s"} declare a per-resource guard that nothing enforces:`,
|
|
213
|
+
...e.slice(0, 10).map((e) => ` ${e}`),
|
|
214
|
+
...e.length > 10 ? [` … and ${e.length - 10} more`] : [],
|
|
215
|
+
"",
|
|
216
|
+
" `guards: [{ scope, resource }]` reads as \"may you do this to THIS resource\",",
|
|
217
|
+
" but with no resource-scope resolver registered the extractor is advisory and",
|
|
218
|
+
" the check runs against the caller's GLOBAL scopes. For an app whose",
|
|
219
|
+
" authorization is per-resource, that guard passes callers it should refuse.",
|
|
220
|
+
"",
|
|
221
|
+
" Fix it one of two ways:",
|
|
222
|
+
" • setResourceScopeResolver((req) => Effect<boolean>) — answers \"does this",
|
|
223
|
+
" subject hold `req.scope` on `req.resource`\" from your own tables.",
|
|
224
|
+
" • Drop the `resource` extractor if a global scope check is what you meant."
|
|
225
|
+
].join("\n"), ce = () => q, Y, le = (e) => {
|
|
226
|
+
Y = e;
|
|
227
|
+
}, X = () => Y, Z = (e) => new I({
|
|
228
|
+
required: "access-decision",
|
|
229
|
+
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>'\`.`
|
|
230
|
+
}), Q = (e, t) => {
|
|
231
|
+
let n = e.credentialRejected;
|
|
232
|
+
return n === void 0 ? t : new A({ reason: `${n} — the request carried a credential and it was REJECTED, so this caller resolved to anonymous. The guard then refused (${t.message}); the scope is not the problem. Re-authenticate and retry.` });
|
|
233
|
+
}, $ = (e, t, n) => {
|
|
234
|
+
if (!t || t.length === 0) return n?.defaultDeny === !0 ? Z(n.procedure) : null;
|
|
235
|
+
for (let n of t) {
|
|
236
|
+
if (W(n)) continue;
|
|
237
|
+
if (U(n)) {
|
|
238
|
+
if (V(e, "admin:full")) continue;
|
|
239
|
+
return Q(e, new I({
|
|
240
|
+
required: `${n.resourceType}:${n.action}`,
|
|
241
|
+
message: `relationship guard '${n.action}' on '${n.resourceType}' cannot be checked synchronously`
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
let t = K(n.scope);
|
|
245
|
+
if (t.length === 0) continue;
|
|
246
|
+
let r = t.filter((t) => V(e, t));
|
|
247
|
+
if (!((n.mode ?? "all") === "any" ? r.length > 0 : r.length === t.length)) {
|
|
248
|
+
let r = (n.mode ?? "all") === "any" ? t[0] : t.find((t) => !V(e, t));
|
|
249
|
+
return Q(e, new I({
|
|
250
|
+
required: r,
|
|
251
|
+
message: (n.mode ?? "all") === "any" ? `missing all of required scopes [${t.join(", ")}]` : `missing required scope '${r}'`
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}, ue = (e, n, r) => {
|
|
257
|
+
let i = (e) => new I({
|
|
258
|
+
required: `${n.resourceType}:${n.action}`,
|
|
259
|
+
message: e
|
|
260
|
+
});
|
|
261
|
+
if (V(e, "admin:full")) return t.succeed(null);
|
|
262
|
+
let a = Y;
|
|
263
|
+
if (a === void 0) return t.succeed(i(`no policy resolver registered — cannot authorize '${n.action}' on '${n.resourceType}'`));
|
|
264
|
+
let o = n.resource(r);
|
|
265
|
+
return o === void 0 || o === "" ? t.succeed(i(`input does not identify a '${n.resourceType}' to authorize`)) : t.suspend(() => a({
|
|
266
|
+
subject: e,
|
|
267
|
+
action: n.action,
|
|
268
|
+
resourceType: n.resourceType,
|
|
269
|
+
resourceId: o
|
|
270
|
+
})).pipe(t.catchAllCause(() => t.succeed(!1)), t.map((e) => e ? null : i(`no '${n.action}' relation on ${n.resourceType}:${o}`)));
|
|
271
|
+
}, de = (e, n, r, i) => {
|
|
272
|
+
if (!n || n.length === 0) return t.succeed($(e, n, i));
|
|
273
|
+
let a = q;
|
|
274
|
+
return !n.some(U) && (!a || !n.some((e) => e.resource !== void 0)) ? t.succeed($(e, n, i)) : t.gen(function* () {
|
|
275
|
+
for (let i of n) {
|
|
276
|
+
if (W(i)) continue;
|
|
277
|
+
if (U(i)) {
|
|
278
|
+
let t = yield* ue(e, i, r);
|
|
279
|
+
if (t) return t;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
let n = K(i.scope);
|
|
283
|
+
if (n.length === 0) continue;
|
|
284
|
+
let o = i.resource ? i.resource(r) : void 0, s = [];
|
|
285
|
+
for (let r of n) {
|
|
286
|
+
if (V(e, r)) {
|
|
287
|
+
s.push(!0);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (o !== void 0 && a !== void 0) {
|
|
291
|
+
let n = yield* t.suspend(() => a({
|
|
292
|
+
subject: e,
|
|
293
|
+
scope: r,
|
|
294
|
+
resource: o
|
|
295
|
+
})).pipe(t.catchAllCause(() => t.succeed(!1)));
|
|
296
|
+
s.push(n);
|
|
297
|
+
} else s.push(!1);
|
|
298
|
+
}
|
|
299
|
+
let c = i.mode ?? "all";
|
|
300
|
+
if (!(c === "any" ? s.some(Boolean) : s.every(Boolean))) {
|
|
301
|
+
let e = c === "any" ? n[0] : n[n.findIndex((e, t) => !s[t])];
|
|
302
|
+
return new I({
|
|
303
|
+
required: e,
|
|
304
|
+
message: c === "any" ? `missing all of required scopes [${n.join(", ")}]` : `missing required scope '${e}'` + (o === void 0 ? "" : ` on resource '${o}'`)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
});
|
|
301
310
|
};
|
|
302
311
|
//#endregion
|
|
303
|
-
export {
|
|
312
|
+
export { ee as A, D as B, L as C, re as D, S as E, T as F, h as G, E as H, j as I, k as L, y as M, A as N, ne as O, p as P, C as R, J as S, b as T, f as U, v as V, m as W, Z as _, I as a, z as b, de as c, X as d, ce as f, U as g, W as h, N as i, d as j, _ as k, B as l, H as m, P as n, se as o, V as p, F as r, $ as s, M as t, oe as u, G as v, x as w, le as x, ae as y, g as z };
|
package/dist/session.d.ts
CHANGED
|
@@ -180,6 +180,7 @@ declare const SessionPayload_2: Schema.Struct<{
|
|
|
180
180
|
type: Schema.Literal<["anonymous"]>;
|
|
181
181
|
id: typeof Schema.Null;
|
|
182
182
|
tenantId: Schema.NullOr<typeof Schema.String>;
|
|
183
|
+
credentialRejected: Schema.optional<typeof Schema.String>;
|
|
183
184
|
}>, Schema.Struct<{
|
|
184
185
|
id: typeof Schema.String;
|
|
185
186
|
type: Schema.Literal<["system"]>;
|
|
@@ -279,6 +280,23 @@ declare const Subject: Schema.Union<[Schema.Struct<{
|
|
|
279
280
|
type: Schema.Literal<["anonymous"]>;
|
|
280
281
|
id: typeof Schema.Null;
|
|
281
282
|
tenantId: Schema.NullOr<typeof Schema.String>;
|
|
283
|
+
/**
|
|
284
|
+
* Set when this caller PRESENTED a credential and it was rejected — an
|
|
285
|
+
* expired token above all. Absent when they presented none.
|
|
286
|
+
*
|
|
287
|
+
* The two are the same Subject and must not be the same ANSWER. A consumer
|
|
288
|
+
* measured the cost: a user's tab outlived their IdP's token lifetime, the
|
|
289
|
+
* strategy logged `supabase jwt expired`, the caller fell through to
|
|
290
|
+
* anonymous, and the guard then refused with `missing required scope
|
|
291
|
+
* 'task:u:o'`. Technically true — an anonymous caller holds no scopes — and
|
|
292
|
+
* it sent everyone who read it into the permissions system while the problem
|
|
293
|
+
* was an expired session. They did that round.
|
|
294
|
+
*
|
|
295
|
+
* It stays a FALLBACK rather than a hard failure on purpose: a stale cookie
|
|
296
|
+
* must not break an `openAccess` procedure that needs no session at all. The
|
|
297
|
+
* fact travels, and only a guard that actually refuses spends it.
|
|
298
|
+
*/
|
|
299
|
+
credentialRejected: Schema.optional<typeof Schema.String>;
|
|
282
300
|
}>, Schema.Struct<{
|
|
283
301
|
type: Schema.Literal<["system"]>;
|
|
284
302
|
id: typeof Schema.String;
|
|
@@ -308,6 +326,23 @@ declare const SubjectIdentity: Schema.Union<[Schema.Struct<{
|
|
|
308
326
|
type: Schema.Literal<["anonymous"]>;
|
|
309
327
|
id: typeof Schema.Null;
|
|
310
328
|
tenantId: Schema.NullOr<typeof Schema.String>;
|
|
329
|
+
/**
|
|
330
|
+
* Set when this caller PRESENTED a credential and it was rejected — an
|
|
331
|
+
* expired token above all. Absent when they presented none.
|
|
332
|
+
*
|
|
333
|
+
* The two are the same Subject and must not be the same ANSWER. A consumer
|
|
334
|
+
* measured the cost: a user's tab outlived their IdP's token lifetime, the
|
|
335
|
+
* strategy logged `supabase jwt expired`, the caller fell through to
|
|
336
|
+
* anonymous, and the guard then refused with `missing required scope
|
|
337
|
+
* 'task:u:o'`. Technically true — an anonymous caller holds no scopes — and
|
|
338
|
+
* it sent everyone who read it into the permissions system while the problem
|
|
339
|
+
* was an expired session. They did that round.
|
|
340
|
+
*
|
|
341
|
+
* It stays a FALLBACK rather than a hard failure on purpose: a stale cookie
|
|
342
|
+
* must not break an `openAccess` procedure that needs no session at all. The
|
|
343
|
+
* fact travels, and only a guard that actually refuses spends it.
|
|
344
|
+
*/
|
|
345
|
+
credentialRejected: Schema.optional<typeof Schema.String>;
|
|
311
346
|
}>, Schema.Struct<{
|
|
312
347
|
id: typeof Schema.String;
|
|
313
348
|
type: Schema.Literal<["system"]>;
|
package/dist/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as e, U as t, j as n } from "./scopes-B0kGvAnI.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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@effect/sql": "^0.52.0",
|
|
57
|
-
"@voltro/database": "0.
|
|
58
|
-
"@voltro/logger": "0.
|
|
57
|
+
"@voltro/database": "0.40.0",
|
|
58
|
+
"@voltro/logger": "0.40.0",
|
|
59
59
|
"jose": "^6.2.8"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|