@vidofy/mcp 0.1.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 +151 -0
- package/dist/backend.d.ts +133 -0
- package/dist/backend.js +455 -0
- package/dist/config.d.ts +85 -0
- package/dist/config.js +207 -0
- package/dist/heartbeat.d.ts +11 -0
- package/dist/heartbeat.js +70 -0
- package/dist/http.d.ts +44 -0
- package/dist/http.js +885 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +661 -0
- package/dist/log.d.ts +19 -0
- package/dist/log.js +22 -0
- package/dist/map/b2c.d.ts +139 -0
- package/dist/map/b2c.js +236 -0
- package/dist/media-duration.d.ts +33 -0
- package/dist/media-duration.js +137 -0
- package/dist/oauth/authorize.d.ts +61 -0
- package/dist/oauth/authorize.js +275 -0
- package/dist/oauth/clients.d.ts +137 -0
- package/dist/oauth/clients.js +621 -0
- package/dist/oauth/ratelimit.d.ts +92 -0
- package/dist/oauth/ratelimit.js +116 -0
- package/dist/oauth/store.d.ts +135 -0
- package/dist/oauth/store.js +277 -0
- package/dist/oauth/token.d.ts +29 -0
- package/dist/oauth/token.js +165 -0
- package/dist/schema.d.ts +147 -0
- package/dist/schema.js +629 -0
- package/dist/tools/account.d.ts +28 -0
- package/dist/tools/account.js +130 -0
- package/dist/tools/generation.d.ts +102 -0
- package/dist/tools/generation.js +1194 -0
- package/dist/tools/info.d.ts +50 -0
- package/dist/tools/info.js +95 -0
- package/dist/ui/generation-card.html +869 -0
- package/package.json +79 -0
- package/server.json +36 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the server points, and which credential it carries.
|
|
3
|
+
*
|
|
4
|
+
* ONE credential, ONE door — never both. The two modes bill different wallets:
|
|
5
|
+
*
|
|
6
|
+
* VIDOFY_TOKEN (vmt_…) → the user's own coins (account mode — served)
|
|
7
|
+
* VIDOFY_API_KEY (vky_…) → a different balance (key mode — refused)
|
|
8
|
+
*
|
|
9
|
+
* Mixing them would mean a caller could not tell which balance a generation
|
|
10
|
+
* was going to spend until after it spent it, so a request carrying both is
|
|
11
|
+
* refused at startup rather than resolved by precedence.
|
|
12
|
+
*
|
|
13
|
+
* Key mode is DETECTED but NOT SERVED, and that is the settled shape of the
|
|
14
|
+
* product rather than a gap (owner decision, 2026-09-11): this server is for
|
|
15
|
+
* personal Vidofy accounts and spends their own coins. It is detected only so
|
|
16
|
+
* that a key can be refused with an explanation instead of failing later as an
|
|
17
|
+
* unexplained 401.
|
|
18
|
+
*
|
|
19
|
+
* Detected rather than ignored so that someone who sets VIDOFY_API_KEY is told
|
|
20
|
+
* where to go, instead of watching the server start and then fail on every
|
|
21
|
+
* call. See the refusal in index.ts.
|
|
22
|
+
*/
|
|
23
|
+
export class ConfigError extends Error {
|
|
24
|
+
}
|
|
25
|
+
const DEFAULT_BASE = 'https://vidofy.ai';
|
|
26
|
+
/**
|
|
27
|
+
* Read the environment into a Config, or throw a message a human can act on.
|
|
28
|
+
*
|
|
29
|
+
* @param env Defaults to process.env; injectable so this is testable without
|
|
30
|
+
* mutating the real environment.
|
|
31
|
+
*/
|
|
32
|
+
export function loadConfig(env = process.env, version = '0.0.0') {
|
|
33
|
+
const token = (env['VIDOFY_TOKEN'] ?? '').trim();
|
|
34
|
+
const apiKey = (env['VIDOFY_API_KEY'] ?? '').trim();
|
|
35
|
+
if (token !== '' && apiKey !== '') {
|
|
36
|
+
throw new ConfigError('Both VIDOFY_TOKEN and VIDOFY_API_KEY are set. They bill different balances — ' +
|
|
37
|
+
'your personal coins and the API credit wallet — so pick one and remove the other.');
|
|
38
|
+
}
|
|
39
|
+
if (token === '' && apiKey === '') {
|
|
40
|
+
throw new ConfigError('No credential. Set VIDOFY_TOKEN to a personal MCP token from ' +
|
|
41
|
+
'https://vidofy.ai/en/studio/account/mcp-tokens (this bills your own Vidofy coins).');
|
|
42
|
+
}
|
|
43
|
+
const mode = token !== '' ? 'account' : 'key';
|
|
44
|
+
const credential = token !== '' ? token : apiKey;
|
|
45
|
+
/* Catch a swapped credential HERE rather than as a 401 on the first call.
|
|
46
|
+
The prefixes are the server's own: vmt_ for a personal MCP token,
|
|
47
|
+
vky_ for a partner API key. */
|
|
48
|
+
if (mode === 'account' && !credential.startsWith('vmt_')) {
|
|
49
|
+
throw new ConfigError('VIDOFY_TOKEN does not look like a personal MCP token (they start with "vmt_"). ' +
|
|
50
|
+
(credential.startsWith('vky_')
|
|
51
|
+
? 'That looks like an API key — set it as VIDOFY_API_KEY instead.'
|
|
52
|
+
: 'Create one at https://vidofy.ai/en/studio/account/mcp-tokens'));
|
|
53
|
+
}
|
|
54
|
+
if (mode === 'key' && !credential.startsWith('vky_')) {
|
|
55
|
+
throw new ConfigError('VIDOFY_API_KEY does not look like an API key (they start with "vky_"). ' +
|
|
56
|
+
(credential.startsWith('vmt_')
|
|
57
|
+
? 'That looks like a personal MCP token — set it as VIDOFY_TOKEN instead.'
|
|
58
|
+
: 'Find yours in the API platform on vidofy.ai'));
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
mode,
|
|
62
|
+
credential,
|
|
63
|
+
baseUrl: resolveBaseUrl(env),
|
|
64
|
+
version,
|
|
65
|
+
// Recorded server-side against the request in key mode and against the
|
|
66
|
+
// token in account mode, which is how "how much traffic came from MCP"
|
|
67
|
+
// is answered without any new tracking.
|
|
68
|
+
userAgent: `vidofy-mcp/${version}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A Config for ONE `vmt_` token, with the origin still taken from the process
|
|
73
|
+
* environment.
|
|
74
|
+
*
|
|
75
|
+
* This exists for the remote transport, where the two halves of a Config arrive
|
|
76
|
+
* from different places and at different times: the origin is server
|
|
77
|
+
* configuration, fixed at boot, while the credential belongs to whoever is
|
|
78
|
+
* making this particular request. loadConfig cannot serve that case — it reads
|
|
79
|
+
* the credential from the environment, which in a multi-user process would mean
|
|
80
|
+
* every caller spending one account's coins.
|
|
81
|
+
*
|
|
82
|
+
* The prefix check is deliberately the SAME one loadConfig applies. A remote
|
|
83
|
+
* caller pasting `vky_` into an Authorization header deserves the answer the
|
|
84
|
+
* local user gets, not a 401 that says nothing.
|
|
85
|
+
*
|
|
86
|
+
* @throws ConfigError — the caller turns it into a 401 with a readable message.
|
|
87
|
+
*/
|
|
88
|
+
export function configForToken(token, env = process.env, version = '0.0.0', resource) {
|
|
89
|
+
const credential = token.trim();
|
|
90
|
+
if (credential === '') {
|
|
91
|
+
throw new ConfigError('No bearer token. Send Authorization: Bearer vmt_… with every request.');
|
|
92
|
+
}
|
|
93
|
+
if (!credential.startsWith('vmt_')) {
|
|
94
|
+
throw new ConfigError(credential.startsWith('vky_')
|
|
95
|
+
/* No full stop after either URL. A terminal that auto-links takes the
|
|
96
|
+
trailing period as part of the href, and the user clicks through to
|
|
97
|
+
a 404 — from a message whose whole job was to get them to that page. */
|
|
98
|
+
? 'That is an API key (vky_…). This connector serves personal Vidofy accounts and bills '
|
|
99
|
+
+ 'your own coins — set VIDOFY_TOKEN (vmt_…) instead, from '
|
|
100
|
+
+ 'https://vidofy.ai/en/studio/account/mcp-tokens'
|
|
101
|
+
: 'Not a personal MCP token. They start with "vmt_" — create one at '
|
|
102
|
+
+ 'https://vidofy.ai/en/studio/account/mcp-tokens');
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
mode: 'account',
|
|
106
|
+
credential,
|
|
107
|
+
baseUrl: resolveBaseUrl(env),
|
|
108
|
+
version,
|
|
109
|
+
userAgent: `vidofy-mcp/${version}`,
|
|
110
|
+
...(resource !== undefined && resource !== '' ? { resource } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate and normalise VIDOFY_API_BASE.
|
|
115
|
+
*
|
|
116
|
+
* Factored out of loadConfig so the remote transport cannot end up with a
|
|
117
|
+
* weaker check than the local one — the checks below are the whole reason a
|
|
118
|
+
* hostile value in claude_desktop_config.json cannot redirect a credential, and
|
|
119
|
+
* a second copy of them would be a second chance to get one wrong.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveBaseUrl(env = process.env) {
|
|
122
|
+
// VIDOFY_API_BASE exists for development against a local instance. Trailing
|
|
123
|
+
// slashes are stripped so callers can join paths without doubling them.
|
|
124
|
+
const baseUrl = ((env['VIDOFY_API_BASE'] ?? '').trim() || DEFAULT_BASE).replace(/\/+$/, '');
|
|
125
|
+
if (!/^https?:\/\//i.test(baseUrl)) {
|
|
126
|
+
throw new ConfigError(`VIDOFY_API_BASE must start with http:// or https:// — got "${baseUrl}"`);
|
|
127
|
+
}
|
|
128
|
+
/* The scheme check alone is not a check.
|
|
129
|
+
*
|
|
130
|
+
* This value is set in claude_desktop_config.json, which people copy and
|
|
131
|
+
* paste from install snippets they found somewhere — so it is exactly the
|
|
132
|
+
* field a hostile snippet would target, and the credential goes wherever it
|
|
133
|
+
* points. Measured, all of these passed the scheme test:
|
|
134
|
+
*
|
|
135
|
+
* https://vidofy.ai@evil.example → host is evil.example; the token ships there
|
|
136
|
+
* http://vidofy.ai → the vmt_ token crosses the network in clear
|
|
137
|
+
* https://vidofy.ai/# → every path lands on the homepage instead
|
|
138
|
+
* https://vidofy.ai?x=1 → same, and nothing says why
|
|
139
|
+
*
|
|
140
|
+
* The last two are not attacks, just silent breakage — which is worse to
|
|
141
|
+
* diagnose than a refusal at startup. */
|
|
142
|
+
let parsedBase;
|
|
143
|
+
try {
|
|
144
|
+
parsedBase = new URL(baseUrl);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new ConfigError(`VIDOFY_API_BASE is not a valid URL — got "${baseUrl}"`);
|
|
148
|
+
}
|
|
149
|
+
if (parsedBase.username !== '' || parsedBase.password !== '') {
|
|
150
|
+
throw new ConfigError(`VIDOFY_API_BASE must not contain a username or password. In "${baseUrl}" the real ` +
|
|
151
|
+
`host is "${parsedBase.host}", not what appears before the "@".`);
|
|
152
|
+
}
|
|
153
|
+
/* Tested on the RAW string, not on the parsed parts: a bare "#" produces
|
|
154
|
+
an empty `hash`, so `parsedBase.hash !== ''` waves through
|
|
155
|
+
"https://vidofy.ai/#" — which then swallows every path appended to it
|
|
156
|
+
and sends the whole API to the homepage. */
|
|
157
|
+
/* Whitespace is rejected on the RAW string too. WHATWG URL silently strips
|
|
158
|
+
a newline, so "https://vidofy.ai\nevil" parses as host `vidofy.aievil`
|
|
159
|
+
while the value a user skims in claude_desktop_config.json still reads
|
|
160
|
+
as vidofy.ai — the same smuggling this block exists to stop. */
|
|
161
|
+
if (/\s/.test(baseUrl)) {
|
|
162
|
+
throw new ConfigError('VIDOFY_API_BASE must not contain whitespace or line breaks.');
|
|
163
|
+
}
|
|
164
|
+
if (/[?#]/.test(baseUrl) || parsedBase.pathname !== '/') {
|
|
165
|
+
throw new ConfigError(`VIDOFY_API_BASE must be an origin only — no path, query or fragment. Got "${baseUrl}".`);
|
|
166
|
+
}
|
|
167
|
+
/* URL.hostname keeps the brackets on an IPv6 literal — `new URL('http://[::1]')`
|
|
168
|
+
gives '[::1]', not '::1' — so comparing against the bare form refused
|
|
169
|
+
IPv6 loopback although it is listed right here as allowed. Strip them
|
|
170
|
+
once and compare. `.localhost` and `.test` are RFC 6761 loopback/testing
|
|
171
|
+
names, and host.docker.internal is how a container reaches this host;
|
|
172
|
+
all three are development, none reaches the internet. */
|
|
173
|
+
const host = parsedBase.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
174
|
+
const localHost = host === 'localhost' ||
|
|
175
|
+
host === '127.0.0.1' ||
|
|
176
|
+
host === '::1' ||
|
|
177
|
+
host === 'host.docker.internal' ||
|
|
178
|
+
host.endsWith('.local') ||
|
|
179
|
+
host.endsWith('.localhost') ||
|
|
180
|
+
host.endsWith('.test');
|
|
181
|
+
if (parsedBase.protocol !== 'https:' && !localHost) {
|
|
182
|
+
throw new ConfigError(`VIDOFY_API_BASE must use https:// — "${baseUrl}" would send your credential in clear text. ` +
|
|
183
|
+
'Plain http is accepted only for localhost and .local development hosts.');
|
|
184
|
+
}
|
|
185
|
+
// The parsed origin, not the raw string — so whatever survived the checks
|
|
186
|
+
// above is still normalised to scheme://host[:port] before anything
|
|
187
|
+
// concatenates a path onto it.
|
|
188
|
+
return parsedBase.origin;
|
|
189
|
+
}
|
|
190
|
+
/** The credential header for this mode. Kept next to loadConfig so the two cannot drift. */
|
|
191
|
+
export function authHeaders(cfg) {
|
|
192
|
+
const creds = cfg.mode === 'account'
|
|
193
|
+
? { Authorization: `Bearer ${cfg.credential}` }
|
|
194
|
+
: { 'X-API-Key': cfg.credential };
|
|
195
|
+
/* The audience travels with the credential because it is part of deciding
|
|
196
|
+
whether the credential is valid HERE — see Config.resource. Omitted
|
|
197
|
+
entirely when there is none, since an empty header and no header are the
|
|
198
|
+
same thing to the reader but not to a proxy. */
|
|
199
|
+
return cfg.resource !== undefined && cfg.resource !== ''
|
|
200
|
+
? { ...creds, 'X-Vidofy-MCP-Resource': cfg.resource }
|
|
201
|
+
: creds;
|
|
202
|
+
}
|
|
203
|
+
/** `/app/v1` or `/api/v1` — the door this mode speaks to. */
|
|
204
|
+
export function apiPrefix(cfg) {
|
|
205
|
+
return cfg.mode === 'account' ? '/app/v1' : '/api/v1';
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start reporting liveness. Safe to call twice; the second call does nothing.
|
|
3
|
+
*
|
|
4
|
+
* Failure here must never take the connector down — it is a report, not the
|
|
5
|
+
* work. But it must not be silent either, because a silent failure looks exactly
|
|
6
|
+
* like a dead process on the dashboard and would send someone to restart a
|
|
7
|
+
* server that was serving traffic the whole time. So the first failure logs, and
|
|
8
|
+
* the rest do not (a Redis outage would otherwise write a line every 30s
|
|
9
|
+
* forever).
|
|
10
|
+
*/
|
|
11
|
+
export declare function startHeartbeat(): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Liveness reporting, for the operator's own process dashboard.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. The remote connector runs as a long-lived managed process,
|
|
5
|
+
* and every such process on this platform is expected to report that it is
|
|
6
|
+
* alive. The dashboard does not interrogate the process manager — it reads a
|
|
7
|
+
* key the program writes for itself. A program that is registered and writes
|
|
8
|
+
* nothing therefore shows as **down forever** and raises a CRITICAL alert on a
|
|
9
|
+
* process that is perfectly healthy, which is worse than not registering it at
|
|
10
|
+
* all: the one alert nobody can act on is the one that teaches people to ignore
|
|
11
|
+
* the page.
|
|
12
|
+
*
|
|
13
|
+
* The contract is the same one every other worker on the platform writes, and
|
|
14
|
+
* the platform is the reference if the two ever disagree:
|
|
15
|
+
*
|
|
16
|
+
* value the unix timestamp, as a decimal string
|
|
17
|
+
* TTL 300s — deliberately longer than the dashboard's 120s down window, so a
|
|
18
|
+
* healthy program's key never expires between writes. Down-detection
|
|
19
|
+
* compares the STORED timestamp, not key presence, so an expired key and
|
|
20
|
+
* a stale one read the same.
|
|
21
|
+
* every 30s
|
|
22
|
+
*
|
|
23
|
+
* The `cache:` prefix is correct here, unlike `mcp_oauth:`: this value is a
|
|
24
|
+
* report about something else, so an operator clearing the cache erases one
|
|
25
|
+
* 30-second window and no state. A pending authorization erased would cost a
|
|
26
|
+
* user their sign-in, which is why that one sits outside the prefix.
|
|
27
|
+
*/
|
|
28
|
+
import { redisClient } from './oauth/store.js';
|
|
29
|
+
import { log } from './log.js';
|
|
30
|
+
/** Matches the platform's own workers. The dashboard's window is 120s. */
|
|
31
|
+
const INTERVAL_MS = 30_000;
|
|
32
|
+
const TTL_SEC = 300;
|
|
33
|
+
/** The managed-process name — this IS the dashboard's key, so it must match. */
|
|
34
|
+
const PROGRAM = 'vidofy-mcp';
|
|
35
|
+
let timer = null;
|
|
36
|
+
let warned = false;
|
|
37
|
+
async function beat() {
|
|
38
|
+
const client = await redisClient();
|
|
39
|
+
await client.setEx(`cache:scheduled_tasks:hb:${PROGRAM}`, TTL_SEC, String(Math.floor(Date.now() / 1000)));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Start reporting liveness. Safe to call twice; the second call does nothing.
|
|
43
|
+
*
|
|
44
|
+
* Failure here must never take the connector down — it is a report, not the
|
|
45
|
+
* work. But it must not be silent either, because a silent failure looks exactly
|
|
46
|
+
* like a dead process on the dashboard and would send someone to restart a
|
|
47
|
+
* server that was serving traffic the whole time. So the first failure logs, and
|
|
48
|
+
* the rest do not (a Redis outage would otherwise write a line every 30s
|
|
49
|
+
* forever).
|
|
50
|
+
*/
|
|
51
|
+
export function startHeartbeat() {
|
|
52
|
+
if (timer !== null)
|
|
53
|
+
return;
|
|
54
|
+
const tick = () => {
|
|
55
|
+
beat().catch((err) => {
|
|
56
|
+
if (warned)
|
|
57
|
+
return;
|
|
58
|
+
warned = true;
|
|
59
|
+
log(`heartbeat failed, so the process dashboard will read this connector as DOWN `
|
|
60
|
+
+ `even while it serves traffic: ${err instanceof Error ? err.message : String(err)}`);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
tick(); // the dashboard should not wait 30s for the first one
|
|
64
|
+
timer = setInterval(tick, INTERVAL_MS);
|
|
65
|
+
/* Unreferenced so it can never be the reason the process stays alive — the
|
|
66
|
+
HTTP server is what holds it open, and a lingering interval would keep a
|
|
67
|
+
shutting-down process from exiting. */
|
|
68
|
+
timer.unref();
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=heartbeat.js.map
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The REMOTE entry point — the same nine tools over Streamable HTTP.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists at all: a web client cannot run a process on the user's
|
|
6
|
+
* machine, so claude.ai and ChatGPT can never reach the stdio package however
|
|
7
|
+
* well it works. They speak to a URL or to nothing. This is that URL.
|
|
8
|
+
*
|
|
9
|
+
* node dist/http.js → listens on VIDOFY_MCP_PORT, path /mcp-app
|
|
10
|
+
* node dist/index.js → the stdio server, unchanged
|
|
11
|
+
*
|
|
12
|
+
* In production nginx terminates TLS on vidofy.ai and proxies /mcp-app here;
|
|
13
|
+
* this process never sees the internet directly and never holds a certificate.
|
|
14
|
+
*
|
|
15
|
+
* IT IS OAUTH NOW — and the two paragraphs that used to stand here said the
|
|
16
|
+
* opposite
|
|
17
|
+
* ------------------------------------------------------------------------
|
|
18
|
+
* They described a staging step ("it is not OAuth… a fixed bearer header… NOT
|
|
19
|
+
* the shipping shape") and were true for about a day. They are left mentioned
|
|
20
|
+
* rather than silently deleted because the thing they got wrong is worth
|
|
21
|
+
* knowing: the staged shape is now **impossible**, not merely superseded. A
|
|
22
|
+
* hand-made `vmt_` from the tokens page declares NO resource, this
|
|
23
|
+
* endpoint always declares a resource (see MCP_PATH below), and the server
|
|
24
|
+
* refuses that pairing outright. So "No sign-in
|
|
25
|
+
* + Request headers" cannot work here any more, and a reader who trusted those
|
|
26
|
+
* paragraphs would spend an afternoon finding out.
|
|
27
|
+
*
|
|
28
|
+
* What is built: authorization (`/mcp-app/authorize`), the consent hand-off to
|
|
29
|
+
* PHP, the code exchange (`/mcp-app/token`), Client ID Metadata Documents, PKCE
|
|
30
|
+
* S256, and RFC 8707 audience binding. What is NOT built: token revocation over
|
|
31
|
+
* the protocol — revocation lives on the website's own tokens page, which is why
|
|
32
|
+
* `revocation_endpoint` is absent from the metadata below rather than advertised
|
|
33
|
+
* and unrouted.
|
|
34
|
+
*
|
|
35
|
+
* STATELESS ON PURPOSE
|
|
36
|
+
* --------------------
|
|
37
|
+
* sessionIdGenerator is undefined, so every request stands alone. That is a
|
|
38
|
+
* measured fit rather than a simplification: this server never pushes a
|
|
39
|
+
* notification (zero sendNotification calls in the package), because the card
|
|
40
|
+
* polls get_status through the host instead. Sessions exist to carry
|
|
41
|
+
* server-initiated messages; with none to carry, holding per-session state would
|
|
42
|
+
* be a memory leak with a session id on it.
|
|
43
|
+
*/
|
|
44
|
+
export {};
|