@voltro/protocol 0.11.3 → 0.12.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 +436 -0
- package/dist/apikey.d.ts +40 -5
- package/dist/apikey.js +24 -18
- package/dist/{auth-DCE6m7Bo.js → auth-S3cnL6Tz.js} +14 -5
- package/dist/index.d.ts +89 -0
- package/dist/index.js +109 -109
- package/dist/rest.d.ts +93 -3
- package/dist/rest.js +68 -47
- package/dist/serverErrorBus-DhIVDkCi.js +0 -0
- package/dist/session.js +1 -1
- package/package.json +2 -2
- package/dist/serverErrorBus-B3hDwGoL.js +0 -0
package/dist/apikey.js
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
import { createHash as e } from "node:crypto";
|
|
2
2
|
//#region src/apiKeyStrategy.ts
|
|
3
3
|
var t = (t) => e("sha256").update(t, "utf8").digest("hex"), n = (e) => {
|
|
4
|
-
let
|
|
4
|
+
let t = {};
|
|
5
|
+
for (let [n, r] of Object.entries(e)) r !== void 0 && (t[n] = r);
|
|
6
|
+
return t;
|
|
7
|
+
}, r = (e) => {
|
|
8
|
+
let r = e.id ?? "voltro-apikey";
|
|
5
9
|
return {
|
|
6
|
-
id:
|
|
7
|
-
resolve: async (
|
|
8
|
-
let
|
|
9
|
-
if (!i) return { kind: "skip" };
|
|
10
|
-
let a = /^Bearer\s+(.+)$/i.exec(i.trim());
|
|
10
|
+
id: r,
|
|
11
|
+
resolve: async (i) => {
|
|
12
|
+
let a = i.headers.authorization ?? i.headers.Authorization;
|
|
11
13
|
if (!a) return { kind: "skip" };
|
|
12
|
-
let o = a
|
|
13
|
-
if (
|
|
14
|
-
let c =
|
|
15
|
-
|
|
14
|
+
let o = /^Bearer\s+(.+)$/i.exec(a.trim());
|
|
15
|
+
if (!o) return { kind: "skip" };
|
|
16
|
+
let s = o[1].trim(), c = e.prefix ?? "";
|
|
17
|
+
if (c && !s.startsWith(c)) return { kind: "skip" };
|
|
18
|
+
let l = await e.resolveKey(t(s));
|
|
19
|
+
return l ? {
|
|
16
20
|
kind: "matched",
|
|
17
21
|
subject: {
|
|
18
22
|
type: "apiKey",
|
|
19
|
-
id:
|
|
20
|
-
tenantId:
|
|
21
|
-
scopes:
|
|
22
|
-
metadata:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
id: l.id,
|
|
24
|
+
tenantId: l.tenantId,
|
|
25
|
+
scopes: l.scopes,
|
|
26
|
+
metadata: n({
|
|
27
|
+
...l.metadata ?? {},
|
|
28
|
+
provider: r,
|
|
29
|
+
userId: l.onBehalfOf ?? void 0,
|
|
30
|
+
createdBy: l.createdBy ?? void 0
|
|
31
|
+
})
|
|
26
32
|
}
|
|
27
33
|
} : {
|
|
28
34
|
kind: "failed",
|
|
@@ -32,4 +38,4 @@ var t = (t) => e("sha256").update(t, "utf8").digest("hex"), n = (e) => {
|
|
|
32
38
|
};
|
|
33
39
|
};
|
|
34
40
|
//#endregion
|
|
35
|
-
export {
|
|
41
|
+
export { r as apiKeyStrategy };
|
|
@@ -41,7 +41,16 @@ var r = t.Record({
|
|
|
41
41
|
id: e,
|
|
42
42
|
tenantId: null,
|
|
43
43
|
scopes: t
|
|
44
|
-
}), p = (e
|
|
44
|
+
}), p = (e, t) => {
|
|
45
|
+
let n = e;
|
|
46
|
+
return {
|
|
47
|
+
type: "serviceAccount",
|
|
48
|
+
id: e.id ?? "system",
|
|
49
|
+
tenantId: t,
|
|
50
|
+
scopes: n.scopes ?? ["admin:full"],
|
|
51
|
+
...n.metadata === void 0 ? {} : { metadata: n.metadata }
|
|
52
|
+
};
|
|
53
|
+
}, m = (e) => e.type === "system", h = class extends e.Tag("@voltro/Subject")() {}, g = class extends e.Tag("@voltro/ConnectionInfo")() {}, _ = class extends n.Tag()("@voltro/AuthMiddleware", { provides: h }) {}, v = class extends n.Tag()("@voltro/ConnectionInfoMiddleware", { provides: g }) {}, y = (e) => typeof e.mountRoutes == "function", b = (e, t) => async (n) => {
|
|
45
54
|
for (let r of e) {
|
|
46
55
|
let e = await r.resolve(n);
|
|
47
56
|
if (e.kind === "matched") return e.subject;
|
|
@@ -55,10 +64,10 @@ var r = t.Record({
|
|
|
55
64
|
}
|
|
56
65
|
if (t?.fallback) return t.fallback(n);
|
|
57
66
|
let r = n.headers["x-tenant"] ?? null;
|
|
58
|
-
if (r === null && t?.anonymousTenantRequired === !0) throw new
|
|
67
|
+
if (r === null && t?.anonymousTenantRequired === !0) throw new x({ reason: "tenant required (x-tenant header missing)" });
|
|
59
68
|
return d(r);
|
|
60
|
-
},
|
|
61
|
-
if (e.type === "anonymous") throw new
|
|
69
|
+
}, x = class extends t.TaggedError()("Unauthenticated", { reason: t.optional(t.String) }) {}, S = (e, t) => {
|
|
70
|
+
if (e.type === "anonymous") throw new x(t === void 0 ? {} : { reason: t });
|
|
62
71
|
};
|
|
63
72
|
//#endregion
|
|
64
|
-
export {
|
|
73
|
+
export { h as a, S as c, m as d, f, u as i, b as l, g as n, x as o, p, v as r, d as s, _ as t, y as u };
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,37 @@ export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec;
|
|
|
39
39
|
/** A guard is either a scope check or a relationship check. */
|
|
40
40
|
export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
|
|
41
41
|
|
|
42
|
+
export declare const APIKEY_ISSUE_ORG_SCOPE = "apikeys:issue:org";
|
|
43
|
+
|
|
44
|
+
export declare const APIKEY_ISSUE_OTHER_SCOPE = "apikeys:issue:other";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* API-key issuance rights.
|
|
48
|
+
*
|
|
49
|
+
* One `admin:full` gate for all key minting was too coarse to express what
|
|
50
|
+
* products actually need: it means only a full admin can ever mint a key, so a
|
|
51
|
+
* normal user cannot create their own even narrowly-scoped credential, and an
|
|
52
|
+
* admin minting one FOR someone is indistinguishable from minting one for
|
|
53
|
+
* themselves.
|
|
54
|
+
*
|
|
55
|
+
* Three separable capabilities instead:
|
|
56
|
+
*
|
|
57
|
+
* `apikeys:issue:self` - mint a key that acts as ME. The common self-service
|
|
58
|
+
* case; the key can never exceed the holder's own
|
|
59
|
+
* scopes.
|
|
60
|
+
* `apikeys:issue:org` - mint an ORG key: acts as no person, belongs to the
|
|
61
|
+
* organization. A CI credential. Separate because
|
|
62
|
+
* "may create a personal token" and "may create a
|
|
63
|
+
* credential that outlives my account" are genuinely
|
|
64
|
+
* different levels of trust.
|
|
65
|
+
* `apikeys:issue:other` - mint a key ON BEHALF OF another user. Admin
|
|
66
|
+
* territory: it is the ability to act as someone
|
|
67
|
+
* else, so it is never implied by the other two.
|
|
68
|
+
*
|
|
69
|
+
* `admin:full` satisfies all three, as it does every scope.
|
|
70
|
+
*/
|
|
71
|
+
export declare const APIKEY_ISSUE_SELF_SCOPE = "apikeys:issue:self";
|
|
72
|
+
|
|
42
73
|
/**
|
|
43
74
|
* Apply a `RowPatch` to `prev`, producing `next`. Exact inverse of
|
|
44
75
|
* `diffRows`: `applyRowPatch(prev, diffRows(prev, next))` deep-equals
|
|
@@ -1257,6 +1288,27 @@ export declare interface PluginHttpRouteResult {
|
|
|
1257
1288
|
readonly body?: string | Uint8Array;
|
|
1258
1289
|
readonly contentType?: string;
|
|
1259
1290
|
readonly headers?: Record<string, string>;
|
|
1291
|
+
/** Stream the response (SSE) instead of sending `body`. See
|
|
1292
|
+
* {@link PluginHttpRouteStream}. */
|
|
1293
|
+
readonly stream?: PluginHttpRouteStream;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* A long-lived Server-Sent-Events body. When a route result carries this, the
|
|
1298
|
+
* serve layer streams the response until the client disconnects instead of
|
|
1299
|
+
* sending a buffered body — `body` is ignored.
|
|
1300
|
+
*
|
|
1301
|
+
* `subscribe` receives an `emit` that takes ONE already-SSE-framed chunk (e.g.
|
|
1302
|
+
* `` `event: snapshot\ndata: ${json}\n\n` ``) and MUST return an unsubscribe
|
|
1303
|
+
* function. The serve layer runs that unsubscribe when the client goes away, so
|
|
1304
|
+
* whatever the route opened (a dispatcher subscription, an interval) is released
|
|
1305
|
+
* — a stream route that leaks its subscription leaks it per connection.
|
|
1306
|
+
*/
|
|
1307
|
+
export declare interface PluginHttpRouteStream {
|
|
1308
|
+
readonly subscribe: (emit: (chunk: string) => void) => () => void;
|
|
1309
|
+
/** Keep-alive comment interval in ms (default 15000; `0` disables). Without
|
|
1310
|
+
* it an idle SSE connection is dropped by proxies after ~30–60s. */
|
|
1311
|
+
readonly keepAliveMs?: number;
|
|
1260
1312
|
}
|
|
1261
1313
|
|
|
1262
1314
|
export declare interface PluginInspectEndpoint {
|
|
@@ -2201,6 +2253,25 @@ export declare type Target<Input = unknown, Row = unknown> = TargetSpec<Input, R
|
|
|
2201
2253
|
|
|
2202
2254
|
export declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
|
|
2203
2255
|
|
|
2256
|
+
/**
|
|
2257
|
+
* The subject a `storeForTenant(id)` view runs as — the caller's identity,
|
|
2258
|
+
* re-pointed at ONE explicit tenant.
|
|
2259
|
+
*
|
|
2260
|
+
* Lives here, next to the other subject constructors, because BOTH the serve
|
|
2261
|
+
* context builder (`@voltro/cli`) and the test harness (`@voltro/testing`)
|
|
2262
|
+
* must produce the identical subject. When it lived in the CLI, the harness
|
|
2263
|
+
* could not reach it, and `ctx.storeForTenant` was simply absent under test —
|
|
2264
|
+
* a handler that used it had no way to be tested at all.
|
|
2265
|
+
*
|
|
2266
|
+
* The narrowed return type is the point, not decoration: it states at the type
|
|
2267
|
+
* level that this can never hand back a `system` subject, which is the variant
|
|
2268
|
+
* whose null tenant means "all tenants". Producing one here would silently
|
|
2269
|
+
* widen a deliberately narrow view back to every tenant.
|
|
2270
|
+
*/
|
|
2271
|
+
export declare const tenantScopedSubject: (subject: Subject, tenantId: string) => Extract<Subject, {
|
|
2272
|
+
readonly type: "serviceAccount";
|
|
2273
|
+
}>;
|
|
2274
|
+
|
|
2204
2275
|
export declare const toRpc: <Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Err extends Schema.Schema.All>(descriptor: QueryProcedureDescriptor<Name, Input, Output, Err> | MutationProcedureDescriptor<Name, Input, Output, Err> | ActionProcedureDescriptor<Name, Input, Output, Err> | StreamProcedureDescriptor<Name, Input, Output, Err>) => Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Stream<Schema.Union<[Schema.Struct<{
|
|
2205
2276
|
_tag: Schema.Literal<["snapshot"]>;
|
|
2206
2277
|
revision: typeof Schema.Number;
|
|
@@ -2428,6 +2499,24 @@ export declare interface VoltroPlugin {
|
|
|
2428
2499
|
* dashboard Env panel. Mirror exactly what the plugin reads.
|
|
2429
2500
|
*/
|
|
2430
2501
|
readonly declaredEnv?: ReadonlyArray<PluginEnvVar>;
|
|
2502
|
+
/**
|
|
2503
|
+
* The authorization scopes this plugin DEFINES for the app — its scope
|
|
2504
|
+
* vocabulary. Distinct from `permissions` above, which is what the plugin
|
|
2505
|
+
* itself asks to be granted.
|
|
2506
|
+
*
|
|
2507
|
+
* `@voltro/plugin-rbac` fills this with every scope its `roles` map grants,
|
|
2508
|
+
* because that map already IS the app's declared vocabulary. The capability
|
|
2509
|
+
* manifest unions these, and `voltro check` compares each handler's required
|
|
2510
|
+
* scopes against the union: a guard demanding a scope no role can grant (a
|
|
2511
|
+
* typo, a rename) makes that procedure permanently uncallable, silently.
|
|
2512
|
+
*
|
|
2513
|
+
* Only declare this when the list is EXHAUSTIVE. A plugin that also grants
|
|
2514
|
+
* scopes from a dynamic source (a custom resolver, per-row ACLs) must leave
|
|
2515
|
+
* it undefined — the check treats "no declared scopes" as "cannot conclude"
|
|
2516
|
+
* and stays dormant, which is the honest outcome. A partial list would flag
|
|
2517
|
+
* correct code, and a check that cries wolf gets ignored.
|
|
2518
|
+
*/
|
|
2519
|
+
readonly declaredScopes?: ReadonlyArray<string>;
|
|
2431
2520
|
/**
|
|
2432
2521
|
* Optional `effect/Schema` describing the plugin's user-supplied
|
|
2433
2522
|
* config. When present the framework decodes the operator's config
|