@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,52 @@
|
|
|
1
|
+
import type { PluginHost } from './plugin.host.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reading an operator's typed-in config field.
|
|
4
|
+
*
|
|
5
|
+
* `host.config.get()` answers `Record<string, unknown>`, because the values came
|
|
6
|
+
* out of a database column an operator edits through a text box. Every plugin
|
|
7
|
+
* therefore narrows each field itself, and every plugin was narrowing it the
|
|
8
|
+
* same two ways: "a string, and blank counts as unset", and "a base URL with no
|
|
9
|
+
* trailing slash".
|
|
10
|
+
*
|
|
11
|
+
* Blank counting as unset is the important half. A cleared text box stores `''`
|
|
12
|
+
* rather than removing the row, so a plugin comparing against `undefined` alone
|
|
13
|
+
* sees an empty string, treats it as a real value, and sends it upstream.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A config field as a trimmed string, or `undefined` when it is not set.
|
|
17
|
+
*
|
|
18
|
+
* Whitespace-only is unset for the same reason blank is: an operator who
|
|
19
|
+
* selected a value and deleted it has said "none", and a space is not a model
|
|
20
|
+
* name.
|
|
21
|
+
*/
|
|
22
|
+
export declare function configString(value: unknown): string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* A base URL with no trailing slash, so a path can be appended with one.
|
|
25
|
+
*
|
|
26
|
+
* Answers `''` rather than `undefined` for a field that is not set, which is
|
|
27
|
+
* deliberate and is what every caller already expected: a plugin holds its base
|
|
28
|
+
* URL as a plain `string` and reports "not configured" by testing whether it is
|
|
29
|
+
* empty, in a sentence of its own naming the thing it cannot reach ("No
|
|
30
|
+
* analyzer URL set."). Making this optional would push a `?? ''` to every use
|
|
31
|
+
* and change nothing else.
|
|
32
|
+
*/
|
|
33
|
+
export declare function configBaseUrl(value: unknown): string;
|
|
34
|
+
/**
|
|
35
|
+
* The credential one ROW of a `list` field holds, or `undefined` when the operator has not set it.
|
|
36
|
+
*
|
|
37
|
+
* The whole of what a plugin has to know about secret cells. A `secret` column is never in the row
|
|
38
|
+
* that {@link parseRows} hands back — that is what makes it a secret rather than a JSON string with
|
|
39
|
+
* a password in it — so this is how the value is reached, and the key it is stored under is nobody's
|
|
40
|
+
* business but this function's.
|
|
41
|
+
*
|
|
42
|
+
* Answers `undefined` for a row the host has never saved, which is the honest answer: a row with no
|
|
43
|
+
* {@link ROW_ID_KEY} has never been stored, so there is nothing under it.
|
|
44
|
+
*
|
|
45
|
+
* ```ts
|
|
46
|
+
* for (const row of parseRows(config.providers)) {
|
|
47
|
+
* const apiKey = await readRowSecret(this.host, 'providers', row, 'apiKey');
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function readRowSecret(host: PluginHost, fieldKey: string, row: Record<string, string>, columnKey: string): Promise<string | undefined>;
|
|
52
|
+
//# sourceMappingURL=plugin.config.read.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.config.read.d.ts","sourceRoot":"","sources":["../src/plugin.config.read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;;;;;;;;;GAYG;AAEH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAK/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAKnJ"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one error shape that means the same thing on both sides of the plugin
|
|
3
|
+
* boundary.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT a subclass of the API's `ServerkitError`: that class lives
|
|
6
|
+
* in `@maroonedsoftware/errors`, a server dependency, and inheriting from it
|
|
7
|
+
* here would pull the host's framework into every plugin's dependency tree,
|
|
8
|
+
* which is exactly what this package exists to avoid. So a plugin says what
|
|
9
|
+
* went wrong in its own vocabulary and the host decides what that means over
|
|
10
|
+
* HTTP (see `plugin.error.http.ts` in the API).
|
|
11
|
+
*
|
|
12
|
+
* The classification fields (`code`, `retryable`, `retryAfterMs`,
|
|
13
|
+
* `upstreamStatus`, `message`) are all JSON-safe on purpose: nothing here
|
|
14
|
+
* crosses the boundary as a live object today, but the day plugins move out of
|
|
15
|
+
* process, what has to change is how this is transported, not what it says.
|
|
16
|
+
* `cause` is the exception, and is in-process debugging detail only.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Why a plugin call failed, in terms the host can act on.
|
|
20
|
+
*
|
|
21
|
+
* These are semantic, not HTTP: an upstream's status code is a diagnostic
|
|
22
|
+
* (`upstreamStatus`), never the answer to what this API should respond. A
|
|
23
|
+
* provider's 404 and "no plugin by that id" are not the same 404.
|
|
24
|
+
*/
|
|
25
|
+
export type PluginErrorCode =
|
|
26
|
+
/** Credentials are missing, expired or rejected. The operator has to reauthorize. */
|
|
27
|
+
'auth'
|
|
28
|
+
/** The plugin's stored settings are wrong or incomplete. The operator has to fix the form. */
|
|
29
|
+
| 'config'
|
|
30
|
+
/** The upstream has no such resource. Often not an error at all to the caller. */
|
|
31
|
+
| 'not_found'
|
|
32
|
+
/**
|
|
33
|
+
* The upstream understood, and refused for this specific resource. Distinct
|
|
34
|
+
* from `auth`: the credentials are fine and the next call for something
|
|
35
|
+
* else will succeed.
|
|
36
|
+
*/
|
|
37
|
+
| 'forbidden'
|
|
38
|
+
/** The upstream is throttling. Honour `retryAfterMs` when it is set. */
|
|
39
|
+
| 'rate_limited'
|
|
40
|
+
/** The call did not finish in time. */
|
|
41
|
+
| 'timeout'
|
|
42
|
+
/** The plugin or its upstream is temporarily out of service. */
|
|
43
|
+
| 'unavailable'
|
|
44
|
+
/** The plugin does not implement what was asked of it. */
|
|
45
|
+
| 'unsupported'
|
|
46
|
+
/** The upstream answered, and what it said was a failure. */
|
|
47
|
+
| 'upstream'
|
|
48
|
+
/** Anything else, including a bug in the plugin. */
|
|
49
|
+
| 'internal';
|
|
50
|
+
/** Every {@link PluginErrorCode}, for validating a code that arrived from third-party code. */
|
|
51
|
+
export declare const PLUGIN_ERROR_CODES: readonly ["auth", "config", "not_found", "forbidden", "rate_limited", "timeout", "unavailable", "unsupported", "upstream", "internal"];
|
|
52
|
+
/**
|
|
53
|
+
* Whether this failure is about the requested resource rather than the plugin.
|
|
54
|
+
*
|
|
55
|
+
* See {@link RESOURCE_SCOPED_CODES}. Hosts use this to decide whether a
|
|
56
|
+
* failure counts against a plugin's health.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isResourceScopedCode(code: PluginErrorCode): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Marker that survives a plugin carrying its own copy of this module.
|
|
61
|
+
*
|
|
62
|
+
* `Symbol.for` reads from the global symbol registry, which is shared by every
|
|
63
|
+
* realm in the agent, so a second copy of this file computes the *identical*
|
|
64
|
+
* symbol rather than a private one. That is the whole trick: `instanceof`
|
|
65
|
+
* compares class identity and a second copy has its own class, while this
|
|
66
|
+
* compares a value two copies independently agree on.
|
|
67
|
+
*
|
|
68
|
+
* A symbol rather than a string property because it then stays out of
|
|
69
|
+
* `JSON.stringify`, `Object.keys` and log output, and because a plain object
|
|
70
|
+
* decoded off the wire cannot carry one by accident the way a well-guessed
|
|
71
|
+
* string field could.
|
|
72
|
+
*
|
|
73
|
+
* Only {@link toPluginError} reads it. See the note on {@link isPluginError}
|
|
74
|
+
* for why recognition and adoption are deliberately different questions.
|
|
75
|
+
*/
|
|
76
|
+
declare const PLUGIN_ERROR_BRAND: unique symbol;
|
|
77
|
+
/**
|
|
78
|
+
* A failure a plugin can describe well enough for the host to answer properly.
|
|
79
|
+
*
|
|
80
|
+
* Throwing a bare `Error` stays perfectly legal; the host treats it as
|
|
81
|
+
* `internal` and behaves exactly as it did before this class existed. Reach
|
|
82
|
+
* for `PluginError` when the caller can do something different with the answer:
|
|
83
|
+
* reauthorize, wait, fix a setting, or give up.
|
|
84
|
+
*
|
|
85
|
+
* The classification is applied with the `with*` builders rather than through
|
|
86
|
+
* the constructor, so a subclass only has to forward `(message, options)` to
|
|
87
|
+
* `super` and can then say what it means on its own terms:
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* throw new PluginError('slow down').withCode('rate_limited').withUpstreamStatus(429).withRetry(30_000);
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* Call {@link withCode} first: it resets `retryable` to the default for the
|
|
94
|
+
* code, so a later `withCode` would undo an earlier {@link withRetry}.
|
|
95
|
+
*/
|
|
96
|
+
export declare class PluginError extends Error {
|
|
97
|
+
/** @internal Recognition marker for a foreign copy. See {@link PLUGIN_ERROR_BRAND}. */
|
|
98
|
+
readonly [PLUGIN_ERROR_BRAND] = true;
|
|
99
|
+
/** What went wrong, in host vocabulary. Defaults to `internal`; set it with {@link withCode}. */
|
|
100
|
+
code: PluginErrorCode;
|
|
101
|
+
/** Whether repeating the call could plausibly succeed. See {@link RETRYABLE_BY_CODE}. */
|
|
102
|
+
retryable: boolean;
|
|
103
|
+
/** How long to wait before retrying, when the upstream said so (`Retry-After`). */
|
|
104
|
+
retryAfterMs?: number;
|
|
105
|
+
/**
|
|
106
|
+
* The upstream's HTTP status, for logs and for plugin-internal branching.
|
|
107
|
+
* Diagnostic only: the host never forwards it as its own response status.
|
|
108
|
+
*/
|
|
109
|
+
upstreamStatus?: number;
|
|
110
|
+
constructor(message: string, options?: {
|
|
111
|
+
cause?: unknown;
|
|
112
|
+
});
|
|
113
|
+
/** Classifies the failure, and resets `retryable` to the default for that code. */
|
|
114
|
+
withCode(code: PluginErrorCode): this;
|
|
115
|
+
/**
|
|
116
|
+
* Attaches the upstream's retry advice. Saying "wait this long and try
|
|
117
|
+
* again" is itself a statement that a retry is worth making, so this marks
|
|
118
|
+
* the failure retryable regardless of what the code defaults to.
|
|
119
|
+
*/
|
|
120
|
+
withRetry(retryAfterMs: number): this;
|
|
121
|
+
withUpstreamStatus(upstreamStatus: number): this;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether `value` is one of ours: an error this copy of the module built.
|
|
125
|
+
*
|
|
126
|
+
* Deliberately `instanceof`, and deliberately NOT the same question
|
|
127
|
+
* {@link toPluginError} answers. Almost every caller asking this is host code
|
|
128
|
+
* downstream of the invoker, where the error has already been adopted and the
|
|
129
|
+
* honest question is "did we make this", to which `instanceof` is the exact,
|
|
130
|
+
* unforgeable answer. It also keeps subclasses (`SpotifyRequestError`) working
|
|
131
|
+
* for the plugin's own branching, via the constructor's `new.target` fix-up.
|
|
132
|
+
*
|
|
133
|
+
* Tolerating a foreign copy is the boundary's job, and the boundary is one
|
|
134
|
+
* function wide. Making this guard structural instead would spread that
|
|
135
|
+
* laxness across every call site that only ever sees host-built errors.
|
|
136
|
+
*/
|
|
137
|
+
export declare const isPluginError: (error: unknown) => error is PluginError;
|
|
138
|
+
/**
|
|
139
|
+
* Whatever a plugin threw, as a {@link PluginError} this copy owns.
|
|
140
|
+
*
|
|
141
|
+
* This is the boundary function, and the only place tolerant recognition
|
|
142
|
+
* belongs: the host funnels every call into plugin code through one door
|
|
143
|
+
* (`PluginInvoker.invoke`), so a foreign error is adopted exactly once and
|
|
144
|
+
* everything downstream deals only with errors the host itself constructed.
|
|
145
|
+
*
|
|
146
|
+
* Three cases, in order of how much is trusted:
|
|
147
|
+
*
|
|
148
|
+
* 1. Ours: passed straight through, keeping its identity so a `catch` further
|
|
149
|
+
* up can still recognize the specific subclass that was thrown.
|
|
150
|
+
* 2. Branded but from another copy: rebuilt here, field by field, with the
|
|
151
|
+
* code checked against {@link PLUGIN_ERROR_CODES}. An unrecognized code
|
|
152
|
+
* degrades to `fallback` rather than leaking a made-up string into the
|
|
153
|
+
* host's HTTP mapping, and a non-numeric `retryAfterMs` is dropped rather
|
|
154
|
+
* than turned into a `Retry-After` header of `NaN`.
|
|
155
|
+
* 3. Anything else: adopted under `fallback`, original kept as the `cause`.
|
|
156
|
+
*
|
|
157
|
+
* The rebuild in case 2 is the same operation that will be needed when plugins
|
|
158
|
+
* move out of process and a failure arrives as JSON rather than as a live
|
|
159
|
+
* object: what changes then is how it is transported, not what it says.
|
|
160
|
+
*/
|
|
161
|
+
export declare function toPluginError(error: unknown, fallback?: PluginErrorCode): PluginError;
|
|
162
|
+
/**
|
|
163
|
+
* A caught `unknown` as a sentence, for a message a person reads.
|
|
164
|
+
*
|
|
165
|
+
* `catch` binds `unknown`, so every plugin reporting a failure narrows it before
|
|
166
|
+
* it can say anything — five sites here did, four of them inline. The fallback
|
|
167
|
+
* is not padding: a rejected fetch, a thrown string and an aborted signal all
|
|
168
|
+
* arrive here and only some of them are `Error`.
|
|
169
|
+
*
|
|
170
|
+
* This is NOT error handling and is not a substitute for {@link toPluginError}.
|
|
171
|
+
* It produces a string for a log line or a `testConnection` result, deliberately
|
|
172
|
+
* losing the code, the cause and the retry advice. Anything deciding what to DO
|
|
173
|
+
* about a failure wants the error itself.
|
|
174
|
+
*/
|
|
175
|
+
export declare const errorText: (error: unknown) => string;
|
|
176
|
+
export {};
|
|
177
|
+
//# sourceMappingURL=plugin.error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.error.d.ts","sourceRoot":"","sources":["../src/plugin.error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,eAAe;AACvB,qFAAqF;AACnF,MAAM;AACR,8FAA8F;GAC5F,QAAQ;AACV,kFAAkF;GAChF,WAAW;AACb;;;;GAIG;GACD,WAAW;AACb,wEAAwE;GACtE,cAAc;AAChB,uCAAuC;GACrC,SAAS;AACX,gEAAgE;GAC9D,aAAa;AACf,0DAA0D;GACxD,aAAa;AACf,6DAA6D;GAC3D,UAAU;AACZ,oDAAoD;GAClD,UAAU,CAAC;AAEjB,+FAA+F;AAC/F,eAAO,MAAM,kBAAkB,wIAWrB,CAAC;AAoBX;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAEnE;AAwBD;;;;;;;;;;;;;;;;GAgBG;AACH,QAAA,MAAM,kBAAkB,eAAwC,CAAC;AAEjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAClC,uFAAuF;IACvF,QAAQ,CAAC,CAAC,kBAAkB,CAAC,QAAQ;IAErC,iGAAiG;IACjG,IAAI,EAAE,eAAe,CAAc;IACnC,yFAAyF;IACzF,SAAS,EAAE,OAAO,CAA8B;IAChD,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;gBAEZ,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;IAW1D,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,eAAe;IAM9B;;;;OAIG;IACH,SAAS,CAAC,YAAY,EAAE,MAAM;IAM9B,kBAAkB,CAAC,cAAc,EAAE,MAAM;CAI5C;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,WAEvD,CAAC;AAcF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,GAAE,eAA4B,GAAG,WAAW,CAgBjG;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,OAAO,KAAG,MAAkE,CAAC"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the host lends a plugin. Everything here is an ordinary in-process call: the host and the
|
|
3
|
+
* plugin share a realm, permanently, so `fetch` hands back a real `Response` and `signal` is a real
|
|
4
|
+
* `AbortSignal`. See `packages/plugin-sdk/CLAUDE.md` § "Trust and egress".
|
|
5
|
+
*
|
|
6
|
+
* The JSON-safe rule that used to govern this file has not gone away, it has
|
|
7
|
+
* moved to where it pays for itself: `boundary.json.safe.ts` still asserts it
|
|
8
|
+
* over every payload that is stored in Postgres or sent over HTTP, which is
|
|
9
|
+
* most of `capabilities/` plus the manifest. What is no longer asserted is the
|
|
10
|
+
* arguments and return values of the methods below, because nothing serializes
|
|
11
|
+
* them. `TrackFetchSession` and `TrackFetchRequest` are the exceptions in this
|
|
12
|
+
* file: they go over HTTP to the track fetcher, so they stay asserted.
|
|
13
|
+
*/
|
|
14
|
+
import type { ProviderStream } from './capabilities/music.provider.js';
|
|
15
|
+
/** Structured logging. Goes to the host's logger, tagged with the plugin id. */
|
|
16
|
+
export interface PluginLogger {
|
|
17
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
18
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
19
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
20
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
21
|
+
}
|
|
22
|
+
export type HostFetchMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
|
|
23
|
+
/** JSON-safe subset of `RequestInit`. */
|
|
24
|
+
export interface HostFetchInit {
|
|
25
|
+
method?: HostFetchMethod;
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
/** Already-serialised body. Set `content-type` yourself. */
|
|
28
|
+
body?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Budget in ms for the WHOLE call, clamped to the host's ceiling: waiting
|
|
31
|
+
* for rate-limit headroom, the request, any `Retry-After` back-off and the
|
|
32
|
+
* retry all come out of this one number. The ceiling is the host's own
|
|
33
|
+
* deadline for a call into plugin code, so asking for more buys nothing.
|
|
34
|
+
*/
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Your own reason to give up, on top of the host's.
|
|
38
|
+
*
|
|
39
|
+
* Composed with the host's deadline rather than replacing it: whichever
|
|
40
|
+
* fires first ends the request, and {@link PluginHost.signal} is already
|
|
41
|
+
* watched for you. Pass one when the plugin has a cancellation of its own,
|
|
42
|
+
* such as a caller that walked away or a race between two upstreams.
|
|
43
|
+
*/
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Namespaced key/value store, private to this plugin. Values must be
|
|
48
|
+
* JSON-serialisable. Requires the `storage` permission.
|
|
49
|
+
*/
|
|
50
|
+
export interface PluginStorage {
|
|
51
|
+
get(key: string): Promise<unknown>;
|
|
52
|
+
set(key: string, value: unknown): Promise<void>;
|
|
53
|
+
delete(key: string): Promise<void>;
|
|
54
|
+
/** Keys, optionally filtered to those starting with `prefix`. */
|
|
55
|
+
list(prefix?: string): Promise<string[]>;
|
|
56
|
+
}
|
|
57
|
+
/** Read access to this plugin's own decrypted secret config values. */
|
|
58
|
+
export interface PluginSecrets {
|
|
59
|
+
/** Resolves to `undefined` when the operator never set the value. */
|
|
60
|
+
get(key: string): Promise<string | undefined>;
|
|
61
|
+
}
|
|
62
|
+
/** Read access to this plugin's validated non-secret config values. */
|
|
63
|
+
export interface PluginConfigAccess {
|
|
64
|
+
get(): Promise<Record<string, unknown>>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* OAuth token vault. The host owns the redirect endpoint and the encryption;
|
|
68
|
+
* the plugin only builds the authorize URL and exchanges the code.
|
|
69
|
+
* Requires the `oauth` permission.
|
|
70
|
+
*/
|
|
71
|
+
export interface PluginOAuth {
|
|
72
|
+
/** The host-owned redirect URI to register with the provider. */
|
|
73
|
+
getRedirectUri(): Promise<string>;
|
|
74
|
+
saveTokens(tokens: Record<string, string>): Promise<void>;
|
|
75
|
+
/** Resolves to `undefined` when the plugin has never completed a flow. */
|
|
76
|
+
getTokens(): Promise<Record<string, string> | undefined>;
|
|
77
|
+
}
|
|
78
|
+
/** Fire-and-forget notifications onto the host's event bus. */
|
|
79
|
+
export interface PluginEvents {
|
|
80
|
+
emit(event: string, payload?: Record<string, unknown>): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A login the station's track fetcher opens its own session with.
|
|
84
|
+
*
|
|
85
|
+
* Handed to {@link PluginTrackFetcher.serve} and nowhere else: it is not
|
|
86
|
+
* persisted, not written to config, and not readable back out of the host. The
|
|
87
|
+
* plugin still owns the account and the refresh.
|
|
88
|
+
*/
|
|
89
|
+
export interface TrackFetchSession {
|
|
90
|
+
/** The account the fetcher should log in as. */
|
|
91
|
+
username: string;
|
|
92
|
+
/** A currently-valid access token. */
|
|
93
|
+
accessToken: string;
|
|
94
|
+
/** Unix epoch millis after which `accessToken` stops working. */
|
|
95
|
+
expiresAt?: number;
|
|
96
|
+
}
|
|
97
|
+
export interface TrackFetchRequest {
|
|
98
|
+
/** Provider-scoped track id, exactly as the plugin's own catalog reports it. */
|
|
99
|
+
trackId: string;
|
|
100
|
+
session: TrackFetchSession;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The station's own track fetcher: a helper process, running beside the audio
|
|
104
|
+
* player, that speaks a provider's protocol and re-serves the result as plain
|
|
105
|
+
* audio over HTTP.
|
|
106
|
+
*
|
|
107
|
+
* This exists for one shape of provider: the audio is reachable, but only to a
|
|
108
|
+
* process speaking a protocol the plugin does not. Spotify is the reason —
|
|
109
|
+
* its tracks come off the CDN encrypted and are fetched by a separate binary
|
|
110
|
+
* beside Liquidsoap. Without this, such a provider could not implement
|
|
111
|
+
* `MusicProviderStream.resolveStreamUrl` at all, because there is no URL for it
|
|
112
|
+
* to mint. Named in backticks rather than linked: importing that type solely to
|
|
113
|
+
* make a doc reference clickable leaves an import nothing uses, which is a lint
|
|
114
|
+
* failure in a package whose gate is zero warnings.
|
|
115
|
+
*
|
|
116
|
+
* Do NOT reach for this when your provider's audio can simply be fetched. Mint
|
|
117
|
+
* the URL yourself and keep your credentials to yourself, which is both simpler
|
|
118
|
+
* and narrower. Requires the `trackFetcher` permission.
|
|
119
|
+
*/
|
|
120
|
+
export interface PluginTrackFetcher {
|
|
121
|
+
/**
|
|
122
|
+
* Lend the fetcher a login and get back a URL for one track.
|
|
123
|
+
*
|
|
124
|
+
* Resolves to `undefined` when this station has no fetcher configured,
|
|
125
|
+
* which a plugin should pass straight through as "cannot resolve this
|
|
126
|
+
* item" rather than treat as an error: an operator who never set the
|
|
127
|
+
* stream side up is a normal state, not a fault.
|
|
128
|
+
*/
|
|
129
|
+
serve(request: TrackFetchRequest): Promise<ProviderStream | undefined>;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The single object handed to a plugin at init, and the sanctioned way to get
|
|
133
|
+
* at the outside world: network, persistence, secrets, tokens.
|
|
134
|
+
*
|
|
135
|
+
* NOT A SANDBOX, and never going to be one. The host imports plugins into its own realm,
|
|
136
|
+
* permanently (`packages/plugin-sdk/CLAUDE.md` § "Trust and egress"), so global `fetch`, `fs`, and
|
|
137
|
+
* `process.env` are all reachable and nothing stops a plugin from using them. What this interface
|
|
138
|
+
* buys is a manifest that honestly describes a well-behaved plugin's blast radius, plus a set of
|
|
139
|
+
* guarantees (rate limiting, timeouts, SSRF-safe redirects) that no plugin has to reimplement.
|
|
140
|
+
* Containment is not among them, and the console says so before an operator enables anything.
|
|
141
|
+
*/
|
|
142
|
+
export interface PluginHost {
|
|
143
|
+
logger: PluginLogger;
|
|
144
|
+
/**
|
|
145
|
+
* The sanctioned network egress. Enforces the manifest's hostname
|
|
146
|
+
* allowlist, a timeout, a rate limit, and `Retry-After` back-off, and
|
|
147
|
+
* re-checks the allowlist on every redirect hop so an upstream cannot
|
|
148
|
+
* bounce a plugin somewhere its manifest never asked for. Rejects if
|
|
149
|
+
* `url`'s hostname is not in `permissions.network`.
|
|
150
|
+
*
|
|
151
|
+
* The allowlist is advisory against a plugin that simply calls global
|
|
152
|
+
* `fetch` instead (see the note on {@link PluginHost}). It is not advisory
|
|
153
|
+
* against a hostile *server*: the redirect and credential-stripping rules
|
|
154
|
+
* protect an honest plugin, and it gains nothing by going around them.
|
|
155
|
+
*
|
|
156
|
+
* A real `Response`, so `await response.json()` is how you read JSON and
|
|
157
|
+
* `response.body` is how you stream audio. There is no second egress for
|
|
158
|
+
* bytes: a body you do not buffer is one you read off `response.body`, and
|
|
159
|
+
* a body you never read at all should be `response.body?.cancel()`-ed
|
|
160
|
+
* rather than dropped.
|
|
161
|
+
*
|
|
162
|
+
* `timeoutMs` bounds getting the response: connect, headers and the whole
|
|
163
|
+
* redirect chain. It does NOT bound reading the body, because a large body
|
|
164
|
+
* legitimately outlives the call that asked for it. The body is bounded
|
|
165
|
+
* instead by an idle deadline between chunks, a total byte cap and a
|
|
166
|
+
* lifetime cap, all host-enforced. Exceeding any of them fails the read
|
|
167
|
+
* rather than truncating it, so bytes you get are always bytes the server
|
|
168
|
+
* actually sent.
|
|
169
|
+
*/
|
|
170
|
+
fetch(url: string, init?: HostFetchInit): Promise<Response>;
|
|
171
|
+
/**
|
|
172
|
+
* Aborts when the host gives up on the call you are currently inside.
|
|
173
|
+
*
|
|
174
|
+
* The same signal the host itself races the call against, not a copy, so
|
|
175
|
+
* honouring it and being abandoned are the same moment rather than two
|
|
176
|
+
* clocks that nearly agree. `host.fetch` already watches it; pass it on to
|
|
177
|
+
* anything else of yours that takes one.
|
|
178
|
+
*
|
|
179
|
+
* Outside any host call (from a timer you set yourself) this is a signal
|
|
180
|
+
* that never aborts, because nothing is waiting on that work.
|
|
181
|
+
*/
|
|
182
|
+
signal: AbortSignal;
|
|
183
|
+
/**
|
|
184
|
+
* Milliseconds left before the host abandons the call you are currently
|
|
185
|
+
* inside, so work that does not fit can be dropped deliberately instead of
|
|
186
|
+
* being cut off halfway.
|
|
187
|
+
*
|
|
188
|
+
* The number behind {@link PluginHost.signal}, for deciding whether to
|
|
189
|
+
* START something rather than for being interrupted during it. The signal
|
|
190
|
+
* tells you the call is over; this tells you it is nearly over, which is
|
|
191
|
+
* the only one of the two that can save a partial result.
|
|
192
|
+
*
|
|
193
|
+
* The host gives every call into your code a deadline, and it is not a
|
|
194
|
+
* constant: a background job may run on a far longer budget than a request
|
|
195
|
+
* a person is waiting on, and the same method of yours can be called both
|
|
196
|
+
* ways. Guessing at it is how a plugin ends up either quitting early on a
|
|
197
|
+
* budget it had, or being killed mid-flight with nothing useful to return.
|
|
198
|
+
*
|
|
199
|
+
* The pattern this exists for is a sequence where the later steps are
|
|
200
|
+
* optional:
|
|
201
|
+
*
|
|
202
|
+
* ```ts
|
|
203
|
+
* const core = await this.lookup(ref);
|
|
204
|
+
* if (host.remainingMs() < 2_000) return core; // good enough, out of time
|
|
205
|
+
* return { ...core, ...(await this.enrich(core)) };
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* Not a budget you are given: it is the host's, spent by everything the
|
|
209
|
+
* call does, and `host.fetch` already caps its own timeout by it. Treat a
|
|
210
|
+
* small number as advice to wrap up, not as permission to run that long.
|
|
211
|
+
*/
|
|
212
|
+
remainingMs(): number;
|
|
213
|
+
storage: PluginStorage;
|
|
214
|
+
secrets: PluginSecrets;
|
|
215
|
+
config: PluginConfigAccess;
|
|
216
|
+
oauth: PluginOAuth;
|
|
217
|
+
events: PluginEvents;
|
|
218
|
+
trackFetcher: PluginTrackFetcher;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=plugin.host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.host.d.ts","sourceRoot":"","sources":["../src/plugin.host.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sugar over the `Response` that `host.fetch` hands back.
|
|
3
|
+
*
|
|
4
|
+
* Free functions rather than a `Response` subclass with methods, because
|
|
5
|
+
* `host.fetch` returns the platform's own `Response` and a plugin has to be
|
|
6
|
+
* able to pass it to any library that takes one. Anything that made it a
|
|
7
|
+
* special response would take that away for the sake of dot notation.
|
|
8
|
+
*
|
|
9
|
+
* What they buy over `await response.json()` is the error. The common failure
|
|
10
|
+
* is an API answering 200 with an HTML error page or a rate-limit notice, and
|
|
11
|
+
* `Unexpected token < in JSON at position 0` says nothing about which call did
|
|
12
|
+
* it.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The body parsed as JSON.
|
|
16
|
+
*
|
|
17
|
+
* Throws when it is not JSON, with the status, the URL and the start of the
|
|
18
|
+
* body in the message. That detail is the point, and it is why this reads the
|
|
19
|
+
* body as text and parses that rather than calling `response.json()`: the text
|
|
20
|
+
* is what names what the server actually sent.
|
|
21
|
+
*
|
|
22
|
+
* A body that fails to READ rather than to parse (over the host's byte cap, a
|
|
23
|
+
* socket that went quiet) rejects with the host's own `PluginError` untouched.
|
|
24
|
+
* That is a different failure from a body that arrived and was not JSON, and
|
|
25
|
+
* relabelling it would lose the code the caller branches on.
|
|
26
|
+
*
|
|
27
|
+
* Note this does not check `response.ok`. A 4xx with a JSON error body is worth
|
|
28
|
+
* parsing, so deciding what a bad status means is left to the caller.
|
|
29
|
+
*/
|
|
30
|
+
export declare function jsonBody<T>(response: Response): Promise<T>;
|
|
31
|
+
/**
|
|
32
|
+
* {@link jsonBody}, but `undefined` instead of a throw when the body will not
|
|
33
|
+
* parse. For the callers that treat an unparseable body the same as a missing
|
|
34
|
+
* one and have nothing useful to add to the error.
|
|
35
|
+
*
|
|
36
|
+
* A body that fails to read still rejects, for the reason above: the host
|
|
37
|
+
* refusing an oversized body is not the same event as a server answering with
|
|
38
|
+
* something that is not JSON, and swallowing the first would report a
|
|
39
|
+
* misconfiguration as an empty result.
|
|
40
|
+
*/
|
|
41
|
+
export declare function tryJsonBody<T>(response: Response): Promise<T | undefined>;
|
|
42
|
+
//# sourceMappingURL=plugin.host.response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.host.response.d.ts","sourceRoot":"","sources":["../src/plugin.host.response.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAWH;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAQhE;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAO/E"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { HostFetchMethod } from './plugin.host.js';
|
|
2
|
+
import type { PluginErrorCode } from './plugin.error.js';
|
|
3
|
+
/**
|
|
4
|
+
* An upstream's sentence, bounded.
|
|
5
|
+
*
|
|
6
|
+
* An error body is untrusted text that ends up in a log line and on a settings
|
|
7
|
+
* card, so its length is not the upstream's decision to make. Three clients cut
|
|
8
|
+
* it at the same 200 characters with the same ellipsis.
|
|
9
|
+
*/
|
|
10
|
+
export declare function truncateUpstreamMessage(message: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* A string field out of a JSON error body, or `undefined`.
|
|
13
|
+
*
|
|
14
|
+
* Defensive on purpose, and in a specific way: the body is whatever the edge
|
|
15
|
+
* happened to send — an HTML page from a proxy, an empty 429, a truncated
|
|
16
|
+
* response — so a failed parse has to read as "said nothing" rather than throw
|
|
17
|
+
* inside the code that was already handling a failure. An empty string is also
|
|
18
|
+
* nothing, since it would otherwise print as a blank reason.
|
|
19
|
+
*
|
|
20
|
+
* @param body - The raw response body. `undefined` when it was never read.
|
|
21
|
+
* @param pick - Reaches the field. Runs on `unknown`, so it casts; anything it
|
|
22
|
+
* returns that is not a non-empty string is discarded.
|
|
23
|
+
*/
|
|
24
|
+
export declare function upstreamField(body: string | undefined, pick: (parsed: unknown) => unknown): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* `Retry-After` in whole seconds, as milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* Only the seconds form is read. The HTTP-date form is legal and no upstream
|
|
29
|
+
* here sends it, and guessing wrong would tell the host to sit out a wait that
|
|
30
|
+
* was never asked for. Anything unparseable or negative means "no advice
|
|
31
|
+
* given", which is different from "wait zero".
|
|
32
|
+
*
|
|
33
|
+
* Takes `null` as well as `undefined` because one caller reads it off a
|
|
34
|
+
* `Headers` (which answers `null`) and another off a record (which answers
|
|
35
|
+
* `undefined`).
|
|
36
|
+
*/
|
|
37
|
+
export declare function retryAfterMs(header: string | null | undefined): number | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The rows of the status ladder that mean the same thing at every upstream.
|
|
40
|
+
*
|
|
41
|
+
* A plugin layers its own service's readings in FRONT of this rather than
|
|
42
|
+
* replacing it, because the deviations are the interesting part and each one is
|
|
43
|
+
* a decision worth writing down beside the plugin it belongs to. Spotify reads
|
|
44
|
+
* 401 as `auth` and 403 as `forbidden`; MusicBrainz reads a 503 carrying a
|
|
45
|
+
* `Retry-After` as `rate_limited`; the two token-authenticated services read
|
|
46
|
+
* 401 as `config`, because a pasted token is a setting. None of those belongs
|
|
47
|
+
* here.
|
|
48
|
+
*
|
|
49
|
+
* What does belong here is the part nobody disagrees about: a 404 is a missing
|
|
50
|
+
* thing, a 429 is going too fast, any other 5xx is the upstream being down, and
|
|
51
|
+
* everything else is the upstream saying something unhelpful.
|
|
52
|
+
*/
|
|
53
|
+
export declare function pluginCodeForStatus(status: number): PluginErrorCode;
|
|
54
|
+
/**
|
|
55
|
+
* A one-line summary of a failed response, for a message a human reads.
|
|
56
|
+
*
|
|
57
|
+
* The status is always there; the other two often are not, and an upstream that
|
|
58
|
+
* sent neither should not produce a line with holes in it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function upstreamDetail(status: number, statusText?: string, reason?: string): string;
|
|
61
|
+
/** The methods `host.fetch` will carry. */
|
|
62
|
+
export declare const HOST_FETCH_METHODS: readonly HostFetchMethod[];
|
|
63
|
+
/**
|
|
64
|
+
* A method name as one `host.fetch` accepts, or `undefined` if it is not one.
|
|
65
|
+
*
|
|
66
|
+
* Answers rather than throws, because the two callers refuse in different words
|
|
67
|
+
* and to different audiences — one is telling a plugin author that the AI SDK
|
|
68
|
+
* asked for something impossible, the other is a bridge reporting its own bug —
|
|
69
|
+
* and a shared throw would have flattened both into one message.
|
|
70
|
+
*/
|
|
71
|
+
export declare function hostFetchMethod(method: string | undefined): HostFetchMethod | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Headers as the plain lower-cased record `HostFetchInit.headers` takes.
|
|
74
|
+
*
|
|
75
|
+
* A `Headers` instance has already lower-cased its names and joined repeats,
|
|
76
|
+
* which is the behaviour to want: the host's header handling carries one value
|
|
77
|
+
* per name. Lower-casing again is free and makes the guarantee local.
|
|
78
|
+
*/
|
|
79
|
+
export declare function headersToRecord(headers: Headers): Record<string, string>;
|
|
80
|
+
//# sourceMappingURL=plugin.http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.http.d.ts","sourceRoot":"","sources":["../src/plugin.http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAqBzD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,MAAM,GAAG,SAAS,CAW9G;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOlF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAMnE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3F;AAED,2CAA2C;AAC3C,eAAO,MAAM,kBAAkB,EAAE,SAAS,eAAe,EAAsD,CAAC;AAEhH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,eAAe,GAAG,SAAS,CAIvF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxE"}
|