@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.
- package/LICENSE +21 -0
- package/README.md +1062 -0
- package/dist/article.parse.d.ts +82 -0
- package/dist/article.parse.d.ts.map +1 -0
- package/dist/boundary.json.safe.d.ts +191 -0
- package/dist/boundary.json.safe.d.ts.map +1 -0
- package/dist/capabilities/analysis.d.ts +330 -0
- package/dist/capabilities/analysis.d.ts.map +1 -0
- package/dist/capabilities/charts.d.ts +134 -0
- package/dist/capabilities/charts.d.ts.map +1 -0
- package/dist/capabilities/enrichment.d.ts +255 -0
- package/dist/capabilities/enrichment.d.ts.map +1 -0
- package/dist/capabilities/llm.d.ts +318 -0
- package/dist/capabilities/llm.d.ts.map +1 -0
- package/dist/capabilities/mixer.d.ts +183 -0
- package/dist/capabilities/mixer.d.ts.map +1 -0
- package/dist/capabilities/music.provider.d.ts +245 -0
- package/dist/capabilities/music.provider.d.ts.map +1 -0
- package/dist/capabilities/news.d.ts +171 -0
- package/dist/capabilities/news.d.ts.map +1 -0
- package/dist/capabilities/scrobble.d.ts +133 -0
- package/dist/capabilities/scrobble.d.ts.map +1 -0
- package/dist/capabilities/search.d.ts +122 -0
- package/dist/capabilities/search.d.ts.map +1 -0
- package/dist/capabilities/similarity.d.ts +101 -0
- package/dist/capabilities/similarity.d.ts.map +1 -0
- package/dist/capabilities/speech.d.ts +211 -0
- package/dist/capabilities/speech.d.ts.map +1 -0
- package/dist/capabilities/weather.d.ts +192 -0
- package/dist/capabilities/weather.d.ts.map +1 -0
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/define.plugin.d.ts +56 -0
- package/dist/define.plugin.d.ts.map +1 -0
- package/dist/feed.parse.d.ts +97 -0
- package/dist/feed.parse.d.ts.map +1 -0
- package/dist/html.text.d.ts +71 -0
- package/dist/html.text.d.ts.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1153 -0
- package/dist/index.js.map +1 -0
- package/dist/match.text.d.ts +28 -0
- package/dist/match.text.d.ts.map +1 -0
- package/dist/plugin.api.version.d.ts +9 -0
- package/dist/plugin.api.version.d.ts.map +1 -0
- package/dist/plugin.base.d.ts +80 -0
- package/dist/plugin.base.d.ts.map +1 -0
- package/dist/plugin.config.fields.d.ts +524 -0
- package/dist/plugin.config.fields.d.ts.map +1 -0
- package/dist/plugin.config.read.d.ts +52 -0
- package/dist/plugin.config.read.d.ts.map +1 -0
- package/dist/plugin.error.d.ts +177 -0
- package/dist/plugin.error.d.ts.map +1 -0
- package/dist/plugin.host.d.ts +220 -0
- package/dist/plugin.host.d.ts.map +1 -0
- package/dist/plugin.host.response.d.ts +42 -0
- package/dist/plugin.host.response.d.ts.map +1 -0
- package/dist/plugin.http.d.ts +80 -0
- package/dist/plugin.http.d.ts.map +1 -0
- package/dist/plugin.lifecycle.d.ts +68 -0
- package/dist/plugin.lifecycle.d.ts.map +1 -0
- package/dist/plugin.manifest.d.ts +282 -0
- package/dist/plugin.manifest.d.ts.map +1 -0
- package/dist/plugin.permissions.d.ts +186 -0
- package/dist/plugin.permissions.d.ts.map +1 -0
- package/dist/testing/fake.plugin.host.d.ts +85 -0
- package/dist/testing/fake.plugin.host.d.ts.map +1 -0
- package/dist/testing/index.d.ts +10 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +141 -0
- package/dist/testing/index.js.map +1 -0
- 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"}
|