@voltro/protocol 0.24.0 → 0.26.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 +670 -0
- package/dist/index.d.ts +567 -5
- package/dist/index.js +351 -279
- package/dist/rest.d.ts +66 -0
- package/dist/session.d.ts +27 -0
- package/dist/session.js +6 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,28 @@ export declare interface ActionProcedureDescriptor<Name extends string, Input ex
|
|
|
38
38
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
39
39
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
40
40
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
41
|
+
/**
|
|
42
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
43
|
+
*
|
|
44
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
45
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
46
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
47
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
48
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
49
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
50
|
+
* For a frontend developer that is the worst possible partition.
|
|
51
|
+
*
|
|
52
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
53
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
54
|
+
* already composes, since the collision check compares FULL tags and not
|
|
55
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
56
|
+
*
|
|
57
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
58
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
59
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
60
|
+
* file that performs it.
|
|
61
|
+
*/
|
|
62
|
+
readonly overridesPlugin: boolean | undefined;
|
|
41
63
|
readonly internal: boolean | undefined;
|
|
42
64
|
}
|
|
43
65
|
|
|
@@ -54,6 +76,9 @@ export declare const anonymousSubject: (tenantId: string | null) => Subject;
|
|
|
54
76
|
/** A guard entry is either a scope check or a relationship check. */
|
|
55
77
|
export declare type AnyCheckSpec = GuardCheckSpec | PolicyCheckSpec;
|
|
56
78
|
|
|
79
|
+
/** Any declared event, with the generics erased — for registries and audits. */
|
|
80
|
+
export declare type AnyEventDescriptor = EventDescriptor<string, Schema.Schema.Any, Schema.Schema.Any>;
|
|
81
|
+
|
|
57
82
|
/** A guard is either a scope check or a relationship check. */
|
|
58
83
|
export declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
|
|
59
84
|
|
|
@@ -485,6 +510,28 @@ export declare interface ConnectionInfoValue {
|
|
|
485
510
|
* (every `useMutation` call does). Read by `bindMutation` to dedupe a retried
|
|
486
511
|
* mutation. Absent for callers that don't send it. */
|
|
487
512
|
readonly idempotencyKey?: string;
|
|
513
|
+
/**
|
|
514
|
+
* Unix-SECONDS expiry of the credential that authorized this call, when it
|
|
515
|
+
* has one. Absent for credentials with no expiry (anonymous, a non-expiring
|
|
516
|
+
* strategy) — and absent means "no bound", so the failure direction is the
|
|
517
|
+
* behaviour that already existed.
|
|
518
|
+
*
|
|
519
|
+
* It exists for LONG-LIVED work. A request is checked once and is over in
|
|
520
|
+
* milliseconds, so expiry never mattered; an event subscription is a
|
|
521
|
+
* standing state that reconnects forever by design, so one opened a minute
|
|
522
|
+
* before the token dies would otherwise keep delivering for days on a
|
|
523
|
+
* credential that is long gone. `bindEvent` ends the stream here, and
|
|
524
|
+
* `useEvent`'s existing reconnect immediately re-opens it — which is a NEW
|
|
525
|
+
* request, so it re-resolves the subject and re-runs the guards for real.
|
|
526
|
+
* That is what makes the bound seamless rather than a disconnection the app
|
|
527
|
+
* has to handle: still entitled, it continues; no longer entitled, it fails
|
|
528
|
+
* loudly instead of quietly continuing.
|
|
529
|
+
*
|
|
530
|
+
* This bounds EXPIRY, not revocation. A role revoked mid-session is not
|
|
531
|
+
* observed until the credential runs out — do not let this field grow a
|
|
532
|
+
* doc comment that claims otherwise.
|
|
533
|
+
*/
|
|
534
|
+
readonly credentialExpiresAt?: number;
|
|
488
535
|
}
|
|
489
536
|
|
|
490
537
|
/** The two credential shapes a connection can hold. `oauth2` = an
|
|
@@ -691,8 +738,99 @@ export declare const defineAction: <const Name extends string, Input extends Sch
|
|
|
691
738
|
* need to check who is asking.
|
|
692
739
|
*/
|
|
693
740
|
readonly internal?: boolean;
|
|
741
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
742
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
743
|
+
readonly overridesPlugin?: boolean;
|
|
694
744
|
}) => ActionProcedureDescriptor<Name, Input, Output, Error>;
|
|
695
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Declare an event.
|
|
748
|
+
*
|
|
749
|
+
* ```ts
|
|
750
|
+
* export const gameStarted = defineEvent({
|
|
751
|
+
* name: 'games.started',
|
|
752
|
+
* key: Schema.Struct({ arenaId: Schema.String }),
|
|
753
|
+
* payload: Schema.Struct({ gameId: Schema.String, startedAt: Schema.Number }),
|
|
754
|
+
* guards: [{ scope: 'display:read' }],
|
|
755
|
+
* })
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
export declare const defineEvent: <const Name extends string, Key extends Schema.Schema.Any, Payload extends Schema.Schema.Any>(options: {
|
|
759
|
+
/**
|
|
760
|
+
* Wire identifier, `camelCase.dotted` like an rpc tag — and it shares the rpc
|
|
761
|
+
* tag COLLISION SPACE, so a duplicate aborts boot rather than resolving itself
|
|
762
|
+
* at the first delivery. Two declarations answering to one name is precisely
|
|
763
|
+
* the failure mode a string channel has; a name that can collide silently
|
|
764
|
+
* would reintroduce it one level up.
|
|
765
|
+
*/
|
|
766
|
+
readonly name: Name;
|
|
767
|
+
/**
|
|
768
|
+
* WHERE it goes. Part of the contract, not a filter the client applies:
|
|
769
|
+
* a subscriber receives only events published under a key it asked for, so
|
|
770
|
+
* the server never sends the others at all.
|
|
771
|
+
*
|
|
772
|
+
* ONLY ROUTING FIELDS BELONG HERE. Every field fragments the subscriber set
|
|
773
|
+
* and costs fan-out dedup — a discriminator the handler reads (`gameType`) is
|
|
774
|
+
* payload, an address the delivery is decided by (`arenaId`) is key.
|
|
775
|
+
*
|
|
776
|
+
* The tenant is NOT part of it and must never be added: it is derived from the
|
|
777
|
+
* subject on both sides, so a cross-tenant delivery is impossible by
|
|
778
|
+
* construction rather than by remembering to filter.
|
|
779
|
+
*/
|
|
780
|
+
readonly key: Key;
|
|
781
|
+
/** WHAT happened. Decoded when publishing, so a mismatch is a typed error at
|
|
782
|
+
* the PRODUCER instead of a broken handler at every consumer. */
|
|
783
|
+
readonly payload: Payload;
|
|
784
|
+
/**
|
|
785
|
+
* WHO MAY LISTEN — the same vocabulary as a query's guards, so a
|
|
786
|
+
* resource-scoped rule ("may this terminal watch this arena") stays
|
|
787
|
+
* declarative. `ScopeError` is merged into the wire error union automatically.
|
|
788
|
+
*
|
|
789
|
+
* Re-checked when the SUBJECT changes (a revoked role ends the stream), not on
|
|
790
|
+
* every delivery. Per-delivery authorization is the design a comparable
|
|
791
|
+
* product measured into a scaling wall: one check per subscriber per message
|
|
792
|
+
* makes throughput scale with the audience instead of the publish rate.
|
|
793
|
+
*/
|
|
794
|
+
readonly guards?: Guards<Schema.Schema.Type<Key>>;
|
|
795
|
+
/**
|
|
796
|
+
* Deliver recently-buffered events on a FIRST attach. Default `false`, and the
|
|
797
|
+
* default is the interesting half.
|
|
798
|
+
*
|
|
799
|
+
* A fresh subscriber wanting only what happens from now on, and a RECONNECTING
|
|
800
|
+
* subscriber wanting the messages it missed, are different requests that read
|
|
801
|
+
* as one contradiction ("never replay history" vs "never lose a message"). They
|
|
802
|
+
* are separated here: a first attach starts empty unless it opts in, while a
|
|
803
|
+
* re-attach always resumes from the last serial the client saw. Mounting a
|
|
804
|
+
* component is not the same event as a WebSocket dropping, and the framework
|
|
805
|
+
* knows which one it is.
|
|
806
|
+
*/
|
|
807
|
+
readonly rewind?: boolean;
|
|
808
|
+
/**
|
|
809
|
+
* Deliver this event to subscribed HTTP targets as well.
|
|
810
|
+
*
|
|
811
|
+
* The unification the whole design is for: ONE declaration, and the audiences
|
|
812
|
+
* are consumers of it. Without this an app that both fans an event out to its
|
|
813
|
+
* screens and posts it to a partner declares the thing twice, in two shapes,
|
|
814
|
+
* and the two drift — which is the defect a declaration exists to remove, one
|
|
815
|
+
* level up from the string channel it already removed.
|
|
816
|
+
*
|
|
817
|
+
* Requires `@voltro/plugin-webhooks`. Absent ⇒ no outbound delivery, and no
|
|
818
|
+
* cost.
|
|
819
|
+
*/
|
|
820
|
+
readonly webhook?: EventWebhookSpec;
|
|
821
|
+
/**
|
|
822
|
+
* `'each'` (default) — every delivery matters; a slow subscriber loses the
|
|
823
|
+
* oldest and is told how many.
|
|
824
|
+
*
|
|
825
|
+
* `'latest'` — a newer delivery supersedes a pending one; a slow subscriber
|
|
826
|
+
* gets the current value and is told nothing, because nothing was lost.
|
|
827
|
+
*
|
|
828
|
+
* See {@link EventDeliverySemantics}. The test is "would a consumer be wrong
|
|
829
|
+
* to miss one?" — not "is this event frequent?".
|
|
830
|
+
*/
|
|
831
|
+
readonly delivery?: EventDeliverySemantics;
|
|
832
|
+
}) => EventDescriptor<Name, Key, Payload>;
|
|
833
|
+
|
|
696
834
|
export declare const defineMutation: <const Name extends string, Input extends Schema.Schema.Any, Output extends Schema.Schema.Any, Error extends Schema.Schema.All = typeof Schema.Never>(options: {
|
|
697
835
|
readonly name: Name;
|
|
698
836
|
readonly input: Input;
|
|
@@ -734,6 +872,9 @@ export declare const defineMutation: <const Name extends string, Input extends S
|
|
|
734
872
|
* need to check who is asking.
|
|
735
873
|
*/
|
|
736
874
|
readonly internal?: boolean;
|
|
875
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
876
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
877
|
+
readonly overridesPlugin?: boolean;
|
|
737
878
|
}) => MutationProcedureDescriptor<Name, Input, Output, Error>;
|
|
738
879
|
|
|
739
880
|
/**
|
|
@@ -828,6 +969,9 @@ export declare const defineQuery: <const Name extends string, Input extends Sche
|
|
|
828
969
|
* need to check who is asking.
|
|
829
970
|
*/
|
|
830
971
|
readonly internal?: boolean;
|
|
972
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
973
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
974
|
+
readonly overridesPlugin?: boolean;
|
|
831
975
|
}) => QueryProcedureDescriptor<Name, Input, Output, Error>;
|
|
832
976
|
|
|
833
977
|
/**
|
|
@@ -844,6 +988,22 @@ export declare const defineStream: <const Name extends string, Input extends Sch
|
|
|
844
988
|
* dev or serve. Same contract as `internal` on the other definers; a stream
|
|
845
989
|
* without it would be a hole in the same boundary. */
|
|
846
990
|
readonly internal?: boolean;
|
|
991
|
+
/** Replace a PLUGIN route answering to this same tag. Explicit, never
|
|
992
|
+
* inferred — see `overridesPlugin` on the descriptor. */
|
|
993
|
+
readonly overridesPlugin?: boolean;
|
|
994
|
+
/**
|
|
995
|
+
* WHO MAY LISTEN.
|
|
996
|
+
*
|
|
997
|
+
* A stream is the same long-lived grant a subscription is, and it was the one
|
|
998
|
+
* primitive that could not express authorization at all — queries, mutations
|
|
999
|
+
* and actions carry `guards:`, streams did not, so any protection lived
|
|
1000
|
+
* hand-written inside an executor where nothing could verify it existed.
|
|
1001
|
+
*
|
|
1002
|
+
* Checked at subscribe and re-checked before every element, so a resource
|
|
1003
|
+
* un-shared or a membership ended stops the stream rather than continuing to
|
|
1004
|
+
* push. Same shape and same semantics as a query's.
|
|
1005
|
+
*/
|
|
1006
|
+
readonly guards?: Guards;
|
|
847
1007
|
}) => StreamProcedureDescriptor<Name, Input, Element, Error>;
|
|
848
1008
|
|
|
849
1009
|
export declare interface DeleteTarget<Input = unknown> extends NestedTargetFields<Input> {
|
|
@@ -878,6 +1038,17 @@ export declare const diffRows: (prev: ReadonlyArray<PatchRow>, next: ReadonlyArr
|
|
|
878
1038
|
*/
|
|
879
1039
|
export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string>;
|
|
880
1040
|
|
|
1041
|
+
/**
|
|
1042
|
+
* Canonical string for a routing key.
|
|
1043
|
+
*
|
|
1044
|
+
* Object key ORDER must not change the routing: `{a,b}` and `{b,a}` are the same
|
|
1045
|
+
* address, and a subscriber that spelled its literal in the other order is the
|
|
1046
|
+
* kind of never-matches bug this whole design exists to make impossible. Sorting
|
|
1047
|
+
* the entries is what makes the two sides agree without either knowing about the
|
|
1048
|
+
* other.
|
|
1049
|
+
*/
|
|
1050
|
+
export declare const encodeEventKey: (key: unknown) => string;
|
|
1051
|
+
|
|
881
1052
|
/**
|
|
882
1053
|
* Read the `_tag` off a thrown error value, or `undefined` when it has none.
|
|
883
1054
|
*
|
|
@@ -895,6 +1066,285 @@ export declare const effectiveScopes: (subject: Subject) => ReadonlyArray<string
|
|
|
895
1066
|
*/
|
|
896
1067
|
export declare const errorTag: (err: unknown) => string | undefined;
|
|
897
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* The full routing address: tenant, event, key.
|
|
1071
|
+
*
|
|
1072
|
+
* The tenant is FIRST and is supplied by the caller of this function from the
|
|
1073
|
+
* SUBJECT — never from anything a client sent. Two apps in two tenants
|
|
1074
|
+
* publishing `games.started` for `arena-1` are addressing different channels,
|
|
1075
|
+
* and no amount of key collision can make them the same one.
|
|
1076
|
+
*
|
|
1077
|
+
* Separated by `EVENT_ROUTE_SEP` — NUL, and NOBODY WRITES IT INLINE. The character is
|
|
1078
|
+
* the right separator (it cannot occur in a tenant id or an event name, so no
|
|
1079
|
+
* pair of parts can spell another pair's address), but a source file containing
|
|
1080
|
+
* a literal one is BINARY to every text tool: `grep` skips it and prints
|
|
1081
|
+
* nothing, which reads exactly like a clean file. This repo has already lost an
|
|
1082
|
+
* audit that way.
|
|
1083
|
+
*
|
|
1084
|
+
* The constant exists because escaping it correctly at each site does NOT work
|
|
1085
|
+
* in practice: writing this module produced three literals in three different
|
|
1086
|
+
* files — including inside the comment warning against them — before the
|
|
1087
|
+
* separator was hoisted. So there is exactly one place the character appears,
|
|
1088
|
+
* and `parseEventRoute` / `formatEventRoute` mean no consumer needs to name it
|
|
1089
|
+
* at all. `eventRouteHygiene` in the test file scans the whole package.
|
|
1090
|
+
*/
|
|
1091
|
+
export declare const EVENT_ROUTE_SEP = "\0";
|
|
1092
|
+
|
|
1093
|
+
/** First element of every subscription: the stream is live from here on. */
|
|
1094
|
+
export declare const eventAttached: Schema.Struct<{
|
|
1095
|
+
_tag: Schema.Literal<["attached"]>;
|
|
1096
|
+
}>;
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* What it means for a subscriber to fall behind.
|
|
1100
|
+
*
|
|
1101
|
+
* `'each'` — every delivery matters. A subscriber that cannot keep up loses the
|
|
1102
|
+
* OLDEST and is told exactly how many. This is the default because it is the
|
|
1103
|
+
* safe reading: an arena that misses a game-start signal must find out.
|
|
1104
|
+
*
|
|
1105
|
+
* `'latest'` — only the current value matters, and a newer delivery SUPERSEDES a
|
|
1106
|
+
* pending one. A subscriber that falls behind receives the current state on its
|
|
1107
|
+
* next read and is told nothing, because nothing was lost: for a 60Hz stream of
|
|
1108
|
+
* positions, frame 1 stopped being interesting the moment frame 2 existed.
|
|
1109
|
+
*
|
|
1110
|
+
* The distinction is semantic, not a performance knob. Choosing `'latest'` for a
|
|
1111
|
+
* stream where each delivery matters silently drops the ones in between; choosing
|
|
1112
|
+
* `'each'` for a per-frame stream makes a slow client work through a backlog to
|
|
1113
|
+
* reach a state it could have had immediately, and report a "loss" that was
|
|
1114
|
+
* never a loss.
|
|
1115
|
+
*
|
|
1116
|
+
* The honest test: **would a consumer be wrong to miss one?** If the next value
|
|
1117
|
+
* supersedes it, that is `'latest'` — and it is probably state rather than an
|
|
1118
|
+
* event at all.
|
|
1119
|
+
*/
|
|
1120
|
+
export declare type EventDeliverySemantics = 'each' | 'latest';
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* A declared event: a name, a routing key, a payload, and who may listen.
|
|
1124
|
+
*
|
|
1125
|
+
* Type parameters are inferred from the Schemas, and every call site — `publish`,
|
|
1126
|
+
* `useEvent`, a workflow trigger — derives its types from this one value.
|
|
1127
|
+
*/
|
|
1128
|
+
export declare interface EventDescriptor<Name extends string, Key extends Schema.Schema.Any, Payload extends Schema.Schema.Any> {
|
|
1129
|
+
readonly kind: 'event';
|
|
1130
|
+
readonly name: Name;
|
|
1131
|
+
readonly key: Key;
|
|
1132
|
+
readonly payload: Payload;
|
|
1133
|
+
/** Who may LISTEN. Re-checked when the subject changes, not per delivery —
|
|
1134
|
+
* see `EventDefinition.guards` for why that distinction is deliberate. */
|
|
1135
|
+
readonly guards: Guards<Schema.Schema.Type<Key>> | undefined;
|
|
1136
|
+
/** Deliver buffered events on a FIRST attach. Default false. */
|
|
1137
|
+
readonly rewind: boolean | undefined;
|
|
1138
|
+
/** `'each'` (default) or `'latest'` — see the definer. */
|
|
1139
|
+
readonly delivery: EventDeliverySemantics | undefined;
|
|
1140
|
+
/** Opt this event into outbound HTTP delivery. See `EventWebhookSpec`. */
|
|
1141
|
+
readonly webhook: EventWebhookSpec | undefined;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* One delivery.
|
|
1146
|
+
*
|
|
1147
|
+
* `origin` + `n` are what make a lost message COUNTABLE. `n` is monotonic per
|
|
1148
|
+
* (origin, event, tenant, key), so a subscriber that sees 7 then 9 knows exactly
|
|
1149
|
+
* one delivery is gone — where a dropping buffer, which is what every
|
|
1150
|
+
* `onSlowConsumer: 'drop-oldest'` option in this space actually is, discards in
|
|
1151
|
+
* silence by definition. Silence is the one outcome you cannot build on: an
|
|
1152
|
+
* arena cannot tell "no game started" from "the start signal was dropped".
|
|
1153
|
+
*
|
|
1154
|
+
* It is per ORIGIN rather than global because two instances publishing the same
|
|
1155
|
+
* key have no shared clock and no shared counter. A global sequence would need
|
|
1156
|
+
* one owner per key — an extra hop on every publish and a failover story for
|
|
1157
|
+
* every key — which is a real architecture (a single-threaded actor per room)
|
|
1158
|
+
* and not the one this framework has.
|
|
1159
|
+
*/
|
|
1160
|
+
export declare const eventEnvelope: <P extends Schema.Schema.Any>(payload: P) => Schema.Struct<{
|
|
1161
|
+
_tag: Schema.Literal<["event"]>;
|
|
1162
|
+
/** Publishing instance id — serials are only comparable within one. */
|
|
1163
|
+
origin: typeof Schema.String;
|
|
1164
|
+
/** Monotonic per (origin, event, tenant, key). */
|
|
1165
|
+
n: typeof Schema.Number;
|
|
1166
|
+
emittedAt: typeof Schema.Number;
|
|
1167
|
+
payload: P;
|
|
1168
|
+
}>;
|
|
1169
|
+
|
|
1170
|
+
/** Told to the subscriber when the SERVER knows it cannot fill a gap. */
|
|
1171
|
+
export declare const eventGap: Schema.Struct<{
|
|
1172
|
+
_tag: Schema.Literal<["gap"]>;
|
|
1173
|
+
/** How many deliveries are known lost. Never a guess: a shortfall is computed
|
|
1174
|
+
* from the requested serial against what the ring still holds. */
|
|
1175
|
+
missed: typeof Schema.Number;
|
|
1176
|
+
/** `buffer` — evicted before this subscriber could be served.
|
|
1177
|
+
* `resume` — the re-attach asked for a serial older than the ring. */
|
|
1178
|
+
reason: Schema.Literal<["buffer", "resume"]>;
|
|
1179
|
+
}>;
|
|
1180
|
+
|
|
1181
|
+
/** The routing key did not decode against the descriptor's schema. */
|
|
1182
|
+
export declare class EventKeyInvalid extends EventKeyInvalid_base {
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
declare const EventKeyInvalid_base: Schema.TaggedErrorClass<EventKeyInvalid, "EventKeyInvalid", {
|
|
1186
|
+
readonly _tag: Schema.tag<"EventKeyInvalid">;
|
|
1187
|
+
} & {
|
|
1188
|
+
event: typeof Schema.String;
|
|
1189
|
+
message: typeof Schema.String;
|
|
1190
|
+
}>;
|
|
1191
|
+
|
|
1192
|
+
/** The payload did not decode against the descriptor's schema. */
|
|
1193
|
+
export declare class EventPayloadInvalid extends EventPayloadInvalid_base {
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
declare const EventPayloadInvalid_base: Schema.TaggedErrorClass<EventPayloadInvalid, "EventPayloadInvalid", {
|
|
1197
|
+
readonly _tag: Schema.tag<"EventPayloadInvalid">;
|
|
1198
|
+
} & {
|
|
1199
|
+
event: typeof Schema.String;
|
|
1200
|
+
message: typeof Schema.String;
|
|
1201
|
+
}>;
|
|
1202
|
+
|
|
1203
|
+
/** The encoded envelope exceeds `MAX_EVENT_ENVELOPE_BYTES`. */
|
|
1204
|
+
export declare class EventPayloadTooLarge extends EventPayloadTooLarge_base {
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
declare const EventPayloadTooLarge_base: Schema.TaggedErrorClass<EventPayloadTooLarge, "EventPayloadTooLarge", {
|
|
1208
|
+
readonly _tag: Schema.tag<"EventPayloadTooLarge">;
|
|
1209
|
+
} & {
|
|
1210
|
+
event: typeof Schema.String;
|
|
1211
|
+
bytes: typeof Schema.Number;
|
|
1212
|
+
limit: typeof Schema.Number;
|
|
1213
|
+
}>;
|
|
1214
|
+
|
|
1215
|
+
export declare type EventResumePoint = {
|
|
1216
|
+
readonly origin: string;
|
|
1217
|
+
readonly n: number;
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
/** Where a re-attaching subscriber left off, per origin. */
|
|
1221
|
+
export declare const eventResumePoint: Schema.Struct<{
|
|
1222
|
+
origin: typeof Schema.String;
|
|
1223
|
+
n: typeof Schema.Number;
|
|
1224
|
+
}>;
|
|
1225
|
+
|
|
1226
|
+
export declare const eventRoute: (tenantId: string | null, event: string, key: unknown) => string;
|
|
1227
|
+
|
|
1228
|
+
export declare type EventStreamEvent<P> = {
|
|
1229
|
+
readonly _tag: 'attached';
|
|
1230
|
+
} | {
|
|
1231
|
+
readonly _tag: 'event';
|
|
1232
|
+
readonly origin: string;
|
|
1233
|
+
readonly n: number;
|
|
1234
|
+
readonly emittedAt: number;
|
|
1235
|
+
readonly payload: P;
|
|
1236
|
+
} | {
|
|
1237
|
+
readonly _tag: 'gap';
|
|
1238
|
+
readonly missed: number;
|
|
1239
|
+
readonly reason: 'buffer' | 'resume';
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* What a subscription emits. `attached` first, then `event`s, with `gap`
|
|
1244
|
+
* interleaved whenever the server can prove a loss.
|
|
1245
|
+
*/
|
|
1246
|
+
export declare const eventStreamEvent: <P extends Schema.Schema.Any>(payload: P) => Schema.Union<[Schema.Struct<{
|
|
1247
|
+
_tag: Schema.Literal<["attached"]>;
|
|
1248
|
+
}>, Schema.Struct<{
|
|
1249
|
+
_tag: Schema.Literal<["event"]>;
|
|
1250
|
+
/** Publishing instance id — serials are only comparable within one. */
|
|
1251
|
+
origin: typeof Schema.String;
|
|
1252
|
+
/** Monotonic per (origin, event, tenant, key). */
|
|
1253
|
+
n: typeof Schema.Number;
|
|
1254
|
+
emittedAt: typeof Schema.Number;
|
|
1255
|
+
payload: P;
|
|
1256
|
+
}>, Schema.Struct<{
|
|
1257
|
+
_tag: Schema.Literal<["gap"]>;
|
|
1258
|
+
/** How many deliveries are known lost. Never a guess: a shortfall is computed
|
|
1259
|
+
* from the requested serial against what the ring still holds. */
|
|
1260
|
+
missed: typeof Schema.Number;
|
|
1261
|
+
/** `buffer` — evicted before this subscriber could be served.
|
|
1262
|
+
* `resume` — the re-attach asked for a serial older than the ring. */
|
|
1263
|
+
reason: Schema.Literal<["buffer", "resume"]>;
|
|
1264
|
+
}>]>;
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* The subscription payload: the key, plus where to resume.
|
|
1268
|
+
*
|
|
1269
|
+
* `resume` present ⇒ this is a RE-attach and the client is owed continuity;
|
|
1270
|
+
* absent ⇒ a first attach, which gets nothing older than itself unless the
|
|
1271
|
+
* descriptor opted into `rewind`. The distinction lives on the wire because only
|
|
1272
|
+
* the client knows whether it has seen this stream before — the server cannot
|
|
1273
|
+
* tell a reconnect from a fresh mount.
|
|
1274
|
+
*/
|
|
1275
|
+
export declare const eventSubscribeInput: <K extends Schema.Schema.Any>(key: K) => Schema.Struct<{
|
|
1276
|
+
key: K;
|
|
1277
|
+
resume: Schema.optional<Schema.Array$<Schema.Struct<{
|
|
1278
|
+
origin: typeof Schema.String;
|
|
1279
|
+
n: typeof Schema.Number;
|
|
1280
|
+
}>>>;
|
|
1281
|
+
}>;
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Lift an event descriptor into its rpc.
|
|
1285
|
+
*
|
|
1286
|
+
* A `stream: true` rpc, like a query — the framework's push transport is the
|
|
1287
|
+
* WebSocket a subscription already holds open, and reusing it is why `useEvent`
|
|
1288
|
+
* shares one connection lifecycle, one reconnect policy and one devtools view
|
|
1289
|
+
* with `useSubscription` instead of being a second realtime concept in the same
|
|
1290
|
+
* app.
|
|
1291
|
+
*/
|
|
1292
|
+
export declare const eventToRpc: <Name extends string, Key extends Schema.Schema.Any, Payload extends Schema.Schema.Any>(descriptor: EventDescriptor<Name, Key, Payload>, extraErrors?: ReadonlyArray<Schema.Schema.All>) => Rpc.Rpc<Name, Schema.Struct<{
|
|
1293
|
+
key: Key;
|
|
1294
|
+
resume: Schema.optional<Schema.Array$<Schema.Struct<{
|
|
1295
|
+
origin: typeof Schema.String;
|
|
1296
|
+
n: typeof Schema.Number;
|
|
1297
|
+
}>>>;
|
|
1298
|
+
}>, Stream<Schema.Union<[Schema.Struct<{
|
|
1299
|
+
_tag: Schema.Literal<["attached"]>;
|
|
1300
|
+
}>, Schema.Struct<{
|
|
1301
|
+
_tag: Schema.Literal<["event"]>;
|
|
1302
|
+
/** Publishing instance id — serials are only comparable within one. */
|
|
1303
|
+
origin: typeof Schema.String;
|
|
1304
|
+
/** Monotonic per (origin, event, tenant, key). */
|
|
1305
|
+
n: typeof Schema.Number;
|
|
1306
|
+
emittedAt: typeof Schema.Number;
|
|
1307
|
+
payload: Payload;
|
|
1308
|
+
}>, Schema.Struct<{
|
|
1309
|
+
_tag: Schema.Literal<["gap"]>;
|
|
1310
|
+
/** How many deliveries are known lost. Never a guess: a shortfall is computed
|
|
1311
|
+
* from the requested serial against what the ring still holds. */
|
|
1312
|
+
missed: typeof Schema.Number;
|
|
1313
|
+
/** `buffer` — evicted before this subscriber could be served.
|
|
1314
|
+
* `resume` — the re-attach asked for a serial older than the ring. */
|
|
1315
|
+
reason: Schema.Literal<["buffer", "resume"]>;
|
|
1316
|
+
}>]>, Schema.Schema.Any | Schema.Schema<never, never, unknown>>, typeof Schema.Never, never>;
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* The WEBHOOK audience of a declared event.
|
|
1320
|
+
*
|
|
1321
|
+
* Present ⇒ `@voltro/plugin-webhooks` treats this event as an outbound one:
|
|
1322
|
+
* subscribers can register HTTP targets for it and every publish is delivered to
|
|
1323
|
+
* them, signed and retried, from the SAME call that fans it out to clients.
|
|
1324
|
+
*
|
|
1325
|
+
* Structurally typed rather than importing the plugin's own types, and that is a
|
|
1326
|
+
* dependency direction rather than a preference: `@voltro/protocol` is
|
|
1327
|
+
* browser-safe and must not reach a plugin. The plugin reads this block and maps
|
|
1328
|
+
* it onto its own knobs — the shapes are the plugin's to define, so a value here
|
|
1329
|
+
* is a plain number or string, never one of its enums.
|
|
1330
|
+
*
|
|
1331
|
+
* Namespaced under `webhook:` rather than spread across the descriptor because
|
|
1332
|
+
* these settings are meaningless to the other three audiences. A `retry` at the
|
|
1333
|
+
* top level would read as if it applied to client delivery, which is
|
|
1334
|
+
* at-most-once by design and has no retry at all.
|
|
1335
|
+
*/
|
|
1336
|
+
export declare interface EventWebhookSpec {
|
|
1337
|
+
/** Human-readable summary for the dashboard's event list. */
|
|
1338
|
+
readonly description?: string;
|
|
1339
|
+
/** Payload schema version. Bump when subscribers must adapt. Default 1. */
|
|
1340
|
+
readonly version?: number;
|
|
1341
|
+
/** Shared ceiling across ALL deliveries of this event — the runaway-emit
|
|
1342
|
+
* guard. Over-limit deliveries are deferred, never dropped. */
|
|
1343
|
+
readonly rateLimit?: {
|
|
1344
|
+
readonly perMinute: number;
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
898
1348
|
/** Normalize the `exposeAsTool` shorthand. `true` is only valid when the
|
|
899
1349
|
* descriptor carries a top-level `description`; callers pass that in. */
|
|
900
1350
|
export declare type ExposeAsTool = boolean | ExposeAsToolSpec;
|
|
@@ -952,6 +1402,9 @@ export declare const findAdvisoryResourceGuards: (procedures: ReadonlyArray<{
|
|
|
952
1402
|
/** Record a completed response for replay. */
|
|
953
1403
|
export declare const finishIdempotent: (store: IdempotencyStore, scope: string, key: string, response: IdempotencyResponse, now: number) => Promise<void>;
|
|
954
1404
|
|
|
1405
|
+
/** A route rendered for humans — logs, the inspect surface, the dashboard. */
|
|
1406
|
+
export declare const formatEventRoute: (route: string) => string;
|
|
1407
|
+
|
|
955
1408
|
/** The currently-registered policy-guard resolver, or `undefined`. */
|
|
956
1409
|
export declare const getPolicyGuardResolver: () => PolicyGuardResolver | undefined;
|
|
957
1410
|
|
|
@@ -1133,6 +1586,8 @@ export declare interface InsertTarget<Input = unknown, Row = unknown, Item = Rec
|
|
|
1133
1586
|
readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
|
|
1134
1587
|
}
|
|
1135
1588
|
|
|
1589
|
+
export declare const isEventDescriptor: (value: unknown) => value is AnyEventDescriptor;
|
|
1590
|
+
|
|
1136
1591
|
/** True when every row in the set carries an `id`. The dispatcher uses
|
|
1137
1592
|
* this to decide patch-vs-full-snapshot: a result whose rows aren't
|
|
1138
1593
|
* id-keyed (rare — a custom projection that drops the id) can't be
|
|
@@ -1165,6 +1620,23 @@ export declare const isWireReachable: (descriptor: {
|
|
|
1165
1620
|
readonly internal?: boolean | undefined;
|
|
1166
1621
|
}) => boolean;
|
|
1167
1622
|
|
|
1623
|
+
/**
|
|
1624
|
+
* How large one encoded envelope may be, on EVERY dialect.
|
|
1625
|
+
*
|
|
1626
|
+
* Postgres `NOTIFY` dies past 8000 bytes and the other transports have no such
|
|
1627
|
+
* bound. Enforcing the limit only where the wire imposes it would make "switch
|
|
1628
|
+
* the broadcast provider" a silent behaviour change — an app developed against
|
|
1629
|
+
* Redis would start failing when it moved to the pg-native path, at the worst
|
|
1630
|
+
* possible moment. So the smallest transport's ceiling is the framework's
|
|
1631
|
+
* ceiling, minus room for the envelope around the payload.
|
|
1632
|
+
*
|
|
1633
|
+
* An oversized payload is also a design smell in its own right: an event says
|
|
1634
|
+
* that something happened, so `photo.added` carries a photo REFERENCE, and the
|
|
1635
|
+
* consumer fetches the photo through a route that can stream, cache and
|
|
1636
|
+
* authorize it.
|
|
1637
|
+
*/
|
|
1638
|
+
export declare const MAX_EVENT_ENVELOPE_BYTES = 7500;
|
|
1639
|
+
|
|
1168
1640
|
/** In-memory store — the default for single-process dev + the test double. */
|
|
1169
1641
|
export declare const memoryIdempotencyStore: () => IdempotencyStore;
|
|
1170
1642
|
|
|
@@ -1189,6 +1661,28 @@ export declare interface MutationProcedureDescriptor<Name extends string, Input
|
|
|
1189
1661
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
1190
1662
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
1191
1663
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
1664
|
+
/**
|
|
1665
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
1666
|
+
*
|
|
1667
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
1668
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
1669
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
1670
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
1671
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
1672
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
1673
|
+
* For a frontend developer that is the worst possible partition.
|
|
1674
|
+
*
|
|
1675
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
1676
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
1677
|
+
* already composes, since the collision check compares FULL tags and not
|
|
1678
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
1679
|
+
*
|
|
1680
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
1681
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
1682
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
1683
|
+
* file that performs it.
|
|
1684
|
+
*/
|
|
1685
|
+
readonly overridesPlugin: boolean | undefined;
|
|
1192
1686
|
readonly internal: boolean | undefined;
|
|
1193
1687
|
}
|
|
1194
1688
|
|
|
@@ -1235,6 +1729,13 @@ export declare interface ObservabilityContribution {
|
|
|
1235
1729
|
readonly sampler?: unknown;
|
|
1236
1730
|
}
|
|
1237
1731
|
|
|
1732
|
+
/** Split a route back into its three parts. */
|
|
1733
|
+
export declare const parseEventRoute: (route: string) => {
|
|
1734
|
+
readonly tenantId: string | null;
|
|
1735
|
+
readonly event: string;
|
|
1736
|
+
readonly key: string;
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1238
1739
|
/** A single row in a subscription result. Must carry an `id`; everything
|
|
1239
1740
|
* else is opaque to the patch layer. */
|
|
1240
1741
|
export declare type PatchRow = Readonly<Record<string, unknown>> & {
|
|
@@ -1270,6 +1771,41 @@ export declare type PluginActivateHook = (ctx: PluginLifecycleContext) => Effect
|
|
|
1270
1771
|
* touches them.
|
|
1271
1772
|
*/
|
|
1272
1773
|
export declare interface PluginBindContext {
|
|
1774
|
+
/**
|
|
1775
|
+
* This process's replica identity — the same value the event bus stamps as
|
|
1776
|
+
* its publish `origin` and the membership registry announces under.
|
|
1777
|
+
*
|
|
1778
|
+
* ONE id across all three on purpose: a plugin holding state per replica
|
|
1779
|
+
* (presence is the case) must be able to correlate "who owns this" with "is
|
|
1780
|
+
* that one still alive", and three identities for one process would make the
|
|
1781
|
+
* correlation quietly wrong rather than obviously broken.
|
|
1782
|
+
*/
|
|
1783
|
+
readonly instanceId?: string;
|
|
1784
|
+
/**
|
|
1785
|
+
* Live instance membership — subscribe to learn when a replica joins, leaves
|
|
1786
|
+
* or restarts.
|
|
1787
|
+
*
|
|
1788
|
+
* Present only when the host provides one. A plugin holding per-replica state
|
|
1789
|
+
* needs it and cannot derive it: pub/sub delivers messages, it does not report
|
|
1790
|
+
* who is on the channel, and an instance that dies simply goes quiet.
|
|
1791
|
+
*/
|
|
1792
|
+
readonly membership?: {
|
|
1793
|
+
readonly onChange: (listener: (event: {
|
|
1794
|
+
readonly kind: 'joined' | 'left' | 'restarted';
|
|
1795
|
+
readonly instanceId: string;
|
|
1796
|
+
}) => void) => () => void;
|
|
1797
|
+
};
|
|
1798
|
+
/**
|
|
1799
|
+
* The app's cross-replica broadcast transport, when one is configured.
|
|
1800
|
+
*
|
|
1801
|
+
* Absent ⇒ single instance, which is a complete answer rather than a
|
|
1802
|
+
* degraded one. A plugin must not branch on "do we have a cluster" — that
|
|
1803
|
+
* branch is how a feature comes to work in dev and differ in production.
|
|
1804
|
+
*/
|
|
1805
|
+
readonly broadcast?: {
|
|
1806
|
+
readonly publish: (channel: string, payload: string) => unknown;
|
|
1807
|
+
readonly subscribe: (channel: string, handler: (payload: string) => void) => unknown;
|
|
1808
|
+
};
|
|
1273
1809
|
/**
|
|
1274
1810
|
* The framework's already-open `SqlClient` (from `@effect/sql`) — the
|
|
1275
1811
|
* SAME pool the app's store uses. A plugin runs raw SQL through this
|
|
@@ -2083,6 +2619,28 @@ export declare interface QueryProcedureDescriptor<Name extends string, Input ext
|
|
|
2083
2619
|
readonly exposeAsTool: ExposeAsTool | undefined;
|
|
2084
2620
|
/** True when the procedure is kept OFF the wire — no client-group entry and no
|
|
2085
2621
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
2622
|
+
/**
|
|
2623
|
+
* Replace a PLUGIN route that answers to this same tag.
|
|
2624
|
+
*
|
|
2625
|
+
* Without it, a user route and a plugin route sharing a tag is a hard error,
|
|
2626
|
+
* and correctly so — two handlers behind one name is not a thing a caller can
|
|
2627
|
+
* reason about. But refusing is the wrong answer when the app deliberately
|
|
2628
|
+
* wants its own version: the two escapes available otherwise are to rename
|
|
2629
|
+
* your procedure (so the split runs along "who built it" rather than along a
|
|
2630
|
+
* domain boundary) or to `alias` the whole plugin away (same, one level up).
|
|
2631
|
+
* For a frontend developer that is the worst possible partition.
|
|
2632
|
+
*
|
|
2633
|
+
* A reporter wanted exactly this: adopt `@voltro/plugin-notifications`, whose
|
|
2634
|
+
* surface is richer than theirs, add `archive`/`unarchive` beside it — which
|
|
2635
|
+
* already composes, since the collision check compares FULL tags and not
|
|
2636
|
+
* prefixes — and replace `markRead`, because theirs maintains archive state.
|
|
2637
|
+
*
|
|
2638
|
+
* Explicit, never inferred. Silently letting the app win would mean a plugin
|
|
2639
|
+
* upgrade that adds a route could shadow an app procedure with no diff to
|
|
2640
|
+
* read; declaring it makes the intent reviewable and puts the override in the
|
|
2641
|
+
* file that performs it.
|
|
2642
|
+
*/
|
|
2643
|
+
readonly overridesPlugin: boolean | undefined;
|
|
2086
2644
|
readonly internal: boolean | undefined;
|
|
2087
2645
|
}
|
|
2088
2646
|
|
|
@@ -2440,6 +2998,10 @@ export declare interface StreamProcedureDescriptor<Name extends string, Input ex
|
|
|
2440
2998
|
/** True when the stream is kept OFF the wire — no client-group entry and no
|
|
2441
2999
|
* route in dev or serve. See `internal` on the definer's options. */
|
|
2442
3000
|
readonly internal: boolean | undefined;
|
|
3001
|
+
/** WHO MAY LISTEN. Checked at subscribe AND re-checked before every element,
|
|
3002
|
+
* the same as a query's — a stream is a long-lived grant and the scopes that
|
|
3003
|
+
* justified it can be withdrawn while it is still open. */
|
|
3004
|
+
readonly guards: Guards | undefined;
|
|
2443
3005
|
}
|
|
2444
3006
|
|
|
2445
3007
|
export declare const streamToRpc: <Name extends string, Input extends Schema.Schema.Any, Element extends Schema.Schema.Any, Err extends Schema.Schema.All>(descriptor: StreamProcedureDescriptor<Name, Input, Element, Err>, extraErrors?: ExtraErrors) => Rpc.Rpc<Name, Input extends Schema.Struct.Fields ? Schema.Struct<Input> : Input, Stream<Element, Schema.Schema.All>, typeof Schema.Never, never>;
|
|
@@ -2484,36 +3046,36 @@ export declare class SubjectService extends SubjectService_base {
|
|
|
2484
3046
|
|
|
2485
3047
|
declare const SubjectService_base: Context.TagClass<SubjectService, "@voltro/Subject", {
|
|
2486
3048
|
readonly id: string;
|
|
2487
|
-
readonly tenantId: string;
|
|
2488
3049
|
readonly type: "user";
|
|
3050
|
+
readonly tenantId: string;
|
|
2489
3051
|
readonly scopes?: readonly string[] | undefined;
|
|
2490
3052
|
readonly metadata?: {
|
|
2491
3053
|
readonly [x: string]: unknown;
|
|
2492
3054
|
} | undefined;
|
|
2493
3055
|
} | {
|
|
2494
3056
|
readonly id: string;
|
|
2495
|
-
readonly tenantId: string;
|
|
2496
3057
|
readonly type: "apiKey";
|
|
3058
|
+
readonly tenantId: string;
|
|
2497
3059
|
readonly scopes?: readonly string[] | undefined;
|
|
2498
3060
|
readonly metadata?: {
|
|
2499
3061
|
readonly [x: string]: unknown;
|
|
2500
3062
|
} | undefined;
|
|
2501
3063
|
} | {
|
|
2502
3064
|
readonly id: string;
|
|
2503
|
-
readonly tenantId: string;
|
|
2504
3065
|
readonly type: "serviceAccount";
|
|
3066
|
+
readonly tenantId: string;
|
|
2505
3067
|
readonly scopes?: readonly string[] | undefined;
|
|
2506
3068
|
readonly metadata?: {
|
|
2507
3069
|
readonly [x: string]: unknown;
|
|
2508
3070
|
} | undefined;
|
|
2509
3071
|
} | {
|
|
2510
3072
|
readonly id: null;
|
|
2511
|
-
readonly tenantId: string | null;
|
|
2512
3073
|
readonly type: "anonymous";
|
|
3074
|
+
readonly tenantId: string | null;
|
|
2513
3075
|
} | {
|
|
2514
3076
|
readonly id: string;
|
|
2515
|
-
readonly tenantId: null;
|
|
2516
3077
|
readonly type: "system";
|
|
3078
|
+
readonly tenantId: null;
|
|
2517
3079
|
readonly scopes?: readonly string[] | undefined;
|
|
2518
3080
|
readonly metadata?: {
|
|
2519
3081
|
readonly [x: string]: unknown;
|