@tx5dr/plugin-api 1.7.12 → 2.5.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 (181) hide show
  1. package/README.md +475 -17
  2. package/dist/__tests__/adif.test.js +107 -0
  3. package/dist/__tests__/adif.test.js.map +1 -1
  4. package/dist/__tests__/capability-context.test.d.ts +2 -0
  5. package/dist/__tests__/capability-context.test.d.ts.map +1 -0
  6. package/dist/__tests__/capability-context.test.js +99 -0
  7. package/dist/__tests__/capability-context.test.js.map +1 -0
  8. package/dist/__tests__/testing-utils.test.js +234 -15
  9. package/dist/__tests__/testing-utils.test.js.map +1 -1
  10. package/dist/capabilities.d.ts +38 -0
  11. package/dist/capabilities.d.ts.map +1 -0
  12. package/dist/capabilities.js +50 -0
  13. package/dist/capabilities.js.map +1 -0
  14. package/dist/compatibility.d.ts +15 -0
  15. package/dist/compatibility.d.ts.map +1 -0
  16. package/dist/compatibility.js +71 -0
  17. package/dist/compatibility.js.map +1 -0
  18. package/dist/contest-logbook-ui/assets/contest-log.css +1 -0
  19. package/dist/contest-logbook-ui/assets/contest-log.js +40 -0
  20. package/dist/contest-logbook-ui/contest-log.html +13 -0
  21. package/dist/context.d.ts +125 -54
  22. package/dist/context.d.ts.map +1 -1
  23. package/dist/definition.d.ts +98 -21
  24. package/dist/definition.d.ts.map +1 -1
  25. package/dist/definition.js +8 -1
  26. package/dist/definition.js.map +1 -1
  27. package/dist/ft8/StandardQSOPluginRuntime.d.ts +142 -0
  28. package/dist/ft8/StandardQSOPluginRuntime.d.ts.map +1 -0
  29. package/dist/ft8/StandardQSOPluginRuntime.js +1592 -0
  30. package/dist/ft8/StandardQSOPluginRuntime.js.map +1 -0
  31. package/dist/ft8/index.d.ts +22 -0
  32. package/dist/ft8/index.d.ts.map +1 -0
  33. package/dist/ft8/index.js +176 -0
  34. package/dist/ft8/index.js.map +1 -0
  35. package/dist/helpers.d.ts +417 -132
  36. package/dist/helpers.d.ts.map +1 -1
  37. package/dist/hooks.d.ts +56 -34
  38. package/dist/hooks.d.ts.map +1 -1
  39. package/dist/host-dependencies.d.ts +101 -0
  40. package/dist/host-dependencies.d.ts.map +1 -1
  41. package/dist/index.d.ts +20 -6
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +12 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/runtime.d.ts +539 -13
  46. package/dist/runtime.d.ts.map +1 -1
  47. package/dist/runtime.js +9 -1
  48. package/dist/runtime.js.map +1 -1
  49. package/dist/settings.d.ts +31 -0
  50. package/dist/settings.d.ts.map +1 -1
  51. package/dist/simulation.d.ts +60 -0
  52. package/dist/simulation.d.ts.map +1 -0
  53. package/dist/simulation.js +2 -0
  54. package/dist/simulation.js.map +1 -0
  55. package/dist/sync.d.ts +98 -6
  56. package/dist/sync.d.ts.map +1 -1
  57. package/dist/sync.js +6 -0
  58. package/dist/sync.js.map +1 -1
  59. package/dist/testing/index.d.ts +74 -12
  60. package/dist/testing/index.d.ts.map +1 -1
  61. package/dist/testing/index.js +372 -105
  62. package/dist/testing/index.js.map +1 -1
  63. package/dist/toolkit/contest/CabrilloBuilder.d.ts +7 -0
  64. package/dist/toolkit/contest/CabrilloBuilder.d.ts.map +1 -0
  65. package/dist/toolkit/contest/CabrilloBuilder.js +11 -0
  66. package/dist/toolkit/contest/CabrilloBuilder.js.map +1 -0
  67. package/dist/toolkit/contest/ContestCategorySchema.d.ts +11 -0
  68. package/dist/toolkit/contest/ContestCategorySchema.d.ts.map +1 -0
  69. package/dist/toolkit/contest/ContestCategorySchema.js +6 -0
  70. package/dist/toolkit/contest/ContestCategorySchema.js.map +1 -0
  71. package/dist/toolkit/contest/ContestLogbook.d.ts +223 -0
  72. package/dist/toolkit/contest/ContestLogbook.d.ts.map +1 -0
  73. package/dist/toolkit/contest/ContestLogbook.js +845 -0
  74. package/dist/toolkit/contest/ContestLogbook.js.map +1 -0
  75. package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.d.ts +28 -0
  76. package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.d.ts.map +1 -0
  77. package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.js +79 -0
  78. package/dist/toolkit/contest/ContestQsoEnvelopeAdapter.js.map +1 -0
  79. package/dist/toolkit/contest/ContestQsoProjector.d.ts +12 -0
  80. package/dist/toolkit/contest/ContestQsoProjector.d.ts.map +1 -0
  81. package/dist/toolkit/contest/ContestQsoProjector.js +13 -0
  82. package/dist/toolkit/contest/ContestQsoProjector.js.map +1 -0
  83. package/dist/toolkit/contest/ContestSessionNotifier.d.ts +9 -0
  84. package/dist/toolkit/contest/ContestSessionNotifier.d.ts.map +1 -0
  85. package/dist/toolkit/contest/ContestSessionNotifier.js +13 -0
  86. package/dist/toolkit/contest/ContestSessionNotifier.js.map +1 -0
  87. package/dist/toolkit/contest/ContestSessionRepository.d.ts +21 -0
  88. package/dist/toolkit/contest/ContestSessionRepository.d.ts.map +1 -0
  89. package/dist/toolkit/contest/ContestSessionRepository.js +27 -0
  90. package/dist/toolkit/contest/ContestSessionRepository.js.map +1 -0
  91. package/dist/toolkit/contest/DefaultContestSession.d.ts +105 -0
  92. package/dist/toolkit/contest/DefaultContestSession.d.ts.map +1 -0
  93. package/dist/toolkit/contest/DefaultContestSession.js +387 -0
  94. package/dist/toolkit/contest/DefaultContestSession.js.map +1 -0
  95. package/dist/toolkit/contest/DefaultContestWorkbench.d.ts +100 -0
  96. package/dist/toolkit/contest/DefaultContestWorkbench.d.ts.map +1 -0
  97. package/dist/toolkit/contest/DefaultContestWorkbench.js +33 -0
  98. package/dist/toolkit/contest/DefaultContestWorkbench.js.map +1 -0
  99. package/dist/toolkit/contest/FT8ContestDefinition.d.ts +83 -0
  100. package/dist/toolkit/contest/FT8ContestDefinition.d.ts.map +1 -0
  101. package/dist/toolkit/contest/FT8ContestDefinition.js +167 -0
  102. package/dist/toolkit/contest/FT8ContestDefinition.js.map +1 -0
  103. package/dist/toolkit/contest/FT8ContestModules.d.ts +178 -0
  104. package/dist/toolkit/contest/FT8ContestModules.d.ts.map +1 -0
  105. package/dist/toolkit/contest/FT8ContestModules.js +247 -0
  106. package/dist/toolkit/contest/FT8ContestModules.js.map +1 -0
  107. package/dist/toolkit/contest/FT8ContestPlugin.d.ts +55 -0
  108. package/dist/toolkit/contest/FT8ContestPlugin.d.ts.map +1 -0
  109. package/dist/toolkit/contest/FT8ContestPlugin.js +322 -0
  110. package/dist/toolkit/contest/FT8ContestPlugin.js.map +1 -0
  111. package/dist/toolkit/contest/FT8ContestTestKit.d.ts +12 -0
  112. package/dist/toolkit/contest/FT8ContestTestKit.d.ts.map +1 -0
  113. package/dist/toolkit/contest/FT8ContestTestKit.js +60 -0
  114. package/dist/toolkit/contest/FT8ContestTestKit.js.map +1 -0
  115. package/dist/toolkit/contest/contest-toolkit.test.d.ts +2 -0
  116. package/dist/toolkit/contest/contest-toolkit.test.d.ts.map +1 -0
  117. package/dist/toolkit/contest/contest-toolkit.test.js +1196 -0
  118. package/dist/toolkit/contest/contest-toolkit.test.js.map +1 -0
  119. package/dist/toolkit/contest/index.d.ts +14 -0
  120. package/dist/toolkit/contest/index.d.ts.map +1 -0
  121. package/dist/toolkit/contest/index.js +14 -0
  122. package/dist/toolkit/contest/index.js.map +1 -0
  123. package/dist/toolkit/index.d.ts +3 -0
  124. package/dist/toolkit/index.d.ts.map +1 -0
  125. package/dist/toolkit/index.js +3 -0
  126. package/dist/toolkit/index.js.map +1 -0
  127. package/dist/toolkit/parallel-qso/AuthorizationLease.d.ts +15 -0
  128. package/dist/toolkit/parallel-qso/AuthorizationLease.d.ts.map +1 -0
  129. package/dist/toolkit/parallel-qso/AuthorizationLease.js +25 -0
  130. package/dist/toolkit/parallel-qso/AuthorizationLease.js.map +1 -0
  131. package/dist/toolkit/parallel-qso/BoundedCallSessionController.d.ts +40 -0
  132. package/dist/toolkit/parallel-qso/BoundedCallSessionController.d.ts.map +1 -0
  133. package/dist/toolkit/parallel-qso/BoundedCallSessionController.js +81 -0
  134. package/dist/toolkit/parallel-qso/BoundedCallSessionController.js.map +1 -0
  135. package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.d.ts +2 -0
  136. package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.d.ts.map +1 -0
  137. package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.js +31 -0
  138. package/dist/toolkit/parallel-qso/BoundedCallSessionController.test.js.map +1 -0
  139. package/dist/toolkit/parallel-qso/CompletionTracker.d.ts +20 -0
  140. package/dist/toolkit/parallel-qso/CompletionTracker.d.ts.map +1 -0
  141. package/dist/toolkit/parallel-qso/CompletionTracker.js +20 -0
  142. package/dist/toolkit/parallel-qso/CompletionTracker.js.map +1 -0
  143. package/dist/toolkit/parallel-qso/LaneFrequencyController.d.ts +19 -0
  144. package/dist/toolkit/parallel-qso/LaneFrequencyController.d.ts.map +1 -0
  145. package/dist/toolkit/parallel-qso/LaneFrequencyController.js +23 -0
  146. package/dist/toolkit/parallel-qso/LaneFrequencyController.js.map +1 -0
  147. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.d.ts +134 -0
  148. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.d.ts.map +1 -0
  149. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.js +554 -0
  150. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.js.map +1 -0
  151. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.d.ts +2 -0
  152. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.d.ts.map +1 -0
  153. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.js +470 -0
  154. package/dist/toolkit/parallel-qso/ParallelQSOCoordinator.test.js.map +1 -0
  155. package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.d.ts +20 -0
  156. package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.d.ts.map +1 -0
  157. package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.js +29 -0
  158. package/dist/toolkit/parallel-qso/PostCompletionRecoveryLease.js.map +1 -0
  159. package/dist/toolkit/parallel-qso/ProtocolLane.d.ts +50 -0
  160. package/dist/toolkit/parallel-qso/ProtocolLane.d.ts.map +1 -0
  161. package/dist/toolkit/parallel-qso/ProtocolLane.js +2 -0
  162. package/dist/toolkit/parallel-qso/ProtocolLane.js.map +1 -0
  163. package/dist/toolkit/parallel-qso/controllers.test.d.ts +2 -0
  164. package/dist/toolkit/parallel-qso/controllers.test.d.ts.map +1 -0
  165. package/dist/toolkit/parallel-qso/controllers.test.js +36 -0
  166. package/dist/toolkit/parallel-qso/controllers.test.js.map +1 -0
  167. package/dist/toolkit/parallel-qso/index.d.ts +8 -0
  168. package/dist/toolkit/parallel-qso/index.d.ts.map +1 -0
  169. package/dist/toolkit/parallel-qso/index.js +8 -0
  170. package/dist/toolkit/parallel-qso/index.js.map +1 -0
  171. package/dist/utils/adif.d.ts +1 -1
  172. package/dist/utils/adif.d.ts.map +1 -1
  173. package/dist/utils/adif.js +96 -51
  174. package/dist/utils/adif.js.map +1 -1
  175. package/dist/utils/qso-text-fields.d.ts +7 -1
  176. package/dist/utils/qso-text-fields.d.ts.map +1 -1
  177. package/dist/utils/qso-text-fields.js +74 -6
  178. package/dist/utils/qso-text-fields.js.map +1 -1
  179. package/package.json +28 -6
  180. package/src/bridge.d.ts +3 -0
  181. package/tokens.css +51 -0
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import type { ParsedFT8Message, SlotInfo, SlotPack, QSORecord, FrameMessage, OperatorSlots, ModeDescriptor, EngineMode, PermissionGrant, PluginPanelDescriptor, CapabilityList, CapabilityState, RadioPowerResponse, RadioPowerStateEvent, RadioPowerSupportInfo, RadioPowerTarget, WriteCapabilityPayload } from '@tx5dr/contracts';
1
+ import type { ParsedFT8Message, SlotInfo, SlotPack, FrameMessage, ModeDescriptor, EngineMode, PermissionGrant, PluginPanelDescriptor, CapabilityList, CapabilityState, RadioPowerResponse, RadioPowerStateEvent, RadioPowerSupportInfo, RadioPowerTarget } from '@tx5dr/contracts';
2
2
  import type { StrategyRuntimeSnapshot } from './runtime.js';
3
+ import type { LogbookBatchMutation, LogbookBatchResult, LogbookQsoSnapshot } from '@tx5dr/core';
3
4
  /**
4
5
  * Simple persistent key-value store exposed to plugins.
5
6
  *
@@ -10,19 +11,27 @@ export interface KVStore {
10
11
  /**
11
12
  * Reads a stored value.
12
13
  *
13
- * When the key is missing, the provided `defaultValue` is returned instead.
14
+ * Stored values are returned by value, so mutating the result does not update
15
+ * persistence until {@link set} is called. When the key is missing, the
16
+ * caller-owned `defaultValue` is returned unchanged.
14
17
  */
15
18
  get<T = unknown>(key: string, defaultValue?: T): T;
16
19
  /**
17
- * Persists a value under the given key.
20
+ * Persists a JSON-compatible snapshot under the given key.
21
+ *
22
+ * `undefined` follows JSON object semantics and removes the key. Cycles,
23
+ * BigInt values, functions and Host capabilities are rejected with
24
+ * `PLUGIN_DATA_NOT_SERIALIZABLE`.
18
25
  */
19
26
  set(key: string, value: unknown): void;
27
+ /** Atomically updates one value shared by every instance of this plugin. */
28
+ update<T = unknown>(key: string, reducer: (current: T | undefined) => T | undefined): T | undefined;
20
29
  /**
21
30
  * Removes a stored key and its value.
22
31
  */
23
32
  delete(key: string): void;
24
33
  /**
25
- * Returns a shallow snapshot of all stored entries in this scope.
34
+ * Returns an independent snapshot of all stored entries in this scope.
26
35
  */
27
36
  getAll(): Record<string, unknown>;
28
37
  /**
@@ -34,6 +43,40 @@ export interface KVStore {
34
43
  */
35
44
  flush(): Promise<void>;
36
45
  }
46
+ /** Read-only live view of one plugin storage scope. */
47
+ export interface ReadonlyKVStore {
48
+ /** Reads a detached stored value or the caller-provided default. */
49
+ get<T = unknown>(key: string, defaultValue?: T): T;
50
+ /** Reports whether the scope currently contains an explicit key. */
51
+ has(key: string): boolean;
52
+ /** Returns the current key names as a detached array. */
53
+ keys(): string[];
54
+ }
55
+ /** Exact digital-mode text that a plugin wants the Host encoder to validate. */
56
+ export interface DigitalMessagePreflightRequest {
57
+ /** FT8 or FT4 encoder to use for validation. */
58
+ mode: 'FT8' | 'FT4';
59
+ /** Operator-visible message text before Host normalization and encoding. */
60
+ text: string;
61
+ }
62
+ /** Detached result of validating one message without producing audio or transmitting. */
63
+ export interface DigitalMessagePreflightResult {
64
+ /** Whether the Host encoder accepts the normalized text exactly. */
65
+ encodable: boolean;
66
+ /** Normalized text that was submitted to the encoder. */
67
+ requestedText: string;
68
+ /** Exact text recovered from the encoded payload when encoding succeeded. */
69
+ transmittedText?: string;
70
+ /** Stable reason explaining why exact encoding was rejected. */
71
+ reason?: 'empty' | 'encoder_changed_text' | 'encode_failed';
72
+ /** Sanitized encoder diagnostic intended for plugin logs. */
73
+ error?: string;
74
+ }
75
+ /** Read-only digital-mode validation; no audio or encoder handle is exposed. */
76
+ export interface DigitalMessagePreflight {
77
+ /** Validates and round-trips one FT8/FT4 message through the Host encoder. */
78
+ check(request: DigitalMessagePreflightRequest): Promise<DigitalMessagePreflightResult>;
79
+ }
37
80
  /**
38
81
  * Structured logger dedicated to a plugin instance.
39
82
  *
@@ -70,40 +113,134 @@ export interface PluginTimers {
70
113
  * Remote UDP endpoint metadata for datagrams received by plugin-owned sockets.
71
114
  */
72
115
  export interface PluginUdpRemoteInfo {
116
+ /** Source IP address reported by the UDP socket. */
73
117
  address: string;
118
+ /** Source UDP port. */
74
119
  port: number;
120
+ /** Address family reported by Node.js, typically `IPv4` or `IPv6`. */
75
121
  family: string;
122
+ /** Datagram size in bytes. */
76
123
  size: number;
77
124
  }
125
+ /** Local endpoint used when binding a plugin-owned UDP socket. */
78
126
  export interface PluginUdpBindOptions {
127
+ /** Local interface/address. Omit to use the Host default. */
79
128
  host?: string;
129
+ /** Local port. Omit or use `0` to let the operating system choose one. */
80
130
  port?: number;
81
131
  }
132
+ /** Options applied when the Host creates a plugin-owned UDP socket. */
82
133
  export interface PluginUdpSocketOptions {
134
+ /** IP family. Defaults to `udp4`. */
83
135
  type?: 'udp4' | 'udp6';
136
+ /** Whether multiple sockets may reuse the local address. */
84
137
  reuseAddr?: boolean;
138
+ /** Whether the socket may send IPv4 broadcast datagrams. */
85
139
  broadcast?: boolean;
140
+ /** Multicast time-to-live applied to outbound multicast packets. */
86
141
  multicastTtl?: number;
87
142
  }
143
+ /**
144
+ * Host-owned UDP socket capability.
145
+ *
146
+ * The handle may be stored by the plugin, but its methods are invocation
147
+ * guarded. Close it during unload when possible; Host cleanup also closes all
148
+ * sockets owned by the plugin instance.
149
+ */
88
150
  export interface PluginUdpSocket {
151
+ /** Binds the socket and resolves when it is ready to receive datagrams. */
89
152
  bind(options?: PluginUdpBindOptions): Promise<void>;
153
+ /** Sends one datagram to the exact remote host and port. */
90
154
  send(data: Uint8Array | string, port: number, host: string): Promise<void>;
155
+ /** Registers the callback used for received datagrams. */
91
156
  onMessage(handler: (data: Uint8Array, remote: PluginUdpRemoteInfo) => void | Promise<void>): void;
157
+ /** Registers the callback used for socket-level errors. */
92
158
  onError(handler: (error: Error) => void): void;
159
+ /** Closes the socket. Calling it again is safe. */
93
160
  close(): Promise<void>;
94
161
  }
162
+ /** Factory and bulk-cleanup surface for UDP sockets owned by one plugin instance. */
95
163
  export interface PluginUdpControl {
164
+ /** Creates an unbound socket with the requested options. */
96
165
  createSocket(options?: PluginUdpSocketOptions): PluginUdpSocket;
166
+ /** Closes every UDP socket created through this control. */
97
167
  closeAll(): Promise<void>;
98
168
  }
169
+ /** Network capability exposed when the plugin declares `network`. */
99
170
  export interface PluginNetworkControl {
171
+ /** UDP socket factory. HTTP requests use the sibling `ctx.fetch` capability. */
100
172
  readonly udp: PluginUdpControl;
101
173
  }
102
174
  /**
103
- * Control surface for the active operator instance.
175
+ * A message delivered through the plugin-to-plugin event bus.
104
176
  *
105
- * This interface lets plugins inspect operator state and request host-managed
106
- * actions such as starting automation, calling a target or notifying the UI.
177
+ * Every message carries metadata about its publisher so subscribers can
178
+ * apply routing or filtering logic based on the source plugin.
179
+ */
180
+ export interface PluginEventBusMessage {
181
+ /** The topic this message was published to. */
182
+ topic: string;
183
+ /**
184
+ * Structured-clone-compatible payload. The host does not interpret its
185
+ * business schema, but delivers an independent value to each subscriber.
186
+ */
187
+ payload: unknown;
188
+ /** Epoch milliseconds when the host dispatched the message. */
189
+ timestamp: number;
190
+ /** Identity of the plugin instance that published this message. */
191
+ publisher: {
192
+ /** Name of the publishing plugin (from its `PluginDefinition.name`). */
193
+ pluginName: string;
194
+ /** Whether the publisher is a global or per-operator instance. */
195
+ instanceScope: 'operator' | 'global';
196
+ /** Operator ID when the publisher is an operator-scoped instance. */
197
+ operatorId?: string;
198
+ };
199
+ }
200
+ /**
201
+ * Permission-gated pub/sub bus for in-process plugin-to-plugin communication.
202
+ *
203
+ * Topics are plain strings shared across all plugin instances within the same
204
+ * host process. Handlers are started synchronously in subscription order.
205
+ * Async handlers run independently; their errors are captured and logged by
206
+ * the host rather than propagated to the publisher.
207
+ *
208
+ * **Lifecycle**: the host automatically removes all subscriptions owned by a
209
+ * plugin instance when it unloads. Individual subscriptions can be cancelled
210
+ * earlier by calling the function returned from {@link subscribe}.
211
+ *
212
+ * **Topic naming**: use dot-separated, plugin-prefixed names to avoid
213
+ * collisions — for example `my-plugin.status.changed` or
214
+ * `callsign-filter.match.found`.
215
+ */
216
+ export interface PluginEventBus {
217
+ /**
218
+ * Publishes a message to all current subscribers of the given topic.
219
+ *
220
+ * This is a fire-and-forget operation. The host guarantees that subscriber
221
+ * exceptions never propagate back to the caller. The call itself throws
222
+ * synchronously when the payload is not structured-clone compatible or
223
+ * contains a Host capability.
224
+ *
225
+ * @param topic - Exact topic string to publish to.
226
+ * @param payload - Optional structured-clone-compatible data. Keep payloads reasonably small.
227
+ */
228
+ publish(topic: string, payload?: unknown): void;
229
+ /**
230
+ * Subscribes to messages on the given topic.
231
+ *
232
+ * The same handler function instance will only be added once per topic.
233
+ * Different closures with identical logic are treated as distinct subscribers.
234
+ *
235
+ * @param topic - Exact topic string to listen on.
236
+ * @param handler - Callback invoked for each matching message. May return a
237
+ * `Promise`; the host catches rejections and logs them.
238
+ * @returns An unsubscribe function. Calling it more than once is a no-op.
239
+ */
240
+ subscribe(topic: string, handler: (message: PluginEventBusMessage) => void | Promise<void>): () => void;
241
+ }
242
+ /**
243
+ * Read-only summary of another operator in the same Host.
107
244
  */
108
245
  export interface OtherOperatorSnapshot {
109
246
  /** Unique operator identifier used by the host. */
@@ -120,8 +257,15 @@ export interface OtherOperatorSnapshot {
120
257
  readonly isTransmitting: boolean;
121
258
  /** Current transmit cycle selection where `0` is even and `1` is odd. */
122
259
  readonly transmitCycles: number[];
260
+ /** Current automation runtime snapshot when available. */
261
+ readonly automation?: StrategyRuntimeSnapshot | null;
123
262
  }
124
- export interface OperatorControl {
263
+ /**
264
+ * Read-only state and query surface for the current operator-scoped plugin
265
+ * instance. Mutations are submitted through `ctx.operatorCommands` when the
266
+ * plugin declares `operator:transmit-control`.
267
+ */
268
+ export interface OperatorSnapshot {
125
269
  /** Unique operator identifier used by the host. */
126
270
  readonly id: string;
127
271
  /** Whether this operator is currently transmitting or otherwise armed. */
@@ -136,43 +280,12 @@ export interface OperatorControl {
136
280
  readonly mode: ModeDescriptor;
137
281
  /** Current transmit cycle selection where `0` is even and `1` is odd. */
138
282
  readonly transmitCycles: number[];
283
+ /** Host-admitted stream ceiling after radio-frequency and operator safety policy. */
284
+ readonly maxConcurrentStreams: number;
139
285
  /** Current automation runtime snapshot visible to the operator UI. */
140
286
  readonly automation: StrategyRuntimeSnapshot | null;
141
287
  /** Returns read-only snapshots for operators other than the current instance. */
142
288
  getOtherOperators(): OtherOperatorSnapshot[];
143
- /** Enables transmission/automation for the current operator. */
144
- startTransmitting(): void;
145
- /** Disables transmission/automation for the current operator. */
146
- stopTransmitting(): void;
147
- /**
148
- * Requests that the operator call the specified target station.
149
- *
150
- * Passing `lastMessage` helps the host preserve the triggering context.
151
- */
152
- call(callsign: string, lastMessage?: {
153
- message: FrameMessage;
154
- slotInfo: SlotInfo;
155
- }): void;
156
- /**
157
- * Requests host-managed reply behavior for a decoded message.
158
- *
159
- * This is equivalent to an operator selecting a decode in the RX view while
160
- * keeping the API independent from any specific UDP/control protocol.
161
- */
162
- replyToDecode(decode: {
163
- callsign: string;
164
- lastMessage: {
165
- message: FrameMessage;
166
- slotInfo: SlotInfo;
167
- };
168
- modifiers?: number;
169
- }): void;
170
- /**
171
- * Updates the operator's transmit cycle preference.
172
- *
173
- * Pass a single value or an array to support alternating or multi-cycle modes.
174
- */
175
- setTransmitCycles(cycles: number | number[]): void;
176
289
  /**
177
290
  * Checks whether this operator has previously worked the given callsign.
178
291
  */
@@ -184,40 +297,81 @@ export interface OperatorControl {
184
297
  * working the target callsign.
185
298
  */
186
299
  isTargetBeingWorkedByOthers(targetCallsign: string): boolean;
187
- /** Clears host-managed decoded-message views when available. */
188
- clearDecodes(window?: number): void;
189
- /** Stops current transmission/automation. */
190
- haltTransmission(options?: {
191
- autoOnly?: boolean;
192
- }): void;
193
- /** Stores the current free-text message without necessarily transmitting it. */
194
- setFreeText(text: string): void;
195
- /** Requests transmission of free text. If text is provided it is stored first. */
196
- sendFreeText(text?: string): void;
197
- /** Applies a temporary session grid/location override when the host supports it. */
198
- setTemporaryLocation(location: string): void;
199
- /** Requests callsign highlighting in host decode views when available. */
200
- highlightCallsign(rule: {
201
- callsign: string;
202
- background?: string | null;
203
- foreground?: string | null;
204
- lastOnly?: boolean;
205
- }): void;
206
- /**
207
- * Records a completed QSO through the host logbook pipeline.
208
- */
209
- recordQSO(record: QSORecord): void;
210
- /**
211
- * Pushes updated slot text content to the frontend operator view.
212
- */
213
- notifySlotsUpdated(slots: OperatorSlots): void;
214
- /**
215
- * Pushes a strategy state change notification to the frontend operator view.
216
- */
217
- notifyStateChanged(state: string): void;
218
300
  }
219
301
  /**
220
- * Read/write access to radio state that is safe for plugins.
302
+ * Declarative operator mutations accepted by the host transmission framework.
303
+ *
304
+ * The command set deliberately contains no PTT, audio, mixer, encoder, raw
305
+ * transmit or emergency-stop primitive. Plugins can request product actions;
306
+ * only the host coordinators may translate them into a physical RF lifecycle.
307
+ */
308
+ export type PluginOperatorCommand = {
309
+ type: 'start-automation';
310
+ } | {
311
+ type: 'stop-automation';
312
+ } | {
313
+ type: 'request-call';
314
+ callsign: string;
315
+ lastMessage?: {
316
+ message: FrameMessage;
317
+ slotInfo: SlotInfo;
318
+ };
319
+ } | {
320
+ type: 'reply-to-decode';
321
+ callsign: string;
322
+ lastMessage: {
323
+ message: FrameMessage;
324
+ slotInfo: SlotInfo;
325
+ };
326
+ modifiers?: number;
327
+ } | {
328
+ type: 'set-transmit-cycles';
329
+ cycles: number | number[];
330
+ } | {
331
+ type: 'remove-contribution';
332
+ } | {
333
+ type: 'clear-decodes';
334
+ window?: number;
335
+ } | {
336
+ type: 'set-free-text';
337
+ text: string;
338
+ } | {
339
+ type: 'send-free-text';
340
+ text?: string;
341
+ } | {
342
+ type: 'set-temporary-location';
343
+ location: string;
344
+ } | {
345
+ type: 'highlight-callsign';
346
+ callsign: string;
347
+ background?: string | null;
348
+ foreground?: string | null;
349
+ lastOnly?: boolean;
350
+ };
351
+ /** Settlement returned after the Host accepts an operator command. */
352
+ export interface PluginOperatorCommandResult {
353
+ /** Host command epoch allocated before any asynchronous work begins. */
354
+ epoch: number;
355
+ /** `superseded` means a newer host command revoked this request. */
356
+ outcome: 'completed' | 'superseded';
357
+ }
358
+ /**
359
+ * Capability-scoped command port for plugins with
360
+ * `operator:transmit-control` and API v2.
361
+ *
362
+ * The property is omitted from contexts without that capability. Every submit
363
+ * is invocation-guarded and enters the host's per-operator intent lane.
364
+ */
365
+ export interface OperatorCommandPort {
366
+ /**
367
+ * Submits one high-level operator command through the Host intent lane.
368
+ * Rejects when the invocation expired, the plugin safety gate is disabled,
369
+ * or the current physical lifecycle cannot accept the command.
370
+ */
371
+ submit(command: PluginOperatorCommand): Promise<PluginOperatorCommandResult>;
372
+ }
373
+ /**
374
+ * Read-only operating-mode projection that is safe for plugins.
221
375
  */
222
376
  export interface RadioOperatingMode {
223
377
  /**
@@ -241,7 +395,8 @@ export interface RadioOperatingMode {
241
395
  */
242
396
  readonly descriptor: ModeDescriptor;
243
397
  }
244
- export interface RadioControl {
398
+ /** Read-only frequency, band, mode and connection state for the active radio. */
399
+ export interface RadioView {
245
400
  /** Current tuned radio frequency in Hz. */
246
401
  readonly frequency: number;
247
402
  /** Human-readable current band label, for example `20m`. */
@@ -250,31 +405,54 @@ export interface RadioControl {
250
405
  readonly mode: RadioOperatingMode;
251
406
  /** Whether the radio transport is currently connected. */
252
407
  readonly isConnected: boolean;
253
- /** Negotiated radio capability controls. Requires radio plugin permissions. */
254
- readonly capabilities: RadioCapabilitiesControl;
255
- /** Physical radio power controls. Requires radio plugin permissions. */
256
- readonly power: RadioPowerControl;
257
- /**
258
- * Requests a frequency change.
259
- *
260
- * The host remains responsible for serializing hardware access and enforcing
261
- * any safety or capability constraints.
262
- */
263
- setFrequency(freq: number): Promise<void>;
408
+ /** Whether the active radio is a Host-provided simulation rather than physical RF. */
409
+ readonly isSimulation: boolean;
264
410
  }
265
411
  /**
266
412
  * Access to the host-managed radio capability negotiation system.
267
413
  */
268
- export interface RadioCapabilitiesControl {
269
- /** Returns the current capability descriptor/state snapshot. Requires `radio:read`. */
414
+ export interface RadioCapabilitiesView {
415
+ /** Returns the current capability descriptor/state snapshot. */
270
416
  getSnapshot(): CapabilityList;
271
- /** Returns a single capability state from the current snapshot, or null. Requires `radio:read`. */
417
+ /** Returns a single capability state from the current snapshot, or null. */
272
418
  getState(id: string): CapabilityState | null;
273
- /** Refreshes readable capability values and returns the updated snapshot. Requires `radio:read`. */
419
+ /** Refreshes readable capability values and returns the updated snapshot. */
274
420
  refresh(): Promise<CapabilityList>;
275
- /** Writes a capability value or triggers an action capability. Requires `radio:control`. */
276
- write(payload: WriteCapabilityPayload): Promise<void>;
277
421
  }
422
+ /** Declarative radio mutations accepted by the host radio coordinator. */
423
+ export type PluginRadioCommand = {
424
+ type: 'set-frequency';
425
+ frequency: number;
426
+ } | {
427
+ /** Atomically changes band and optionally starts the radio's tuner while RF is idle. */
428
+ type: 'switch-band';
429
+ frequency: number;
430
+ autoTune?: boolean;
431
+ };
432
+ /**
433
+ * Capability-scoped radio command port.
434
+ *
435
+ * This port exists only for plugins with `radio:control`. It deliberately does
436
+ * not expose a radio connection, PTT primitive, mode switch, audio output or
437
+ * any other physical device object.
438
+ */
439
+ export interface RadioCommandPort {
440
+ /** Submits a frequency/band command after Host physical-idle validation. */
441
+ submit(command: PluginRadioCommand): Promise<void>;
442
+ }
443
+ /** Explicit tuner operations; no arbitrary capability identifier is accepted. */
444
+ export type PluginRadioTunerCommand = {
445
+ type: 'set-enabled';
446
+ enabled: boolean;
447
+ } | {
448
+ type: 'start-manual-tune';
449
+ };
450
+ /** Capability-scoped tuner command port for `radio:tuner-control` plugins. */
451
+ export interface RadioTunerCommandPort {
452
+ /** Submits one explicit tuner operation after Host safety validation. */
453
+ submit(command: PluginRadioTunerCommand): Promise<void>;
454
+ }
455
+ /** Optional target profile and startup behavior for a radio power command. */
278
456
  export interface RadioPowerSetOptions {
279
457
  /** Profile to target. Defaults to the active profile. */
280
458
  profileId?: string;
@@ -284,13 +462,25 @@ export interface RadioPowerSetOptions {
284
462
  /**
285
463
  * Access to physical radio power management.
286
464
  */
287
- export interface RadioPowerControl {
288
- /** Returns power support information for the active or specified profile. Requires `radio:read`. */
465
+ export interface RadioPowerView {
466
+ /** Returns power support information for the active or specified profile. */
289
467
  getSupport(profileId?: string): Promise<RadioPowerSupportInfo>;
290
- /** Returns the last known power transition state for the active or specified profile. Requires `radio:read`. */
468
+ /** Returns the last known power transition state for the active or specified profile. */
291
469
  getState(profileId?: string): RadioPowerStateEvent | null;
292
- /** Requests a physical power transition. Requires `radio:power`. */
293
- set(state: RadioPowerTarget, options?: RadioPowerSetOptions): Promise<RadioPowerResponse>;
470
+ }
471
+ /** Declarative power-state transition accepted by `ctx.radioPowerCommands`. */
472
+ export type PluginRadioPowerCommand = {
473
+ /** Command discriminator. */
474
+ type: 'set-power';
475
+ /** Requested physical/controller power target. */
476
+ state: RadioPowerTarget;
477
+ /** Optional profile selection and automatic engine startup behavior. */
478
+ options?: RadioPowerSetOptions;
479
+ };
480
+ /** Capability-scoped physical power command port for `radio:power` plugins. */
481
+ export interface RadioPowerCommandPort {
482
+ /** Requests a power transition and resolves with the Host's final state. */
483
+ submit(command: PluginRadioPowerCommand): Promise<RadioPowerResponse>;
294
484
  }
295
485
  /**
296
486
  * Filter criteria for querying QSO records from the logbook.
@@ -333,35 +523,74 @@ export interface QSOQueryFilter {
333
523
  /**
334
524
  * Callsign-bound view over a single logbook.
335
525
  *
336
- * The host resolves the concrete logbook lazily on each operation, which keeps
337
- * the handle valid even if the underlying logbook is created or reloaded later.
526
+ * The host resolves an already registered concrete logbook on each operation,
527
+ * which keeps the handle valid across reloads without implicitly creating data.
338
528
  */
339
- export interface CallsignLogbookAccess {
529
+ export interface CallsignLogbookReadAccess {
340
530
  /** Normalized callsign that scopes this accessor. */
341
531
  readonly callsign: string;
342
- /** Returns the resolved logbook id, or null when no logbook exists yet. */
532
+ /** Returns the resolved logbook id, or null when no logbook is registered. */
343
533
  getLogBookId(): Promise<string | null>;
534
+ /** Waits until the Host has finished opening this logbook and it is readable. */
535
+ awaitReady(options?: {
536
+ timeoutMs?: number;
537
+ }): Promise<void>;
344
538
  /** Queries QSO records matching the given filter. */
345
539
  queryQSOs(filter: QSOQueryFilter): Promise<import('@tx5dr/contracts').QSORecord[]>;
540
+ /** Reads records and their content revision from one consistent logbook snapshot. */
541
+ readQsoSnapshot(filter?: QSOQueryFilter): Promise<LogbookQsoSnapshot>;
346
542
  /** Counts QSO records matching the given filter. */
347
543
  countQSOs(filter?: QSOQueryFilter): Promise<number>;
348
- /** Adds a new QSO record to this callsign's logbook. */
349
- addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<void>;
350
- /** Updates partial fields of an existing QSO record. */
351
- updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<void>;
352
544
  /** Returns current statistics for this callsign's logbook. */
353
545
  getStatistics(): Promise<import('@tx5dr/contracts').LogBookStatistics | null>;
546
+ }
547
+ /** Durable mutation operations scoped to one normalized station callsign. */
548
+ export interface CallsignLogbookCommandPort {
549
+ /** Normalized callsign that scopes this accessor. */
550
+ readonly callsign: string;
551
+ /** Adds a QSO and resolves with the final record after durable commit. */
552
+ addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<import('@tx5dr/contracts').QSORecord>;
553
+ /** Updates a QSO and resolves with the final record after durable commit. */
554
+ updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<import('@tx5dr/contracts').QSORecord>;
555
+ /** Applies a revision-guarded set of QSO additions and updates as one durable transaction. */
556
+ applyQsoBatch(mutations: readonly LogbookBatchMutation[], options: {
557
+ expectedRevision: string;
558
+ }): Promise<LogbookBatchResult>;
354
559
  /** Notifies the frontend that this callsign's logbook changed. */
355
560
  notifyUpdated(operatorId?: string): Promise<void>;
356
561
  }
357
- /**
358
- * Full logbook access for plugins.
359
- *
360
- * Extends the original read-only helpers with query, write and notification
361
- * capabilities so that sync providers can self-orchestrate their entire flow
362
- * without host-side special handling.
363
- */
364
- export interface LogbookAccess {
562
+ /** Combined read/write callsign-bound logbook capability. */
563
+ export interface CallsignLogbookAccess extends CallsignLogbookReadAccess, CallsignLogbookCommandPort {
564
+ }
565
+ /** Stable descriptor for one Host-managed, plugin-owned logbook session. */
566
+ export interface PluginLogbookSessionDescriptor {
567
+ /** Stable key within the owning plugin and station callsign. */
568
+ sessionKey: string;
569
+ /** Station callsign whose QSOs belong to this session. */
570
+ stationCallsign: string;
571
+ /** User-facing session title. */
572
+ title: string;
573
+ /** Durable by default; runtime sessions are deleted when explicitly destroyed or the Host exits. */
574
+ retention?: 'durable' | 'runtime';
575
+ }
576
+ /** Read/write access to one plugin-owned logbook session. */
577
+ export interface PluginLogbookSessionAccess extends CallsignLogbookAccess {
578
+ /** Opaque Host-issued session logbook identifier. */
579
+ readonly id: string;
580
+ /** User-facing title supplied when the session was opened. */
581
+ readonly title: string;
582
+ /** Destroys a runtime-retained session. Durable sessions reject this operation. */
583
+ destroy(): Promise<void>;
584
+ }
585
+ /** Host-arbitrated access to logbook sessions owned by the current plugin. */
586
+ export interface PluginLogbookSessions {
587
+ /** Opens or reuses a durable session without changing the station's primary logbook. */
588
+ open(descriptor: PluginLogbookSessionDescriptor): Promise<PluginLogbookSessionAccess>;
589
+ /** Destroys an existing runtime-retained session owned by this plugin and operator. */
590
+ destroy(sessionKey: string): Promise<void>;
591
+ }
592
+ /** Read-only worked-status and QSO query capability for `logbook:read`. */
593
+ export interface LogbookReadAccess {
365
594
  /** Checks whether the callsign has already been worked. */
366
595
  hasWorked(callsign: string, options?: {
367
596
  anyBand?: boolean;
@@ -372,16 +601,32 @@ export interface LogbookAccess {
372
601
  hasWorkedGrid(grid: string): Promise<boolean>;
373
602
  /** Queries QSO records matching the given filter. */
374
603
  queryQSOs(filter: QSOQueryFilter): Promise<import('@tx5dr/contracts').QSORecord[]>;
604
+ /** Reads records and their content revision from one consistent logbook snapshot. */
605
+ readQsoSnapshot(filter?: QSOQueryFilter): Promise<LogbookQsoSnapshot>;
375
606
  /** Counts QSO records matching the given filter. */
376
607
  countQSOs(filter?: QSOQueryFilter): Promise<number>;
377
- /** Returns a callsign-bound accessor suitable for global plugin instances. */
378
- forCallsign(callsign: string): CallsignLogbookAccess;
379
- /** Adds a new QSO record. Deduplication is the caller's responsibility. */
380
- addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<void>;
381
- /** Updates partial fields of an existing QSO record (e.g. QSL status). */
382
- updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<void>;
608
+ /** Returns a read-only callsign-bound accessor suitable for global plugin instances. */
609
+ forCallsign(callsign: string): CallsignLogbookReadAccess;
610
+ }
611
+ /** Durable mutation operations exposed by the `logbook:write` permission. */
612
+ export interface LogbookCommandPort {
613
+ /** Adds a QSO and resolves with the final record after durable commit. */
614
+ addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<import('@tx5dr/contracts').QSORecord>;
615
+ /** Updates a QSO and resolves with the final record after durable commit. */
616
+ updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<import('@tx5dr/contracts').QSORecord>;
617
+ /** Applies a revision-guarded set of QSO additions and updates as one durable transaction. */
618
+ applyQsoBatch(mutations: readonly LogbookBatchMutation[], options: {
619
+ expectedRevision: string;
620
+ }): Promise<LogbookBatchResult>;
383
621
  /** Notifies the frontend to refresh logbook data (call after batch writes). */
384
622
  notifyUpdated(): Promise<void>;
623
+ /** Returns a callsign-bound durable mutation port for global plugin instances. */
624
+ forCallsign(callsign: string): CallsignLogbookCommandPort;
625
+ }
626
+ /** @deprecated Prefer capability-specific LogbookReadAccess and LogbookCommandPort. */
627
+ export interface LogbookAccess extends LogbookReadAccess, LogbookCommandPort {
628
+ /** Returns a combined read/write accessor for the requested station callsign. */
629
+ forCallsign(callsign: string): CallsignLogbookAccess;
385
630
  }
386
631
  /**
387
632
  * Optional constraints used when asking the host for a quieter transmit offset.
@@ -431,7 +676,9 @@ export interface BandAccess {
431
676
  * decode environment.
432
677
  *
433
678
  * Returns `null` when the host cannot evaluate the slot or when no suitable
434
- * idle window is found.
679
+ * idle window is found. A successful result also reserves that offset for the
680
+ * current operator and analyzed slot so later operators avoid selecting the
681
+ * same window.
435
682
  */
436
683
  findIdleTransmitFrequency(options?: IdleTransmitFrequencyOptions): number | null;
437
684
  /**
@@ -473,7 +720,8 @@ export interface PanelMeta {
473
720
  */
474
721
  export interface UIBridge {
475
722
  /**
476
- * Publishes new panel data for the given declarative panel id.
723
+ * Publishes a JSON-compatible snapshot for the given declarative panel id.
724
+ * Mutating the caller's object after this call does not alter panel state.
477
725
  */
478
726
  send(panelId: string, data: unknown): void;
479
727
  /**
@@ -492,17 +740,20 @@ export interface UIBridge {
492
740
  * Clears a runtime-owned panel contribution group for this plugin instance.
493
741
  */
494
742
  clearPanelContributions(groupId: string): void;
743
+ /** Requests a fresh operator/runtime projection after plugin-owned state changes. */
744
+ refreshOperatorProjection(): void;
495
745
  /**
496
746
  * Registers a handler for custom messages sent from iframe UI pages via the
497
747
  * `bridge.invoke()` SDK method. The host routes incoming invoke requests to
498
748
  * the handler and sends the return value back to the iframe.
499
749
  *
500
- * Only one handler can be registered per plugin instance. Calling this method
501
- * again replaces the previous handler.
750
+ * A registration with `pageIds` only handles those pages and composes with
751
+ * other page-scoped registrations. Omitting `pageIds` preserves the legacy
752
+ * fallback behavior; a later fallback registration replaces the previous one.
502
753
  */
503
- registerPageHandler(handler: PluginUIHandler): void;
754
+ registerPageHandler(handler: PluginUIHandler, registration?: PluginUIHandlerRegistration): void;
504
755
  /**
505
- * Pushes a custom message to the specific page session.
756
+ * Pushes a JSON-compatible data snapshot to the specific page session.
506
757
  *
507
758
  * Prefer this API whenever the plugin already knows the target session id
508
759
  * (for example from {@link PluginUIRequestContext.pageSessionId} or
@@ -517,7 +768,7 @@ export interface UIBridge {
517
768
  */
518
769
  listActivePageSessions(pageId: string): PluginUIPageSessionInfo[];
519
770
  /**
520
- * Pushes a custom message to an iframe UI page by page id.
771
+ * Pushes a JSON-compatible data snapshot to an iframe UI page by page id.
521
772
  *
522
773
  * This compatibility helper only succeeds when exactly one active session of
523
774
  * the current plugin instance matches the page id. If multiple sessions are
@@ -529,8 +780,9 @@ export interface UIBridge {
529
780
  * Handler for custom messages sent from iframe UI pages.
530
781
  *
531
782
  * Plugins register a handler via `ctx.ui.registerPageHandler()` to receive
532
- * arbitrary invoke requests from their iframe-based UIs. The host acts as a
533
- * transparent router it does not inspect or interpret the action or data.
783
+ * application-defined invoke requests from their iframe-based UIs. The Host
784
+ * does not interpret the business schema, but it enforces the page/session
785
+ * authorization and JSON data boundary in both directions.
534
786
  */
535
787
  export interface PluginUIHandler {
536
788
  /**
@@ -538,49 +790,82 @@ export interface PluginUIHandler {
538
790
  *
539
791
  * @param pageId - The page that sent the message.
540
792
  * @param action - Developer-defined action identifier.
541
- * @param data - Arbitrary payload from the iframe.
793
+ * @param data - JSON-compatible snapshot from the iframe; validate it as
794
+ * untrusted input before use.
542
795
  * @param requestContext - Host-authenticated page context, including any
543
796
  * bound resource for this page session.
544
- * @returns The response value sent back to the iframe.
797
+ * @returns A JSON-compatible response snapshot sent back to the iframe.
545
798
  */
546
799
  onMessage(pageId: string, action: string, data: unknown, requestContext: PluginUIRequestContext): Promise<unknown>;
547
800
  }
801
+ /** Optional routing scope for one iframe page handler registration. */
802
+ export interface PluginUIHandlerRegistration {
803
+ /** Page ids owned by this handler. An explicitly empty list is invalid. */
804
+ pageIds?: readonly string[];
805
+ }
806
+ /** Host-authenticated user identity attached to an iframe invoke request. */
548
807
  export interface PluginUIRequestUser {
808
+ /** Stable token/session identifier; not the raw credential. */
549
809
  readonly tokenId: string;
810
+ /** Effective role at the time the Host authorizes the request. */
550
811
  readonly role: 'viewer' | 'operator' | 'admin';
812
+ /** Operator IDs the current user is allowed to access. */
551
813
  readonly operatorIds: string[];
814
+ /** Fine-grained grants associated with the authenticated user, when present. */
552
815
  readonly permissionGrants?: PermissionGrant[];
553
816
  }
817
+ /** Resource identity resolved and authorized from the page descriptor binding. */
554
818
  export interface PluginUIBoundResource {
819
+ /** Kind declared by `resourceBinding`. */
555
820
  readonly kind: 'callsign' | 'operator';
821
+ /** Normalized callsign or authorized operator ID. */
556
822
  readonly value: string;
557
823
  }
824
+ /** Plugin instance selected by the Host for this page request. */
558
825
  export type PluginUIInstanceTarget = {
559
826
  readonly kind: 'global';
560
827
  } | {
561
828
  readonly kind: 'operator';
562
829
  readonly operatorId: string;
563
830
  };
831
+ /** Read-only identity of one active plugin iframe page session. */
564
832
  export interface PluginUIPageSessionInfo {
833
+ /** Unique ID used for exact session pushes. */
565
834
  readonly sessionId: string;
835
+ /** `PluginDefinition.ui.pages` entry rendered by this session. */
566
836
  readonly pageId: string;
837
+ /** Host-authorized resource binding, when the page declares one. */
567
838
  readonly resource?: PluginUIBoundResource;
568
839
  }
840
+ /** Page-session identity plus an exact push channel back to that iframe. */
569
841
  export interface PluginUIPageContext extends PluginUIPageSessionInfo {
842
+ /** Sends a JSON-compatible snapshot to this exact page session. */
570
843
  push(action: string, data?: unknown): void;
571
844
  }
845
+ /**
846
+ * Host-authenticated context passed to an iframe page handler.
847
+ *
848
+ * Treat `data` from the iframe as untrusted input. Use this context, rather
849
+ * than caller-supplied IDs, for authorization and storage scoping.
850
+ */
572
851
  export interface PluginUIRequestContext {
852
+ /** Same exact page session identifier exposed as `page.sessionId`. */
573
853
  readonly pageSessionId: string;
854
+ /** User identity authorized by the Host for this request. */
574
855
  readonly user: PluginUIRequestUser;
856
+ /** Bound callsign/operator, when required by the page descriptor. */
575
857
  readonly resource?: PluginUIBoundResource;
858
+ /** Global or operator plugin instance receiving the request. */
576
859
  readonly instanceTarget: PluginUIInstanceTarget;
860
+ /** Exact page session/push capability, valid only during the current handler invocation. */
577
861
  readonly page: PluginUIPageContext;
578
862
  /**
579
863
  * Page-scoped file storage shared with iframe `tx5dr.file*()` calls.
580
864
  *
581
865
  * Use this in `registerPageHandler()` handlers to read files uploaded by the
582
866
  * current iframe page session without reconstructing host-internal scope
583
- * paths.
867
+ * paths. Both `page` and `files` are exact-invocation capabilities: do not
868
+ * retain and invoke them after the current `onMessage()` promise settles.
584
869
  */
585
870
  readonly files: PluginFileStore;
586
871
  }
@@ -591,9 +876,9 @@ export interface PluginUIRequestContext {
591
876
  * traversal outside the sandbox is rejected by the host.
592
877
  */
593
878
  export interface PluginFileStore {
594
- /** Writes (or overwrites) a file at the given path. */
879
+ /** Writes a copy of the Buffer, creating or replacing the file. */
595
880
  write(path: string, data: Buffer): Promise<void>;
596
- /** Reads a file. Returns `null` when the path does not exist. */
881
+ /** Reads a file into a new Buffer. Returns `null` when the path does not exist. */
597
882
  read(path: string): Promise<Buffer | null>;
598
883
  /** Deletes a file. Returns `true` if the file existed and was removed. */
599
884
  delete(path: string): Promise<boolean>;