@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/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,4 +1,4 @@
1
- import { d as e } from "./auth-CKS3UNnf.js";
1
+ import { P as e } from "./scopes-B0kGvAnI.js";
2
2
  import { a as t, i as n, o as r, r as i, t as a } from "./serverErrorBus-BeN9pOpY.js";
3
3
  import { Effect as o, Schema as s } from "effect";
4
4
  //#region src/publicApi.ts
@@ -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/scopes.ts
4
- var i = "admin:full", a = "apikeys:issue:self", o = "apikeys:issue:org", s = "apikeys:issue:other", c = class extends n.TaggedError()("ScopeError", {
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
- }), k = n.optional(n.Array(n.String)), A = n.Struct({
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: k,
130
- metadata: n.optional(O)
131
- }), j = n.Struct({
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: k,
136
- metadata: n.optional(O)
137
- }), M = n.Struct({
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: k,
142
- metadata: n.optional(O)
143
- }), N = n.Struct({
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
- }), P = n.Struct({
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: k,
152
- metadata: n.optional(O)
153
- }), F = n.Union(A, j, M, N, P), I = n.Union(A.omit("scopes"), j.omit("scopes"), M.omit("scopes"), N, P.omit("scopes")), L = (e) => {
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
- }, R = (e) => e.scopes ?? [], z = (e) => ({
40
+ }, te = (e) => e.scopes ?? [], p = (e, t) => ({
158
41
  type: "anonymous",
159
42
  id: null,
160
- tenantId: e
161
- }), B = (e, t = ["admin:full"]) => ({
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
- }), V = (e, t) => {
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
- }, H = (e) => e.type === "system", U = "impersonation", W = (e) => e.metadata?.impersonation ?? null, G = class extends e.Tag("@voltro/Subject")() {}, K = class extends e.Tag("@voltro/ConnectionInfo")() {}, q = class extends r.Tag()("@voltro/AuthMiddleware", { provides: G }) {}, J = class extends r.Tag()("@voltro/ConnectionInfoMiddleware", { provides: K }) {}, Y = (e) => typeof e.mountRoutes == "function", ie = (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 }) {}, C = (e) => typeof e.mountRoutes == "function", w = (e) => Array.isArray(e) ? {
176
60
  kind: "grant",
177
61
  scopes: e
178
- } : e, X = (e, t) => {
62
+ } : e, T = (e, t) => {
179
63
  if (e.type === "anonymous") return {
180
64
  subject: e,
181
65
  removed: []
182
66
  };
183
- let n = R(e);
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
- }, ae = 3e4, oe = 1e4, Z = (e) => `${e.type}\u0000${e.tenantId ?? ""}\u0000${e.id ?? ""}`, se = () => {
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
- }, Q = (e = {}) => {
213
- let t = e.ttlMs ?? se() ?? 3e4, n = e.maxEntries ?? 1e4, r = e.now ?? Date.now, i = /* @__PURE__ */ new Map(), a = () => {
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
- }, ce = (e) => typeof e.resolve == "function", le = (e, t) => {
250
- let n = t?.resolveScopes, r = n === void 0 || t?.scopeCache === !1 ? null : t?.scopeCache !== void 0 && ce(t.scopeCache) ? t.scopeCache : Q(t?.scopeCache ?? {});
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 () => ie(await n(e.subject, o)), { decision: c, fresh: l } = r === null ? {
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(Z(e.subject), s);
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 $({ reason: e });
154
+ }), new A({ reason: e });
271
155
  }
272
- let u = X(e.subject, c);
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
- if (t?.fallback) return { subject: await t.fallback(o) };
295
- let s = o.headers["x-tenant"] ?? null;
296
- if (s === null && t?.anonymousTenantRequired === !0) throw new $({ reason: "tenant required (x-tenant header missing)" });
297
- return { subject: z(s) };
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
- }, $ = class extends n.TaggedError()("Unauthenticated", { reason: n.optional(n.String) }) {}, ue = (e, t) => {
300
- if (e.type === "anonymous") throw new $(t === void 0 ? {} : { reason: t });
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 { D as A, ne as B, i as C, c as D, a as E, p as F, l as G, d as H, ee as I, h as L, y as M, C as N, b as O, x as P, m as R, V as S, s as T, re as U, te as V, v as W, Q as _, ae as a, L as b, I as c, z as d, X as f, H as g, Y as h, oe as i, f as j, T as k, G as l, le as m, K as n, U as o, ue as p, J as r, F as s, q as t, $ as u, W as v, o as w, B as x, Z as y, w as z };
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 { G as e, b as t, c as n } from "./auth-CKS3UNnf.js";
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.39.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.39.0",
58
- "@voltro/logger": "0.39.0",
57
+ "@voltro/database": "0.40.0",
58
+ "@voltro/logger": "0.40.0",
59
59
  "jose": "^6.2.8"
60
60
  },
61
61
  "peerDependencies": {