@voltro/protocol 0.85.0 → 0.86.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/README.md +14 -0
  3. package/THIRD-PARTY-NOTICES.md +5 -33
  4. package/dist/approval.js +64 -0
  5. package/dist/approvalDescriptors.js +26 -0
  6. package/dist/auth.js +369 -0
  7. package/dist/boundedRing.js +27 -0
  8. package/dist/businessRule.js +11 -0
  9. package/dist/connection.js +73 -0
  10. package/dist/devActivity.js +4 -0
  11. package/dist/errorValue.js +21 -0
  12. package/dist/event.js +104 -0
  13. package/dist/idempotency.js +66 -0
  14. package/dist/identity.js +5 -9
  15. package/dist/index.d.ts +225 -21
  16. package/dist/index.js +42 -1700
  17. package/dist/inspect.js +14 -240
  18. package/dist/inspectBackfill.js +52 -0
  19. package/dist/inspectCommandRejection.js +21 -0
  20. package/dist/inspectProjection.js +133 -0
  21. package/dist/inspectStorageProjection.js +86 -0
  22. package/dist/inspectValueIdentity.js +4 -0
  23. package/dist/inspectWorkflowExecution.js +34 -0
  24. package/dist/inspectWorkflowQueuedStart.js +31 -0
  25. package/dist/jsonSchema.js +54 -0
  26. package/dist/outbox.js +10 -0
  27. package/dist/pairing.js +75 -0
  28. package/dist/plugin.js +61 -0
  29. package/dist/pluginNaming.js +32 -0
  30. package/dist/procedure.js +262 -0
  31. package/dist/publicApi.js +344 -0
  32. package/dist/reactivityChannel.js +34 -0
  33. package/dist/rest.d.ts +33 -0
  34. package/dist/rest.js +213 -470
  35. package/dist/restFormat.js +163 -0
  36. package/dist/rowEquality.js +31 -0
  37. package/dist/rowPatch.js +182 -0
  38. package/dist/schemaShape.js +67 -0
  39. package/dist/scopes.js +240 -0
  40. package/dist/serverErrorBus.js +15 -0
  41. package/dist/serviceCall.js +14 -0
  42. package/dist/session.js +13 -15
  43. package/dist/{sessionHeader-DriTZhmv.js → sessionHeader.js} +1 -1
  44. package/dist/storageAccess.js +14 -0
  45. package/dist/storeErrors.js +66 -0
  46. package/dist/storeScreenshots.js +90 -0
  47. package/dist/strictInput.js +27 -0
  48. package/dist/subscriptionEvent.js +37 -0
  49. package/dist/tenantSelection.js +40 -0
  50. package/dist/time.js +7 -0
  51. package/dist/undo.js +38 -0
  52. package/dist/validation.js +39 -0
  53. package/dist/webSocketGateway.js +10 -0
  54. package/dist/workflow.js +297 -0
  55. package/dist/workflowAccess.js +61 -0
  56. package/dist/workflowRestart.js +45 -0
  57. package/dist/workflowRetry.js +44 -0
  58. package/dist/workflowStartOutcome.js +30 -0
  59. package/package.json +3 -3
  60. package/dist/restFormat-BlgTLxm1.js +0 -298
  61. package/dist/scopes-ClywBij8.js +0 -548
package/dist/event.js ADDED
@@ -0,0 +1,104 @@
1
+ import { RequestedTenantRefused as e } from "./tenantSelection.js";
2
+ import { Unauthenticated as t } from "./auth.js";
3
+ import { NoSuchResource as n, ScopeError as r, declaresResourceGuard as i, isOpenAccess as a, openAccessSpec as o } from "./scopes.js";
4
+ import { strictInput as s } from "./strictInput.js";
5
+ import { hasEnforcedGuard as c } from "./procedure.js";
6
+ import { Schema as l } from "effect";
7
+ import { Rpc as u } from "effect/rpc";
8
+ //#region src/event.ts
9
+ var d = 7500, f = (e) => typeof e == "object" && !!e && e.kind === "event", p = (e) => {
10
+ if (e.name.length === 0) throw Error("defineEvent: `name` must not be empty");
11
+ if (/\s/.test(e.name)) throw Error(`defineEvent("${e.name}"): \`name\` must not contain whitespace.\n The name is used as a broker subject segment. NATS refuses a subject with
12
+ whitespace and delivers nothing — silently, and only on that broker, so an
13
+ app that works on Redis stops working when the transport changes.
14
+ Use a dot for namespacing: "orders.paid".`);
15
+ if (e.guards !== void 0 && e.guards.length === 0) throw Error(`defineEvent("${e.name}"): \`guards: []\` is empty, so it enforces nothing —\n but it reads at the call site as if the event were protected. Omit the field
16
+ for an unguarded event, or list the scopes that may subscribe.`);
17
+ if (e.openAccess !== void 0) {
18
+ if (e.openAccess.trim() === "") throw Error(`defineEvent("${e.name}"): \`openAccess\` needs a REASON, not an empty string. It is\n the sentence a reviewer reads to decide whether anyone should really be able to
19
+ subscribe without an authorization check — e.g. \`openAccess: 'public scoreboard,
20
+ no caller data'\`.`);
21
+ if (e.guards !== void 0 && e.guards.length > 0) throw Error(`defineEvent("${e.name}"): \`openAccess\` and \`guards\` are two different access\n decisions, so declaring both says the event is protected AND open. Keep the guards
22
+ if a subscriber must hold a scope; drop them if anyone may listen.`);
23
+ }
24
+ if (e.webhook?.rateLimit !== void 0 && e.webhook.rateLimit.perMinute < 1) throw Error(`defineEvent("${e.name}"): \`webhook.rateLimit.perMinute\` is ${e.webhook.rateLimit.perMinute}, which defers every delivery forever.\n There is no "unlimited" spelling here — omit \`rateLimit\` for no ceiling.`);
25
+ if (e.webhook?.version !== void 0 && e.webhook.version < 1) throw Error(`defineEvent("${e.name}"): \`webhook.version\` must be >= 1 (default 1).\n A subscriber pinned to 1 would read this event as BEHIND its own version,
26
+ which is the opposite of what a version bump is for.`);
27
+ if (e.delivery === "latest" && e.webhook !== void 0) throw Error(`defineEvent(${e.name}): \`delivery: 'latest'\` cannot be combined with \`webhook\`.\n \`latest\` means a superseded delivery did not matter — but a webhook delivery is a
28
+ durable side effect at a third party, and one already sent cannot be superseded.
29
+ A high-rate event with an HTTP audience is also 60 deliveries per second per target;
30
+ the rate limit DEFERS the excess as pending rows, so it grows a table instead of failing.
31
+ Split them: publish the high-rate event for clients, and publish a separate, coarser
32
+ event (a summary, a state change) for the outside world.`);
33
+ return {
34
+ kind: "event",
35
+ name: e.name,
36
+ key: e.key,
37
+ payload: e.payload,
38
+ guards: e.openAccess === void 0 ? e.guards : [o(e.openAccess.trim())],
39
+ openAccess: e.openAccess,
40
+ rewind: e.rewind,
41
+ webhook: e.webhook,
42
+ delivery: e.delivery
43
+ };
44
+ }, m = (e) => l.Struct({
45
+ _tag: l.Literal("event"),
46
+ origin: l.String,
47
+ n: l.Number,
48
+ emittedAt: l.Number,
49
+ payload: e
50
+ }), h = l.Struct({
51
+ _tag: l.Literal("gap"),
52
+ missed: l.Number,
53
+ reason: l.Literals(["buffer", "resume"])
54
+ }), g = l.Struct({ _tag: l.Literal("attached") }), _ = (e) => l.Union([
55
+ g,
56
+ m(e),
57
+ h
58
+ ]), v = l.Struct({
59
+ origin: l.String,
60
+ n: l.Number
61
+ }), y = (e) => l.Struct({
62
+ key: e,
63
+ resume: l.optional(l.Array(v))
64
+ }), b = (o, d) => {
65
+ let f = o.guards !== void 0 && o.guards.some((e) => !a(e)) ? l.Union([
66
+ r,
67
+ t,
68
+ e
69
+ ]) : l.Union([t, e]), p = i(o.guards) ? l.Union([f, n]) : f, m = d && d.length > 0 ? l.Union([p, ...d]) : p;
70
+ return u.make(o.name, {
71
+ payload: s(y(o.key), o.name, c(o.guards)),
72
+ success: _(o.payload),
73
+ error: m,
74
+ stream: !0
75
+ });
76
+ }, x = class extends l.TaggedError()("EventPayloadInvalid", {
77
+ event: l.String,
78
+ message: l.String
79
+ }) {}, S = class extends l.TaggedError()("EventKeyInvalid", {
80
+ event: l.String,
81
+ message: l.String
82
+ }) {}, C = class extends l.TaggedError()("EventPayloadTooLarge", {
83
+ event: l.String,
84
+ bytes: l.Number,
85
+ limit: l.Number
86
+ }) {}, w = (e) => {
87
+ if (typeof e != "object" || !e) return JSON.stringify(e) ?? "null";
88
+ let t = Object.entries(e), n = 0;
89
+ for (let e of t) e[1] !== void 0 && (t[n++] = e);
90
+ return t.length = n, t.sort(([e], [t]) => e < t ? -1 : +(e > t)), JSON.stringify(t);
91
+ }, T = "\0", E = (e, t, n) => [
92
+ e ?? "~",
93
+ t,
94
+ w(n)
95
+ ].join("\0"), D = (e) => {
96
+ let [t = "~", n = "", r = ""] = e.split("\0");
97
+ return {
98
+ tenantId: t === "~" ? null : t,
99
+ event: n,
100
+ key: r
101
+ };
102
+ }, O = (e) => e.split("\0").join(" · ");
103
+ //#endregion
104
+ export { T as EVENT_ROUTE_SEP, S as EventKeyInvalid, x as EventPayloadInvalid, C as EventPayloadTooLarge, d as MAX_EVENT_ENVELOPE_BYTES, p as defineEvent, w as encodeEventKey, g as eventAttached, m as eventEnvelope, h as eventGap, v as eventResumePoint, E as eventRoute, _ as eventStreamEvent, y as eventSubscribeInput, b as eventToRpc, O as formatEventRoute, f as isEventDescriptor, D as parseEventRoute };
@@ -0,0 +1,66 @@
1
+ import { Schema as e } from "effect";
2
+ //#region src/idempotency.ts
3
+ var t = (e, t, n) => `${e ?? "(global)"}|${t.toUpperCase()}|${n}`, n = (e, t, n, r) => r === void 0 ? `${e ?? "(global)"}|${t ?? "(anon)"}|WS|${n}` : `WS-connection:${JSON.stringify([
4
+ e ?? null,
5
+ t ?? null,
6
+ n,
7
+ r
8
+ ])}`, r = class extends e.TaggedError()("MutationConnectionRequired", { traceId: e.String }) {
9
+ get message() {
10
+ return "this mutation requires a live native WebSocket connection";
11
+ }
12
+ }, i = async (e, t, n, r, i, o = a) => {
13
+ let s = await e.claim(t, n, i, r, o);
14
+ return s === "claimed" ? { kind: "fresh" } : s.status === "completed" && s.response ? {
15
+ kind: "replay",
16
+ response: s.response
17
+ } : { kind: "conflict" };
18
+ }, a = 3e4, o = (e, t, n, r = a, i = Date.now) => {
19
+ let o = setInterval(() => {
20
+ e.renew(t, n, i()).catch(() => {});
21
+ }, Math.max(1, Math.floor(r / 3)));
22
+ return o.unref?.(), () => clearInterval(o);
23
+ }, s = (e, t, n, r, i) => e.complete(t, n, r, i), c = (e, t, n) => e.release(t, n), l = () => {
24
+ let e = /* @__PURE__ */ new Map(), t = (e, t) => `${e}\u0000${t}`;
25
+ return {
26
+ get: async (n, r) => e.get(t(n, r)) ?? null,
27
+ claim: async (n, r, i, a, o) => {
28
+ let s = e.get(t(n, r));
29
+ return s && i - s.createdAt < u(s.status, a, o) ? s : (e.set(t(n, r), {
30
+ scope: n,
31
+ key: r,
32
+ status: "in_flight",
33
+ response: null,
34
+ createdAt: i
35
+ }), "claimed");
36
+ },
37
+ complete: async (n, r, i, a) => {
38
+ e.set(t(n, r), {
39
+ scope: n,
40
+ key: r,
41
+ status: "completed",
42
+ response: i,
43
+ createdAt: a
44
+ });
45
+ },
46
+ release: async (n, r) => {
47
+ e.delete(t(n, r));
48
+ },
49
+ renew: async (n, r, i) => {
50
+ let a = e.get(t(n, r));
51
+ a?.status === "in_flight" && e.set(t(n, r), {
52
+ ...a,
53
+ createdAt: i
54
+ });
55
+ }
56
+ };
57
+ }, u = (e, t, n) => e === "completed" ? t : Math.min(t, n), d = class extends e.TaggedError()("MutationInFlight", {
58
+ traceId: e.String,
59
+ retryAfterMs: e.Number
60
+ }) {
61
+ get message() {
62
+ return `a request with this idempotency key is still in flight — replay the same key after ${this.retryAfterMs}ms`;
63
+ }
64
+ }, f = 1e3;
65
+ //#endregion
66
+ export { a as DEFAULT_IN_FLIGHT_LEASE_MS, f as MUTATION_IN_FLIGHT_RETRY_AFTER_MS, r as MutationConnectionRequired, d as MutationInFlight, i as beginIdempotent, c as failIdempotent, s as finishIdempotent, u as freshFor, o as holdIdempotentClaim, t as idempotencyScope, l as memoryIdempotencyStore, n as wsMutationIdempotencyScope };
package/dist/identity.js CHANGED
@@ -33,15 +33,11 @@ var a = e({ scope: "voltro:identity" }), o = Symbol.for("@voltro/runtime/process
33
33
  } catch {}
34
34
  return l.version;
35
35
  }, m = (e) => {
36
- if (l.frozen !== void 0 && l.frozen.bootPath !== e) {
37
- a.warn(`process identity was already resolved as ${l.frozen.replicaId} (bootPath=${l.frozen.bootPath}); keeping it rather than becoming ${e}. Call setProcessBootPath before anything reads the identity.`, {
38
- resolved: l.frozen.replicaId,
39
- was: l.frozen.bootPath,
40
- requested: e
41
- });
42
- return;
43
- }
44
- l.bootPath = e;
36
+ l.frozen !== void 0 && l.frozen.bootPath !== e ? a.warn(`process identity was already resolved as ${l.frozen.replicaId} (bootPath=${l.frozen.bootPath}); keeping it rather than becoming ${e}. Call setProcessBootPath before anything reads the identity.`, {
37
+ resolved: l.frozen.replicaId,
38
+ was: l.frozen.bootPath,
39
+ requested: e
40
+ }) : l.bootPath = e;
45
41
  }, h = (e) => {
46
42
  l.appVersion = e, l.frozen !== void 0 && (l.frozen = {
47
43
  ...l.frozen,
package/dist/index.d.ts CHANGED
@@ -546,7 +546,7 @@ export declare interface AuthenticatedGuardSpec {
546
546
  * an auth resolver before reaching its handler. The application provides
547
547
  * the resolver via `Layer.succeed(AuthMiddleware, AuthMiddleware.of(fn))`,
548
548
  * where `fn` receives `{headers, payload, rpc, clientId}` and returns an
549
- * `Effect<Subject, Unauthenticated>` — typically by inspecting headers / Bearer token and
549
+ * `Effect<Subject, Unauthenticated | RequestedTenantRefused>` — typically by inspecting headers / Bearer token and
550
550
  * looking the caller up.
551
551
  *
552
552
  * `provides: SubjectService` means the resolver's return value is placed
@@ -562,7 +562,7 @@ export declare interface AuthenticatedGuardSpec {
562
562
  export declare class AuthMiddleware extends AuthMiddleware_base {
563
563
  }
564
564
 
565
- declare const AuthMiddleware_base: RpcMiddleware.ServiceClass<AuthMiddleware, "@voltro/AuthMiddleware", SubjectService, typeof Unauthenticated, never, never, false>;
565
+ declare const AuthMiddleware_base: RpcMiddleware.ServiceClass<AuthMiddleware, "@voltro/AuthMiddleware", SubjectService, Schema.Union<readonly [typeof Unauthenticated, typeof RequestedTenantRefused]>, never, never, false>;
566
566
 
567
567
  export declare type AuthRouteHandler = (req: AuthRouteRequest) => Promise<AuthRouteResponse>;
568
568
 
@@ -726,6 +726,11 @@ export declare type AuthSubjectGuardVerdict = {
726
726
  */
727
727
  export declare const beginIdempotent: (store: IdempotencyStore, scope: string, key: string, ttlMs: number, now: number, inFlightLeaseMs?: number) => Promise<IdempotencyOutcome>;
728
728
 
729
+ /** Bind a process-local authority refresh to an independent subject snapshot.
730
+ * Neither the resolver nor its credentials become serializable subject fields.
731
+ * Request and durable-execution hosts use the same identity-preserving refresh. */
732
+ export declare const bindSubjectAuthority: (subject: Subject, resolve: () => Promise<Subject>) => Subject;
733
+
729
734
  /** Bounded insertion-order storage. Appending overwrites one slot when full;
730
735
  * reads never expose its circular physical order or retained array. */
731
736
  export declare class BoundedRing<T> implements Iterable<T> {
@@ -809,7 +814,10 @@ export declare const checkGuards: (subject: Subject, guards: ReadonlyArray<AnyCh
809
814
  * guard has a `resource`, it extracts the resource id from `input` and asks the
810
815
  * resolver whether the caller holds each scope on THAT resource. A globally-held
811
816
  * scope (or the `admin:full` bypass) still satisfies without a resolver call;
812
- * only the gap ("no global grant") falls through to the resolver. A failed
817
+ * only the gap ("no global grant") falls through to the resolver. A guard with
818
+ * `resourceOnly: true` always requires the resource resolver's verdict, even
819
+ * for globally-held scopes or `admin:full`, and refuses if its resource or
820
+ * resolver is unavailable. A failed
813
821
  * resolver Effect is treated as a denial (fail-closed).
814
822
  *
815
823
  * Returns a typed `ScopeError` naming the first unmet scope, or `null` when
@@ -893,15 +901,15 @@ export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrat
893
901
  /** What an unauthenticated caller is — see {@link AnonymousPolicy}.
894
902
  * Default `'no-tenant'`. Ignored when `fallback` is supplied. */
895
903
  readonly anonymous?: AnonymousPolicyOrDevTenant;
896
- /**
897
- * The subject's memberships — the ONE source, strategy-independent. Read
898
- * only when a stored tenant selection exists for the subject and names a
899
- * tenant other than the credential's, and then on every such request: a
900
- * membership revoked a moment ago must not keep a selection alive, and a
901
- * switch made a moment ago must be visible on the next call, so there is
902
- * no staleness window to configure here. Requires `tenantSelection`.
903
- */
904
+ /** Fresh strategy-independent memberships. Explicit requested tenants are
905
+ * checked on every resolution, including the credential tenant. Stored
906
+ * selections read memberships only when they differ from the credential.
907
+ * Requires `tenantSelection` or `requestedTenant`. */
904
908
  readonly memberships?: MembershipsSource;
909
+ /** Request-local tenant hint, checked against fresh `memberships` before
910
+ * scopes and guards. Explicit targets bypass the stored selection without
911
+ * changing it. Invalid hints fail closed, never fall back. */
912
+ readonly requestedTenant?: RequestedTenantSource;
905
913
  /**
906
914
  * Where a subject's tenant selection is stored. Read for every matched
907
915
  * user / apiKey / serviceAccount subject (one point lookup); a stored
@@ -1413,6 +1421,9 @@ export declare const createRowPatchApplier: () => {
1413
1421
  readonly reset: () => void;
1414
1422
  };
1415
1423
 
1424
+ /** The latest authenticated authority for a request or live subscription. */
1425
+ export declare const currentSubjectAuthority: (subject: Subject) => Subject;
1426
+
1416
1427
  /**
1417
1428
  * What a descriptor CARRIES: the author's guards, or the erased form of their
1418
1429
  * `openAccess:` decision — never both.
@@ -1990,6 +2001,38 @@ export declare interface DeleteTarget<Input = unknown> extends NestedTargetField
1990
2001
  readonly identify?: ((input: Input) => string | ReadonlyArray<string>) | undefined;
1991
2002
  }
1992
2003
 
2004
+ /** The HMR custom event `voltro dev` publishes its activity on. */
2005
+ export declare const DEV_ACTIVITY_EVENT = "voltro:dev-activity";
2006
+
2007
+ /** What `voltro dev` reports over {@link DEV_ACTIVITY_EVENT}. A `document`
2008
+ * message describes ONE document request (`id` is per server process); a
2009
+ * `compile` message says whether Vite is transforming browser modules. */
2010
+ export declare type DevActivityMessage = {
2011
+ readonly type: 'document';
2012
+ readonly id: number;
2013
+ /** Path + search exactly as the browser asked for it. */
2014
+ readonly url: string;
2015
+ /** `start` once, when the request arrives; then whichever the server
2016
+ * is doing as it changes — `compiling` while Vite transforms modules
2017
+ * for it, `rendering` once a server render runs, `waiting` for
2018
+ * anything else (Vite's own html, a middleware). */
2019
+ readonly phase: 'start' | 'waiting' | 'compiling' | 'rendering';
2020
+ } | {
2021
+ readonly type: 'document';
2022
+ readonly id: number;
2023
+ readonly url: string;
2024
+ readonly phase: 'end';
2025
+ /** `document`: the browser got a page to show. `redirect`: it will ask
2026
+ * for `location` next. `no-document`: nothing will replace this page
2027
+ * (204/205, an attachment). `aborted`: the browser went away first. */
2028
+ readonly outcome: 'document' | 'redirect' | 'no-document' | 'aborted';
2029
+ readonly location?: string;
2030
+ } | {
2031
+ readonly type: 'compile';
2032
+ readonly consumer: 'client';
2033
+ readonly active: boolean;
2034
+ };
2035
+
1993
2036
  /** Pair, list and revoke devices (`devices: true`) for the caller's tenant.
1994
2037
  * One scope, not three: a device always acts as itself, in the pairer's
1995
2038
  * tenant, so there is no self / org / other distinction to draw.
@@ -2059,6 +2102,13 @@ export declare const encodeRestError: (format: RestFormat, error: RestFormatErro
2059
2102
  */
2060
2103
  export declare const encodeRestSuccess: (format: RestFormat, value: unknown, ctx: RestFormatContext) => RestFormatResponse;
2061
2104
 
2105
+ export declare interface EnqueueOptions {
2106
+ /** Deduplicate while an earlier intent is still pending, not permanently. */
2107
+ readonly idempotencyKey?: string;
2108
+ readonly maxAttempts?: number;
2109
+ readonly delayMs?: number;
2110
+ }
2111
+
2062
2112
  /**
2063
2113
  * The text to SHOW for a failure value — its `message`, else its `_tag`, else
2064
2114
  * `String(value)`.
@@ -2076,8 +2126,8 @@ export declare const errorMessage: (err: unknown) => string;
2076
2126
  /**
2077
2127
  * Read the `_tag` off a thrown error value, or `undefined` when it has none.
2078
2128
  *
2079
- * It lives here, beside `toRpc`, because `toRpc` is what puts the tag on the
2080
- * wire — one file owns both ends of that contract. It used to live in
2129
+ * It lives in protocol because `toRpc` (procedure.ts) is what puts the tag on
2130
+ * the wire — one package owns both ends of that contract. It used to live in
2081
2131
  * `@voltro/client`, and being there had a cost nobody could see from inside the
2082
2132
  * framework: an app's shared error handler lived in a package that pulled only
2083
2133
  * `@voltro/i18n`, so reading a tag meant taking a dependency on the whole client
@@ -2481,10 +2531,10 @@ export declare const formatEventRoute: (route: string) => string;
2481
2531
  * test pins it against `wireErrorUnion` per kind, so the two cannot drift.
2482
2532
  */
2483
2533
  export declare const FRAMEWORK_WIRE_ERRORS: {
2484
- readonly query: readonly [typeof ScopeError, typeof Unauthenticated];
2485
- readonly stream: readonly [typeof ScopeError, typeof Unauthenticated];
2486
- readonly action: readonly [typeof ScopeError, typeof Unauthenticated, typeof ValidationError, typeof ValidationErrors];
2487
- readonly mutation: readonly [typeof ScopeError, typeof Unauthenticated, typeof BusinessRuleViolation, typeof MutationInFlight, typeof VersionConflict, typeof ValidationError, typeof ValidationErrors];
2534
+ readonly query: readonly [typeof ScopeError, typeof Unauthenticated, typeof RequestedTenantRefused];
2535
+ readonly stream: readonly [typeof ScopeError, typeof Unauthenticated, typeof RequestedTenantRefused];
2536
+ readonly action: readonly [typeof ScopeError, typeof Unauthenticated, typeof RequestedTenantRefused, typeof ValidationError, typeof ValidationErrors];
2537
+ readonly mutation: readonly [typeof ScopeError, typeof Unauthenticated, typeof RequestedTenantRefused, typeof BusinessRuleViolation, typeof MutationInFlight, typeof VersionConflict, typeof ValidationError, typeof ValidationErrors];
2488
2538
  };
2489
2539
 
2490
2540
  /** The error values {@link FRAMEWORK_WIRE_ERRORS} admits for `Kind`. */
@@ -2545,6 +2595,9 @@ export declare interface GuardCheckSpec {
2545
2595
  * global-scope check (the extractor is advisory), exactly as before.
2546
2596
  */
2547
2597
  readonly resource?: (input: unknown) => string | undefined;
2598
+ /** Require an explicit resource resolver verdict, even for globally held
2599
+ * scopes and `admin:full`. Missing resolver/resource refuses access. */
2600
+ readonly resourceOnly?: boolean;
2548
2601
  }
2549
2602
 
2550
2603
  /**
@@ -2983,6 +3036,25 @@ export declare const missingAccessDecision: (procedure?: string) => ScopeError;
2983
3036
  * fits several tries. */
2984
3037
  export declare const MUTATION_IN_FLIGHT_RETRY_AFTER_MS = 1000;
2985
3038
 
3039
+ /** Transaction-bound context for a plugin's required database contribution.
3040
+ * The executor result is available before the business transaction commits. */
3041
+ export declare interface MutationCommitContext extends RpcInterceptorContext {
3042
+ readonly kind: 'mutation';
3043
+ readonly store: DataStore;
3044
+ readonly output: unknown;
3045
+ /** Same enqueue facade as the executor, bound to this replay attempt. */
3046
+ readonly outbox?: OutboxEnqueueFacade;
3047
+ /** Runner invocation time, including guards and rolled-back retries. */
3048
+ readonly startedAt?: number;
3049
+ /** Elapsed milliseconds before required contribution work. */
3050
+ readonly durationMs?: number;
3051
+ }
3052
+
3053
+ /** Required transactional work, not an external side effect. A failure rolls
3054
+ * back the mutation. The hook may run again after a rolled-back contention
3055
+ * attempt, so all effects must remain on the supplied transaction store. */
3056
+ export declare type MutationCommitHook = (context: MutationCommitContext) => Effect.Effect<void, unknown>;
3057
+
2986
3058
  /** A connection-scoped RPC cannot execute or replay without a live WebSocket.
2987
3059
  * This is a transport precondition, not an authorization or membership grant. */
2988
3060
  export declare class MutationConnectionRequired extends MutationConnectionRequired_base {
@@ -3252,6 +3324,30 @@ export declare const openAccessSpec: (reason: string) => OpenAccessSpec;
3252
3324
  */
3253
3325
  export declare const orderByBearerPrefix: (strategies: ReadonlyArray<AuthStrategy>, headers: Readonly<Record<string, string | undefined>>) => ReadonlyArray<AuthStrategy>;
3254
3326
 
3327
+ /** Enqueue-only capability bound to the caller's actual transaction store. */
3328
+ export declare interface OutboxEnqueueFacade {
3329
+ /** Returns the stable delivery identity. Delivery happens after commit. */
3330
+ enqueue(effect: string, payload: Record<string, unknown>, options?: EnqueueOptions): Promise<string>;
3331
+ }
3332
+
3333
+ /** Stable delivery context, shared by app and plugin handlers. */
3334
+ export declare interface OutboxHandlerContext {
3335
+ readonly outboxId: string;
3336
+ readonly idempotencyKey: string | null;
3337
+ readonly payload: Record<string, unknown>;
3338
+ readonly attempt: number;
3339
+ readonly subjectId: string | null;
3340
+ readonly tenantId: string | null;
3341
+ readonly traceId: string | null;
3342
+ }
3343
+
3344
+ /** Delivery happens after commit and may repeat; handlers must be idempotent. */
3345
+ export declare interface OutboxHandlerDefinition<R = never> {
3346
+ readonly effect: string;
3347
+ readonly handler: (context: OutboxHandlerContext) => Promise<unknown> | Effect.Effect<unknown, unknown, R>;
3348
+ readonly maxAttempts?: number;
3349
+ }
3350
+
3255
3351
  export declare interface Pairing {
3256
3352
  /** The api's base url as the DEVICE reaches it — a LAN address, never `localhost`. */
3257
3353
  readonly api: string;
@@ -3826,6 +3922,9 @@ export declare interface PluginHttpRoute {
3826
3922
  * handler branches on: exempting `/saml` exempts `POST /saml/anything`.
3827
3923
  */
3828
3924
  readonly originGuard?: 'exempt';
3925
+ readonly cors?: RestCors;
3926
+ /** Bearer-only routes must discard cookie authority before authentication. */
3927
+ readonly credentials?: 'bearer';
3829
3928
  }
3830
3929
 
3831
3930
  /**
@@ -4257,7 +4356,7 @@ export declare type PluginMigrationSqlClient = SqlClient.SqlClient;
4257
4356
  * boot log + `/_voltro/inspect/plugins` so operators audit what each
4258
4357
  * plugin asks for before installing.
4259
4358
  */
4260
- export declare type PluginPermission = 'rpc:intercept:mutation' | 'rpc:intercept:query' | 'rpc:intercept:action' | 'rpc:intercept:workflow' | 'schedule:fire' | 'schedule:read' | 'workflow:step' | 'http:intercept' | 'inspect:read' | 'inspect:write' | 'dashboard:mount' | 'store:write' | 'migration:run' | 'store:changes:read' | `secrets:read:${string}` | `network:outbound:${string}` | `plugin:hook:${string}`;
4359
+ export declare type PluginPermission = 'rpc:intercept:mutation' | 'rpc:intercept:query' | 'rpc:intercept:action' | 'rpc:intercept:workflow' | 'schedule:fire' | 'schedule:read' | 'workflow:step' | 'http:intercept' | 'inspect:read' | 'inspect:write' | 'dashboard:mount' | 'outbox:handle' | 'store:write' | 'migration:run' | 'store:changes:read' | `secrets:read:${string}` | `network:outbound:${string}` | `plugin:hook:${string}`;
4261
4360
 
4262
4361
  /** What a plugin collects and uses — folded by `voltro compliance generate`
4263
4362
  * into PrivacyInfo.xcprivacy and the Play Data-safety answers. A plugin that
@@ -4950,7 +5049,7 @@ export declare const queryToRpc: <Name extends string, Input extends Schema.Top,
4950
5049
  }>;
4951
5050
  }>, Schema.Struct<{
4952
5051
  readonly _tag: Schema.Literal<"error">;
4953
- readonly error: Schema.Unknown;
5052
+ readonly error: Schema.decodeTo<Schema.Unknown, Schema.Codec<Schema.Json, Schema.Json, never, never>, never, never>;
4954
5053
  readonly revision: Schema.optional<Schema.Number>;
4955
5054
  }>]>, Schema.Top>, Schema.Never, never, never>;
4956
5055
 
@@ -5031,6 +5130,20 @@ export declare type ReactivitySourceValue = string | ReactivityChannel;
5031
5130
 
5032
5131
  export declare const readPairing: (storage: PairingStorage) => Promise<Pairing | null>;
5033
5132
 
5133
+ /**
5134
+ * Reauthenticate a live grant through its original strategy chain. Tenant and
5135
+ * principal identity stay bound to the original subscription: changing either
5136
+ * requires a new subscription, never reuse of an already scoped descriptor.
5137
+ * Runtime-owned subjects without a request binding retain their explicit grant.
5138
+ */
5139
+ export declare const reauthorizeSubject: (subject: Subject, prepare?: (fresh: Subject) => Promise<void>) => Promise<Subject>;
5140
+
5141
+ /** Recheck a live grant through fresh authentication and derived authority. */
5142
+ export declare const recheckGuardsEffect: (subject: Subject, guards: ReadonlyArray<AnyCheckSpec> | undefined, input: unknown, options?: GuardCheckOptions) => Effect.Effect<ScopeError | Unauthenticated | RequestedTenantRefused | NoSuchResource | null>;
5143
+
5144
+ /** Refresh derived authority without replaying unrelated RPC plugin hooks. */
5145
+ export declare const refreshEffectiveScopes: (original: Subject, fresh: Subject) => Promise<void>;
5146
+
5034
5147
  /** Every error a plugin registers, as one union (`never` before codegen). */
5035
5148
  export declare type RegisteredPluginError = VoltroPluginErrors[keyof VoltroPluginErrors];
5036
5149
 
@@ -5063,6 +5176,25 @@ export declare const registerPluginAliases: (aliases: Readonly<Record<string, st
5063
5176
  */
5064
5177
  export declare const reportableError: (err: unknown) => Error;
5065
5178
 
5179
+ /** An authenticated request cannot use its requested workspace. This is
5180
+ * scoped access refusal, not evidence that the credential or session ended.
5181
+ * Consumers must keep unrelated workspace sessions intact. */
5182
+ export declare class RequestedTenantRefused extends RequestedTenantRefused_base {
5183
+ }
5184
+
5185
+ declare const RequestedTenantRefused_base: Schema.Class<RequestedTenantRefused, Schema.TaggedStruct<"RequestedTenantRefused", {
5186
+ readonly reason: Schema.Literals<readonly ["target-unavailable", "invalid-target", "memberships-unavailable", "not-a-member", "subject-has-no-tenant"]>;
5187
+ }>, YieldableError>;
5188
+
5189
+ /** A request's tenant hint, read only after a strategy authenticates the caller.
5190
+ * Return null for the credential / stored selection. A non-null target is
5191
+ * checked against fresh memberships on every resolution, including when it
5192
+ * equals the credential's tenant. It never writes a persistent selection.
5193
+ * Do not return a tenant default for an absent hint: that would silently change
5194
+ * the authority of requests which did not select a workspace.
5195
+ */
5196
+ export declare type RequestedTenantSource = (subject: Subject, input: AuthStrategyInput) => Promise<string | null> | string | null;
5197
+
5066
5198
  /** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
5067
5199
  * Checks the EFFECTIVE set so role-derived scopes count. */
5068
5200
  export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
@@ -5179,6 +5311,16 @@ declare interface RestCache {
5179
5311
  readonly audience?: (output: unknown, input: unknown, subject: Subject) => 'public' | 'private';
5180
5312
  }
5181
5313
 
5314
+ /** Cross-origin browser access for one route. Origins are exact, including opaque `null`. */
5315
+ declare interface RestCors {
5316
+ readonly origins: ReadonlyArray<string>;
5317
+ readonly headers?: ReadonlyArray<string>;
5318
+ readonly methods?: ReadonlyArray<RestMethod>;
5319
+ /** Preflight cache lifetime in seconds. */
5320
+ readonly maxAge?: number;
5321
+ readonly exposeHeaders?: ReadonlyArray<string>;
5322
+ }
5323
+
5182
5324
  export declare type RestDates = 'iso' | 'epoch-ms';
5183
5325
 
5184
5326
  export declare type RestEnvelope = 'none' | 'data' | 'jsonapi';
@@ -5279,6 +5421,13 @@ export declare type RestNaming = 'camelCase' | 'snake_case';
5279
5421
 
5280
5422
  export declare type RestPagination = 'link-header' | 'body-cursor';
5281
5423
 
5424
+ /** Uses the mounted rate-limit plugin's store, algorithms and dynamic policy. */
5425
+ declare interface RestRateLimit {
5426
+ readonly limit: number;
5427
+ readonly window: string;
5428
+ readonly by?: 'subject' | 'tenant' | 'apiKey' | 'ip' | 'global';
5429
+ }
5430
+
5282
5431
  /** A schema-encoded REST body with an application-selected HTTP status and
5283
5432
  * response headers. The marker keeps ordinary domain objects unambiguous.
5284
5433
  * `output` still validates/encodes `body`; use `Schema.Void` with a 204 when
@@ -5377,6 +5526,10 @@ declare interface RestRouteDescriptor<I, O> {
5377
5526
  */
5378
5527
  readonly version?: `v${number}`;
5379
5528
  readonly guards?: ReadonlyArray<RestGuard>;
5529
+ readonly cors?: RestCors;
5530
+ /** Only an explicit bearer header may authenticate; cookies are removed before auth and context construction. */
5531
+ readonly credentials?: 'bearer';
5532
+ readonly rateLimit?: RestRateLimit;
5380
5533
  /**
5381
5534
  * This route STREAMS (Server-Sent Events) rather than resolving one value — its
5382
5535
  * handler returns `sse(...)`. Declared on the descriptor, not left implicit in
@@ -5436,6 +5589,35 @@ declare interface RestRouteProjection {
5436
5589
  readonly format: RestFormat;
5437
5590
  }
5438
5591
 
5592
+ /**
5593
+ * Default ctx resolver used when the serve pipeline doesn't inject one
5594
+ * (e.g. unit tests). Real mounts pass a resolver that reads the framework's
5595
+ * auth subject + the bound DataStore.
5596
+ */
5597
+ declare interface RestServeBindings {
5598
+ /** Bound once from the app's mounted rate-limit plugin. */
5599
+ readonly rateLimit?: (policy: RestRateLimit, ctx: RestRouteContext, route: RestRouteDescriptor<any, any>) => Effect.Effect<void, unknown>;
5600
+ /** Resolve the calling subject from the request headers. */
5601
+ readonly resolveSubject?: (headers: Readonly<Record<string, string>>) => Subject | Promise<Subject>;
5602
+ /** The bound framework DataStore. */
5603
+ readonly store?: unknown;
5604
+ /** Build the full per-request app context after authentication. */
5605
+ readonly buildContext?: (subject: Subject, headers: Readonly<Record<string, string>>) => unknown | Promise<unknown>;
5606
+ /**
5607
+ * HTTP idempotency — when set, a mutating request (POST/PUT/PATCH/DELETE)
5608
+ * carrying the `header` is deduplicated: the first call runs + caches its
5609
+ * response; a replay within `ttlMs` returns the cached response
5610
+ * (`Idempotency-Replayed: true`); an in-flight duplicate gets `409`. Scoped
5611
+ * per (tenant, method, path). The serve path injects the store + config from
5612
+ * `app.config.ts`'s `idempotency` field.
5613
+ */
5614
+ readonly idempotency?: {
5615
+ readonly store: IdempotencyStore;
5616
+ readonly header: string;
5617
+ readonly ttlMs: number;
5618
+ };
5619
+ }
5620
+
5439
5621
  /**
5440
5622
  * What a REST handler returns to stream its response instead of resolving one
5441
5623
  * value: `sse((emit) => unsubscribe)`. The route's schema `output` is not used
@@ -5584,6 +5766,10 @@ export declare type RpcInterceptor = (next: Effect.Effect<unknown, unknown>, con
5584
5766
  * them automatically.
5585
5767
  */
5586
5768
  export declare interface RpcInterceptorContext {
5769
+ /** Enqueue a settled invocation outcome on the host's root store, outside
5770
+ * the business transaction. Required transactional success capture must use
5771
+ * MutationCommitContext.outbox instead. Older/custom hosts may omit this. */
5772
+ readonly enqueueOutcome?: OutboxEnqueueFacade['enqueue'];
5587
5773
  /** Procedure tag, e.g. `todos.create`. */
5588
5774
  readonly tag: string;
5589
5775
  /** Discriminator — `'mutation' | 'query' | 'action' | 'workflow'`. */
@@ -5977,6 +6163,9 @@ export declare const sessionValueFromHeaders: (headers: Readonly<Record<string,
5977
6163
  */
5978
6164
  export declare const setEffectiveScopes: (subject: Subject, scopes: ReadonlyArray<string>) => void;
5979
6165
 
6166
+ /** Register a request's dynamic role/permission resolver for live delivery. */
6167
+ export declare const setEffectiveScopesRefresher: (subject: Subject, refresh: (fresh: Subject) => Promise<void>) => void;
6168
+
5980
6169
  /** Register (or clear) the process-global policy-guard resolver. Last write
5981
6170
  * wins. The runtime installs one backed by the resource-policy registry and
5982
6171
  * the registered tuple source. */
@@ -6430,7 +6619,7 @@ export declare const subscriptionEvent: <D extends Schema.Top>(data: D) => Schem
6430
6619
  }>;
6431
6620
  }>, Schema.Struct<{
6432
6621
  readonly _tag: Schema.Literal<"error">;
6433
- readonly error: Schema.Unknown;
6622
+ readonly error: Schema.decodeTo<Schema.Unknown, Schema.Codec<Schema.Json, Schema.Json, never, never>, never, never>;
6434
6623
  readonly revision: Schema.optional<Schema.Number>;
6435
6624
  }>]>;
6436
6625
 
@@ -6710,7 +6899,7 @@ export declare const toRpc: <Name extends string, Input extends Schema.Top, Outp
6710
6899
  }>;
6711
6900
  }>, Schema.Struct<{
6712
6901
  readonly _tag: Schema.Literal<"error">;
6713
- readonly error: Schema.Unknown;
6902
+ readonly error: Schema.decodeTo<Schema.Unknown, Schema.Codec<Schema.Json, Schema.Json, never, never>, never, never>;
6714
6903
  readonly revision: Schema.optional<Schema.Number>;
6715
6904
  }>]>, Schema.Top>, Schema.Never, never, never> | Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Output, Schema.Top, never, never> | Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Stream<Output, Schema.Top>, Schema.Never, never, never>;
6716
6905
 
@@ -6865,6 +7054,9 @@ export declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Rec
6865
7054
  readonly shapeItem?: ((input: Input, current: Item) => Item) | undefined;
6866
7055
  }
6867
7056
 
7057
+ /** Refuse malformed runtime contributions before starting a worker. */
7058
+ export declare function validateOutboxHandlerDefinition(value: unknown, owner: string): asserts value is OutboxHandlerDefinition<unknown>;
7059
+
6868
7060
  /**
6869
7061
  * A server-side validation failure pinned to ONE field.
6870
7062
  *
@@ -7048,6 +7240,16 @@ export declare interface VoltroPlugin {
7048
7240
  * (schema-only / lifecycle-only plugins are fine).
7049
7241
  */
7050
7242
  readonly interceptMutation?: RpcInterceptor;
7243
+ /** Runs after successful executor and business-rule evaluation, before the
7244
+ * transaction commits. Use only the supplied store for required records or
7245
+ * durable intents. External I/O is prohibited; transaction retries replay
7246
+ * this hook. Requires rpc:intercept:mutation and store:write permissions.
7247
+ * Ordinary RPC interceptors retain their existing outside-transaction scope. */
7248
+ readonly beforeMutationCommit?: MutationCommitHook;
7249
+ /** Post-commit delivery handlers, registered with the shared app worker in
7250
+ * Dev and serve. Requires outbox:handle; effects must not collide with app,
7251
+ * framework or other plugin contributions. Handlers must be idempotent. */
7252
+ readonly outboxHandlers?: ReadonlyArray<OutboxHandlerDefinition<unknown>>;
7051
7253
  /**
7052
7254
  * Wraps every QUERY (streaming subscription handler) call. Same
7053
7255
  * composition semantics as `interceptMutation`. Most observability
@@ -7332,6 +7534,8 @@ export declare interface VoltroPlugin {
7332
7534
  * SDK here). Returns of `undefined`/empty are no-ops.
7333
7535
  */
7334
7536
  readonly contributeObservability?: (ctx: PluginLifecycleContext) => Promise<ObservabilityContribution | undefined> | ObservabilityContribution | undefined;
7537
+ /** Enforce a hand-authored REST route's quota through this plugin. */
7538
+ readonly restRateLimit?: RestServeBindings['rateLimit'];
7335
7539
  /**
7336
7540
  * Told the app's mounted REST surface — hand-authored `restRoutes` AND the
7337
7541
  * routes projected from `publicApi` annotations — once the boot has built it,