@deadair/plugin-sdk 0.2.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1062 -0
  3. package/dist/article.parse.d.ts +82 -0
  4. package/dist/article.parse.d.ts.map +1 -0
  5. package/dist/boundary.json.safe.d.ts +191 -0
  6. package/dist/boundary.json.safe.d.ts.map +1 -0
  7. package/dist/capabilities/analysis.d.ts +330 -0
  8. package/dist/capabilities/analysis.d.ts.map +1 -0
  9. package/dist/capabilities/charts.d.ts +134 -0
  10. package/dist/capabilities/charts.d.ts.map +1 -0
  11. package/dist/capabilities/enrichment.d.ts +255 -0
  12. package/dist/capabilities/enrichment.d.ts.map +1 -0
  13. package/dist/capabilities/llm.d.ts +318 -0
  14. package/dist/capabilities/llm.d.ts.map +1 -0
  15. package/dist/capabilities/mixer.d.ts +183 -0
  16. package/dist/capabilities/mixer.d.ts.map +1 -0
  17. package/dist/capabilities/music.provider.d.ts +245 -0
  18. package/dist/capabilities/music.provider.d.ts.map +1 -0
  19. package/dist/capabilities/news.d.ts +171 -0
  20. package/dist/capabilities/news.d.ts.map +1 -0
  21. package/dist/capabilities/scrobble.d.ts +133 -0
  22. package/dist/capabilities/scrobble.d.ts.map +1 -0
  23. package/dist/capabilities/search.d.ts +122 -0
  24. package/dist/capabilities/search.d.ts.map +1 -0
  25. package/dist/capabilities/similarity.d.ts +101 -0
  26. package/dist/capabilities/similarity.d.ts.map +1 -0
  27. package/dist/capabilities/speech.d.ts +211 -0
  28. package/dist/capabilities/speech.d.ts.map +1 -0
  29. package/dist/capabilities/weather.d.ts +192 -0
  30. package/dist/capabilities/weather.d.ts.map +1 -0
  31. package/dist/chunk-7QVYU63E.js +7 -0
  32. package/dist/chunk-7QVYU63E.js.map +1 -0
  33. package/dist/define.plugin.d.ts +56 -0
  34. package/dist/define.plugin.d.ts.map +1 -0
  35. package/dist/feed.parse.d.ts +97 -0
  36. package/dist/feed.parse.d.ts.map +1 -0
  37. package/dist/html.text.d.ts +71 -0
  38. package/dist/html.text.d.ts.map +1 -0
  39. package/dist/index.d.ts +30 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +1153 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/match.text.d.ts +28 -0
  44. package/dist/match.text.d.ts.map +1 -0
  45. package/dist/plugin.api.version.d.ts +9 -0
  46. package/dist/plugin.api.version.d.ts.map +1 -0
  47. package/dist/plugin.base.d.ts +80 -0
  48. package/dist/plugin.base.d.ts.map +1 -0
  49. package/dist/plugin.config.fields.d.ts +524 -0
  50. package/dist/plugin.config.fields.d.ts.map +1 -0
  51. package/dist/plugin.config.read.d.ts +52 -0
  52. package/dist/plugin.config.read.d.ts.map +1 -0
  53. package/dist/plugin.error.d.ts +177 -0
  54. package/dist/plugin.error.d.ts.map +1 -0
  55. package/dist/plugin.host.d.ts +220 -0
  56. package/dist/plugin.host.d.ts.map +1 -0
  57. package/dist/plugin.host.response.d.ts +42 -0
  58. package/dist/plugin.host.response.d.ts.map +1 -0
  59. package/dist/plugin.http.d.ts +80 -0
  60. package/dist/plugin.http.d.ts.map +1 -0
  61. package/dist/plugin.lifecycle.d.ts +68 -0
  62. package/dist/plugin.lifecycle.d.ts.map +1 -0
  63. package/dist/plugin.manifest.d.ts +282 -0
  64. package/dist/plugin.manifest.d.ts.map +1 -0
  65. package/dist/plugin.permissions.d.ts +186 -0
  66. package/dist/plugin.permissions.d.ts.map +1 -0
  67. package/dist/testing/fake.plugin.host.d.ts +85 -0
  68. package/dist/testing/fake.plugin.host.d.ts.map +1 -0
  69. package/dist/testing/index.d.ts +10 -0
  70. package/dist/testing/index.d.ts.map +1 -0
  71. package/dist/testing/index.js +141 -0
  72. package/dist/testing/index.js.map +1 -0
  73. package/package.json +67 -0
@@ -0,0 +1,68 @@
1
+ import type { ConfigFieldOption } from './plugin.config.fields.js';
2
+ import type { PluginHost } from './plugin.host.js';
3
+ /** Result of a "does this configuration actually work?" probe. */
4
+ export interface PluginConnectionResult {
5
+ ok: boolean;
6
+ /** Short operator-facing explanation, shown in the settings UI either way. */
7
+ message?: string;
8
+ }
9
+ /**
10
+ * The part of a plugin every plugin implements, whatever its kind.
11
+ *
12
+ * The host calls `init` exactly once per instance, before any capability
13
+ * method. `dispose` is called on unload, config change, or shutdown.
14
+ */
15
+ export interface PluginLifecycle {
16
+ init(host: PluginHost): Promise<void>;
17
+ /**
18
+ * Called from the settings UI's "Test connection" button.
19
+ *
20
+ * It is also how a quarantined plugin gets back on the station. The host
21
+ * runs it even while your plugin's breaker is open, and `ok: true` closes
22
+ * the breaker, so reach the provider with the saved credentials rather than
23
+ * checking that a setting is present. Report a failure as `ok: false` with a
24
+ * message: what the host counts as healthy is `ok`, not the call resolving.
25
+ *
26
+ * The host also calls it on its own while your plugin is quarantined by a
27
+ * failure retrying could end: after a minute, then on a backoff to one call
28
+ * every half hour. So keep it to one cheap request, and never to anything
29
+ * that changes state on the provider.
30
+ */
31
+ testConnection?(): Promise<PluginConnectionResult>;
32
+ /**
33
+ * Live choices for your config fields, keyed by field key.
34
+ *
35
+ * The settings form asks once when it opens, and again when the operator hits
36
+ * refresh. A field that comes back with options is rendered as a picker: a
37
+ * `string` or `url` becomes free text WITH suggestions, so a value you could
38
+ * not enumerate is still typeable, and a `select` or `multiselect` has its
39
+ * declared options replaced by these.
40
+ *
41
+ * ## Why this exists
42
+ *
43
+ * `ConfigField.options` is fixed when the manifest is written, which is fine
44
+ * for a closed set and useless for anything the operator's own server decides.
45
+ * Without it the only way to learn what a server offers is to read it out of a
46
+ * "Test connection" message and type it back in, which is a setup loop with no
47
+ * way in: the address cannot be tested before it is saved, so a field that
48
+ * requires one of those values cannot be filled before the address is.
49
+ *
50
+ * ## Notes
51
+ *
52
+ * Implementing this IS the opt-in; there is nothing to declare on the field.
53
+ * Return a map rather than answering per field so the form costs one call
54
+ * however many fields you have, and omit a key you have nothing to say about
55
+ * rather than returning an empty array for it.
56
+ *
57
+ * It runs against your SAVED config, like `testConnection` does, because that
58
+ * is the config your instance was loaded with. So the shape of a first-time
59
+ * setup is: save what you can, refresh, choose from what comes back.
60
+ *
61
+ * Answer with what you have rather than throwing when an upstream is
62
+ * unreachable: a field with no suggestions is still a field somebody can type
63
+ * into, and a throw costs them the whole form.
64
+ */
65
+ suggestConfigOptions?(): Promise<Record<string, ConfigFieldOption[]>>;
66
+ dispose?(): Promise<void>;
67
+ }
68
+ //# sourceMappingURL=plugin.lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.lifecycle.d.ts","sourceRoot":"","sources":["../src/plugin.lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,kEAAkE;AAClE,MAAM,WAAW,sBAAsB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC5B,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEtC;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,oBAAoB,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;IAEtE,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B"}
@@ -0,0 +1,282 @@
1
+ import { z } from 'zod';
2
+ import { type ConfigField } from './plugin.config.fields.js';
3
+ import { type PluginPermissions } from './plugin.permissions.js';
4
+ /**
5
+ * What a plugin can do, and the only thing the host ever dispatches on.
6
+ *
7
+ * There is no second axis. A manifest used to also carry a `kind`
8
+ * (`music-provider`, `enrichment`, `tts`) which nothing checked: every call
9
+ * site asked the capability list, because a plugin that declares a kind and
10
+ * forgets the method is a `TypeError` mid-request. So the label went and this
11
+ * is what is left.
12
+ */
13
+ export declare const PLUGIN_CAPABILITY_CATALOG = "catalog";
14
+ /**
15
+ * The plugin can get the station audio to play (`resolveStreamUrl`). Separate
16
+ * from {@link PLUGIN_CAPABILITY_CATALOG} so a manifest can say it: "will fetch
17
+ * audio from your server" is a thing an operator should read before installing,
18
+ * and it used to be invisible.
19
+ */
20
+ export declare const PLUGIN_CAPABILITY_STREAM = "stream";
21
+ /**
22
+ * The plugin owns its own audio output and deadair only tells it what to do.
23
+ * The opposite of deadair's `playout` module, which is why it is not called
24
+ * that.
25
+ */
26
+ export declare const PLUGIN_CAPABILITY_STEER = "steer";
27
+ export declare const PLUGIN_CAPABILITY_OAUTH = "oauth";
28
+ export declare const PLUGIN_CAPABILITY_ENRICHMENT = "enrichment";
29
+ /** The plugin can say something out loud: text in, audio out. */
30
+ export declare const PLUGIN_CAPABILITY_SPEECH = "speech";
31
+ /**
32
+ * The plugin can produce words: a conversation in, text out.
33
+ *
34
+ * A transport rather than a writer. What to say is the station's business, which
35
+ * is why this capability knows nothing about breaks, shows or running orders.
36
+ */
37
+ export declare const PLUGIN_CAPABILITY_LLM = "llm";
38
+ /**
39
+ * The plugin can measure a track's audio: bytes in, offsets out.
40
+ *
41
+ * Separate from {@link PLUGIN_CAPABILITY_ENRICHMENT} because the two answer
42
+ * different kinds of question. Enrichment asks an upstream what it knows and
43
+ * merges several answers; this computes one answer from the samples, and no
44
+ * upstream sells it.
45
+ */
46
+ export declare const PLUGIN_CAPABILITY_ANALYSIS = "analysis";
47
+ /**
48
+ * The plugin can make one piece of audio out of several: parts in, audio out.
49
+ *
50
+ * Separate from {@link PLUGIN_CAPABILITY_ANALYSIS}, and NOT because the work is
51
+ * different: both need decoded PCM, and the bundled adapter serves both off one
52
+ * sidecar. It is separate because **a capability is the unit of SELECTION**. The
53
+ * host picks one plugin per capability, so a joiner carried as an optional method
54
+ * on the analyzer is the analyzer the operator chose to MEASURE with — install
55
+ * one that measures better and cannot join, name it, and joining stops with
56
+ * nothing to do about it but choose a worse analyzer. Two keys is what lets a
57
+ * station measure with one engine and mix with another.
58
+ *
59
+ * One plugin may of course declare both, and the bundled one does.
60
+ */
61
+ export declare const PLUGIN_CAPABILITY_MIXER = "mixer";
62
+ /**
63
+ * The plugin can say what is popular: a chart id in, an ordered list of names
64
+ * out.
65
+ *
66
+ * Separate from {@link PLUGIN_CAPABILITY_ENRICHMENT} because it is not a fact
67
+ * about a record the station holds — it is an opinion about records in general,
68
+ * most of which the library has never seen. And separate from
69
+ * {@link PLUGIN_CAPABILITY_CATALOG} because a chart is not a source of audio:
70
+ * naming a record is the whole of what it does.
71
+ */
72
+ export declare const PLUGIN_CAPABILITY_CHARTS = "charts";
73
+ /**
74
+ * The plugin can say what happened outside the station: a feed in, published
75
+ * entries out.
76
+ *
77
+ * Separate from {@link PLUGIN_CAPABILITY_CHARTS} even though both read somebody
78
+ * else's document, because what comes back is not about records at all. A chart
79
+ * entry is a name the pick path can turn into something that airs; a news item
80
+ * is a fact, and the only thing that can be done with it is say it.
81
+ */
82
+ export declare const PLUGIN_CAPABILITY_NEWS = "news";
83
+ /**
84
+ * The plugin can say who else sounds like this: an artist in, artists out.
85
+ *
86
+ * Separate from {@link PLUGIN_CAPABILITY_ENRICHMENT} for the reason
87
+ * `capabilities/similarity.ts` gives at length: enrichment describes rows the
88
+ * catalog holds, and the artists worth asking about here are the ones it does
89
+ * not.
90
+ */
91
+ export declare const PLUGIN_CAPABILITY_SIMILARITY = "similarity";
92
+ /**
93
+ * The plugin can ask the open web a question: words in, pages out.
94
+ *
95
+ * Separate from {@link PLUGIN_CAPABILITY_NEWS}, which is the other capability
96
+ * that answers about the world, because the two are asked different things. News
97
+ * serves a MENU an operator assembled and answers "what happened"; this is given
98
+ * a subject the caller chose and answers "what does the web say about it", with
99
+ * nothing stable to de-duplicate against because no two calls ask the same
100
+ * question.
101
+ *
102
+ * And separate from {@link PLUGIN_CAPABILITY_CATALOG} for the reason
103
+ * {@link PLUGIN_CAPABILITY_CHARTS} is: a result is a page, and a page cannot be
104
+ * played. Looking for something to PLAY is `searchTracks` on the catalog.
105
+ */
106
+ export declare const PLUGIN_CAPABILITY_SEARCH = "search";
107
+ /**
108
+ * The plugin can say what it is like outside: a place in, measurements out.
109
+ *
110
+ * The third capability that answers about the world, and separate from both of
111
+ * the others because it is asked a different question. News serves a menu
112
+ * somebody assembled and answers "what happened"; search takes words a caller
113
+ * made up and answers "what does the web say". This is asked about one PLACE,
114
+ * and what comes back is numbers rather than sentences — the station does
115
+ * arithmetic on them and then decides what to say, where a headline is already
116
+ * the words.
117
+ *
118
+ * A capability rather than a general-purpose tool the model calls, which was the
119
+ * older plan: a plugin answering a question the STATION has is a capability, and
120
+ * this station wants a reading for the moment it is playing into as well as for
121
+ * the presenter to mention. See [tool-plugins](https://github.com/robert-dean/deadair/discussions/44).
122
+ */
123
+ export declare const PLUGIN_CAPABILITY_WEATHER = "weather";
124
+ /**
125
+ * The plugin can report what the station played to somebody else's service.
126
+ *
127
+ * The only capability that SENDS. Everything else here reads an upstream; this
128
+ * publishes the operator's own listening to an account they hold, which is why
129
+ * the SDK gives it a way to be declined per installation rather than assuming
130
+ * that installing a plugin is consent to broadcast from it.
131
+ */
132
+ export declare const PLUGIN_CAPABILITY_SCROBBLE = "scrobble";
133
+ export declare const KNOWN_PLUGIN_CAPABILITIES: readonly ["catalog", "stream", "steer", "oauth", "enrichment", "speech", "llm", "analysis", "mixer", "charts", "news", "similarity", "search", "weather", "scrobble"];
134
+ export type KnownPluginCapability = (typeof KNOWN_PLUGIN_CAPABILITIES)[number];
135
+ /**
136
+ * Known capabilities get autocomplete; the type stays open so a plugin built
137
+ * against a newer host can declare one this SDK has never heard of without
138
+ * failing manifest validation.
139
+ */
140
+ export type PluginCapability = KnownPluginCapability | (string & Record<never, never>);
141
+ /**
142
+ * A zod schema instance. Typed loosely so a plugin can hand over any schema
143
+ * shape (object, union, refined object) without fighting the compiler.
144
+ */
145
+ export type PluginConfigSchema = z.ZodType;
146
+ /**
147
+ * Duck-typed "is this a zod schema?". Deliberately not a bare `instanceof`:
148
+ * a plugin may resolve its own copy of zod, and `instanceof` fails across
149
+ * module instances.
150
+ */
151
+ export declare function isZodSchema(value: unknown): value is PluginConfigSchema;
152
+ /**
153
+ * Everything the host needs to know about a plugin before it runs any of the
154
+ * plugin's code: who it is, what it can do, what it needs permission for, and
155
+ * what to ask the operator for.
156
+ */
157
+ export interface PluginManifest {
158
+ /** Reverse-DNS identifier, e.g. `deadair.spotify`. Globally unique, stable across versions. */
159
+ id: string;
160
+ /** Display name, e.g. `Spotify`. */
161
+ name: string;
162
+ /** Semver version of the plugin itself. */
163
+ version: string;
164
+ /** Which capability interfaces the factory result actually implements. */
165
+ capabilities: PluginCapability[];
166
+ /**
167
+ * Semver RANGE of the plugin API this plugin works against, e.g. `^1.0.0`.
168
+ * Compared against {@link PLUGIN_API_VERSION} at load time.
169
+ */
170
+ apiVersion: string;
171
+ description?: string;
172
+ homepage?: string;
173
+ /** Data URI or absolute https URL of a small square icon. */
174
+ icon?: string;
175
+ permissions: PluginPermissions;
176
+ /** Declarative settings form. */
177
+ configFields: ConfigField[];
178
+ /**
179
+ * Server-side validation of the submitted config. The host parses the
180
+ * operator's submission with this before storing it, so a plugin never
181
+ * has to defend against malformed config at runtime.
182
+ */
183
+ configSchema: PluginConfigSchema;
184
+ }
185
+ /**
186
+ * Validates everything in a manifest except the contents of `configSchema`,
187
+ * which is only checked to be a zod schema instance.
188
+ */
189
+ export declare const pluginManifestSchema: z.ZodObject<{
190
+ id: z.ZodString;
191
+ name: z.ZodString;
192
+ version: z.ZodString;
193
+ capabilities: z.ZodArray<z.ZodString>;
194
+ apiVersion: z.ZodString;
195
+ description: z.ZodOptional<z.ZodString>;
196
+ homepage: z.ZodOptional<z.ZodString>;
197
+ icon: z.ZodOptional<z.ZodString>;
198
+ permissions: z.ZodObject<{
199
+ network: z.ZodArray<z.ZodType<import("./plugin.permissions.js").NetworkPermission, unknown, z.core.$ZodTypeInternals<import("./plugin.permissions.js").NetworkPermission, unknown>>>;
200
+ storage: z.ZodBoolean;
201
+ oauth: z.ZodBoolean;
202
+ trackFetcher: z.ZodOptional<z.ZodBoolean>;
203
+ grants: z.ZodOptional<z.ZodArray<z.ZodType<import("./plugin.permissions.js").PluginGrantRequest, unknown, z.core.$ZodTypeInternals<import("./plugin.permissions.js").PluginGrantRequest, unknown>>>>;
204
+ }, z.core.$strip>;
205
+ configFields: z.ZodArray<z.ZodObject<{
206
+ key: z.ZodString;
207
+ label: z.ZodString;
208
+ type: z.ZodEnum<{
209
+ string: "string";
210
+ number: "number";
211
+ boolean: "boolean";
212
+ text: "text";
213
+ url: "url";
214
+ secret: "secret";
215
+ select: "select";
216
+ multiselect: "multiselect";
217
+ list: "list";
218
+ note: "note";
219
+ }>;
220
+ required: z.ZodOptional<z.ZodBoolean>;
221
+ default: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
222
+ unit: z.ZodOptional<z.ZodEnum<{
223
+ bytes: "bytes";
224
+ fraction: "fraction";
225
+ }>>;
226
+ control: z.ZodOptional<z.ZodEnum<{
227
+ slider: "slider";
228
+ tags: "tags";
229
+ }>>;
230
+ step: z.ZodOptional<z.ZodNumber>;
231
+ min: z.ZodOptional<z.ZodNumber>;
232
+ max: z.ZodOptional<z.ZodNumber>;
233
+ placeholder: z.ZodOptional<z.ZodString>;
234
+ help: z.ZodOptional<z.ZodString>;
235
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
236
+ value: z.ZodString;
237
+ label: z.ZodString;
238
+ }, z.core.$strip>>>;
239
+ optionsFrom: z.ZodOptional<z.ZodEnum<{
240
+ "station.newsCategories": "station.newsCategories";
241
+ "station.newsFeeds": "station.newsFeeds";
242
+ "intl.timeZones": "intl.timeZones";
243
+ "plugins.speech": "plugins.speech";
244
+ "plugins.llm": "plugins.llm";
245
+ "plugins.mixer": "plugins.mixer";
246
+ "plugins.analysis": "plugins.analysis";
247
+ "llm.models": "llm.models";
248
+ }>>;
249
+ columns: z.ZodOptional<z.ZodArray<z.ZodObject<{
250
+ key: z.ZodString;
251
+ label: z.ZodString;
252
+ type: z.ZodEnum<{
253
+ string: "string";
254
+ url: "url";
255
+ secret: "secret";
256
+ select: "select";
257
+ }>;
258
+ required: z.ZodOptional<z.ZodBoolean>;
259
+ placeholder: z.ZodOptional<z.ZodString>;
260
+ options: z.ZodOptional<z.ZodArray<z.ZodObject<{
261
+ value: z.ZodString;
262
+ label: z.ZodString;
263
+ }, z.core.$strip>>>;
264
+ optionsFrom: z.ZodOptional<z.ZodEnum<{
265
+ "station.newsCategories": "station.newsCategories";
266
+ "station.newsFeeds": "station.newsFeeds";
267
+ "intl.timeZones": "intl.timeZones";
268
+ "plugins.speech": "plugins.speech";
269
+ "plugins.llm": "plugins.llm";
270
+ "plugins.mixer": "plugins.mixer";
271
+ "plugins.analysis": "plugins.analysis";
272
+ "llm.models": "llm.models";
273
+ }>>;
274
+ dependsOn: z.ZodOptional<z.ZodString>;
275
+ dependsOnValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
276
+ }, z.core.$strip>>>;
277
+ dependsOn: z.ZodOptional<z.ZodString>;
278
+ rangeWith: z.ZodOptional<z.ZodString>;
279
+ }, z.core.$strip>>;
280
+ configSchema: z.ZodCustom<PluginConfigSchema, PluginConfigSchema>;
281
+ }, z.core.$strip>;
282
+ //# sourceMappingURL=plugin.manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.manifest.d.ts","sourceRoot":"","sources":["../src/plugin.manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAChF,OAAO,EAA2B,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE1F;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,YAAY,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAC/C,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAC/C,eAAO,MAAM,4BAA4B,eAAe,CAAC;AAEzD,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,aAAa,CAAC;AAErD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,SAAS,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,eAAe,CAAC;AAEzD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,wBAAwB,WAAW,CAAC;AAEjD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,yBAAyB,YAAY,CAAC;AAEnD;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,aAAa,CAAC;AAErD,eAAO,MAAM,yBAAyB,uKAgB5B,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE/E;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAEvF;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,OAAO,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,kBAAkB,CAKvE;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC3B,+FAA+F;IAC/F,EAAE,EAAE,MAAM,CAAC;IAEX,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IAEb,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAEhB,0EAA0E;IAC1E,YAAY,EAAE,gBAAgB,EAAE,CAAC;IAEjC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,WAAW,EAAE,iBAAiB,CAAC;IAE/B,iCAAiC;IACjC,YAAY,EAAE,WAAW,EAAE,CAAC;IAE5B;;;;OAIG;IACH,YAAY,EAAE,kBAAkB,CAAC;CACpC;AAKD;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY/B,CAAC"}
@@ -0,0 +1,186 @@
1
+ import { z } from 'zod';
2
+ /** The pacing knobs, shared by both kinds of entry. */
3
+ interface NetworkPermissionPacing {
4
+ /**
5
+ * Requests per second the host will let through to this entry, capped by
6
+ * the host's own ceiling. You can ask to be slower, never faster.
7
+ *
8
+ * Omit it and the host's default applies. Set it and `host.fetch` paces you
9
+ * automatically, parking each call until there is headroom rather than
10
+ * failing it, so there is nothing left for the plugin to implement. The
11
+ * wait is spent from the call's budget, so see `host.remainingMs()` for
12
+ * deciding whether the work still fits.
13
+ */
14
+ ratePerSecond?: number;
15
+ /**
16
+ * Name of the rate-limit bucket this entry draws from. Entries sharing a
17
+ * bucket share one limiter; the default is the hostname the entry resolves
18
+ * to.
19
+ *
20
+ * For when a published limit covers a service rather than a hostname.
21
+ * `musicbrainz.org` and `*.musicbrainz.org` are two entries against one
22
+ * 1 req/s policy, and without a shared bucket declaring both would quietly
23
+ * buy 2 req/s and get the station blocked.
24
+ */
25
+ bucket?: string;
26
+ }
27
+ /**
28
+ * One upstream a plugin may reach, named outright.
29
+ *
30
+ * The bare string shorthand means exactly this with no pacing set. Reach for
31
+ * the object form when the upstream publishes a limit of its own: MusicBrainz
32
+ * allows roughly one request per second to anonymous clients, a tenth of what
33
+ * the host would otherwise let through.
34
+ */
35
+ export interface NetworkPermissionHost extends NetworkPermissionPacing {
36
+ /**
37
+ * Bare hostname (`api.spotify.com`), no scheme and no path. A leading `*.`
38
+ * marks a wildcard subdomain match (`*.example.com`) and does NOT match the
39
+ * bare apex.
40
+ */
41
+ host: string;
42
+ }
43
+ /**
44
+ * One upstream the operator names, not the plugin: the hostname comes from one
45
+ * of your own config fields, resolved when the plugin is initialized.
46
+ *
47
+ * For anything self-hosted or mirrored, where there is no hostname to write
48
+ * down at authoring time. A MusicBrainz mirror, a Navidrome server, an internal
49
+ * API: the plugin declares which setting holds the address and the host reads
50
+ * the hostname out of it.
51
+ *
52
+ * The value is read as a URL, or as a bare hostname if it does not look like
53
+ * one. Empty, unparseable, or wildcard-bearing values simply contribute no
54
+ * entry, so an unconfigured plugin is refused exactly as if it had asked for
55
+ * an undeclared host. Because it resolves at init, changing the setting
56
+ * reinitializes the plugin and the new address takes effect with it.
57
+ *
58
+ * ## One setting, several addresses
59
+ *
60
+ * A setting holding SEVERAL addresses contributes one entry each. That is for
61
+ * the plugin whose upstreams are a list the operator pasted rather than one
62
+ * server they run — a reader of feeds — where there is no honest number of
63
+ * `url` fields to offer.
64
+ *
65
+ * The shape is one address per line (a `text` field), or the JSON array a
66
+ * `multiselect` stores. Where a line carries more than the address, the address
67
+ * is its last `|`-separated field, so `world|World news|https://…/feed.xml`
68
+ * resolves to that host: a list wants labels, and fixing where they go keeps
69
+ * the hostnames readable out of the operator's own text instead of out of a
70
+ * plugin's private parser.
71
+ *
72
+ * Every rule above is applied per address rather than to the value as a whole,
73
+ * so one mistyped line costs its own upstream and not the rest, and a wildcard
74
+ * still cannot arrive from data. Repeats collapse: two feeds at one publisher
75
+ * are one entry, or the second would install a limiter that doubles the rate
76
+ * this entry asked to be paced at.
77
+ */
78
+ export interface NetworkPermissionFromConfig extends NetworkPermissionPacing {
79
+ /** Key of the config field holding the address, e.g. `baseUrl`. */
80
+ fromConfig: string;
81
+ }
82
+ /** A hostname at the host's default rate, or an entry that says more. */
83
+ export type NetworkPermission = string | NetworkPermissionHost | NetworkPermissionFromConfig;
84
+ /**
85
+ * Something a plugin needs that only the OPERATOR can say yes to.
86
+ *
87
+ * The rest of {@link PluginPermissions} is disclosure: a manifest states what it
88
+ * reaches and the host holds it to that, with nobody asked anything. A grant is
89
+ * the other kind — a capability wide enough that a person should decide, per
90
+ * install, with the plugin's own reason in front of them.
91
+ *
92
+ * ## The manifest is the request, and it is the only record of one
93
+ *
94
+ * Nothing is stored when a plugin asks. The host reads this on every discovery
95
+ * and stores only the ANSWER, keyed by plugin and capability, so a plugin whose
96
+ * manifest stops asking simply stops appearing and cannot be re-enabled by a row
97
+ * nobody can see. A plugin that has not been answered is refused: undecided and
98
+ * denied differ on the operator's page and nowhere else.
99
+ *
100
+ * There is no runtime ask. A `host.requestPermission()` would have to block a
101
+ * plugin mid-work on a person who may be asleep, and a plugin that never runs
102
+ * would never appear to be asked about.
103
+ *
104
+ * ## The vocabulary is the host's
105
+ *
106
+ * {@link capability} is one of a fixed list the HOST publishes, because a
107
+ * capability only means something where the host enforces it. An id nothing
108
+ * recognises is ignored with a warning rather than becoming a row that gates
109
+ * nothing — a permission for a door that does not exist is worse than no
110
+ * permission, since it reads on the page as though it were protecting something.
111
+ */
112
+ export interface PluginGrantRequest extends NetworkPermissionPacing {
113
+ /** A capability id the host publishes, e.g. `network.open`. */
114
+ capability: string;
115
+ /**
116
+ * Why this plugin needs it, in one sentence, addressed to the operator.
117
+ *
118
+ * Required, and the field this whole shape exists to carry. It is what the
119
+ * console shows beside the Allow control, and a manifest that cannot say why
120
+ * it wants something does not get to ask for it: the alternative is an
121
+ * operator deciding on a capability id alone, which is a decision nobody can
122
+ * actually make.
123
+ */
124
+ reason: string;
125
+ }
126
+ /**
127
+ * What a plugin is allowed to do. Declared up front in the manifest so the
128
+ * host (and the operator installing the plugin) can see the full blast radius
129
+ * before any plugin code runs.
130
+ */
131
+ export interface PluginPermissions {
132
+ /**
133
+ * Hostname allowlist for `host.fetch()`: any request to a host not listed
134
+ * here is rejected before it leaves the process.
135
+ *
136
+ * This describes what a plugin says it needs, and it is enforced on every
137
+ * call (and every redirect hop) that goes through `host.fetch`. It is not
138
+ * yet enforced against a plugin that reaches for global `fetch` instead,
139
+ * because the host still imports plugin code into its own realm. Read this
140
+ * field as a disclosure an operator can weigh before installing, not as a
141
+ * containment guarantee.
142
+ *
143
+ * Entries are bare hostnames, {@link NetworkPermissionHost} objects when
144
+ * the upstream needs pacing, or {@link NetworkPermissionFromConfig} when
145
+ * the operator is the one who names it. Order matters only for pacing: the
146
+ * first entry a hostname matches supplies its rate and bucket.
147
+ */
148
+ network: NetworkPermission[];
149
+ /** Whether the plugin may use `host.storage` (namespaced key/value state). */
150
+ storage: boolean;
151
+ /** Whether the plugin may use `host.oauth` (redirect URI + token vault). */
152
+ oauth: boolean;
153
+ /**
154
+ * Whether the plugin may use `host.trackFetcher` (lend the station's
155
+ * fetcher a login, get back a URL).
156
+ *
157
+ * Optional, unlike the two above, because almost no plugin wants it: a
158
+ * provider whose audio can be fetched with a URL mints one itself. Making
159
+ * it required would put a `false` in every manifest to disclaim something
160
+ * only one provider has ever needed.
161
+ */
162
+ trackFetcher?: boolean;
163
+ /**
164
+ * Capabilities this plugin is ASKING for, each with the reason an operator
165
+ * reads before deciding. See {@link PluginGrantRequest}.
166
+ *
167
+ * Optional and usually absent: almost every plugin does its whole job inside
168
+ * what it declares above, and a manifest that asks for nothing is the normal
169
+ * case rather than a modest one.
170
+ *
171
+ * Nothing here is granted by declaring it. Until the operator answers, the
172
+ * capability behaves exactly as it does for a plugin that never asked.
173
+ */
174
+ grants?: PluginGrantRequest[];
175
+ }
176
+ export declare const networkPermissionSchema: z.ZodType<NetworkPermission>;
177
+ export declare const grantRequestSchema: z.ZodType<PluginGrantRequest>;
178
+ export declare const pluginPermissionsSchema: z.ZodObject<{
179
+ network: z.ZodArray<z.ZodType<NetworkPermission, unknown, z.core.$ZodTypeInternals<NetworkPermission, unknown>>>;
180
+ storage: z.ZodBoolean;
181
+ oauth: z.ZodBoolean;
182
+ trackFetcher: z.ZodOptional<z.ZodBoolean>;
183
+ grants: z.ZodOptional<z.ZodArray<z.ZodType<PluginGrantRequest, unknown, z.core.$ZodTypeInternals<PluginGrantRequest, unknown>>>>;
184
+ }, z.core.$strip>;
185
+ export {};
186
+ //# sourceMappingURL=plugin.permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.permissions.d.ts","sourceRoot":"","sources":["../src/plugin.permissions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,uDAAuD;AACvD,UAAU,uBAAuB;IAC7B;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,uBAAuB;IAClE;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,WAAW,2BAA4B,SAAQ,uBAAuB;IACxE,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,yEAAyE;AACzE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,qBAAqB,GAAG,2BAA2B,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,kBAAmB,SAAQ,uBAAuB;IAC/D,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAC9B;;;;;;;;;;;;;;;OAeG;IACH,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAE7B,8EAA8E;IAC9E,OAAO,EAAE,OAAO,CAAC;IAEjB,4EAA4E;IAC5E,KAAK,EAAE,OAAO,CAAC;IAEf;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,kBAAkB,EAAE,CAAC;CACjC;AASD,eAAO,MAAM,uBAAuB,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAI/D,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAO3D,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;;;;iBAMlC,CAAC"}
@@ -0,0 +1,85 @@
1
+ import type { HostFetchInit, HostFetchMethod, PluginHost, ProviderStream } from '../index.js';
2
+ /**
3
+ * Shipped from the SDK rather than copied into each plugin, which three of them
4
+ * were doing — two of those carrying a comment saying the copy was deliberate
5
+ * because plugins do not depend on each other.
6
+ *
7
+ * That reasoning held for the copies and does not hold for this: every plugin
8
+ * already depends on the SDK, and `PluginHost` is the SDK's own contract, so its
9
+ * test double belongs beside the interface it doubles. A plugin importing this
10
+ * gains no dependency on any other plugin. It also means a host method added to
11
+ * the contract turns up in one fake rather than being added to three and
12
+ * forgotten in the other three.
13
+ *
14
+ * Reached as `@deadair/plugin-sdk/testing`, which is kept out of the runtime
15
+ * entry so nothing ships a dependency on vitest.
16
+ */
17
+ /** One `host.fetch` call, recorded with only the fields tests care about. */
18
+ export interface RecordedFetchCall {
19
+ url: string;
20
+ method: HostFetchMethod | undefined;
21
+ headers: Record<string, string> | undefined;
22
+ body: string | undefined;
23
+ }
24
+ /**
25
+ * A `PluginHost` for tests. `fetch` replays a queue of scripted
26
+ * `Response`s (FIFO, one per call) — or a custom handler installed
27
+ * with `setFetchImpl`, for call-count-driven scenarios like a 401-then-200
28
+ * retry — and records every call it received in `calls`. `storage`, `config`,
29
+ * `secrets` and the `oauth` vault are in-memory and can be seeded or read
30
+ * directly, bypassing the `PluginHost` methods, so a test can assert on state
31
+ * without going through the code under test twice.
32
+ */
33
+ export interface FakePluginHost extends PluginHost {
34
+ /** Every `host.fetch` call so far, in call order. */
35
+ readonly calls: RecordedFetchCall[];
36
+ /** Set what `host.remainingMs()` reports, for testing budget shedding. */
37
+ seedRemainingMs(ms: number): void;
38
+ /** Push one scripted response onto the back of the reply queue. */
39
+ queueResponse(response: FakeResponseInit): void;
40
+ /** Replace the fetch handler outright; overrides the queue while set. */
41
+ setFetchImpl(impl: (url: string, init?: HostFetchInit) => Promise<Response>): void;
42
+ /** Seed the value `host.config.get()` resolves to. */
43
+ seedConfig(config: Record<string, unknown>): void;
44
+ /** Seed a value `host.secrets.get(key)` resolves to. */
45
+ seedSecret(key: string, value: string): void;
46
+ /** Seed a storage entry directly, bypassing `host.storage.set`. */
47
+ seedStorage(key: string, value: unknown): void;
48
+ /** Read a storage entry directly, bypassing `host.storage.get`. */
49
+ getStorageEntry(key: string): unknown;
50
+ /** All storage keys currently set, unfiltered. */
51
+ storageKeys(): string[];
52
+ /** Seed the oauth vault directly, bypassing `host.oauth.saveTokens`. */
53
+ seedTokens(tokens: Record<string, string>): void;
54
+ /** Read the oauth vault directly, bypassing `host.oauth.getTokens`. */
55
+ getVaultTokens(): Record<string, string> | undefined;
56
+ /** Set what `host.trackFetcher.serve()` answers with; `undefined` means "this station has no fetcher". */
57
+ seedFetchedTrack(stream: ProviderStream | undefined): void;
58
+ }
59
+ /**
60
+ * What a test says a scripted reply should be.
61
+ *
62
+ * Not `ResponseInit`, because `url` is not on it: `host.fetch` reports the last
63
+ * hop of the redirect chain there, and a `Response` built by hand has an empty
64
+ * one unless it is defined in.
65
+ */
66
+ export interface FakeResponseInit {
67
+ status?: number;
68
+ statusText?: string;
69
+ headers?: Record<string, string>;
70
+ /**
71
+ * Text for a JSON or error reply, bytes for a binary one.
72
+ *
73
+ * `Uint8Array<ArrayBuffer>` rather than a bare `Uint8Array`: the DOM lib's
74
+ * `BodyInit` accepts a view over a real `ArrayBuffer`, and the unparameterized
75
+ * form widens to `ArrayBufferLike`, which includes `SharedArrayBuffer` and is
76
+ * not something `Response` will take. `new Uint8Array([...])` already has the
77
+ * narrow type, so no test has to say so.
78
+ */
79
+ body?: string | Uint8Array<ArrayBuffer>;
80
+ url?: string;
81
+ }
82
+ /** Builds a default 200 `Response`, overridable field by field. */
83
+ export declare function fakeHostFetchResponse(init?: FakeResponseInit): Response;
84
+ export declare function createFakePluginHost(): FakePluginHost;
85
+ //# sourceMappingURL=fake.plugin.host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake.plugin.host.d.ts","sourceRoot":"","sources":["../../src/testing/fake.plugin.host.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9F;;;;;;;;;;;;;;GAcG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,eAAe,GAAG,SAAS,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC5C,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAC9C,qDAAqD;IACrD,QAAQ,CAAC,KAAK,EAAE,iBAAiB,EAAE,CAAC;IACpC,0EAA0E;IAC1E,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,mEAAmE;IACnE,aAAa,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;IAChD,yEAAyE;IACzE,YAAY,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IACnF,sDAAsD;IACtD,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAClD,wDAAwD;IACxD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,mEAAmE;IACnE,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/C,mEAAmE;IACnE,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IACtC,kDAAkD;IAClD,WAAW,IAAI,MAAM,EAAE,CAAC;IACxB,wEAAwE;IACxE,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IACjD,uEAAuE;IACvE,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACrD,0GAA0G;IAC1G,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,CAAC;CAC9D;AASD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;CAChB;AAKD,mEAAmE;AACnE,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,gBAAqB,GAAG,QAAQ,CAa3E;AAED,wBAAgB,oBAAoB,IAAI,cAAc,CAqFrD"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Test doubles for the plugin contract.
3
+ *
4
+ * A separate entry point (`@deadair/plugin-sdk/testing`) rather than part of the
5
+ * package's main export, because this imports `vitest`: anything here would
6
+ * otherwise be a runtime dependency of every plugin the station loads in
7
+ * process.
8
+ */
9
+ export * from './fake.plugin.host.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,uBAAuB,CAAC"}