@tx5dr/plugin-api 1.7.12 → 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 +217 -15
  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 +322 -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 -105
  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,40 +76,134 @@ 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.
139
+ *
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}.
104
174
  *
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.
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
208
  export interface OtherOperatorSnapshot {
109
209
  /** Unique operator identifier used by the host. */
@@ -120,8 +220,15 @@ export interface OtherOperatorSnapshot {
120
220
  readonly isTransmitting: boolean;
121
221
  /** Current transmit cycle selection where `0` is even and `1` is odd. */
122
222
  readonly transmitCycles: number[];
223
+ /** Current automation runtime snapshot when available. */
224
+ readonly automation?: StrategyRuntimeSnapshot | null;
123
225
  }
124
- export interface OperatorControl {
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 {
125
232
  /** Unique operator identifier used by the host. */
126
233
  readonly id: string;
127
234
  /** Whether this operator is currently transmitting or otherwise armed. */
@@ -140,39 +247,6 @@ export interface OperatorControl {
140
247
  readonly automation: StrategyRuntimeSnapshot | null;
141
248
  /** Returns read-only snapshots for operators other than the current instance. */
142
249
  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
250
  /**
177
251
  * Checks whether this operator has previously worked the given callsign.
178
252
  */
@@ -184,40 +258,81 @@ export interface OperatorControl {
184
258
  * working the target callsign.
185
259
  */
186
260
  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
261
  }
219
262
  /**
220
- * 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.
221
336
  */
222
337
  export interface RadioOperatingMode {
223
338
  /**
@@ -241,7 +356,8 @@ export interface RadioOperatingMode {
241
356
  */
242
357
  readonly descriptor: ModeDescriptor;
243
358
  }
244
- export interface RadioControl {
359
+ /** Read-only frequency, band, mode and connection state for the active radio. */
360
+ export interface RadioView {
245
361
  /** Current tuned radio frequency in Hz. */
246
362
  readonly frequency: number;
247
363
  /** Human-readable current band label, for example `20m`. */
@@ -250,31 +366,52 @@ export interface RadioControl {
250
366
  readonly mode: RadioOperatingMode;
251
367
  /** Whether the radio transport is currently connected. */
252
368
  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>;
264
369
  }
265
370
  /**
266
371
  * Access to the host-managed radio capability negotiation system.
267
372
  */
268
- export interface RadioCapabilitiesControl {
269
- /** Returns the current capability descriptor/state snapshot. Requires `radio:read`. */
373
+ export interface RadioCapabilitiesView {
374
+ /** Returns the current capability descriptor/state snapshot. */
270
375
  getSnapshot(): CapabilityList;
271
- /** 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. */
272
377
  getState(id: string): CapabilityState | null;
273
- /** Refreshes readable capability values and returns the updated snapshot. Requires `radio:read`. */
378
+ /** Refreshes readable capability values and returns the updated snapshot. */
274
379
  refresh(): Promise<CapabilityList>;
275
- /** Writes a capability value or triggers an action capability. Requires `radio:control`. */
276
- write(payload: WriteCapabilityPayload): Promise<void>;
277
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. */
278
415
  export interface RadioPowerSetOptions {
279
416
  /** Profile to target. Defaults to the active profile. */
280
417
  profileId?: string;
@@ -284,13 +421,25 @@ export interface RadioPowerSetOptions {
284
421
  /**
285
422
  * Access to physical radio power management.
286
423
  */
287
- export interface RadioPowerControl {
288
- /** 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. */
289
426
  getSupport(profileId?: string): Promise<RadioPowerSupportInfo>;
290
- /** 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. */
291
428
  getState(profileId?: string): RadioPowerStateEvent | null;
292
- /** Requests a physical power transition. Requires `radio:power`. */
293
- 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>;
294
443
  }
295
444
  /**
296
445
  * Filter criteria for querying QSO records from the logbook.
@@ -333,35 +482,37 @@ export interface QSOQueryFilter {
333
482
  /**
334
483
  * Callsign-bound view over a single logbook.
335
484
  *
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.
485
+ * The host resolves an already registered concrete logbook on each operation,
486
+ * which keeps the handle valid across reloads without implicitly creating data.
338
487
  */
339
- export interface CallsignLogbookAccess {
488
+ export interface CallsignLogbookReadAccess {
340
489
  /** Normalized callsign that scopes this accessor. */
341
490
  readonly callsign: string;
342
- /** 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. */
343
492
  getLogBookId(): Promise<string | null>;
344
493
  /** Queries QSO records matching the given filter. */
345
494
  queryQSOs(filter: QSOQueryFilter): Promise<import('@tx5dr/contracts').QSORecord[]>;
346
495
  /** Counts QSO records matching the given filter. */
347
496
  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
497
  /** Returns current statistics for this callsign's logbook. */
353
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>;
354
508
  /** Notifies the frontend that this callsign's logbook changed. */
355
509
  notifyUpdated(operatorId?: string): Promise<void>;
356
510
  }
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 {
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 {
365
516
  /** Checks whether the callsign has already been worked. */
366
517
  hasWorked(callsign: string, options?: {
367
518
  anyBand?: boolean;
@@ -374,14 +525,24 @@ export interface LogbookAccess {
374
525
  queryQSOs(filter: QSOQueryFilter): Promise<import('@tx5dr/contracts').QSORecord[]>;
375
526
  /** Counts QSO records matching the given filter. */
376
527
  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>;
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>;
383
537
  /** Notifies the frontend to refresh logbook data (call after batch writes). */
384
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;
385
546
  }
386
547
  /**
387
548
  * Optional constraints used when asking the host for a quieter transmit offset.
@@ -431,7 +592,9 @@ export interface BandAccess {
431
592
  * decode environment.
432
593
  *
433
594
  * Returns `null` when the host cannot evaluate the slot or when no suitable
434
- * 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.
435
598
  */
436
599
  findIdleTransmitFrequency(options?: IdleTransmitFrequencyOptions): number | null;
437
600
  /**
@@ -473,7 +636,8 @@ export interface PanelMeta {
473
636
  */
474
637
  export interface UIBridge {
475
638
  /**
476
- * 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.
477
641
  */
478
642
  send(panelId: string, data: unknown): void;
479
643
  /**
@@ -502,7 +666,7 @@ export interface UIBridge {
502
666
  */
503
667
  registerPageHandler(handler: PluginUIHandler): void;
504
668
  /**
505
- * Pushes a custom message to the specific page session.
669
+ * Pushes a JSON-compatible data snapshot to the specific page session.
506
670
  *
507
671
  * Prefer this API whenever the plugin already knows the target session id
508
672
  * (for example from {@link PluginUIRequestContext.pageSessionId} or
@@ -517,7 +681,7 @@ export interface UIBridge {
517
681
  */
518
682
  listActivePageSessions(pageId: string): PluginUIPageSessionInfo[];
519
683
  /**
520
- * 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.
521
685
  *
522
686
  * This compatibility helper only succeeds when exactly one active session of
523
687
  * the current plugin instance matches the page id. If multiple sessions are
@@ -529,8 +693,9 @@ export interface UIBridge {
529
693
  * Handler for custom messages sent from iframe UI pages.
530
694
  *
531
695
  * 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.
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.
534
699
  */
535
700
  export interface PluginUIHandler {
536
701
  /**
@@ -538,49 +703,77 @@ export interface PluginUIHandler {
538
703
  *
539
704
  * @param pageId - The page that sent the message.
540
705
  * @param action - Developer-defined action identifier.
541
- * @param data - Arbitrary payload from the iframe.
706
+ * @param data - JSON-compatible snapshot from the iframe; validate it as
707
+ * untrusted input before use.
542
708
  * @param requestContext - Host-authenticated page context, including any
543
709
  * bound resource for this page session.
544
- * @returns The response value sent back to the iframe.
710
+ * @returns A JSON-compatible response snapshot sent back to the iframe.
545
711
  */
546
712
  onMessage(pageId: string, action: string, data: unknown, requestContext: PluginUIRequestContext): Promise<unknown>;
547
713
  }
714
+ /** Host-authenticated user identity attached to an iframe invoke request. */
548
715
  export interface PluginUIRequestUser {
716
+ /** Stable token/session identifier; not the raw credential. */
549
717
  readonly tokenId: string;
718
+ /** Effective role at the time the Host authorizes the request. */
550
719
  readonly role: 'viewer' | 'operator' | 'admin';
720
+ /** Operator IDs the current user is allowed to access. */
551
721
  readonly operatorIds: string[];
722
+ /** Fine-grained grants associated with the authenticated user, when present. */
552
723
  readonly permissionGrants?: PermissionGrant[];
553
724
  }
725
+ /** Resource identity resolved and authorized from the page descriptor binding. */
554
726
  export interface PluginUIBoundResource {
727
+ /** Kind declared by `resourceBinding`. */
555
728
  readonly kind: 'callsign' | 'operator';
729
+ /** Normalized callsign or authorized operator ID. */
556
730
  readonly value: string;
557
731
  }
732
+ /** Plugin instance selected by the Host for this page request. */
558
733
  export type PluginUIInstanceTarget = {
559
734
  readonly kind: 'global';
560
735
  } | {
561
736
  readonly kind: 'operator';
562
737
  readonly operatorId: string;
563
738
  };
739
+ /** Read-only identity of one active plugin iframe page session. */
564
740
  export interface PluginUIPageSessionInfo {
741
+ /** Unique ID used for exact session pushes. */
565
742
  readonly sessionId: string;
743
+ /** `PluginDefinition.ui.pages` entry rendered by this session. */
566
744
  readonly pageId: string;
745
+ /** Host-authorized resource binding, when the page declares one. */
567
746
  readonly resource?: PluginUIBoundResource;
568
747
  }
748
+ /** Page-session identity plus an exact push channel back to that iframe. */
569
749
  export interface PluginUIPageContext extends PluginUIPageSessionInfo {
750
+ /** Sends a JSON-compatible snapshot to this exact page session. */
570
751
  push(action: string, data?: unknown): void;
571
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
+ */
572
759
  export interface PluginUIRequestContext {
760
+ /** Same exact page session identifier exposed as `page.sessionId`. */
573
761
  readonly pageSessionId: string;
762
+ /** User identity authorized by the Host for this request. */
574
763
  readonly user: PluginUIRequestUser;
764
+ /** Bound callsign/operator, when required by the page descriptor. */
575
765
  readonly resource?: PluginUIBoundResource;
766
+ /** Global or operator plugin instance receiving the request. */
576
767
  readonly instanceTarget: PluginUIInstanceTarget;
768
+ /** Exact page session/push capability, valid only during the current handler invocation. */
577
769
  readonly page: PluginUIPageContext;
578
770
  /**
579
771
  * Page-scoped file storage shared with iframe `tx5dr.file*()` calls.
580
772
  *
581
773
  * Use this in `registerPageHandler()` handlers to read files uploaded by the
582
774
  * current iframe page session without reconstructing host-internal scope
583
- * paths.
775
+ * paths. Both `page` and `files` are exact-invocation capabilities: do not
776
+ * retain and invoke them after the current `onMessage()` promise settles.
584
777
  */
585
778
  readonly files: PluginFileStore;
586
779
  }
@@ -591,9 +784,9 @@ export interface PluginUIRequestContext {
591
784
  * traversal outside the sandbox is rejected by the host.
592
785
  */
593
786
  export interface PluginFileStore {
594
- /** Writes (or overwrites) a file at the given path. */
787
+ /** Writes a copy of the Buffer, creating or replacing the file. */
595
788
  write(path: string, data: Buffer): Promise<void>;
596
- /** 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. */
597
790
  read(path: string): Promise<Buffer | null>;
598
791
  /** Deletes a file. Returns `true` if the file existed and was removed. */
599
792
  delete(path: string): Promise<boolean>;