@voltro/protocol 0.63.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/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 either a scope check or a relationship check. */
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;
@@ -532,7 +554,10 @@ declare interface PluginHttpRouteResult {
532
554
  readonly status: number;
533
555
  readonly body?: string | Uint8Array;
534
556
  readonly contentType?: string;
535
- readonly headers?: Record<string, string>;
557
+ /** Response headers. A header that repeats — two `Set-Cookie` lines, a
558
+ * session and a CSRF cookie from one login — is an ARRAY; `set-cookie`
559
+ * arrays become separate lines, every other array joins with `, `. */
560
+ readonly headers?: Record<string, string | ReadonlyArray<string>>;
536
561
  /** Stream the response (SSE) instead of sending `body`. See
537
562
  * {@link PluginHttpRouteStream}. */
538
563
  readonly stream?: PluginHttpRouteStream;
package/dist/rest.js CHANGED
@@ -1,5 +1,5 @@
1
- import { P as e } from "./scopes-B0kGvAnI.js";
2
- import { a as t, i as n, o as r, r as i, t as a } from "./serverErrorBus-BeN9pOpY.js";
1
+ import { F as e } from "./scopes-C86BDyNJ.js";
2
+ import { a as t, c as n, o as r, s as i, t as a } from "./serverErrorBus-QQgyq6fV.js";
3
3
  import { Effect as o, Schema as s } from "effect";
4
4
  //#region src/publicApi.ts
5
5
  var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.path ?? `/${t.version ?? "v1"}/${e.replace(/\./g, "/")}`, u = (e, t, n) => {
@@ -141,9 +141,9 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
141
141
  let t = await e(g);
142
142
  if (t) return x(t.status, { error: t.message }, f);
143
143
  }
144
- let v = l.idempotency, S = v && E.has(c.method) ? s.headers[v.header.toLowerCase()] : void 0, C = v && S ? r(h.tenantId, c.method, c.path) : "";
144
+ let v = l.idempotency, S = v && E.has(c.method) ? s.headers[v.header.toLowerCase()] : void 0, C = v && S ? n(h.tenantId, c.method, c.path) : "";
145
145
  if (v && S) {
146
- let e = await i(v.store, C, S, v.ttlMs, Date.now());
146
+ let e = await t(v.store, C, S, v.ttlMs, Date.now());
147
147
  if (e.kind === "replay") return x(e.response.status, e.response.body, {
148
148
  ...f,
149
149
  "Idempotency-Replayed": "true"
@@ -152,7 +152,7 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
152
152
  }
153
153
  try {
154
154
  let e = await c.handler(m, g);
155
- if (_(e) || y(e)) return v && S && await n(v.store, C, S), y(e) ? {
155
+ if (_(e) || y(e)) return v && S && await r(v.store, C, S), y(e) ? {
156
156
  status: 200,
157
157
  headers: f,
158
158
  byteStream: e.byteStream,
@@ -162,26 +162,26 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
162
162
  headers: f,
163
163
  stream: e.stream
164
164
  };
165
- let r = await o.runPromise(d(e));
166
- if (v && S && await t(v.store, C, S, {
165
+ let t = await o.runPromise(d(e));
166
+ if (v && S && await i(v.store, C, S, {
167
167
  status: 200,
168
- body: r
168
+ body: t
169
169
  }, Date.now()), c.etag === !0 && c.method === "GET") {
170
- let e = `W/"${await b(JSON.stringify(r))}"`, t = s.headers["if-none-match"];
171
- return t !== void 0 && t.split(",").some((t) => t.trim() === e) ? {
170
+ let e = `W/"${await b(JSON.stringify(t))}"`, n = s.headers["if-none-match"];
171
+ return n !== void 0 && n.split(",").some((t) => t.trim() === e) ? {
172
172
  status: 304,
173
173
  headers: {
174
174
  ...f,
175
175
  ETag: e
176
176
  }
177
- } : x(200, r, {
177
+ } : x(200, t, {
178
178
  ...f,
179
179
  ETag: e
180
180
  });
181
181
  }
182
- return x(200, r, f);
182
+ return x(200, t, f);
183
183
  } catch (e) {
184
- if (v && S && await n(v.store, C, S), typeof e == "object" && e && typeof e.status == "number") {
184
+ if (v && S && await r(v.store, C, S), typeof e == "object" && e && typeof e.status == "number") {
185
185
  let t = e;
186
186
  return x(t.status, { error: t.message ?? "Error" }, f);
187
187
  }
@@ -33,21 +33,21 @@ var i = n.Record({
33
33
  tenantId: n.Null,
34
34
  scopes: a,
35
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) => {
36
+ }), d = n.Union(o, s, c, l, u), f = n.Union(o.omit("scopes"), s.omit("scopes"), c.omit("scopes"), l, u.omit("scopes")), p = (e) => {
37
37
  if (e.type === "anonymous") return e;
38
38
  let { scopes: t, ...n } = e;
39
39
  return n;
40
- }, te = (e) => e.scopes ?? [], p = (e, t) => ({
40
+ }, m = (e) => e.scopes ?? [], h = (e, t) => ({
41
41
  type: "anonymous",
42
42
  id: null,
43
43
  tenantId: e,
44
44
  ...t === void 0 ? {} : { credentialRejected: t }
45
- }), m = (e, t = ["admin:full"]) => ({
45
+ }), ee = (e, t = ["admin:full"]) => ({
46
46
  type: "system",
47
47
  id: e,
48
48
  tenantId: null,
49
49
  scopes: t
50
- }), h = (e, t) => {
50
+ }), te = (e, t) => {
51
51
  let n = e;
52
52
  return {
53
53
  type: "serviceAccount",
@@ -56,15 +56,16 @@ 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 }) {}, S = class extends r.Tag()("@voltro/ConnectionInfoMiddleware", { provides: b }) {}, C = (e) => typeof e.mountRoutes == "function", w = (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
- } : e, T = (e, t) => {
62
+ } : e, w = (e, t) => {
63
+ if (t.kind === "vetoed") throw new k({ reason: `${t.code}: ${t.message}` });
63
64
  if (e.type === "anonymous") return {
64
65
  subject: e,
65
66
  removed: []
66
67
  };
67
- let n = te(e);
68
+ let n = m(e);
68
69
  if (t.kind === "grant") {
69
70
  if (t.scopes.length === 0) return {
70
71
  subject: e,
@@ -87,7 +88,7 @@ var i = n.Record({
87
88
  },
88
89
  removed: i
89
90
  };
90
- }, ne = 3e4, re = 1e4, E = (e) => `${e.type}\u0000${e.tenantId ?? ""}\u0000${e.id ?? ""}`, ie = () => {
91
+ }, T = 3e4, re = 1e4, E = (e) => `${e.type}\u0000${e.tenantId ?? ""}\u0000${e.id ?? ""}`, ie = () => {
91
92
  if (typeof process > "u") return;
92
93
  let e = process.env.VOLTRO_AUTH_SCOPE_CACHE_TTL_MS;
93
94
  if (e === void 0 || e === "") return;
@@ -130,85 +131,113 @@ var i = n.Record({
130
131
  };
131
132
  }
132
133
  };
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 ?? {});
135
- return async (i) => {
136
- let a = t?.getStore?.(), o = a === void 0 ? i : {
137
- ...i,
138
- store: a
139
- }, s;
140
- for (let i of e) {
141
- let e = await i.resolve(o);
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
+ return async (o) => {
137
+ let s = t?.getStore?.(), c = s === void 0 ? o : {
138
+ ...o,
139
+ store: s
140
+ }, l;
141
+ for (let o of e) {
142
+ let e = await o.resolve(c);
142
143
  if (e.kind === "matched") {
143
- let a = e.subject;
144
- if (n !== void 0) {
145
- let s = async () => w(await n(e.subject, o)), { decision: c, fresh: l } = r === null ? {
146
- decision: await s(),
144
+ let s = e.subject;
145
+ if (i) {
146
+ let i = async () => {
147
+ let t = n === void 0 ? {
148
+ kind: "grant",
149
+ scopes: []
150
+ } : C(await n(e.subject, c));
151
+ if (t.kind === "unavailable" || r.length === 0) return t;
152
+ let i = w(e.subject, t).subject, a = {
153
+ origin: "request",
154
+ headers: c.headers,
155
+ clientId: c.clientId,
156
+ store: c.store
157
+ };
158
+ for (let e of r) {
159
+ let t = await e(i, a);
160
+ if (!t.ok) return {
161
+ kind: "vetoed",
162
+ code: t.code,
163
+ message: t.message
164
+ };
165
+ }
166
+ return t;
167
+ }, { decision: l, fresh: u } = a === null ? {
168
+ decision: await i(),
147
169
  fresh: !0
148
- } : await r.resolve(E(e.subject), s);
149
- if (c.kind === "unavailable") {
150
- let e = `scope resolution unavailable: ${c.reason}`;
170
+ } : await a.resolve(E(e.subject), i);
171
+ if (l.kind === "vetoed") {
172
+ let e = `${l.code}: ${l.message}`;
151
173
  throw t?.onStrategyFailed?.({
152
- strategyId: i.id,
174
+ strategyId: o.id,
153
175
  reason: e
154
- }), new A({ reason: e });
176
+ }), new k({ reason: e });
155
177
  }
156
- let u = T(e.subject, c);
157
- l && u.removed.length > 0 && t?.onScopesNarrowed?.({
158
- strategyId: i.id,
178
+ if (l.kind === "unavailable") {
179
+ let e = `scope resolution unavailable: ${l.reason}`;
180
+ throw t?.onStrategyFailed?.({
181
+ strategyId: o.id,
182
+ reason: e
183
+ }), new k({ reason: e });
184
+ }
185
+ let d = w(e.subject, l);
186
+ u && d.removed.length > 0 && t?.onScopesNarrowed?.({
187
+ strategyId: o.id,
159
188
  subjectType: e.subject.type,
160
189
  subjectId: e.subject.id,
161
- removed: u.removed,
162
- granted: c.scopes
163
- }), a = u.subject;
190
+ removed: d.removed,
191
+ granted: l.scopes
192
+ }), s = d.subject;
164
193
  }
165
- return e.credentialExpiresAt === void 0 ? { subject: a } : {
166
- subject: a,
194
+ return e.credentialExpiresAt === void 0 ? { subject: s } : {
195
+ subject: s,
167
196
  credentialExpiresAt: e.credentialExpiresAt
168
197
  };
169
198
  }
170
199
  if (e.kind === "failed") {
171
200
  t?.onStrategyFailed?.({
172
- strategyId: i.id,
201
+ strategyId: o.id,
173
202
  reason: e.reason
174
- }), s = e.reason;
203
+ }), l = e.reason;
175
204
  break;
176
205
  }
177
206
  }
178
- let c = (e) => s !== void 0 && e.type === "anonymous" ? {
207
+ let u = (e) => l !== void 0 && e.type === "anonymous" ? {
179
208
  ...e,
180
- credentialRejected: s
209
+ credentialRejected: l
181
210
  } : 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)) };
211
+ if (t?.fallback) return { subject: u(await t.fallback(c)) };
212
+ let d = c.headers["x-tenant"] ?? null;
213
+ if (d === null && t?.anonymousTenantRequired === !0) throw new k({ reason: "tenant required (x-tenant header missing)" });
214
+ return { subject: u(h(d)) };
186
215
  };
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", {
216
+ }, k = class extends n.TaggedError()("Unauthenticated", { reason: n.optional(n.String) }) {}, A = (e, t) => {
217
+ if (e.type === "anonymous") throw new k(t === void 0 ? {} : { reason: t });
218
+ }, j = "admin:full", M = "apikeys:issue:self", N = "apikeys:issue:org", P = "apikeys:issue:other", F = class extends n.TaggedError()("ScopeError", {
190
219
  required: n.String,
191
220
  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) => {
221
+ }) {}, I = (e) => "scopes" in e && e.scopes ? e.scopes : [], L = /* @__PURE__ */ new WeakMap(), R = (e, t) => {
222
+ typeof e == "object" && e && L.set(e, t);
223
+ }, z = (e) => {
195
224
  if (typeof e == "object" && e) {
196
- let t = R.get(e);
225
+ let t = L.get(e);
197
226
  if (t) return t;
198
227
  }
199
- return L(e);
200
- }, V = (e, t) => {
201
- let n = B(e);
228
+ return I(e);
229
+ }, B = (e, t) => {
230
+ let n = z(e);
202
231
  return n.includes("admin:full") || n.includes(t);
203
- }, H = (e, t) => {
204
- let n = L(e);
232
+ }, V = (e, t) => {
233
+ let n = I(e);
205
234
  return n.includes("admin:full") || n.includes(t);
206
- }, ae = (e, n) => V(e, n) ? t.void : t.fail(new I({
235
+ }, H = (e, n) => B(e, n) ? t.void : t.fail(new F({
207
236
  required: n,
208
237
  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) => [
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) => [
212
241
  `${e.length} procedure${e.length === 1 ? "" : "s"} declare a per-resource guard that nothing enforces:`,
213
242
  ...e.slice(0, 10).map((e) => ` ${e}`),
214
243
  ...e.length > 10 ? [` … and ${e.length - 10} more`] : [],
@@ -222,43 +251,50 @@ var i = n.Record({
222
251
  " • setResourceScopeResolver((req) => Effect<boolean>) — answers \"does this",
223
252
  " subject hold `req.scope` on `req.resource`\" from your own tables.",
224
253
  " • Drop the `resource` extractor if a global scope check is what you meant."
225
- ].join("\n"), ce = () => q, Y, le = (e) => {
254
+ ].join("\n"), le = () => J, Y, X = (e) => {
226
255
  Y = e;
227
- }, X = () => Y, Z = (e) => new I({
256
+ }, ue = () => Y, Z = (e) => new F({
228
257
  required: "access-decision",
229
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>'\`.`
230
259
  }), Q = (e, t) => {
231
260
  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.` });
261
+ return n === void 0 ? t : new k({ 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
262
  }, $ = (e, t, n) => {
234
263
  if (!t || t.length === 0) return n?.defaultDeny === !0 ? Z(n.procedure) : null;
235
264
  for (let n of t) {
236
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
+ }
237
273
  if (U(n)) {
238
- if (V(e, "admin:full")) continue;
239
- return Q(e, new I({
274
+ if (B(e, "admin:full")) continue;
275
+ return Q(e, new F({
240
276
  required: `${n.resourceType}:${n.action}`,
241
277
  message: `relationship guard '${n.action}' on '${n.resourceType}' cannot be checked synchronously`
242
278
  }));
243
279
  }
244
- let t = K(n.scope);
280
+ let t = q(n.scope);
245
281
  if (t.length === 0) continue;
246
- let r = t.filter((t) => V(e, t));
282
+ let r = t.filter((t) => B(e, t));
247
283
  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({
284
+ let r = (n.mode ?? "all") === "any" ? t[0] : t.find((t) => !B(e, t));
285
+ return Q(e, new F({
250
286
  required: r,
251
287
  message: (n.mode ?? "all") === "any" ? `missing all of required scopes [${t.join(", ")}]` : `missing required scope '${r}'`
252
288
  }));
253
289
  }
254
290
  }
255
291
  return null;
256
- }, ue = (e, n, r) => {
257
- let i = (e) => new I({
292
+ }, de = (e, n, r) => {
293
+ let i = (e) => new F({
258
294
  required: `${n.resourceType}:${n.action}`,
259
295
  message: e
260
296
  });
261
- if (V(e, "admin:full")) return t.succeed(null);
297
+ if (B(e, "admin:full")) return t.succeed(null);
262
298
  let a = Y;
263
299
  if (a === void 0) return t.succeed(i(`no policy resolver registered — cannot authorize '${n.action}' on '${n.resourceType}'`));
264
300
  let o = n.resource(r);
@@ -268,22 +304,29 @@ var i = n.Record({
268
304
  resourceType: n.resourceType,
269
305
  resourceId: o
270
306
  })).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) => {
307
+ }, fe = (e, n, r, i) => {
272
308
  if (!n || n.length === 0) return t.succeed($(e, n, i));
273
- let a = q;
309
+ let a = J;
274
310
  return !n.some(U) && (!a || !n.some((e) => e.resource !== void 0)) ? t.succeed($(e, n, i)) : t.gen(function* () {
275
311
  for (let i of n) {
276
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
+ }
277
320
  if (U(i)) {
278
- let t = yield* ue(e, i, r);
321
+ let t = yield* de(e, i, r);
279
322
  if (t) return t;
280
323
  continue;
281
324
  }
282
- let n = K(i.scope);
325
+ let n = q(i.scope);
283
326
  if (n.length === 0) continue;
284
327
  let o = i.resource ? i.resource(r) : void 0, s = [];
285
328
  for (let r of n) {
286
- if (V(e, r)) {
329
+ if (B(e, r)) {
287
330
  s.push(!0);
288
331
  continue;
289
332
  }
@@ -299,7 +342,7 @@ var i = n.Record({
299
342
  let c = i.mode ?? "all";
300
343
  if (!(c === "any" ? s.some(Boolean) : s.every(Boolean))) {
301
344
  let e = c === "any" ? n[0] : n[n.findIndex((e, t) => !s[t])];
302
- return new I({
345
+ return new F({
303
346
  required: e,
304
347
  message: c === "any" ? `missing all of required scopes [${n.join(", ")}]` : `missing required scope '${e}'` + (o === void 0 ? "" : ` on resource '${o}'`)
305
348
  });
@@ -309,4 +352,4 @@ var i = n.Record({
309
352
  });
310
353
  };
311
354
  //#endregion
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 };
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,11 +1,12 @@
1
+ import { Schema as e } from "effect";
1
2
  //#region src/idempotency.ts
2
- var e = (e, t, n) => `${e ?? "(global)"}|${t.toUpperCase()}|${n}`, t = (e, t, n) => `${e ?? "(global)"}|${t ?? "(anon)"}|WS|${n}`, n = async (e, t, n, r, i) => {
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
4
  let a = await e.claim(t, n, i, r);
4
5
  return a === "claimed" ? { kind: "fresh" } : a.status === "completed" && a.response ? {
5
6
  kind: "replay",
6
7
  response: a.response
7
8
  } : { kind: "conflict" };
8
- }, r = (e, t, n, r, i) => e.complete(t, n, r, i), i = (e, t, n) => e.release(t, n), a = () => {
9
+ }, i = (e, t, n, r, i) => e.complete(t, n, r, i), a = (e, t, n) => e.release(t, n), o = () => {
9
10
  let e = /* @__PURE__ */ new Map(), t = (e, t) => `${e}\u0000${t}`;
10
11
  return {
11
12
  get: async (n, r) => e.get(t(n, r)) ?? null,
@@ -32,12 +33,19 @@ var e = (e, t, n) => `${e ?? "(global)"}|${t.toUpperCase()}|${n}`, t = (e, t, n)
32
33
  e.delete(t(n, r));
33
34
  }
34
35
  };
35
- }, o = /* @__PURE__ */ new Set(), s = (e) => {
36
- for (let t of o) try {
36
+ }, s = class extends e.TaggedError()("MutationInFlight", {
37
+ traceId: e.String,
38
+ retryAfterMs: e.Number
39
+ }) {
40
+ get message() {
41
+ return `a request with this idempotency key is still in flight — replay the same key after ${this.retryAfterMs}ms`;
42
+ }
43
+ }, c = 1e3, l = /* @__PURE__ */ new Set(), u = (e) => {
44
+ for (let t of l) try {
37
45
  t(e);
38
46
  } catch {}
39
- }, c = (e) => (o.add(e), () => {
40
- o.delete(e);
47
+ }, d = (e) => (l.add(e), () => {
48
+ l.delete(e);
41
49
  });
42
50
  //#endregion
43
- export { r as a, t as c, i, c as n, e as o, n as r, a as s, s as t };
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 };
package/dist/session.js CHANGED
@@ -1,11 +1,11 @@
1
- import { C as e, U as t, j as n } from "./scopes-B0kGvAnI.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.63.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.0",
63
- "@voltro/logger": "0.63.0",
62
+ "@voltro/database": "0.65.0",
63
+ "@voltro/logger": "0.65.0",
64
64
  "jose": "^6.2.8"
65
65
  },
66
66
  "peerDependencies": {