@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/CHANGELOG.md +157 -0
- package/dist/index.d.ts +430 -29
- package/dist/index.js +532 -473
- package/dist/rest.d.ts +28 -3
- package/dist/rest.js +13 -13
- package/dist/{scopes-B0kGvAnI.js → scopes-C86BDyNJ.js} +120 -77
- package/dist/{serverErrorBus-BeN9pOpY.js → serverErrorBus-QQgyq6fV.js} +15 -7
- 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;
|
|
@@ -532,7 +554,10 @@ declare interface PluginHttpRouteResult {
|
|
|
532
554
|
readonly status: number;
|
|
533
555
|
readonly body?: string | Uint8Array;
|
|
534
556
|
readonly contentType?: string;
|
|
535
|
-
|
|
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 {
|
|
2
|
-
import { a as t,
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
166
|
-
if (v && S && await
|
|
165
|
+
let t = await o.runPromise(d(e));
|
|
166
|
+
if (v && S && await i(v.store, C, S, {
|
|
167
167
|
status: 200,
|
|
168
|
-
body:
|
|
168
|
+
body: t
|
|
169
169
|
}, Date.now()), c.etag === !0 && c.method === "GET") {
|
|
170
|
-
let e = `W/"${await b(JSON.stringify(
|
|
171
|
-
return
|
|
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,
|
|
177
|
+
} : x(200, t, {
|
|
178
178
|
...f,
|
|
179
179
|
ETag: e
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
|
-
return x(200,
|
|
182
|
+
return x(200, t, f);
|
|
183
183
|
} catch (e) {
|
|
184
|
-
if (v && S && await
|
|
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
|
-
}),
|
|
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
|
-
},
|
|
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
|
-
}),
|
|
45
|
+
}), ee = (e, t = ["admin:full"]) => ({
|
|
46
46
|
type: "system",
|
|
47
47
|
id: e,
|
|
48
48
|
tenantId: null,
|
|
49
49
|
scopes: t
|
|
50
|
-
}),
|
|
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 }) {},
|
|
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,
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
},
|
|
134
|
-
let n = t?.resolveScopes, r = n
|
|
135
|
-
return async (
|
|
136
|
-
let
|
|
137
|
-
...
|
|
138
|
-
store:
|
|
139
|
-
},
|
|
140
|
-
for (let
|
|
141
|
-
let e = await
|
|
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
|
|
144
|
-
if (
|
|
145
|
-
let
|
|
146
|
-
|
|
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
|
|
149
|
-
if (
|
|
150
|
-
let e =
|
|
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:
|
|
174
|
+
strategyId: o.id,
|
|
153
175
|
reason: e
|
|
154
|
-
}), new
|
|
176
|
+
}), new k({ reason: e });
|
|
155
177
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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:
|
|
162
|
-
granted:
|
|
163
|
-
}),
|
|
190
|
+
removed: d.removed,
|
|
191
|
+
granted: l.scopes
|
|
192
|
+
}), s = d.subject;
|
|
164
193
|
}
|
|
165
|
-
return e.credentialExpiresAt === void 0 ? { subject:
|
|
166
|
-
subject:
|
|
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:
|
|
201
|
+
strategyId: o.id,
|
|
173
202
|
reason: e.reason
|
|
174
|
-
}),
|
|
203
|
+
}), l = e.reason;
|
|
175
204
|
break;
|
|
176
205
|
}
|
|
177
206
|
}
|
|
178
|
-
let
|
|
207
|
+
let u = (e) => l !== void 0 && e.type === "anonymous" ? {
|
|
179
208
|
...e,
|
|
180
|
-
credentialRejected:
|
|
209
|
+
credentialRejected: l
|
|
181
210
|
} : e;
|
|
182
|
-
if (t?.fallback) return { subject:
|
|
183
|
-
let
|
|
184
|
-
if (
|
|
185
|
-
return { subject:
|
|
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
|
-
},
|
|
188
|
-
if (e.type === "anonymous") throw new
|
|
189
|
-
},
|
|
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
|
-
}) {},
|
|
193
|
-
typeof e == "object" && e &&
|
|
194
|
-
},
|
|
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 =
|
|
225
|
+
let t = L.get(e);
|
|
197
226
|
if (t) return t;
|
|
198
227
|
}
|
|
199
|
-
return
|
|
200
|
-
},
|
|
201
|
-
let n =
|
|
228
|
+
return I(e);
|
|
229
|
+
}, B = (e, t) => {
|
|
230
|
+
let n = z(e);
|
|
202
231
|
return n.includes("admin:full") || n.includes(t);
|
|
203
|
-
},
|
|
204
|
-
let n =
|
|
232
|
+
}, V = (e, t) => {
|
|
233
|
+
let n = I(e);
|
|
205
234
|
return n.includes("admin:full") || n.includes(t);
|
|
206
|
-
},
|
|
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 }),
|
|
210
|
-
|
|
211
|
-
},
|
|
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"),
|
|
254
|
+
].join("\n"), le = () => J, Y, X = (e) => {
|
|
226
255
|
Y = e;
|
|
227
|
-
},
|
|
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
|
|
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 (
|
|
239
|
-
return Q(e, new
|
|
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 =
|
|
280
|
+
let t = q(n.scope);
|
|
245
281
|
if (t.length === 0) continue;
|
|
246
|
-
let r = t.filter((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) => !
|
|
249
|
-
return Q(e, new
|
|
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
|
-
},
|
|
257
|
-
let i = (e) => new
|
|
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 (
|
|
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
|
-
},
|
|
307
|
+
}, fe = (e, n, r, i) => {
|
|
272
308
|
if (!n || n.length === 0) return t.succeed($(e, n, i));
|
|
273
|
-
let a =
|
|
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*
|
|
321
|
+
let t = yield* de(e, i, r);
|
|
279
322
|
if (t) return t;
|
|
280
323
|
continue;
|
|
281
324
|
}
|
|
282
|
-
let n =
|
|
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 (
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
},
|
|
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
|
-
},
|
|
36
|
-
|
|
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
|
-
},
|
|
40
|
-
|
|
47
|
+
}, d = (e) => (l.add(e), () => {
|
|
48
|
+
l.delete(e);
|
|
41
49
|
});
|
|
42
50
|
//#endregion
|
|
43
|
-
export { r as a, t as c, i,
|
|
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 {
|
|
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": {
|