@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/README.md CHANGED
@@ -15,12 +15,14 @@ npm install --save-dev @tx5dr/plugin-api
15
15
  ### TypeScript
16
16
 
17
17
  ```typescript
18
- import type { PluginDefinition, PluginContext } from '@tx5dr/plugin-api';
18
+ import { definePlugin } from '@tx5dr/plugin-api';
19
19
 
20
- const plugin: PluginDefinition = {
20
+ const plugin = definePlugin({
21
+ apiVersion: 2,
21
22
  name: 'my-plugin',
22
23
  version: '1.0.0',
23
24
  type: 'utility',
25
+ permissions: [],
24
26
  hooks: {
25
27
  onDecode(messages, ctx) {
26
28
  for (const msg of messages) {
@@ -28,7 +30,7 @@ const plugin: PluginDefinition = {
28
30
  }
29
31
  },
30
32
  },
31
- };
33
+ });
32
34
 
33
35
  export default plugin;
34
36
  ```
@@ -36,11 +38,14 @@ export default plugin;
36
38
  ### JavaScript (with JSDoc types)
37
39
 
38
40
  ```javascript
39
- /** @type {import('@tx5dr/plugin-api').PluginDefinition} */
40
- export default {
41
+ import { definePlugin } from '@tx5dr/plugin-api';
42
+
43
+ export default definePlugin({
44
+ apiVersion: 2,
41
45
  name: 'my-plugin',
42
46
  version: '1.0.0',
43
47
  type: 'utility',
48
+ permissions: [],
44
49
  hooks: {
45
50
  onDecode(messages, ctx) {
46
51
  for (const msg of messages) {
@@ -48,32 +53,118 @@ export default {
48
53
  }
49
54
  },
50
55
  },
51
- };
56
+ });
52
57
  ```
53
58
 
54
59
  ## Exports
55
60
 
56
61
  | Subpath | Description |
57
62
  |---------|-------------|
58
- | `@tx5dr/plugin-api` | Core types: `PluginDefinition`, `PluginContext`, `PluginHooks`, helper interfaces, radio/message types |
59
- | `@tx5dr/plugin-api/testing` | Mock factories for unit testing: `createMockContext()`, `createMockSlotInfo()`, `createMockParsedMessage()` |
63
+ | `@tx5dr/plugin-api` | `definePlugin()`, capability-derived contexts, hooks, structured command ports, and radio/message types |
64
+ | `@tx5dr/plugin-api/testing` | Mock factories for unit testing: `createMockContext()`, `createMockSlotInfo()`, `createMockParsedMessage()`, `createMockEventBus()` |
60
65
  | `@tx5dr/plugin-api/bridge` | Ambient type declarations for the iframe Bridge SDK (`window.tx5dr`) |
61
66
 
67
+ ## Capability Model
68
+
69
+ Privileged Host APIs use an allowlist model. A plugin must declare every
70
+ capability in `permissions`; the Host then projects only those properties into
71
+ that plugin's context. Undeclared properties are absent from both the inferred
72
+ TypeScript type and the runtime object. Use `definePlugin()` without manually
73
+ widening callbacks to `PluginContext`, otherwise TypeScript cannot preserve the
74
+ literal permission tuple.
75
+
76
+ Capabilities grant structured Host ports, not physical device ownership. The
77
+ capability-derived context does not directly expose raw PTT, audio playback,
78
+ the mixer, encoder, physical frame lease, arbitrary radio capability writes,
79
+ or global emergency stop. Strategy runtimes receive a narrower speculative
80
+ context and return declarative decisions; they never receive command ports.
81
+
82
+ For a strategy plugin, `type: 'strategy'` plus `apiVersion: 2` is the explicit
83
+ declaration that it may produce RF decisions when selected by an operator. It
84
+ does not need `operator:transmit-control`. That permission is reserved for
85
+ utility plugins that need the imperative, Host-coordinated
86
+ `ctx.operatorCommands` port.
87
+
88
+ API v2 is required for strategy plugins and any plugin requesting a mutation
89
+ capability. The Host validates and freezes the loaded definition so permissions
90
+ cannot be expanded after load.
91
+
92
+ This is a Host API contract, not a sandbox for hostile Node.js code. Third-party
93
+ plugins currently execute in the server process; process isolation is a
94
+ separate security boundary.
95
+
96
+ ## Data Ownership and Callback Lifetime
97
+
98
+ Configuration, hook arguments, query results and messages cross the Host boundary by value. Plugins may modify their local copies, but changes are not persisted until they call an explicit API such as `ctx.updateConfig()`, `store.set()` or a command port. UI/config/KV channels accept JSON-compatible values; hooks, strategy results and EventBus payloads accept structured-clone-compatible values. Functions, cycles, Host handles and other unsupported values are rejected with `PLUGIN_DATA_NOT_SERIALIZABLE`.
99
+
100
+ Host capabilities such as `ctx.ui`, `ctx.logbook`, radio command ports, network sockets and native `Response` objects are live handles. Use them only before the current Host callback settles. A handle retained past timeout, reload or unload rejects with `PLUGIN_INVOCATION_EXPIRED`; use Host timers and callbacks for later work instead of detached continuations.
101
+
102
+ ## Operator Transmit Control
103
+
104
+ Utility plugins that need to submit operator commands must declare
105
+ `operator:transmit-control` and submit one of the high-level commands accepted
106
+ by `ctx.operatorCommands`. The permission grants API access; it does not by
107
+ itself classify the plugin as an automatic caller.
108
+
109
+ Automatic calling plugins implement `isAutoCallEnabled()`. This both gates the
110
+ command port and opts the plugin into the operator auto-call indicator and pause
111
+ controls:
112
+
113
+ ```ts
114
+ const plugin = definePlugin({
115
+ apiVersion: 2,
116
+ name: 'scheduled-caller',
117
+ version: '1.0.0',
118
+ type: 'utility',
119
+ permissions: ['operator:transmit-control'],
120
+ isAutoCallEnabled: (ctx) => ctx.config.enabled === true,
121
+ hooks: {
122
+ async onTimer(_timerId, ctx) {
123
+ await ctx.operatorCommands.submit({
124
+ type: 'request-call',
125
+ callsign: 'W1AW',
126
+ });
127
+ },
128
+ },
129
+ });
130
+ ```
131
+
132
+ Integrations such as remote-control protocol bridges that may submit occasional
133
+ commands but do not autonomously originate calls implement
134
+ `isTransmitControlEnabled()` instead. They receive the same guarded command
135
+ port but are not shown or paused as auto-call plugins.
136
+
137
+ The Host allocates an operator command epoch and routes the request through the
138
+ operator/frame coordinators. Plugins cannot directly key or unkey the radio.
139
+
62
140
  ## Radio Permissions
63
141
 
64
- Server-side plugins can use `ctx.radio` to inspect negotiated radio capabilities and, when explicitly permitted, control radio capabilities or physical power:
142
+ `ctx.radio` always exposes a small read-only operating snapshot. Additional
143
+ radio capabilities require explicit declarations:
65
144
 
66
145
  ```ts
67
- permissions: ['radio:read', 'radio:control', 'radio:power']
146
+ permissions: ['radio:read', 'radio:control', 'radio:tuner-control', 'radio:power']
68
147
  ```
69
148
 
70
- - `radio:read` enables `ctx.radio.capabilities.getSnapshot()` and `ctx.radio.power.getSupport()`.
71
- - `radio:control` enables `ctx.radio.setFrequency()` and `ctx.radio.capabilities.write()`.
72
- - `radio:power` enables `ctx.radio.power.set('on' | 'off' | 'standby' | 'operate')`.
73
- - `ctx.radio.mode` is always readable and exposes the current best-known operating mode using ADIF `MODE`/`SUBMODE` semantics, for example `SSB` + `USB` in voice USB.
149
+ - `radio:read` exposes `ctx.radioCapabilities` and the read-only `ctx.radioPower` view.
150
+ - `radio:control` exposes Host-arbitrated `set-frequency` and `switch-band` commands.
151
+ - `switch-band` can include `autoTune: true` when `radio:tuner-control` is also declared; the Host keeps the complete operation inside one physical-idle fence.
152
+ - `radio:tuner-control` exposes only `set-enabled` and `start-manual-tune` through `ctx.radioTunerCommands`.
153
+ - Radio writes reject while Digital, Voice, CW, Tune or manual PTT owns the physical transmitter; they never interrupt that transmission.
154
+ - `radio:power` exposes `ctx.radioPowerCommands.submit({ type: 'set-power', state })`.
155
+ - `ctx.radio.mode` remains read-only and uses ADIF `MODE`/`SUBMODE` semantics.
74
156
 
75
157
  These APIs are not exposed directly to iframe pages; custom UI should call a server-side page handler.
76
158
 
159
+ ## Logbook Permissions
160
+
161
+ - `logbook:read` exposes only query and worked-status methods on `ctx.logbook`.
162
+ - `logbook:write` exposes durable `addQSO()`/`updateQSO()` mutations in addition to reads.
163
+ - `logbook:sync` exposes `ctx.logbookSync` for registering a Host-managed sync provider.
164
+
165
+ Write completion means the Host logbook durability contract has completed.
166
+ Logbook APIs never grant PTT or frame lifecycle control.
167
+
77
168
  ## Host Settings Permissions
78
169
 
79
170
  Server-side plugins can use `ctx.settings` to read or update a safe whitelist of host settings when the manifest declares the matching permission. Each settings namespace uses one read/write permission:
@@ -89,9 +180,10 @@ Server-side plugins can use `ctx.settings` to read or update a safe whitelist of
89
180
  | `ctx.settings.ntp` | `settings:ntp` | `get()`, `update({ servers })` |
90
181
 
91
182
  ```ts
92
- import type { PluginDefinition } from '@tx5dr/plugin-api';
183
+ import { definePlugin } from '@tx5dr/plugin-api';
93
184
 
94
- const plugin: PluginDefinition = {
185
+ const plugin = definePlugin({
186
+ apiVersion: 2,
95
187
  name: 'station-policy',
96
188
  version: '1.0.0',
97
189
  type: 'utility',
@@ -102,13 +194,178 @@ const plugin: PluginDefinition = {
102
194
  await ctx.settings.station.update({ callsign: 'W1AW' });
103
195
  },
104
196
  },
105
- };
197
+ });
106
198
 
107
199
  export default plugin;
108
200
  ```
109
201
 
110
202
  The whitelist intentionally excludes authentication tokens, operator CRUD, hardware radio connection settings, audio devices, rigctld, OpenWebRX, profiles, and server host/port settings. These APIs are not exposed directly to iframe pages; custom UI should call a server-side page handler with `window.tx5dr.invoke()`.
111
203
 
204
+ ## Plugin Event Bus
205
+
206
+ Server-side plugins can exchange in-process messages through `ctx.eventBus`, a topic-based pub/sub bus scoped to the host process. Payloads use structured-clone semantics and each subscriber receives an independent value, enabling loose coupling without shared mutable state. Functions, promises, weak collections, and host capability objects cannot be published.
207
+
208
+ ### Permission
209
+
210
+ Declare `plugin:event-bus` in the manifest to enable the bus:
211
+
212
+ ```ts
213
+ permissions: ['plugin:event-bus']
214
+ ```
215
+
216
+ `ctx.eventBus` is optional and should be feature-detected before use.
217
+
218
+ ### API Summary
219
+
220
+ | Method | Description |
221
+ |--------|-------------|
222
+ | `publish(topic, payload?)` | Fire-and-forget message to all current subscribers of the exact topic. |
223
+ | `subscribe(topic, handler)` | Registers a handler; returns an unsubscribe function. |
224
+
225
+ Every message received by a subscriber is a `PluginEventBusMessage`:
226
+
227
+ ```ts
228
+ interface PluginEventBusMessage {
229
+ topic: string; // The topic this message was published to
230
+ payload: unknown; // Independent structured-clone value
231
+ timestamp: number; // Epoch ms when the host dispatched the message
232
+ publisher: {
233
+ pluginName: string; // Publishing plugin's name
234
+ instanceScope: 'operator' | 'global';
235
+ operatorId?: string; // Present when the publisher is operator-scoped
236
+ };
237
+ }
238
+ ```
239
+
240
+ ### Topic Naming Convention
241
+
242
+ Use dot-separated, plugin-prefixed names to avoid collisions between plugins:
243
+
244
+ ```
245
+ <plugin-name>.<domain>.<event>
246
+ ```
247
+
248
+ Examples:
249
+ - `psk-reporter.spot.sent` — a spot was uploaded to PSK Reporter
250
+ - `callsign-filter.match.found` — a callsign matched a filter rule
251
+ - `logbook-sync.upload.complete` — a logbook sync finished
252
+
253
+ Avoid generic names like `update` or `message` — they will collide.
254
+
255
+ ### Basic Usage
256
+
257
+ ```ts
258
+ import { definePlugin } from '@tx5dr/plugin-api';
259
+
260
+ // Publisher plugin
261
+ const publisher = definePlugin({
262
+ apiVersion: 2,
263
+ name: 'spot-monitor',
264
+ version: '1.0.0',
265
+ type: 'utility',
266
+ permissions: ['plugin:event-bus'],
267
+ hooks: {
268
+ onDecode(messages, ctx) {
269
+ for (const msg of messages) {
270
+ ctx.eventBus.publish('spot-monitor.new-spot', {
271
+ callsign: msg.callsign,
272
+ frequency: msg.frequencyHz,
273
+ });
274
+ }
275
+ },
276
+ },
277
+ });
278
+
279
+ // Subscriber plugin
280
+ const subscriber = definePlugin({
281
+ apiVersion: 2,
282
+ name: 'spot-logger',
283
+ version: '1.0.0',
284
+ type: 'utility',
285
+ permissions: ['plugin:event-bus'],
286
+ hooks: {
287
+ onLoad(ctx) {
288
+ ctx.eventBus.subscribe('spot-monitor.new-spot', (message) => {
289
+ ctx.log.info('received spot', {
290
+ from: message.publisher.pluginName,
291
+ callsign: (message.payload as any).callsign,
292
+ });
293
+ });
294
+ },
295
+ },
296
+ });
297
+ ```
298
+
299
+ ### Cross-Operator Communication
300
+
301
+ Operator-scoped plugins can communicate across operators on the same host. The `publisher` metadata lets subscribers identify which operator sent the message:
302
+
303
+ ```ts
304
+ ctx.eventBus.subscribe('qso-monitor.qso-complete', (message) => {
305
+ const { callsign, band } = message.payload as any;
306
+ ctx.log.info('QSO completed by another operator', {
307
+ operator: message.publisher.operatorId,
308
+ callsign,
309
+ band,
310
+ });
311
+ });
312
+ ```
313
+
314
+ ### Lifecycle and Error Handling
315
+
316
+ - **Auto-cleanup**: the host removes all subscriptions when a plugin instance unloads. No manual cleanup required.
317
+ - **Manual unsubscribe**: call the function returned by `subscribe()` to cancel a single subscription early.
318
+ - **Error isolation**: subscriber exceptions (sync or async) are captured and logged by the host. They never propagate back to the publisher.
319
+ - **Delivery order**: subscribers receive messages in registration order. Async handlers are awaited, but the publisher does not wait for completion.
320
+
321
+ ### Testing
322
+
323
+ Use `createMockEventBus()` from `@tx5dr/plugin-api/testing` to test plugin event bus logic in isolation:
324
+
325
+ ```ts
326
+ import { describe, it, expect, vi } from 'vitest';
327
+ import { createMockEventBus } from '@tx5dr/plugin-api/testing';
328
+
329
+ it('publishes spot data', () => {
330
+ const bus = createMockEventBus({ owner: { pluginName: 'spot-monitor' } });
331
+ const handler = vi.fn();
332
+
333
+ bus.subscribe('spot-monitor.new-spot', handler);
334
+ bus.publish('spot-monitor.new-spot', { callsign: 'W1AW', frequency: 14074000 });
335
+
336
+ expect(handler).toHaveBeenCalledTimes(1);
337
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({
338
+ topic: 'spot-monitor.new-spot',
339
+ payload: { callsign: 'W1AW', frequency: 14074000 },
340
+ publisher: expect.objectContaining({ pluginName: 'spot-monitor' }),
341
+ }));
342
+ });
343
+
344
+ it('tracks published messages', () => {
345
+ const bus = createMockEventBus();
346
+
347
+ bus.publish('topic-a', { value: 1 });
348
+ bus.publish('topic-b', { value: 2 });
349
+
350
+ expect(bus._published).toHaveLength(2);
351
+ expect(bus._published[0].topic).toBe('topic-a');
352
+ });
353
+
354
+ it('unsubscribe prevents further delivery', () => {
355
+ const bus = createMockEventBus();
356
+ const handler = vi.fn();
357
+
358
+ const unsub = bus.subscribe('topic', handler);
359
+ bus.publish('topic', 'first');
360
+ unsub();
361
+ bus.publish('topic', 'second');
362
+
363
+ expect(handler).toHaveBeenCalledTimes(1);
364
+ });
365
+ ```
366
+
367
+ The mock records all published messages in `_published` and exposes the internal `_subscriptions` map for advanced inspection.
368
+
112
369
  ## Bridge SDK Types
113
370
 
114
371
  Plugin iframe pages communicate with the host via the Bridge SDK (`window.tx5dr`), which is automatically injected by the host. To get IDE autocomplete for the Bridge SDK, add the type reference to your project:
@@ -29,4 +29,44 @@ describe('ADIF QSO mode projection', () => {
29
29
  expect(parsed?.submode).toBe('USB');
30
30
  });
31
31
  });
32
+ describe('ADIF QSO comments', () => {
33
+ it('exports WSJT-X compatible signal reports in COMMENT', () => {
34
+ const adif = convertQSOToADIF(createQso({
35
+ mode: 'FT8',
36
+ submode: undefined,
37
+ reportSent: '-12',
38
+ reportReceived: '-09',
39
+ }));
40
+ expect(adif).toMatch(/<comment:\d+>FT8 {2}Sent: -12 {2}Rcvd: -09/);
41
+ });
42
+ it('keeps operator comments after the signal report COMMENT prefix', () => {
43
+ const adif = convertQSOToADIF(createQso({
44
+ mode: 'FT8',
45
+ submode: undefined,
46
+ reportSent: '-12',
47
+ reportReceived: '-09',
48
+ comment: 'TU',
49
+ }));
50
+ expect(adif).toMatch(/<comment:\d+>FT8 {2}Sent: -12 {2}Rcvd: -09 \| TU/);
51
+ });
52
+ it('stores message history in a TX-5DR private field instead of COMMENT', () => {
53
+ const adif = convertQSOToADIF(createQso({
54
+ mode: 'FT8',
55
+ submode: undefined,
56
+ reportSent: undefined,
57
+ reportReceived: undefined,
58
+ messageHistory: ['CQ TEST', 'RR73'],
59
+ }));
60
+ expect(adif).toMatch(/<app_tx5dr_message_history:\d+>CQ TEST \| RR73/);
61
+ expect(adif).not.toMatch(/<comment:\d+>CQ TEST/);
62
+ });
63
+ it('parses private message history and legacy COMMENT history', () => {
64
+ const parsedPrivate = parseADIFRecord('<call:6>N0CALL<qso_date:8>20260417<time_on:6>120000<mode:3>FT8<freq:9>14.074000<comment:25>FT8 Sent: -12 Rcvd: -09<app_tx5dr_message_history:14>CQ TEST | RR73<eor>', 'test');
65
+ const parsedLegacy = parseADIFRecord('<call:6>N0CALL<qso_date:8>20260417<time_on:6>120000<mode:3>FT8<freq:9>14.074000<comment:14>CQ TEST | RR73<eor>', 'test');
66
+ expect(parsedPrivate?.comment).toBe('FT8 Sent: -12 Rcvd: -09');
67
+ expect(parsedPrivate?.messageHistory).toEqual(['CQ TEST', 'RR73']);
68
+ expect(parsedLegacy?.comment).toBe('CQ TEST | RR73');
69
+ expect(parsedLegacy?.messageHistory).toEqual(['CQ TEST', 'RR73']);
70
+ });
71
+ });
32
72
  //# sourceMappingURL=adif.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"adif.test.js","sourceRoot":"","sources":["../../src/__tests__/adif.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGrE,SAAS,SAAS,CAAC,YAAgC,EAAE;IACnD,OAAO;QACL,EAAE,EAAE,WAAW;QACf,QAAQ,EAAE,QAAQ;QAClB,SAAS,EAAE,UAAU;QACrB,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,KAAK;QACd,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC;QACjD,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC;QAC/C,UAAU,EAAE,IAAI;QAChB,cAAc,EAAE,IAAI;QACpB,cAAc,EAAE,EAAE;QAClB,UAAU,EAAE,QAAQ;QACpB,MAAM,EAAE,QAAQ;QAChB,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,IAAI,GAAG,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAC;QAE3C,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,eAAe,CAC5B,sFAAsF,EACtF,MAAM,CACP,CAAC;QAEF,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"adif.test.js","sourceRoot":"","sources":["../../src/__tests__/adif.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=capability-context.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-context.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/capability-context.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,99 @@
1
+ import { describe, it } from 'vitest';
2
+ import { definePlugin } from '../definition.js';
3
+ describe('capability-derived plugin contexts', () => {
4
+ it('keeps speculative strategy factories free of command capabilities', () => {
5
+ definePlugin({
6
+ apiVersion: 2,
7
+ name: 'safe-strategy',
8
+ version: '1.0.0',
9
+ type: 'strategy',
10
+ permissions: ['operator:transmit-control', 'radio:control'],
11
+ createStrategyRuntime(ctx) {
12
+ // @ts-expect-error strategy factories receive only the speculative read context
13
+ void ctx.operatorCommands;
14
+ // @ts-expect-error strategy factories cannot capture radio command ports
15
+ void ctx.radioCommands;
16
+ throw new Error('type-only fixture');
17
+ },
18
+ isAutoCallEnabled: () => true,
19
+ });
20
+ });
21
+ it('keeps unload cleanup free of runtime command capabilities', () => {
22
+ definePlugin({
23
+ apiVersion: 2,
24
+ name: 'safe-cleanup',
25
+ version: '1.0.0',
26
+ type: 'utility',
27
+ permissions: ['operator:transmit-control', 'radio:control', 'network'],
28
+ onUnload(ctx) {
29
+ void ctx.store.global.flush();
30
+ ctx.timers.clearAll();
31
+ void ctx.files;
32
+ // @ts-expect-error unload cannot submit operator mutations
33
+ void ctx.operatorCommands;
34
+ // @ts-expect-error unload cannot control the physical radio
35
+ void ctx.radioCommands;
36
+ // @ts-expect-error unload cannot retain network capabilities
37
+ void ctx.network;
38
+ },
39
+ isAutoCallEnabled: () => true,
40
+ });
41
+ });
42
+ it('never exposes raw physical transmission primitives, even with command capabilities', () => {
43
+ definePlugin({
44
+ apiVersion: 2,
45
+ name: 'host-arbitrated-transmit-control',
46
+ version: '1.0.0',
47
+ type: 'utility',
48
+ permissions: ['operator:transmit-control', 'radio:read', 'radio:control', 'radio:power'],
49
+ hooks: {
50
+ async onUserAction(_actionId, _payload, ctx) {
51
+ await ctx.operatorCommands.submit({ type: 'stop-automation' });
52
+ await ctx.radioCommands.submit({ type: 'set-frequency', frequency: 14_074_000 });
53
+ // @ts-expect-error physical PTT ownership is never a plugin capability
54
+ void ctx.setPTT;
55
+ // @ts-expect-error audio playback is owned by PhysicalTxCoordinator
56
+ void ctx.playAudio;
57
+ // @ts-expect-error frame mixing is an internal coordinator detail
58
+ void ctx.audioMixer;
59
+ // @ts-expect-error encoding is submitted through host intents only
60
+ void ctx.encoder;
61
+ // @ts-expect-error global emergency stop is reserved for authenticated host commands
62
+ void ctx.forceStopTransmission;
63
+ },
64
+ },
65
+ isAutoCallEnabled: () => true,
66
+ });
67
+ });
68
+ it('requires an explicit tuner capability for tuner RF actions', () => {
69
+ definePlugin({
70
+ apiVersion: 2,
71
+ name: 'frequency-only',
72
+ version: '1.0.0',
73
+ type: 'utility',
74
+ permissions: ['radio:control'],
75
+ hooks: {
76
+ onUserAction(_actionId, _payload, ctx) {
77
+ void ctx.radioCommands.submit({ type: 'set-frequency', frequency: 7_074_000 });
78
+ // @ts-expect-error tuner RF actions require radio:tuner-control
79
+ void ctx.radioTunerCommands;
80
+ },
81
+ },
82
+ });
83
+ definePlugin({
84
+ apiVersion: 2,
85
+ name: 'tuner-controller',
86
+ version: '1.0.0',
87
+ type: 'utility',
88
+ permissions: ['radio:tuner-control'],
89
+ hooks: {
90
+ onUserAction(_actionId, _payload, ctx) {
91
+ void ctx.radioTunerCommands.submit({ type: 'start-manual-tune' });
92
+ // @ts-expect-error tuner control does not grant frequency control
93
+ void ctx.radioCommands;
94
+ },
95
+ },
96
+ });
97
+ });
98
+ });
99
+ //# sourceMappingURL=capability-context.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-context.test.js","sourceRoot":"","sources":["../../src/__tests__/capability-context.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAU,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,YAAY,CAAC;YACX,UAAU,EAAE,CAAC;YACb,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,UAAU;YAChB,WAAW,EAAE,CAAC,2BAA2B,EAAE,eAAe,CAAC;YAC3D,qBAAqB,CAAC,GAAG;gBACvB,gFAAgF;gBAChF,KAAK,GAAG,CAAC,gBAAgB,CAAC;gBAC1B,yEAAyE;gBACzE,KAAK,GAAG,CAAC,aAAa,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;YACvC,CAAC;YACD,iBAAiB,EAAE,GAAG,EAAE,CAAC,IAAI;SAC9B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,YAAY,CAAC;YACX,UAAU,EAAE,CAAC;YACb,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,CAAC,2BAA2B,EAAE,eAAe,EAAE,SAAS,CAAC;YACtE,QAAQ,CAAC,GAAG;gBACV,KAAK,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBAC9B,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACtB,KAAK,GAAG,CAAC,KAAK,CAAC;gBACf,2DAA2D;gBAC3D,KAAK,GAAG,CAAC,gBAAgB,CAAC;gBAC1B,4DAA4D;gBAC5D,KAAK,GAAG,CAAC,aAAa,CAAC;gBACvB,6DAA6D;gBAC7D,KAAK,GAAG,CAAC,OAAO,CAAC;YACnB,CAAC;YACD,iBAAiB,EAAE,GAAG,EAAE,CAAC,IAAI;SAC9B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oFAAoF,EAAE,GAAG,EAAE;QAC5F,YAAY,CAAC;YACX,UAAU,EAAE,CAAC;YACb,IAAI,EAAE,kCAAkC;YACxC,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,CAAC,2BAA2B,EAAE,YAAY,EAAE,eAAe,EAAE,aAAa,CAAC;YACxF,KAAK,EAAE;gBACL,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG;oBACzC,MAAM,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;oBAC/D,MAAM,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;oBAEjF,uEAAuE;oBACvE,KAAK,GAAG,CAAC,MAAM,CAAC;oBAChB,oEAAoE;oBACpE,KAAK,GAAG,CAAC,SAAS,CAAC;oBACnB,kEAAkE;oBAClE,KAAK,GAAG,CAAC,UAAU,CAAC;oBACpB,mEAAmE;oBACnE,KAAK,GAAG,CAAC,OAAO,CAAC;oBACjB,qFAAqF;oBACrF,KAAK,GAAG,CAAC,qBAAqB,CAAC;gBACjC,CAAC;aACF;YACD,iBAAiB,EAAE,GAAG,EAAE,CAAC,IAAI;SAC9B,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,YAAY,CAAC;YACX,UAAU,EAAE,CAAC;YACb,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,CAAC,eAAe,CAAC;YAC9B,KAAK,EAAE;gBACL,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG;oBACnC,KAAK,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;oBAC/E,gEAAgE;oBAChE,KAAK,GAAG,CAAC,kBAAkB,CAAC;gBAC9B,CAAC;aACF;SACF,CAAC,CAAC;QAEH,YAAY,CAAC;YACX,UAAU,EAAE,CAAC;YACb,IAAI,EAAE,kBAAkB;YACxB,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,CAAC,qBAAqB,CAAC;YACpC,KAAK,EAAE;gBACL,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG;oBACnC,KAAK,GAAG,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC,CAAC;oBAClE,kEAAkE;oBAClE,KAAK,GAAG,CAAC,aAAa,CAAC;gBACzB,CAAC;aACF;SACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AAEL,CAAC,CAAC,CAAC"}