@voltro/protocol 0.54.0 → 0.56.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.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * What an inspect readout should call this process's mode.
3
+ *
4
+ * A projection of `bootPath`, not a second decision — the two boot paths each
5
+ * wrote a literal here, and one of them wrote the wrong one for years.
6
+ * `build`, `cli` and `test` collapse to `'cli'`: none of them serves traffic,
7
+ * and pretending otherwise would be the same category error in reverse.
8
+ */
9
+ export declare const appMode: () => "dev" | "serve" | "start" | "cli";
10
+
11
+ /**
12
+ * The framework version this process runs, or `undefined` when it could not be
13
+ * determined. The ONE answer — there were three, and one of them read
14
+ * `npm_package_version`, which is the APP's version, not the framework's.
15
+ *
16
+ * Deliberately NOT `processIdentity().version`, though it returns the same
17
+ * value. Asking for the version must not resolve the identity: a boot path
18
+ * holds the version in a module-level constant, so routing it through the
19
+ * identity froze `bootPath` at import time — before the command that knows it
20
+ * had run — and the resolver then warned about its own caller. A version is
21
+ * not a fact about which replica this is.
22
+ */
23
+ export declare const frameworkVersion: () => string | undefined;
24
+
25
+ /**
26
+ * Just the instance id — this PROCESS, not the name it wears.
27
+ *
28
+ * The distinction matters wherever a monotonic counter is compared across
29
+ * restarts. A replica id is a NAME and names survive: a StatefulSet pod keeps
30
+ * `POD_NAME`, and `VOLTRO_REPLICA_ID` is stable by definition. A counter does
31
+ * not survive — it starts at 1 again — so anything that reads "is this newer
32
+ * than what I have from X" must key on the process, or it silently answers "no"
33
+ * for as long as the dead process's high-water mark stands.
34
+ */
35
+ export declare const instanceId: () => string;
36
+
37
+ /**
38
+ * Which command this process is.
39
+ *
40
+ * `serve` and `start` are separate because they are separate commands with
41
+ * separate capabilities — `voltro start` is web-only and refuses an api — and
42
+ * collapsing them is what made an inspect readout name a command the process
43
+ * could not have been started by. Only ever affects the LOCAL fallback id.
44
+ */
45
+ export declare type ProcessBootPath = 'dev' | 'serve' | 'start' | 'build' | 'cli' | 'test';
46
+
47
+ export declare interface ProcessIdentity {
48
+ /**
49
+ * The place in the fleet: `VOLTRO_REPLICA_ID` → `POD_NAME` → `HOSTNAME` →
50
+ * `<bootPath>-<pid>`.
51
+ *
52
+ * STABLE ACROSS RESTARTS in a real deployment — a pod that dies and comes
53
+ * back is the same replica. Pair it with `startedAt` to talk about one
54
+ * process.
55
+ */
56
+ readonly replicaId: string;
57
+ /**
58
+ * One PROCESS: `<replicaId>@<startedAt>.<nonce>`. A restart changes it.
59
+ *
60
+ * The nonce is not decoration. `startedAt` has millisecond resolution, so
61
+ * two generations of one replica that start inside the same millisecond
62
+ * produce the same string — and an aggregation keyed on it would then merge
63
+ * two processes' state into one, which is precisely the arithmetic across
64
+ * generations the generation exists to prevent. Rare in a deployment,
65
+ * routine in a test worker, and a wrong merge is silent either way.
66
+ */
67
+ readonly instanceId: string;
68
+ /** This process's start, on ITS OWN clock. An identity, never compared to
69
+ * another process's clock — see `instanceMembership`. */
70
+ readonly startedAt: number;
71
+ /** The framework version. `undefined` when it could not be determined —
72
+ * which must not read the same as a version, so it is not `'0.0.0'`. */
73
+ readonly version: string | undefined;
74
+ /** The app's own version, when a boot path knows it. */
75
+ readonly appVersion: string | undefined;
76
+ readonly bootPath: ProcessBootPath;
77
+ /**
78
+ * `host:port` another replica can reach this one's inspect surface on, or
79
+ * `undefined`.
80
+ *
81
+ * Deliberately not derived from the bind address: a process bound to
82
+ * `0.0.0.0:4000` is not reachable AT `0.0.0.0`, and a loopback address is
83
+ * reachable only by itself. `resolveRunnerIdentity` already carries this
84
+ * distinction as `localhostRisk` for the cluster runner; this is the same
85
+ * fact for the inspect surface.
86
+ */
87
+ readonly reachableAt: string | undefined;
88
+ /** True only when `reachableAt` is set AND is not loopback. */
89
+ readonly reachable: boolean;
90
+ }
91
+
92
+ /** This process's identity. Stable for the lifetime of the process. */
93
+ export declare const processIdentity: () => ProcessIdentity;
94
+
95
+ /**
96
+ * Does the ENVIRONMENT say this process is one of several?
97
+ *
98
+ * A shape question, not a count: it answers "is a process-local default going to
99
+ * be wrong here", which is the only thing the callers need. Several defaults in
100
+ * this framework are correct for one process and quietly wrong for a fleet — a
101
+ * process-local rate-limit store multiplies the limit by the replica count, a
102
+ * process-local read-your-writes position store makes a user's own write
103
+ * invisible on the next request, an in-process broadcast bus carries nothing
104
+ * across pods. Each of those warns on THIS evidence rather than on a guess.
105
+ *
106
+ * Lives here, beside the process identity, because it reads the same env and
107
+ * because the alternative was three copies: it began in `@voltro/cli`'s
108
+ * reactivity audit, and a plugin cannot import the CLI.
109
+ *
110
+ * Returns the SIGNAL as a human-readable string (so the warning can cite what it
111
+ * saw) or `null` for "nothing says so".
112
+ */
113
+ export declare const replicaEvidence: (env?: Record<string, string | undefined>) => string | null;
114
+
115
+ /** Just the replica id — the common case, and the ONLY spelling of it. */
116
+ export declare const replicaId: () => string;
117
+
118
+ /** Reset for tests. A process has one identity; a test worker runs many. */
119
+ export declare const resetProcessIdentityForTest: (bootPath?: ProcessBootPath) => void;
120
+
121
+ /**
122
+ * Resolve the advertised host the same way the cluster runner does, so an
123
+ * operator does not have to configure two things that mean one thing.
124
+ */
125
+ export declare const resolveAdvertisedHost: (env?: NodeJS.ProcessEnv) => {
126
+ readonly host: string;
127
+ readonly declared: boolean;
128
+ };
129
+
130
+ /**
131
+ * Where a PEER can reach this process's inspect surface, as `host:port`.
132
+ *
133
+ * The boot path knows the port; the host comes from `POD_IP` /
134
+ * `VOLTRO_INSPECT_ADVERTISE_HOST` and is a loopback address when neither is
135
+ * set. Passing a loopback host is not an error — it is recorded and reported
136
+ * as `reachable: false`, which is what lets a fleet request answer "this one
137
+ * cannot be reached" instead of waiting for a timeout.
138
+ */
139
+ export declare const setProcessAdvertisedAddress: (hostPort: string | undefined,
140
+ /**
141
+ * Whether the HOST was declared by the operator rather than fallen back to.
142
+ *
143
+ * This is the difference between a loopback address that is a guess and one
144
+ * that is a statement. `POD_IP` unset means we do not know where we are and
145
+ * `127.0.0.1` is our shrug — a peer must not try it. An operator who sets
146
+ * `VOLTRO_INSPECT_ADVERTISE_HOST` has told us, and telling us `127.0.0.1` is
147
+ * a legitimate thing to say when the peers really are on this machine.
148
+ *
149
+ * Same shape as every other declaration in this framework: we do not
150
+ * second-guess one, and we do not treat a fallback as one.
151
+ */
152
+ declared?: boolean) => void;
153
+
154
+ /** The app's own version, when the boot path knows it. */
155
+ export declare const setProcessAppVersion: (version: string | undefined) => void;
156
+
157
+ /**
158
+ * Name which command this process is. Call as early as a boot path can.
159
+ *
160
+ * Only ever affects the LOCAL fallback (`dev-1234` vs `serve-1234`); in any
161
+ * deployment `POD_NAME` or `HOSTNAME` decides and this is cosmetic. Called
162
+ * after the identity has already been handed out, it does NOT rewrite it — an
163
+ * id that changes mid-process is worse than a slightly wrong label — and says
164
+ * so loudly, because silence is worse than both.
165
+ */
166
+ export declare const setProcessBootPath: (kind: ProcessBootPath) => void;
167
+
168
+ export { }
@@ -0,0 +1,106 @@
1
+ import { createLogger as e } from "@voltro/logger";
2
+ import { readFileSync as t } from "node:fs";
3
+ import { dirname as n, join as r } from "node:path";
4
+ import { fileURLToPath as i } from "node:url";
5
+ //#region src/processIdentity.ts
6
+ var a = e({ scope: "voltro:identity" }), o = Symbol.for("@voltro/runtime/processIdentity"), s = globalThis, c = () => Math.random().toString(36).slice(2, 8), l = s[o] ?? (s[o] = {
7
+ bootPath: "cli",
8
+ startedAt: Date.now(),
9
+ nonce: c(),
10
+ versionResolved: !1
11
+ }), u = /* @__PURE__ */ new Set([
12
+ "localhost",
13
+ "127.0.0.1",
14
+ "::1",
15
+ "0.0.0.0",
16
+ ""
17
+ ]), d = (e) => {
18
+ if (e.startsWith("[")) return e.slice(1, e.indexOf("]"));
19
+ let t = e.lastIndexOf(":");
20
+ return t === -1 ? e : e.slice(0, t);
21
+ }, f = () => {
22
+ let e = globalThis;
23
+ return typeof e.__VOLTRO_FRAMEWORK_VERSION__ == "string" && e.__VOLTRO_FRAMEWORK_VERSION__.length > 0 ? e.__VOLTRO_FRAMEWORK_VERSION__ : void 0;
24
+ }, p = () => {
25
+ if (l.versionResolved) return l.version;
26
+ l.versionResolved = !0;
27
+ let e = f();
28
+ if (e !== void 0) return l.version = e, l.version;
29
+ let a = n(i(import.meta.url));
30
+ for (let e of [r(a, "..", "package.json"), r(a, "..", "..", "package.json")]) try {
31
+ let n = JSON.parse(t(e, "utf8"));
32
+ if (n.name === "@voltro/protocol" && typeof n.version == "string") return l.version = n.version, l.version;
33
+ } catch {}
34
+ return l.version;
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;
45
+ }, h = (e) => {
46
+ l.appVersion = e, l.frozen !== void 0 && (l.frozen = {
47
+ ...l.frozen,
48
+ appVersion: e
49
+ });
50
+ }, g = (e, t = !1) => {
51
+ l.advertised = e, l.advertisedDeclared = t, l.frozen !== void 0 && (l.frozen = {
52
+ ...l.frozen,
53
+ reachableAt: e,
54
+ reachable: _(e, t)
55
+ });
56
+ }, _ = (e, t) => e !== void 0 && (t || !u.has(d(e))), v = (e = process.env) => {
57
+ let t = e.VOLTRO_INSPECT_ADVERTISE_HOST;
58
+ if (t !== void 0 && t !== "") return {
59
+ host: t,
60
+ declared: !0
61
+ };
62
+ let n = e.POD_IP;
63
+ return n !== void 0 && n !== "" ? {
64
+ host: n,
65
+ declared: !0
66
+ } : {
67
+ host: "127.0.0.1",
68
+ declared: !1
69
+ };
70
+ }, y = () => {
71
+ if (l.frozen !== void 0) return l.frozen;
72
+ let e = process.env, t = e.VOLTRO_REPLICA_ID ?? e.POD_NAME ?? e.HOSTNAME ?? `${l.bootPath}-${process.pid}`, n = l.advertised, r = {
73
+ replicaId: t,
74
+ instanceId: `${t}@${l.startedAt}.${l.nonce}`,
75
+ startedAt: l.startedAt,
76
+ version: p(),
77
+ appVersion: l.appVersion,
78
+ bootPath: l.bootPath,
79
+ reachableAt: n,
80
+ reachable: _(n, l.advertisedDeclared === !0)
81
+ };
82
+ return l.frozen = r, r;
83
+ }, b = () => y().replicaId, x = () => y().instanceId, S = (e = process.env) => {
84
+ let t = Number(e.REPLICA_COUNT);
85
+ if (Number.isFinite(t) && t >= 1) return t > 1 ? `REPLICA_COUNT=${t}` : null;
86
+ for (let [t, n] of [
87
+ ["KUBERNETES_SERVICE_HOST", "Kubernetes"],
88
+ ["POD_IP", "Kubernetes (downward API)"],
89
+ ["POD_NAME", "Kubernetes (downward API)"],
90
+ ["FLY_ALLOC_ID", "Fly.io"],
91
+ ["FLY_MACHINE_ID", "Fly.io"],
92
+ ["ECS_CONTAINER_METADATA_URI_V4", "AWS ECS"],
93
+ ["ECS_CONTAINER_METADATA_URI", "AWS ECS"],
94
+ ["K_REVISION", "Cloud Run / Knative"],
95
+ ["CONTAINER_APP_REPLICA_NAME", "Azure Container Apps"],
96
+ ["RENDER_INSTANCE_ID", "Render"]
97
+ ]) if ((e[t] ?? "") !== "") return `${t} is set (${n})`;
98
+ return null;
99
+ }, C = (e = "test") => {
100
+ l.frozen = void 0, l.bootPath = e, l.startedAt = Date.now(), l.nonce = c(), l.appVersion = void 0, l.advertised = void 0, l.advertisedDeclared = !1;
101
+ }, w = () => p(), T = () => {
102
+ let e = y().bootPath;
103
+ return e === "dev" || e === "serve" || e === "start" ? e : "cli";
104
+ };
105
+ //#endregion
106
+ export { T as appMode, w as frameworkVersion, x as instanceId, y as processIdentity, S as replicaEvidence, b as replicaId, C as resetProcessIdentityForTest, v as resolveAdvertisedHost, g as setProcessAdvertisedAddress, h as setProcessAppVersion, m as setProcessBootPath };
package/dist/index.d.ts CHANGED
@@ -629,8 +629,11 @@ export declare interface CachedScopeDecision {
629
629
  * - `x.y.z` — exact
630
630
  * - `*` — any
631
631
  */
632
- export declare const checkFrameworkCompat: (pluginName: string, range: string | undefined, runningVersion: string) => {
632
+ export declare const checkFrameworkCompat: (pluginName: string, range: string | undefined,
633
+ /** `undefined` when the running version could not be determined. */
634
+ runningVersion: string | undefined) => {
633
635
  ok: true;
636
+ unverified?: string;
634
637
  } | {
635
638
  ok: false;
636
639
  reason: string;
@@ -696,6 +699,10 @@ export declare interface ClientTarget {
696
699
  * `path` is set. 2nd arg is the optimistic id (insert) or the current item
697
700
  * (update) — `unknown` here since the erased view spans both; normalize casts. */
698
701
  readonly shapeItem?: ((input: Record<string, unknown>, currentOrOptimisticId: unknown) => Record<string, unknown>) | undefined;
702
+ /** Declared junction relations (see {@link TargetRelations}). Pure DATA —
703
+ * three strings per relation — so it crosses to the browser unchanged and
704
+ * drives junction auto-optimistic there. */
705
+ readonly relations?: TargetRelations | undefined;
699
706
  }
700
707
 
701
708
  export declare const composeAuthStrategies: (strategies: ReadonlyArray<AuthStrategy>, options?: {
@@ -2655,6 +2662,18 @@ export declare type PendingApproval = Schema.Schema.Type<typeof PendingApproval>
2655
2662
  */
2656
2663
  export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect.Effect<void, unknown> | Promise<void> | void;
2657
2664
 
2665
+ /**
2666
+ * Derive the short alias used as the rpc-tag prefix for a plugin's routes.
2667
+ * Pure — depends only on the plugin name.
2668
+ *
2669
+ * Examples:
2670
+ * '@voltro/plugin-audit' → 'audit'
2671
+ * '@scope/plugin-rateLimit' → 'rateLimit'
2672
+ * 'plain-name' → 'plainName'
2673
+ * '@voltro/audit' → 'audit' (no plugin- prefix)
2674
+ */
2675
+ export declare const pluginAlias: (pluginName: string) => string;
2676
+
2658
2677
  /**
2659
2678
  * Server-only context delivered to `bindDataStore`, ALONGSIDE the store,
2660
2679
  * AFTER the framework has opened its store + pool. Gives a plugin the
@@ -2727,6 +2746,33 @@ export declare interface PluginBindContext {
2727
2746
  * task early. The framework also stops every armed task at shutdown.
2728
2747
  */
2729
2748
  readonly scheduleCoordinated: (name: string, intervalMs: number, effect: () => void | CoordinatedTickOutcome | Promise<void | CoordinatedTickOutcome>, options?: CoordinatedTaskOptions) => CoordinatedScheduleHandle;
2749
+ /**
2750
+ * Claim ONE change fleet-wide. `true` = this replica may run the effect;
2751
+ * `false` = another replica has it, or the claim could not be taken.
2752
+ *
2753
+ * The counterpart of `scheduleCoordinated` for the OTHER thing that fires on
2754
+ * every replica: a change tap. `onChangeEvent` is delivered to every replica —
2755
+ * that is what makes a `changeScope: 'fleet'` store cross-instance — so a tap
2756
+ * that COUNTS or SENDS multiplies by the replica count. `plugin-billing`
2757
+ * accrued a usage unit per metered row change and a tenant on two pods was
2758
+ * billed twice; the plugin whose entire job is counting had no way to say
2759
+ * "once".
2760
+ *
2761
+ * `scope` namespaces the key (use the plugin's own name); `key` must name the
2762
+ * CHANGE. Derive it with `changeDigest` + `OccurrenceCounter`
2763
+ * (`@voltro/database`) rather than inventing one — a fleet change carries no
2764
+ * LSN, no commit id and no `traceId`, so content plus its position among
2765
+ * content-identical repeats is the only thing two replicas provably agree on.
2766
+ *
2767
+ * AT MOST once: the claim is taken before the effect, so a replica that wins
2768
+ * and dies takes the change with it, and a claim that cannot be written is
2769
+ * taken by nobody. Fail-closed, like the rate slot and the budget guard.
2770
+ *
2771
+ * Required rather than optional, and deliberately: an absent gate reads
2772
+ * exactly like a gate that passed, and the whole point of this field is a
2773
+ * count.
2774
+ */
2775
+ readonly claimChange: (scope: string, key: string) => Promise<boolean>;
2730
2776
  }
2731
2777
 
2732
2778
  /**
@@ -3257,11 +3303,18 @@ export declare type PluginInstallHook = (ctx: PluginLifecycleContext) => Effect.
3257
3303
  *
3258
3304
  * **What an alias costs, stated because it is not obvious and nothing else
3259
3305
  * says it:** the local and cloud dashboards fetch a plugin's inspect panel at
3260
- * `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in. Alias
3261
- * a plugin that ships `inspectEndpoints` and the endpoints keep working, the
3262
- * rpc tags move as intended, and the dashboard panel 404s because the panel
3263
- * is in a different repository and cannot follow. Alias to dodge a tag
3264
- * collision; do not alias a plugin whose dashboard panel you use.
3306
+ * `/_voltro/inspect/plugins/<slug>/…` with the DEFAULT slug compiled in, and
3307
+ * they are in different repositories, so they cannot follow an alias. That
3308
+ * used to mean an aliased plugin kept working while its panel 404'd. It no
3309
+ * longer does: `makePluginInspectRegistry` also mounts each plugin's inspect
3310
+ * endpoints under its CANONICAL slug, so the compiled-in path stays correct
3311
+ * however the app renames the plugin.
3312
+ *
3313
+ * The residue is `instance`, not `alias`. Two installs are two panels with one
3314
+ * canonical name, so they get NO shared mount — showing either under the
3315
+ * canonical path would hand a dashboard the other install's rows under a name
3316
+ * that looks right. Each is reachable at its own slug, which
3317
+ * `/_voltro/inspect/plugins` reports as `inspectSlug`.
3265
3318
  *
3266
3319
  * @param base the plugin's canonical package name, e.g. `'@voltro/plugin-cdc-out'`
3267
3320
  * @param alias replaces the whole namespace — an app-chosen name
@@ -3508,6 +3561,55 @@ export declare interface PluginSchemaContribution {
3508
3561
  readonly migrations?: ReadonlyArray<PluginMigration>;
3509
3562
  }
3510
3563
 
3564
+ /**
3565
+ * Derive the URL slug for a plugin's HTTP-facing surfaces (the inspect mount
3566
+ * `/_voltro/inspect/plugins/<slug>/…`). Unlike `pluginAlias` (an identifier
3567
+ * for rpc tags / codegen, camelCase), the slug stays KEBAB-CASE — the shape
3568
+ * every dashboard fetches (`plugins/cdc-out/sinks`, never `plugins/cdcOut/…`)
3569
+ * and the shape the package name already carries. Instance-suffixed names
3570
+ * (`@voltro/plugin-cdc-out#analytics`) map `#` → `--` so the slug stays a
3571
+ * fetchable URL path segment.
3572
+ *
3573
+ * Examples:
3574
+ * '@voltro/plugin-audit' → 'audit'
3575
+ * '@voltro/plugin-cdc-out' → 'cdc-out'
3576
+ * '@scope/plugin-rateLimit' → 'rate-limit'
3577
+ * '@voltro/plugin-cdc-out#analytics'→ 'cdc-out--analytics'
3578
+ */
3579
+ export declare const pluginSlug: (pluginName: string) => string;
3580
+
3581
+ /**
3582
+ * The wire tag a plugin's own client code must call for one of its routes.
3583
+ *
3584
+ * `pluginTag('@voltro/plugin-notifications', 'inbox')` is `'notifications.inbox'`
3585
+ * in a default app and `'inbox.inbox'` in an app that installed the plugin as
3586
+ * `notificationsPlugin({ alias: 'inbox' })`. Every hook a plugin ships goes
3587
+ * through this instead of spelling its own namespace, because the namespace is
3588
+ * the app's to choose and the hook is the half of the move that a literal
3589
+ * string cannot follow.
3590
+ *
3591
+ * **Resolution, and the one case that fails closed.** A base may have several
3592
+ * registered aliases when an app installs the same plugin twice
3593
+ * (`instance: 'analytics'` → `notifications#analytics`):
3594
+ *
3595
+ * - exactly one registered alias → that one;
3596
+ * - several, exactly one of which carries no `#` → the un-suffixed install.
3597
+ * Two installs of a plugin have a primary and a secondary, and a hook with
3598
+ * no way to name an install means the primary;
3599
+ * - several, none or more than one un-suffixed → **throws**. The hook is being
3600
+ * asked which of two servers it addresses and has no basis to pick; a guess
3601
+ * here writes to the wrong install's tables, which is worse than a stack
3602
+ * trace naming both candidates. Call the tag explicitly (`useSubscription(api,
3603
+ * 'notifications#analytics.inbox')`) to say which you mean.
3604
+ * - none registered → the canonical `pluginAlias(base)`. This is the path a
3605
+ * unit test rendering a hook in isolation takes, and the path an app takes
3606
+ * before its generated rpcGroup has loaded. It is NOT a fail-open guess: an
3607
+ * app that aliased and somehow lost its registration calls a tag the client
3608
+ * group does not contain, and the transport refuses it by name rather than
3609
+ * routing it somewhere plausible.
3610
+ */
3611
+ export declare const pluginTag: (baseName: string, route: string) => string;
3612
+
3511
3613
  /**
3512
3614
  * A scaffold template a plugin contributes to `voltro init` /
3513
3615
  * `voltro add-app`. Templates are listed under the plugin's name in
@@ -3887,10 +3989,29 @@ export declare type ReactivitySource = TableName | ReactivityChannel;
3887
3989
  */
3888
3990
  export declare type ReactivitySourceValue = string | ReactivityChannel;
3889
3991
 
3992
+ /**
3993
+ * Declare, for this realm, what namespace each installed plugin actually
3994
+ * answers to. Emitted into `rpcGroup.generated.ts` by the codegen — one call
3995
+ * per api, mapping each plugin's CANONICAL base name to its EFFECTIVE alias.
3996
+ *
3997
+ * Emitted unconditionally whenever an app installs a plugin that contributes
3998
+ * client routes, not only when something was aliased. A registration that only
3999
+ * appeared in the aliased case would be a second code path exercised by nobody,
4000
+ * and the default case is where a regression would hide.
4001
+ *
4002
+ * Accumulates rather than overwrites: two apis in one browser bundle each
4003
+ * register their own map, and an app may install one plugin twice (`instance`).
4004
+ * The disambiguation happens at READ time — see `pluginTag`.
4005
+ */
4006
+ export declare const registerPluginAliases: (aliases: Readonly<Record<string, string>>) => void;
4007
+
3890
4008
  /** Gate a handler on a scope; fails with a typed `ScopeError` if missing.
3891
4009
  * Checks the EFFECTIVE set so role-derived scopes count. */
3892
4010
  export declare const requireScope: (subject: Subject, scope: string) => Effect.Effect<void, ScopeError>;
3893
4011
 
4012
+ /** Drop every registration. Tests only — production registers once at module load. */
4013
+ export declare const resetPluginAliases: () => void;
4014
+
3894
4015
  /** The question the runtime asks a registered resource-scope resolver. */
3895
4016
  export declare interface ResourceScopeRequest {
3896
4017
  /** The authenticated caller. */
@@ -4704,21 +4825,52 @@ declare const TableValidationFailed_base: Schema.TaggedErrorClass<TableValidatio
4704
4825
 
4705
4826
  export declare type Target<Input = unknown, Row = unknown> = TargetSpec<Input, Row> | ReadonlyArray<TargetSpec<Input, Row>>;
4706
4827
 
4828
+ /**
4829
+ * ONE declared many-to-many relation of a write target: the junction table
4830
+ * plus its two reference columns.
4831
+ *
4832
+ * **Why the columns are declared and not derived.** The server can derive
4833
+ * them (`store.relationLinks` reads the junction's `reference()` targets),
4834
+ * and it still does — but the same declaration drives JUNCTION AUTO-OPTIMISTIC
4835
+ * in the BROWSER, and the browser has no table registry to derive from
4836
+ * (`@voltro/database` is server-only by construction). Guessing a column name
4837
+ * from the table name is the failure mode this codebase refuses everywhere
4838
+ * else, so the pair is stated. It is not taken on trust: before it writes, the
4839
+ * server compares the declaration against the junction's real reference
4840
+ * columns and refuses — naming the right pair — if they disagree. A wrong
4841
+ * declaration is a boot-loud error, never a silent client/server divergence.
4842
+ */
4843
+ export declare interface TargetRelation {
4844
+ /** The junction (link) table. */
4845
+ readonly junction: string;
4846
+ /** The junction's reference column pointing at the TARGET's own table. */
4847
+ readonly anchorColumn: string;
4848
+ /** The junction's other reference column — the linked row's id. */
4849
+ readonly targetColumn: string;
4850
+ }
4851
+
4707
4852
  /**
4708
4853
  * Declared many-to-many RELATIONS of a write target: `{ inputField:
4709
- * junctionTable }`. After the executor succeeds — inside the SAME
4854
+ * TargetRelation }`. After the executor succeeds — inside the SAME
4710
4855
  * transaction — the framework reconciles the junction's links for the
4711
4856
  * written row against `input[inputField]` (an array of target ids) via the
4712
4857
  * diff-based `store.relationLinks`, so a form's multi-reference field saves
4713
- * in one mutation with no hand-written junction code. The anchor column is
4714
- * derived from the junction's `reference()` targets (a self-junction is
4715
- * refused, never guessed).
4858
+ * in one mutation with no hand-written junction code.
4859
+ *
4860
+ * The SAME declaration drives the client's junction auto-optimistic: the
4861
+ * cache reconciles the junction rows of every subscription sourced on
4862
+ * `junction` the moment the mutation is sent, so a multi-reference field
4863
+ * flips as instantly as a scalar one instead of waiting for the server delta.
4864
+ * Those patches ride the ordinary optimistic lane (staged under the mutation
4865
+ * id): reverted on failure, kept on success until the base actually moves.
4716
4866
  *
4717
4867
  * An ABSENT input field leaves the links untouched (absent ≠ empty — an
4718
4868
  * empty array is the explicit "clear them all"). The row id is the
4719
- * executor's `output.id`, falling back to `input.id`.
4869
+ * executor's `output.id`, falling back to `input.id`; the client, which
4870
+ * cannot see `output` yet, uses `input.id` (and, for an insert, the same
4871
+ * optimistic id it stamped on the new row).
4720
4872
  */
4721
- export declare type TargetRelations = Readonly<Record<string, string>>;
4873
+ export declare type TargetRelations = Readonly<Record<string, TargetRelation>>;
4722
4874
 
4723
4875
  export declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
4724
4876
 
@@ -5068,8 +5220,10 @@ export declare interface VoltroPlugin {
5068
5220
  * declarations across seven plugins), so the escape hatch fires on all of
5069
5221
  * them and the alias moves NOTHING on the rpc surface. A user aliasing
5070
5222
  * `notifications` to escape a collision with their own `notifications.*`
5071
- * routes would still collide — and would additionally lose the dashboard
5072
- * panel, which fetches the default slug. Worse than not shipping the field.
5223
+ * routes would still collide — and, at that time, would additionally have
5224
+ * lost the dashboard panel, which fetches the default slug. Worse than not
5225
+ * shipping the field. (The panel half is fixed since: see the canonical
5226
+ * mount in `makePluginInspectRegistry`.)
5073
5227
  *
5074
5228
  * With `baseName` set, both tag derivations strip a leading
5075
5229
  * `<default-alias>.` before applying the EFFECTIVE alias, so
@@ -5497,6 +5651,34 @@ export declare const wireErrorUnion: (descriptor: {
5497
5651
  readonly requiresApproval?: AnyApprovalPolicy | undefined;
5498
5652
  }, kind: "query" | "mutation" | "action" | "stream") => Schema.Schema.All;
5499
5653
 
5654
+ /**
5655
+ * Re-tag a procedure descriptor for the wire.
5656
+ *
5657
+ * Every lifter below is `Rpc.make(descriptor.name, …)`, so the tag a procedure
5658
+ * answers to on the wire is the one baked into the DESCRIPTOR — not the one the
5659
+ * caller thinks it is emitting. That is fine for app-authored procedures, whose
5660
+ * descriptor and tag have one source. It was wrong for PLUGIN routes: an app
5661
+ * that installs a plugin under an `alias` moves the namespace, the server
5662
+ * registers the moved tag, and the generated client lifted the plugin's
5663
+ * descriptor unchanged — so the browser sent the tag the plugin AUTHORED to a
5664
+ * server that had stopped serving it. The identifier moved, the descriptor map
5665
+ * key moved, the type key moved; the only string the transport reads did not.
5666
+ *
5667
+ * The codegen wraps every plugin-contributed descriptor with this, always —
5668
+ * including the un-aliased case, where it is an identity in effect. A wrap that
5669
+ * only appeared under an alias would be a branch nothing exercises, and the
5670
+ * un-aliased case is the one a regression would hide in.
5671
+ *
5672
+ * Returns a COPY. The descriptor is imported by reference from the plugin's
5673
+ * browser-safe module and may be lifted by several apps in one process; mutating
5674
+ * `name` in place would re-tag it for all of them.
5675
+ */
5676
+ export declare const withRpcTag: <D extends {
5677
+ readonly name: string;
5678
+ }, const T extends string>(descriptor: D, tag: T) => Omit<D, "name"> & {
5679
+ readonly name: T;
5680
+ };
5681
+
5500
5682
  export declare const workflowCancelDescriptor: ActionProcedureDescriptor<"__voltro.workflow.cancel", Schema.Struct<{
5501
5683
  workflowName: typeof Schema.String;
5502
5684
  executionId: typeof Schema.String;