@tx5dr/plugin-api 1.7.11 → 2.0.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 (51) hide show
  1. package/README.md +274 -17
  2. package/dist/__tests__/adif.test.js +40 -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 +235 -14
  9. package/dist/__tests__/testing-utils.test.js.map +1 -1
  10. package/dist/capabilities.d.ts +37 -0
  11. package/dist/capabilities.d.ts.map +1 -0
  12. package/dist/capabilities.js +48 -0
  13. package/dist/capabilities.js.map +1 -0
  14. package/dist/context.d.ts +95 -56
  15. package/dist/context.d.ts.map +1 -1
  16. package/dist/definition.d.ts +73 -20
  17. package/dist/definition.d.ts.map +1 -1
  18. package/dist/definition.js +8 -1
  19. package/dist/definition.js.map +1 -1
  20. package/dist/helpers.d.ts +340 -129
  21. package/dist/helpers.d.ts.map +1 -1
  22. package/dist/hooks.d.ts +54 -34
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/host-dependencies.d.ts +101 -0
  25. package/dist/host-dependencies.d.ts.map +1 -1
  26. package/dist/index.d.ts +9 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/runtime.d.ts +164 -9
  31. package/dist/runtime.d.ts.map +1 -1
  32. package/dist/runtime.js +9 -1
  33. package/dist/runtime.js.map +1 -1
  34. package/dist/settings.d.ts +31 -0
  35. package/dist/settings.d.ts.map +1 -1
  36. package/dist/sync.d.ts +98 -6
  37. package/dist/sync.d.ts.map +1 -1
  38. package/dist/sync.js +6 -0
  39. package/dist/sync.js.map +1 -1
  40. package/dist/testing/index.d.ts +70 -12
  41. package/dist/testing/index.d.ts.map +1 -1
  42. package/dist/testing/index.js +307 -104
  43. package/dist/testing/index.js.map +1 -1
  44. package/dist/utils/adif.d.ts.map +1 -1
  45. package/dist/utils/adif.js +6 -2
  46. package/dist/utils/adif.js.map +1 -1
  47. package/dist/utils/qso-text-fields.d.ts +7 -1
  48. package/dist/utils/qso-text-fields.d.ts.map +1 -1
  49. package/dist/utils/qso-text-fields.js +74 -6
  50. package/dist/utils/qso-text-fields.js.map +1 -1
  51. package/package.json +4 -4
package/dist/helpers.d.ts CHANGED
@@ -1,4 +1,4 @@
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
3
  /**
4
4
  * Simple persistent key-value store exposed to plugins.
@@ -10,11 +10,17 @@ export interface KVStore {
10
10
  /**
11
11
  * Reads a stored value.
12
12
  *
13
- * When the key is missing, the provided `defaultValue` is returned instead.
13
+ * Stored values are returned by value, so mutating the result does not update
14
+ * persistence until {@link set} is called. When the key is missing, the
15
+ * caller-owned `defaultValue` is returned unchanged.
14
16
  */
15
17
  get<T = unknown>(key: string, defaultValue?: T): T;
16
18
  /**
17
- * Persists a value under the given key.
19
+ * Persists a JSON-compatible snapshot under the given key.
20
+ *
21
+ * `undefined` follows JSON object semantics and removes the key. Cycles,
22
+ * BigInt values, functions and Host capabilities are rejected with
23
+ * `PLUGIN_DATA_NOT_SERIALIZABLE`.
18
24
  */
19
25
  set(key: string, value: unknown): void;
20
26
  /**
@@ -22,7 +28,7 @@ export interface KVStore {
22
28
  */
23
29
  delete(key: string): void;
24
30
  /**
25
- * Returns a shallow snapshot of all stored entries in this scope.
31
+ * Returns an independent snapshot of all stored entries in this scope.
26
32
  */
27
33
  getAll(): Record<string, unknown>;
28
34
  /**
@@ -70,42 +76,159 @@ export interface PluginTimers {
70
76
  * Remote UDP endpoint metadata for datagrams received by plugin-owned sockets.
71
77
  */
72
78
  export interface PluginUdpRemoteInfo {
79
+ /** Source IP address reported by the UDP socket. */
73
80
  address: string;
81
+ /** Source UDP port. */
74
82
  port: number;
83
+ /** Address family reported by Node.js, typically `IPv4` or `IPv6`. */
75
84
  family: string;
85
+ /** Datagram size in bytes. */
76
86
  size: number;
77
87
  }
88
+ /** Local endpoint used when binding a plugin-owned UDP socket. */
78
89
  export interface PluginUdpBindOptions {
90
+ /** Local interface/address. Omit to use the Host default. */
79
91
  host?: string;
92
+ /** Local port. Omit or use `0` to let the operating system choose one. */
80
93
  port?: number;
81
94
  }
95
+ /** Options applied when the Host creates a plugin-owned UDP socket. */
82
96
  export interface PluginUdpSocketOptions {
97
+ /** IP family. Defaults to `udp4`. */
83
98
  type?: 'udp4' | 'udp6';
99
+ /** Whether multiple sockets may reuse the local address. */
84
100
  reuseAddr?: boolean;
101
+ /** Whether the socket may send IPv4 broadcast datagrams. */
85
102
  broadcast?: boolean;
103
+ /** Multicast time-to-live applied to outbound multicast packets. */
86
104
  multicastTtl?: number;
87
105
  }
106
+ /**
107
+ * Host-owned UDP socket capability.
108
+ *
109
+ * The handle may be stored by the plugin, but its methods are invocation
110
+ * guarded. Close it during unload when possible; Host cleanup also closes all
111
+ * sockets owned by the plugin instance.
112
+ */
88
113
  export interface PluginUdpSocket {
114
+ /** Binds the socket and resolves when it is ready to receive datagrams. */
89
115
  bind(options?: PluginUdpBindOptions): Promise<void>;
116
+ /** Sends one datagram to the exact remote host and port. */
90
117
  send(data: Uint8Array | string, port: number, host: string): Promise<void>;
118
+ /** Registers the callback used for received datagrams. */
91
119
  onMessage(handler: (data: Uint8Array, remote: PluginUdpRemoteInfo) => void | Promise<void>): void;
120
+ /** Registers the callback used for socket-level errors. */
92
121
  onError(handler: (error: Error) => void): void;
122
+ /** Closes the socket. Calling it again is safe. */
93
123
  close(): Promise<void>;
94
124
  }
125
+ /** Factory and bulk-cleanup surface for UDP sockets owned by one plugin instance. */
95
126
  export interface PluginUdpControl {
127
+ /** Creates an unbound socket with the requested options. */
96
128
  createSocket(options?: PluginUdpSocketOptions): PluginUdpSocket;
129
+ /** Closes every UDP socket created through this control. */
97
130
  closeAll(): Promise<void>;
98
131
  }
132
+ /** Network capability exposed when the plugin declares `network`. */
99
133
  export interface PluginNetworkControl {
134
+ /** UDP socket factory. HTTP requests use the sibling `ctx.fetch` capability. */
100
135
  readonly udp: PluginUdpControl;
101
136
  }
102
137
  /**
103
- * Control surface for the active operator instance.
138
+ * A message delivered through the plugin-to-plugin event bus.
104
139
  *
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.
140
+ * Every message carries metadata about its publisher so subscribers can
141
+ * apply routing or filtering logic based on the source plugin.
142
+ */
143
+ export interface PluginEventBusMessage {
144
+ /** The topic this message was published to. */
145
+ topic: string;
146
+ /**
147
+ * Structured-clone-compatible payload. The host does not interpret its
148
+ * business schema, but delivers an independent value to each subscriber.
149
+ */
150
+ payload: unknown;
151
+ /** Epoch milliseconds when the host dispatched the message. */
152
+ timestamp: number;
153
+ /** Identity of the plugin instance that published this message. */
154
+ publisher: {
155
+ /** Name of the publishing plugin (from its `PluginDefinition.name`). */
156
+ pluginName: string;
157
+ /** Whether the publisher is a global or per-operator instance. */
158
+ instanceScope: 'operator' | 'global';
159
+ /** Operator ID when the publisher is an operator-scoped instance. */
160
+ operatorId?: string;
161
+ };
162
+ }
163
+ /**
164
+ * Permission-gated pub/sub bus for in-process plugin-to-plugin communication.
165
+ *
166
+ * Topics are plain strings shared across all plugin instances within the same
167
+ * host process. Handlers are started synchronously in subscription order.
168
+ * Async handlers run independently; their errors are captured and logged by
169
+ * the host rather than propagated to the publisher.
170
+ *
171
+ * **Lifecycle**: the host automatically removes all subscriptions owned by a
172
+ * plugin instance when it unloads. Individual subscriptions can be cancelled
173
+ * earlier by calling the function returned from {@link subscribe}.
174
+ *
175
+ * **Topic naming**: use dot-separated, plugin-prefixed names to avoid
176
+ * collisions — for example `my-plugin.status.changed` or
177
+ * `callsign-filter.match.found`.
178
+ */
179
+ export interface PluginEventBus {
180
+ /**
181
+ * Publishes a message to all current subscribers of the given topic.
182
+ *
183
+ * This is a fire-and-forget operation. The host guarantees that subscriber
184
+ * exceptions never propagate back to the caller. The call itself throws
185
+ * synchronously when the payload is not structured-clone compatible or
186
+ * contains a Host capability.
187
+ *
188
+ * @param topic - Exact topic string to publish to.
189
+ * @param payload - Optional structured-clone-compatible data. Keep payloads reasonably small.
190
+ */
191
+ publish(topic: string, payload?: unknown): void;
192
+ /**
193
+ * Subscribes to messages on the given topic.
194
+ *
195
+ * The same handler function instance will only be added once per topic.
196
+ * Different closures with identical logic are treated as distinct subscribers.
197
+ *
198
+ * @param topic - Exact topic string to listen on.
199
+ * @param handler - Callback invoked for each matching message. May return a
200
+ * `Promise`; the host catches rejections and logs them.
201
+ * @returns An unsubscribe function. Calling it more than once is a no-op.
202
+ */
203
+ subscribe(topic: string, handler: (message: PluginEventBusMessage) => void | Promise<void>): () => void;
204
+ }
205
+ /**
206
+ * Read-only summary of another operator in the same Host.
107
207
  */
108
- export interface OperatorControl {
208
+ export interface OtherOperatorSnapshot {
209
+ /** Unique operator identifier used by the host. */
210
+ readonly id: string;
211
+ /** Configured callsign of the operator/station. */
212
+ readonly callsign: string;
213
+ /** Configured grid locator of the operator/station. */
214
+ readonly grid: string;
215
+ /** Current transmit audio offset in Hz within the passband. */
216
+ readonly audioFrequencyHz: number;
217
+ /** Active digital mode descriptor, for example FT8 or FT4. */
218
+ readonly mode: ModeDescriptor;
219
+ /** Whether this operator is currently transmitting or otherwise armed. */
220
+ readonly isTransmitting: boolean;
221
+ /** Current transmit cycle selection where `0` is even and `1` is odd. */
222
+ readonly transmitCycles: number[];
223
+ /** Current automation runtime snapshot when available. */
224
+ readonly automation?: StrategyRuntimeSnapshot | null;
225
+ }
226
+ /**
227
+ * Read-only state and query surface for the current operator-scoped plugin
228
+ * instance. Mutations are submitted through `ctx.operatorCommands` when the
229
+ * plugin declares `operator:transmit-control`.
230
+ */
231
+ export interface OperatorSnapshot {
109
232
  /** Unique operator identifier used by the host. */
110
233
  readonly id: string;
111
234
  /** Whether this operator is currently transmitting or otherwise armed. */
@@ -122,39 +245,8 @@ export interface OperatorControl {
122
245
  readonly transmitCycles: number[];
123
246
  /** Current automation runtime snapshot visible to the operator UI. */
124
247
  readonly automation: StrategyRuntimeSnapshot | null;
125
- /** Enables transmission/automation for the current operator. */
126
- startTransmitting(): void;
127
- /** Disables transmission/automation for the current operator. */
128
- stopTransmitting(): void;
129
- /**
130
- * Requests that the operator call the specified target station.
131
- *
132
- * Passing `lastMessage` helps the host preserve the triggering context.
133
- */
134
- call(callsign: string, lastMessage?: {
135
- message: FrameMessage;
136
- slotInfo: SlotInfo;
137
- }): void;
138
- /**
139
- * Requests host-managed reply behavior for a decoded message.
140
- *
141
- * This is equivalent to an operator selecting a decode in the RX view while
142
- * keeping the API independent from any specific UDP/control protocol.
143
- */
144
- replyToDecode(decode: {
145
- callsign: string;
146
- lastMessage: {
147
- message: FrameMessage;
148
- slotInfo: SlotInfo;
149
- };
150
- modifiers?: number;
151
- }): void;
152
- /**
153
- * Updates the operator's transmit cycle preference.
154
- *
155
- * Pass a single value or an array to support alternating or multi-cycle modes.
156
- */
157
- setTransmitCycles(cycles: number | number[]): void;
248
+ /** Returns read-only snapshots for operators other than the current instance. */
249
+ getOtherOperators(): OtherOperatorSnapshot[];
158
250
  /**
159
251
  * Checks whether this operator has previously worked the given callsign.
160
252
  */
@@ -166,40 +258,81 @@ export interface OperatorControl {
166
258
  * working the target callsign.
167
259
  */
168
260
  isTargetBeingWorkedByOthers(targetCallsign: string): boolean;
169
- /** Clears host-managed decoded-message views when available. */
170
- clearDecodes(window?: number): void;
171
- /** Stops current transmission/automation. */
172
- haltTransmission(options?: {
173
- autoOnly?: boolean;
174
- }): void;
175
- /** Stores the current free-text message without necessarily transmitting it. */
176
- setFreeText(text: string): void;
177
- /** Requests transmission of free text. If text is provided it is stored first. */
178
- sendFreeText(text?: string): void;
179
- /** Applies a temporary session grid/location override when the host supports it. */
180
- setTemporaryLocation(location: string): void;
181
- /** Requests callsign highlighting in host decode views when available. */
182
- highlightCallsign(rule: {
183
- callsign: string;
184
- background?: string | null;
185
- foreground?: string | null;
186
- lastOnly?: boolean;
187
- }): void;
188
- /**
189
- * Records a completed QSO through the host logbook pipeline.
190
- */
191
- recordQSO(record: QSORecord): void;
192
- /**
193
- * Pushes updated slot text content to the frontend operator view.
194
- */
195
- notifySlotsUpdated(slots: OperatorSlots): void;
196
- /**
197
- * Pushes a strategy state change notification to the frontend operator view.
198
- */
199
- notifyStateChanged(state: string): void;
200
261
  }
201
262
  /**
202
- * Read/write access to radio state that is safe for plugins.
263
+ * Declarative operator mutations accepted by the host transmission framework.
264
+ *
265
+ * The command set deliberately contains no PTT, audio, mixer, encoder, raw
266
+ * transmit or emergency-stop primitive. Plugins can request product actions;
267
+ * only the host coordinators may translate them into a physical RF lifecycle.
268
+ */
269
+ export type PluginOperatorCommand = {
270
+ type: 'start-automation';
271
+ } | {
272
+ type: 'stop-automation';
273
+ } | {
274
+ type: 'request-call';
275
+ callsign: string;
276
+ lastMessage?: {
277
+ message: FrameMessage;
278
+ slotInfo: SlotInfo;
279
+ };
280
+ } | {
281
+ type: 'reply-to-decode';
282
+ callsign: string;
283
+ lastMessage: {
284
+ message: FrameMessage;
285
+ slotInfo: SlotInfo;
286
+ };
287
+ modifiers?: number;
288
+ } | {
289
+ type: 'set-transmit-cycles';
290
+ cycles: number | number[];
291
+ } | {
292
+ type: 'remove-contribution';
293
+ } | {
294
+ type: 'clear-decodes';
295
+ window?: number;
296
+ } | {
297
+ type: 'set-free-text';
298
+ text: string;
299
+ } | {
300
+ type: 'send-free-text';
301
+ text?: string;
302
+ } | {
303
+ type: 'set-temporary-location';
304
+ location: string;
305
+ } | {
306
+ type: 'highlight-callsign';
307
+ callsign: string;
308
+ background?: string | null;
309
+ foreground?: string | null;
310
+ lastOnly?: boolean;
311
+ };
312
+ /** Settlement returned after the Host accepts an operator command. */
313
+ export interface PluginOperatorCommandResult {
314
+ /** Host command epoch allocated before any asynchronous work begins. */
315
+ epoch: number;
316
+ /** `superseded` means a newer host command revoked this request. */
317
+ outcome: 'completed' | 'superseded';
318
+ }
319
+ /**
320
+ * Capability-scoped command port for plugins with
321
+ * `operator:transmit-control` and API v2.
322
+ *
323
+ * The property is omitted from contexts without that capability. Every submit
324
+ * is invocation-guarded and enters the host's per-operator intent lane.
325
+ */
326
+ export interface OperatorCommandPort {
327
+ /**
328
+ * Submits one high-level operator command through the Host intent lane.
329
+ * Rejects when the invocation expired, the plugin safety gate is disabled,
330
+ * or the current physical lifecycle cannot accept the command.
331
+ */
332
+ submit(command: PluginOperatorCommand): Promise<PluginOperatorCommandResult>;
333
+ }
334
+ /**
335
+ * Read-only operating-mode projection that is safe for plugins.
203
336
  */
204
337
  export interface RadioOperatingMode {
205
338
  /**
@@ -223,7 +356,8 @@ export interface RadioOperatingMode {
223
356
  */
224
357
  readonly descriptor: ModeDescriptor;
225
358
  }
226
- export interface RadioControl {
359
+ /** Read-only frequency, band, mode and connection state for the active radio. */
360
+ export interface RadioView {
227
361
  /** Current tuned radio frequency in Hz. */
228
362
  readonly frequency: number;
229
363
  /** Human-readable current band label, for example `20m`. */
@@ -232,31 +366,52 @@ export interface RadioControl {
232
366
  readonly mode: RadioOperatingMode;
233
367
  /** Whether the radio transport is currently connected. */
234
368
  readonly isConnected: boolean;
235
- /** Negotiated radio capability controls. Requires radio plugin permissions. */
236
- readonly capabilities: RadioCapabilitiesControl;
237
- /** Physical radio power controls. Requires radio plugin permissions. */
238
- readonly power: RadioPowerControl;
239
- /**
240
- * Requests a frequency change.
241
- *
242
- * The host remains responsible for serializing hardware access and enforcing
243
- * any safety or capability constraints.
244
- */
245
- setFrequency(freq: number): Promise<void>;
246
369
  }
247
370
  /**
248
371
  * Access to the host-managed radio capability negotiation system.
249
372
  */
250
- export interface RadioCapabilitiesControl {
251
- /** Returns the current capability descriptor/state snapshot. Requires `radio:read`. */
373
+ export interface RadioCapabilitiesView {
374
+ /** Returns the current capability descriptor/state snapshot. */
252
375
  getSnapshot(): CapabilityList;
253
- /** Returns a single capability state from the current snapshot, or null. Requires `radio:read`. */
376
+ /** Returns a single capability state from the current snapshot, or null. */
254
377
  getState(id: string): CapabilityState | null;
255
- /** Refreshes readable capability values and returns the updated snapshot. Requires `radio:read`. */
378
+ /** Refreshes readable capability values and returns the updated snapshot. */
256
379
  refresh(): Promise<CapabilityList>;
257
- /** Writes a capability value or triggers an action capability. Requires `radio:control`. */
258
- write(payload: WriteCapabilityPayload): Promise<void>;
259
380
  }
381
+ /** Declarative radio mutations accepted by the host radio coordinator. */
382
+ export type PluginRadioCommand = {
383
+ type: 'set-frequency';
384
+ frequency: number;
385
+ } | {
386
+ /** Atomically changes band and optionally starts the radio's tuner while RF is idle. */
387
+ type: 'switch-band';
388
+ frequency: number;
389
+ autoTune?: boolean;
390
+ };
391
+ /**
392
+ * Capability-scoped radio command port.
393
+ *
394
+ * This port exists only for plugins with `radio:control`. It deliberately does
395
+ * not expose a radio connection, PTT primitive, mode switch, audio output or
396
+ * any other physical device object.
397
+ */
398
+ export interface RadioCommandPort {
399
+ /** Submits a frequency/band command after Host physical-idle validation. */
400
+ submit(command: PluginRadioCommand): Promise<void>;
401
+ }
402
+ /** Explicit tuner operations; no arbitrary capability identifier is accepted. */
403
+ export type PluginRadioTunerCommand = {
404
+ type: 'set-enabled';
405
+ enabled: boolean;
406
+ } | {
407
+ type: 'start-manual-tune';
408
+ };
409
+ /** Capability-scoped tuner command port for `radio:tuner-control` plugins. */
410
+ export interface RadioTunerCommandPort {
411
+ /** Submits one explicit tuner operation after Host safety validation. */
412
+ submit(command: PluginRadioTunerCommand): Promise<void>;
413
+ }
414
+ /** Optional target profile and startup behavior for a radio power command. */
260
415
  export interface RadioPowerSetOptions {
261
416
  /** Profile to target. Defaults to the active profile. */
262
417
  profileId?: string;
@@ -266,13 +421,25 @@ export interface RadioPowerSetOptions {
266
421
  /**
267
422
  * Access to physical radio power management.
268
423
  */
269
- export interface RadioPowerControl {
270
- /** Returns power support information for the active or specified profile. Requires `radio:read`. */
424
+ export interface RadioPowerView {
425
+ /** Returns power support information for the active or specified profile. */
271
426
  getSupport(profileId?: string): Promise<RadioPowerSupportInfo>;
272
- /** Returns the last known power transition state for the active or specified profile. Requires `radio:read`. */
427
+ /** Returns the last known power transition state for the active or specified profile. */
273
428
  getState(profileId?: string): RadioPowerStateEvent | null;
274
- /** Requests a physical power transition. Requires `radio:power`. */
275
- set(state: RadioPowerTarget, options?: RadioPowerSetOptions): Promise<RadioPowerResponse>;
429
+ }
430
+ /** Declarative power-state transition accepted by `ctx.radioPowerCommands`. */
431
+ export type PluginRadioPowerCommand = {
432
+ /** Command discriminator. */
433
+ type: 'set-power';
434
+ /** Requested physical/controller power target. */
435
+ state: RadioPowerTarget;
436
+ /** Optional profile selection and automatic engine startup behavior. */
437
+ options?: RadioPowerSetOptions;
438
+ };
439
+ /** Capability-scoped physical power command port for `radio:power` plugins. */
440
+ export interface RadioPowerCommandPort {
441
+ /** Requests a power transition and resolves with the Host's final state. */
442
+ submit(command: PluginRadioPowerCommand): Promise<RadioPowerResponse>;
276
443
  }
277
444
  /**
278
445
  * Filter criteria for querying QSO records from the logbook.
@@ -315,35 +482,37 @@ export interface QSOQueryFilter {
315
482
  /**
316
483
  * Callsign-bound view over a single logbook.
317
484
  *
318
- * The host resolves the concrete logbook lazily on each operation, which keeps
319
- * the handle valid even if the underlying logbook is created or reloaded later.
485
+ * The host resolves an already registered concrete logbook on each operation,
486
+ * which keeps the handle valid across reloads without implicitly creating data.
320
487
  */
321
- export interface CallsignLogbookAccess {
488
+ export interface CallsignLogbookReadAccess {
322
489
  /** Normalized callsign that scopes this accessor. */
323
490
  readonly callsign: string;
324
- /** Returns the resolved logbook id, or null when no logbook exists yet. */
491
+ /** Returns the resolved logbook id, or null when no logbook is registered. */
325
492
  getLogBookId(): Promise<string | null>;
326
493
  /** Queries QSO records matching the given filter. */
327
494
  queryQSOs(filter: QSOQueryFilter): Promise<import('@tx5dr/contracts').QSORecord[]>;
328
495
  /** Counts QSO records matching the given filter. */
329
496
  countQSOs(filter?: QSOQueryFilter): Promise<number>;
330
- /** Adds a new QSO record to this callsign's logbook. */
331
- addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<void>;
332
- /** Updates partial fields of an existing QSO record. */
333
- updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<void>;
334
497
  /** Returns current statistics for this callsign's logbook. */
335
498
  getStatistics(): Promise<import('@tx5dr/contracts').LogBookStatistics | null>;
499
+ }
500
+ /** Durable mutation operations scoped to one normalized station callsign. */
501
+ export interface CallsignLogbookCommandPort {
502
+ /** Normalized callsign that scopes this accessor. */
503
+ readonly callsign: string;
504
+ /** Adds a QSO and resolves with the final record after durable commit. */
505
+ addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<import('@tx5dr/contracts').QSORecord>;
506
+ /** Updates a QSO and resolves with the final record after durable commit. */
507
+ updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<import('@tx5dr/contracts').QSORecord>;
336
508
  /** Notifies the frontend that this callsign's logbook changed. */
337
509
  notifyUpdated(operatorId?: string): Promise<void>;
338
510
  }
339
- /**
340
- * Full logbook access for plugins.
341
- *
342
- * Extends the original read-only helpers with query, write and notification
343
- * capabilities so that sync providers can self-orchestrate their entire flow
344
- * without host-side special handling.
345
- */
346
- export interface LogbookAccess {
511
+ /** Combined read/write callsign-bound logbook capability. */
512
+ export interface CallsignLogbookAccess extends CallsignLogbookReadAccess, CallsignLogbookCommandPort {
513
+ }
514
+ /** Read-only worked-status and QSO query capability for `logbook:read`. */
515
+ export interface LogbookReadAccess {
347
516
  /** Checks whether the callsign has already been worked. */
348
517
  hasWorked(callsign: string, options?: {
349
518
  anyBand?: boolean;
@@ -356,14 +525,24 @@ export interface LogbookAccess {
356
525
  queryQSOs(filter: QSOQueryFilter): Promise<import('@tx5dr/contracts').QSORecord[]>;
357
526
  /** Counts QSO records matching the given filter. */
358
527
  countQSOs(filter?: QSOQueryFilter): Promise<number>;
359
- /** Returns a callsign-bound accessor suitable for global plugin instances. */
360
- forCallsign(callsign: string): CallsignLogbookAccess;
361
- /** Adds a new QSO record. Deduplication is the caller's responsibility. */
362
- addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<void>;
363
- /** Updates partial fields of an existing QSO record (e.g. QSL status). */
364
- updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<void>;
528
+ /** Returns a read-only callsign-bound accessor suitable for global plugin instances. */
529
+ forCallsign(callsign: string): CallsignLogbookReadAccess;
530
+ }
531
+ /** Durable mutation operations exposed by the `logbook:write` permission. */
532
+ export interface LogbookCommandPort {
533
+ /** Adds a QSO and resolves with the final record after durable commit. */
534
+ addQSO(record: import('@tx5dr/contracts').QSORecord): Promise<import('@tx5dr/contracts').QSORecord>;
535
+ /** Updates a QSO and resolves with the final record after durable commit. */
536
+ updateQSO(qsoId: string, updates: Partial<import('@tx5dr/contracts').QSORecord>): Promise<import('@tx5dr/contracts').QSORecord>;
365
537
  /** Notifies the frontend to refresh logbook data (call after batch writes). */
366
538
  notifyUpdated(): Promise<void>;
539
+ /** Returns a callsign-bound durable mutation port for global plugin instances. */
540
+ forCallsign(callsign: string): CallsignLogbookCommandPort;
541
+ }
542
+ /** @deprecated Prefer capability-specific LogbookReadAccess and LogbookCommandPort. */
543
+ export interface LogbookAccess extends LogbookReadAccess, LogbookCommandPort {
544
+ /** Returns a combined read/write accessor for the requested station callsign. */
545
+ forCallsign(callsign: string): CallsignLogbookAccess;
367
546
  }
368
547
  /**
369
548
  * Optional constraints used when asking the host for a quieter transmit offset.
@@ -413,7 +592,9 @@ export interface BandAccess {
413
592
  * decode environment.
414
593
  *
415
594
  * Returns `null` when the host cannot evaluate the slot or when no suitable
416
- * idle window is found.
595
+ * idle window is found. A successful result also reserves that offset for the
596
+ * current operator and analyzed slot so later operators avoid selecting the
597
+ * same window.
417
598
  */
418
599
  findIdleTransmitFrequency(options?: IdleTransmitFrequencyOptions): number | null;
419
600
  /**
@@ -455,7 +636,8 @@ export interface PanelMeta {
455
636
  */
456
637
  export interface UIBridge {
457
638
  /**
458
- * Publishes new panel data for the given declarative panel id.
639
+ * Publishes a JSON-compatible snapshot for the given declarative panel id.
640
+ * Mutating the caller's object after this call does not alter panel state.
459
641
  */
460
642
  send(panelId: string, data: unknown): void;
461
643
  /**
@@ -484,7 +666,7 @@ export interface UIBridge {
484
666
  */
485
667
  registerPageHandler(handler: PluginUIHandler): void;
486
668
  /**
487
- * Pushes a custom message to the specific page session.
669
+ * Pushes a JSON-compatible data snapshot to the specific page session.
488
670
  *
489
671
  * Prefer this API whenever the plugin already knows the target session id
490
672
  * (for example from {@link PluginUIRequestContext.pageSessionId} or
@@ -499,7 +681,7 @@ export interface UIBridge {
499
681
  */
500
682
  listActivePageSessions(pageId: string): PluginUIPageSessionInfo[];
501
683
  /**
502
- * Pushes a custom message to an iframe UI page by page id.
684
+ * Pushes a JSON-compatible data snapshot to an iframe UI page by page id.
503
685
  *
504
686
  * This compatibility helper only succeeds when exactly one active session of
505
687
  * the current plugin instance matches the page id. If multiple sessions are
@@ -511,8 +693,9 @@ export interface UIBridge {
511
693
  * Handler for custom messages sent from iframe UI pages.
512
694
  *
513
695
  * Plugins register a handler via `ctx.ui.registerPageHandler()` to receive
514
- * arbitrary invoke requests from their iframe-based UIs. The host acts as a
515
- * transparent router it does not inspect or interpret the action or data.
696
+ * application-defined invoke requests from their iframe-based UIs. The Host
697
+ * does not interpret the business schema, but it enforces the page/session
698
+ * authorization and JSON data boundary in both directions.
516
699
  */
517
700
  export interface PluginUIHandler {
518
701
  /**
@@ -520,49 +703,77 @@ export interface PluginUIHandler {
520
703
  *
521
704
  * @param pageId - The page that sent the message.
522
705
  * @param action - Developer-defined action identifier.
523
- * @param data - Arbitrary payload from the iframe.
706
+ * @param data - JSON-compatible snapshot from the iframe; validate it as
707
+ * untrusted input before use.
524
708
  * @param requestContext - Host-authenticated page context, including any
525
709
  * bound resource for this page session.
526
- * @returns The response value sent back to the iframe.
710
+ * @returns A JSON-compatible response snapshot sent back to the iframe.
527
711
  */
528
712
  onMessage(pageId: string, action: string, data: unknown, requestContext: PluginUIRequestContext): Promise<unknown>;
529
713
  }
714
+ /** Host-authenticated user identity attached to an iframe invoke request. */
530
715
  export interface PluginUIRequestUser {
716
+ /** Stable token/session identifier; not the raw credential. */
531
717
  readonly tokenId: string;
718
+ /** Effective role at the time the Host authorizes the request. */
532
719
  readonly role: 'viewer' | 'operator' | 'admin';
720
+ /** Operator IDs the current user is allowed to access. */
533
721
  readonly operatorIds: string[];
722
+ /** Fine-grained grants associated with the authenticated user, when present. */
534
723
  readonly permissionGrants?: PermissionGrant[];
535
724
  }
725
+ /** Resource identity resolved and authorized from the page descriptor binding. */
536
726
  export interface PluginUIBoundResource {
727
+ /** Kind declared by `resourceBinding`. */
537
728
  readonly kind: 'callsign' | 'operator';
729
+ /** Normalized callsign or authorized operator ID. */
538
730
  readonly value: string;
539
731
  }
732
+ /** Plugin instance selected by the Host for this page request. */
540
733
  export type PluginUIInstanceTarget = {
541
734
  readonly kind: 'global';
542
735
  } | {
543
736
  readonly kind: 'operator';
544
737
  readonly operatorId: string;
545
738
  };
739
+ /** Read-only identity of one active plugin iframe page session. */
546
740
  export interface PluginUIPageSessionInfo {
741
+ /** Unique ID used for exact session pushes. */
547
742
  readonly sessionId: string;
743
+ /** `PluginDefinition.ui.pages` entry rendered by this session. */
548
744
  readonly pageId: string;
745
+ /** Host-authorized resource binding, when the page declares one. */
549
746
  readonly resource?: PluginUIBoundResource;
550
747
  }
748
+ /** Page-session identity plus an exact push channel back to that iframe. */
551
749
  export interface PluginUIPageContext extends PluginUIPageSessionInfo {
750
+ /** Sends a JSON-compatible snapshot to this exact page session. */
552
751
  push(action: string, data?: unknown): void;
553
752
  }
753
+ /**
754
+ * Host-authenticated context passed to an iframe page handler.
755
+ *
756
+ * Treat `data` from the iframe as untrusted input. Use this context, rather
757
+ * than caller-supplied IDs, for authorization and storage scoping.
758
+ */
554
759
  export interface PluginUIRequestContext {
760
+ /** Same exact page session identifier exposed as `page.sessionId`. */
555
761
  readonly pageSessionId: string;
762
+ /** User identity authorized by the Host for this request. */
556
763
  readonly user: PluginUIRequestUser;
764
+ /** Bound callsign/operator, when required by the page descriptor. */
557
765
  readonly resource?: PluginUIBoundResource;
766
+ /** Global or operator plugin instance receiving the request. */
558
767
  readonly instanceTarget: PluginUIInstanceTarget;
768
+ /** Exact page session/push capability, valid only during the current handler invocation. */
559
769
  readonly page: PluginUIPageContext;
560
770
  /**
561
771
  * Page-scoped file storage shared with iframe `tx5dr.file*()` calls.
562
772
  *
563
773
  * Use this in `registerPageHandler()` handlers to read files uploaded by the
564
774
  * current iframe page session without reconstructing host-internal scope
565
- * paths.
775
+ * paths. Both `page` and `files` are exact-invocation capabilities: do not
776
+ * retain and invoke them after the current `onMessage()` promise settles.
566
777
  */
567
778
  readonly files: PluginFileStore;
568
779
  }
@@ -573,9 +784,9 @@ export interface PluginUIRequestContext {
573
784
  * traversal outside the sandbox is rejected by the host.
574
785
  */
575
786
  export interface PluginFileStore {
576
- /** Writes (or overwrites) a file at the given path. */
787
+ /** Writes a copy of the Buffer, creating or replacing the file. */
577
788
  write(path: string, data: Buffer): Promise<void>;
578
- /** Reads a file. Returns `null` when the path does not exist. */
789
+ /** Reads a file into a new Buffer. Returns `null` when the path does not exist. */
579
790
  read(path: string): Promise<Buffer | null>;
580
791
  /** Deletes a file. Returns `true` if the file existed and was removed. */
581
792
  delete(path: string): Promise<boolean>;