@clockworkdog/cogs-client 3.0.3 → 3.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/dist/browser/index.mjs +4605 -1856
- package/dist/browser/index.umd.js +13 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/state-based/MediaClipManager.d.ts +1 -1
- package/dist/state-based/MediaClipManager.js +18 -12
- package/dist/types/CogsPluginManifest.d.ts +56 -0
- package/dist/utils/NetworkHostPattern.d.ts +15 -0
- package/dist/utils/NetworkHostPattern.js +65 -0
- package/dist/utils/getPluginManifestErrors.d.ts +6 -0
- package/dist/utils/getPluginManifestErrors.js +167 -0
- package/package.json +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { SurfaceManager } from './state-based/SurfaceManager';
|
|
|
7
7
|
export { MediaPreloader } from './state-based/MediaPreloader';
|
|
8
8
|
export { assetUrl, preloadUrl } from './utils/urls';
|
|
9
9
|
export { getStateAtTime } from './utils/getStateAtTime';
|
|
10
|
+
export { NetworkHostPattern } from './utils/NetworkHostPattern';
|
|
10
11
|
export * from './types/CogsPluginManifest';
|
|
11
12
|
export * as ManifestTypes from './types/ManifestTypes';
|
|
12
13
|
export * from './DataStore';
|
|
14
|
+
export { getPluginManifestErrors } from './utils/getPluginManifestErrors';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ export { SurfaceManager } from './state-based/SurfaceManager';
|
|
|
5
5
|
export { MediaPreloader } from './state-based/MediaPreloader';
|
|
6
6
|
export { assetUrl, preloadUrl } from './utils/urls';
|
|
7
7
|
export { getStateAtTime } from './utils/getStateAtTime';
|
|
8
|
+
export { NetworkHostPattern } from './utils/NetworkHostPattern';
|
|
8
9
|
export * from './types/CogsPluginManifest';
|
|
9
10
|
export * as ManifestTypes from './types/ManifestTypes';
|
|
10
11
|
export * from './DataStore';
|
|
12
|
+
export { getPluginManifestErrors } from './utils/getPluginManifestErrors';
|
|
@@ -43,7 +43,7 @@ interface TemporalSyncState {
|
|
|
43
43
|
* Makes sure the media is at the correct time and speed.
|
|
44
44
|
* - Algorithms and constants defined above
|
|
45
45
|
*/
|
|
46
|
-
export declare function assertTemporalProperties(mediaElement: HTMLMediaElement, properties: TemporalProperties, keyframes: VideoState['keyframes'], syncState: TemporalSyncState, enablePlaybackRateAdjustment: boolean): TemporalSyncState;
|
|
46
|
+
export declare function assertTemporalProperties(mediaElement: HTMLMediaElement, properties: TemporalProperties, keyframes: VideoState['keyframes'], currentTime: number, syncState: TemporalSyncState, enablePlaybackRateAdjustment: boolean): TemporalSyncState;
|
|
47
47
|
export declare class ImageManager extends MediaClipManager<ImageState> {
|
|
48
48
|
private imageElement;
|
|
49
49
|
protected update(): void;
|
|
@@ -181,7 +181,8 @@ const SYNC_INNER_TARGET_THRESHOLD_MS = 5;
|
|
|
181
181
|
const SYNC_MAX_THRESHOLD_MS = 1_000;
|
|
182
182
|
const SYNC_SEEK_LOOKAHEAD_MS = 10;
|
|
183
183
|
// If the media is scheduled to go back to the start close in time to the end of the video, we'll use the loop attribute.
|
|
184
|
-
|
|
184
|
+
// This value allows disagreement between HTMLVideoElement.duration and the length of the different audio streams we have in COGS.
|
|
185
|
+
const LOOPING_EPSILON_MS = 100;
|
|
185
186
|
const PLAYBACK_ADJUSTMENT_SMOOTHING = 0.3;
|
|
186
187
|
const MAX_PLAYBACK_RATE_ADJUSTMENT = 0.1;
|
|
187
188
|
function playbackSmoothing(deltaTime) {
|
|
@@ -204,7 +205,7 @@ function assertPlaybackRate(mediaElement, playbackRate) {
|
|
|
204
205
|
* Makes sure the media is at the correct time and speed.
|
|
205
206
|
* - Algorithms and constants defined above
|
|
206
207
|
*/
|
|
207
|
-
export function assertTemporalProperties(mediaElement, properties, keyframes, syncState, enablePlaybackRateAdjustment) {
|
|
208
|
+
export function assertTemporalProperties(mediaElement, properties, keyframes, currentTime, syncState, enablePlaybackRateAdjustment) {
|
|
208
209
|
// On Webkit (using the simulator on safari and COGS mobile app on iOS) changes to currentTime and playbackRate are much less responsive.
|
|
209
210
|
// We make sure we only do lower frequency updates, and don't change playbackRate.
|
|
210
211
|
const playbackRateSync = enablePlaybackRateAdjustment && !IS_WEBKIT;
|
|
@@ -212,20 +213,23 @@ export function assertTemporalProperties(mediaElement, properties, keyframes, sy
|
|
|
212
213
|
// Sounds like looping to me!
|
|
213
214
|
let isLooping = false;
|
|
214
215
|
if (mediaElement.duration) {
|
|
215
|
-
const
|
|
216
|
+
const futureTemporalKeyframes = keyframes
|
|
217
|
+
.filter(([t]) => t > keyframes[0][0] && t > currentTime)
|
|
218
|
+
.filter(([, props]) => props?.set?.t !== undefined || props?.set?.rate !== undefined);
|
|
219
|
+
const nextTemporalKeyframe = futureTemporalKeyframes[0];
|
|
216
220
|
if (nextTemporalKeyframe?.[1]?.set?.t === 0) {
|
|
217
|
-
const timeRemaining = (mediaElement.duration - properties.t) / properties.rate;
|
|
218
|
-
const timeUntilKeyframe = nextTemporalKeyframe[0] -
|
|
221
|
+
const timeRemaining = (mediaElement.duration * 1000 - properties.t) / properties.rate;
|
|
222
|
+
const timeUntilKeyframe = nextTemporalKeyframe[0] - currentTime;
|
|
219
223
|
isLooping = Math.abs(timeRemaining - timeUntilKeyframe) <= LOOPING_EPSILON_MS;
|
|
220
224
|
if (mediaElement.loop !== isLooping) {
|
|
221
225
|
mediaElement.loop = isLooping;
|
|
222
226
|
}
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
|
-
const
|
|
229
|
+
const currentMediaTime = mediaElement.currentTime * 1000;
|
|
226
230
|
const deltaTime = isLooping && mediaElement.duration !== undefined
|
|
227
|
-
? moduloDiff(
|
|
228
|
-
:
|
|
231
|
+
? moduloDiff(currentMediaTime, properties.t, mediaElement.duration * 1000)
|
|
232
|
+
: currentMediaTime - properties.t;
|
|
229
233
|
const deltaTimeAbs = Math.abs(deltaTime);
|
|
230
234
|
switch (true) {
|
|
231
235
|
/**
|
|
@@ -362,7 +366,8 @@ export class AudioManager extends MediaClipManager {
|
|
|
362
366
|
audioElement;
|
|
363
367
|
volume = 1;
|
|
364
368
|
update() {
|
|
365
|
-
const
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
const currentState = getStateAtTime(this._state, now);
|
|
366
371
|
if (currentState) {
|
|
367
372
|
this.audioElement = assertElement(this.audioElement, this.clipElement, this._state, this.constructAssetURL, this.mediaPreloader);
|
|
368
373
|
}
|
|
@@ -373,7 +378,7 @@ export class AudioManager extends MediaClipManager {
|
|
|
373
378
|
return;
|
|
374
379
|
const sinkId = this.getAudioOutput(this._state.audioOutput);
|
|
375
380
|
assertAudialProperties(this.audioElement, currentState, sinkId, this.volume);
|
|
376
|
-
const nextSyncState = assertTemporalProperties(this.audioElement, currentState, this._state.keyframes, this.syncState, this._state.enablePlaybackRateAdjustment);
|
|
381
|
+
const nextSyncState = assertTemporalProperties(this.audioElement, currentState, this._state.keyframes, now, this.syncState, this._state.enablePlaybackRateAdjustment);
|
|
377
382
|
this.syncState = nextSyncState;
|
|
378
383
|
}
|
|
379
384
|
destroy() {
|
|
@@ -392,7 +397,8 @@ export class VideoManager extends MediaClipManager {
|
|
|
392
397
|
videoElement;
|
|
393
398
|
volume = 1;
|
|
394
399
|
update() {
|
|
395
|
-
const
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const currentState = getStateAtTime(this._state, now);
|
|
396
402
|
if (currentState) {
|
|
397
403
|
this.videoElement = assertElement(this.videoElement, this.clipElement, this._state, this.constructAssetURL, this.mediaPreloader);
|
|
398
404
|
}
|
|
@@ -404,7 +410,7 @@ export class VideoManager extends MediaClipManager {
|
|
|
404
410
|
const sinkId = this.getAudioOutput(this._state.audioOutput);
|
|
405
411
|
assertVisualProperties(this.videoElement, currentState, this._state.fit);
|
|
406
412
|
assertAudialProperties(this.videoElement, currentState, sinkId, this.volume);
|
|
407
|
-
const nextSyncState = assertTemporalProperties(this.videoElement, currentState, this._state.keyframes, this.syncState, this._state.enablePlaybackRateAdjustment);
|
|
413
|
+
const nextSyncState = assertTemporalProperties(this.videoElement, currentState, this._state.keyframes, now, this.syncState, this._state.enablePlaybackRateAdjustment);
|
|
408
414
|
this.syncState = nextSyncState;
|
|
409
415
|
}
|
|
410
416
|
destroy() {
|
|
@@ -41,6 +41,40 @@ export type PluginManifestStateJson = {
|
|
|
41
41
|
writableFromCogs?: true;
|
|
42
42
|
writableFromClient?: true;
|
|
43
43
|
};
|
|
44
|
+
export type PluginManifestNetworkAccessRuleJson = {
|
|
45
|
+
/**
|
|
46
|
+
* Host patterns this rule grants outbound network access to.
|
|
47
|
+
*
|
|
48
|
+
* Every pattern must include an explicit port.
|
|
49
|
+
*
|
|
50
|
+
* | Pattern | Meaning |
|
|
51
|
+
* | --- | --- |
|
|
52
|
+
* | `"localhost:8080"` | Port 8080 on `localhost` / `127.0.0.1` / `::1` |
|
|
53
|
+
* | `"private:443"` | Port 443 on any RFC 1918 private address range (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) |
|
|
54
|
+
* | `"api.vendor.com:443"` | An exact hostname and port |
|
|
55
|
+
* | `"*.vendor.com:443"` | A wildcard subdomain, exact port |
|
|
56
|
+
* | `"foo.example.com:*"` | Any port on an exact hostname |
|
|
57
|
+
* | `"[fd00::1]:443"` | An IPv6 literal, using bracket syntax |
|
|
58
|
+
* | `"*:*"` | Any host, any port |
|
|
59
|
+
*
|
|
60
|
+
* CIDR ranges (e.g. `"192.168.1.0/24:443"`) are not supported.
|
|
61
|
+
*
|
|
62
|
+
* Every port on `localhost` / `127.0.0.1` / `::1` is always implicitly reachable regardless of
|
|
63
|
+
* these rules, since plugins connect to the COGS server at `localhost:12095` — a `"localhost:*"`
|
|
64
|
+
* pattern here is never required.
|
|
65
|
+
*/
|
|
66
|
+
hosts: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Paths, relative to the root of the packaged `.cogsplugin` archive, of PEM-encoded CA
|
|
69
|
+
* certificates to trust for HTTPS connections to the hosts matched by `hosts`.
|
|
70
|
+
*
|
|
71
|
+
* Use this to reach local devices with vendor-issued certificates that aren't in the system CA
|
|
72
|
+
* store (e.g. a Philips Hue bridge). For matched hosts, the certificate chain is verified
|
|
73
|
+
* against these CAs instead of the system store; all other hosts continue to use the system
|
|
74
|
+
* store.
|
|
75
|
+
*/
|
|
76
|
+
caCertificates?: string[];
|
|
77
|
+
};
|
|
44
78
|
/**
|
|
45
79
|
* `cogs-plugin-manifest.json` is a JSON manifest file describing the content of a COGS plugin or COGS Media Master custom content.
|
|
46
80
|
*
|
|
@@ -141,6 +175,28 @@ export interface CogsPluginManifestJson {
|
|
|
141
175
|
};
|
|
142
176
|
};
|
|
143
177
|
};
|
|
178
|
+
/**
|
|
179
|
+
* Elevated permissions for this plugin.
|
|
180
|
+
*
|
|
181
|
+
* **Only applies to verified, packaged (`.cogsplugin`) plugins.** It is ignored for plugins
|
|
182
|
+
* loaded from a folder, to maintain compatibility with the existing folder-based plugin
|
|
183
|
+
* structure.
|
|
184
|
+
*/
|
|
185
|
+
permissions?: {
|
|
186
|
+
network?: {
|
|
187
|
+
/**
|
|
188
|
+
* Rules granting this plugin outbound network access to specific hosts, optionally with
|
|
189
|
+
* a bundled CA certificate for hosts using vendor-issued (rather than publicly-trusted)
|
|
190
|
+
* TLS certificates.
|
|
191
|
+
*
|
|
192
|
+
* Requests to hosts not matched by any rule here are blocked. By default a plugin has no
|
|
193
|
+
* network access beyond the COGS server itself.
|
|
194
|
+
*
|
|
195
|
+
* Added in COGS 5.11.0
|
|
196
|
+
*/
|
|
197
|
+
access?: PluginManifestNetworkAccessRuleJson[];
|
|
198
|
+
};
|
|
199
|
+
};
|
|
144
200
|
}
|
|
145
201
|
/**
|
|
146
202
|
* A readonly version of `PluginManifestJson`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single host pattern from a `PluginManifestNetworkAccessRuleJson`'s `hosts` array
|
|
3
|
+
* (e.g. `"private:443"`, `"*.vendor.com:443"`).
|
|
4
|
+
*/
|
|
5
|
+
export declare class NetworkHostPattern {
|
|
6
|
+
private readonly host;
|
|
7
|
+
private readonly port;
|
|
8
|
+
/**
|
|
9
|
+
* @throws If `pattern` has no port (or `*`) component.
|
|
10
|
+
*/
|
|
11
|
+
constructor(pattern: string);
|
|
12
|
+
/** Checks whether `host` and `port` are matched by this pattern. */
|
|
13
|
+
validate(host: string, port: number): boolean;
|
|
14
|
+
private matchesHost;
|
|
15
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Address4, Address6 } from 'ip-address';
|
|
2
|
+
/**
|
|
3
|
+
* A single host pattern from a `PluginManifestNetworkAccessRuleJson`'s `hosts` array
|
|
4
|
+
* (e.g. `"private:443"`, `"*.vendor.com:443"`).
|
|
5
|
+
*/
|
|
6
|
+
export class NetworkHostPattern {
|
|
7
|
+
host;
|
|
8
|
+
port;
|
|
9
|
+
/**
|
|
10
|
+
* @throws If `pattern` has no port (or `*`) component.
|
|
11
|
+
*/
|
|
12
|
+
constructor(pattern) {
|
|
13
|
+
// Split on the last `:` rather than the first, since a bracketed IPv6 host (e.g. "[fd00::1]")
|
|
14
|
+
// contains colons of its own.
|
|
15
|
+
const lastColonIndex = pattern.lastIndexOf(':');
|
|
16
|
+
const host = pattern.slice(0, lastColonIndex);
|
|
17
|
+
const port = pattern.slice(lastColonIndex + 1);
|
|
18
|
+
if (lastColonIndex === -1 || !/^(\d+|\*)$/.test(port)) {
|
|
19
|
+
throw new Error(`Invalid host pattern "${pattern}": a port (or "*") is required`);
|
|
20
|
+
}
|
|
21
|
+
this.host = host;
|
|
22
|
+
this.port = port;
|
|
23
|
+
}
|
|
24
|
+
/** Checks whether `host` and `port` are matched by this pattern. */
|
|
25
|
+
validate(host, port) {
|
|
26
|
+
return this.matchesHost(host) && (this.port === '*' || Number(this.port) === port);
|
|
27
|
+
}
|
|
28
|
+
matchesHost(host) {
|
|
29
|
+
// Hostnames are case-insensitive; IP literals are unaffected by lowercasing.
|
|
30
|
+
const patternHost = this.host.toLowerCase();
|
|
31
|
+
host = host.toLowerCase();
|
|
32
|
+
if (patternHost === '*') {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (patternHost === 'localhost') {
|
|
36
|
+
return host === 'localhost' || isLoopback(host);
|
|
37
|
+
}
|
|
38
|
+
if (patternHost === 'private') {
|
|
39
|
+
return isPrivateIpv4(host);
|
|
40
|
+
}
|
|
41
|
+
if (patternHost.startsWith('*.')) {
|
|
42
|
+
// The leading dot in the suffix guarantees a full-label boundary, so "*.vendor.com"
|
|
43
|
+
// matches "api.vendor.com" but not "vendor.com".
|
|
44
|
+
return host.endsWith(patternHost.slice(1));
|
|
45
|
+
}
|
|
46
|
+
return patternHost === host;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* RFC 1918 private address ranges: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`.
|
|
51
|
+
*/
|
|
52
|
+
function isPrivateIpv4(host) {
|
|
53
|
+
return Address4.isValid(host) && new Address4(host).isPrivate();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Loopback ranges: `127.0.0.0/8` (RFC 5735) and `::1` (RFC 4291).
|
|
57
|
+
*/
|
|
58
|
+
function isLoopback(host) {
|
|
59
|
+
// Only IPv6 literals are ever bracketed (e.g. "[::1]"); `Address6` doesn't accept the brackets itself.
|
|
60
|
+
if (host.startsWith('[') && host.endsWith(']')) {
|
|
61
|
+
const address = host.slice(1, -1);
|
|
62
|
+
return Address6.isValid(address) && new Address6(address).isLoopback();
|
|
63
|
+
}
|
|
64
|
+
return (Address4.isValid(host) && new Address4(host).isLoopback()) || (Address6.isValid(host) && new Address6(host).isLoopback());
|
|
65
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CogsPluginManifestJson, CogsValueTypeOptionWithDefault } from '../types/CogsPluginManifest';
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
export declare const cogsValueTypeOptionWithDefaultSchema: (options: {
|
|
4
|
+
defaultRequired: boolean;
|
|
5
|
+
}) => z.ZodType<CogsValueTypeOptionWithDefault>;
|
|
6
|
+
export declare function getPluginManifestErrors(manifest: CogsPluginManifestJson): string[] | null;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
import { NetworkHostPattern } from './NetworkHostPattern';
|
|
4
|
+
const uniqueStringArray = z.array(z.string().min(1)).refine((items) => new Set(items).size === items.length, {
|
|
5
|
+
message: 'Array items must be unique',
|
|
6
|
+
});
|
|
7
|
+
const cogsValueTypeStringSchema = z.strictObject({
|
|
8
|
+
type: z.literal('string'),
|
|
9
|
+
});
|
|
10
|
+
const cogsValueTypeNumberSchema = z.strictObject({
|
|
11
|
+
type: z.literal('number'),
|
|
12
|
+
integer: z.literal(true).optional(),
|
|
13
|
+
min: z.number().optional(),
|
|
14
|
+
max: z.number().optional(),
|
|
15
|
+
});
|
|
16
|
+
const cogsValueTypeBooleanSchema = z.strictObject({
|
|
17
|
+
type: z.literal('boolean'),
|
|
18
|
+
});
|
|
19
|
+
const cogsValueTypeOptionSchema = z.strictObject({
|
|
20
|
+
type: z.literal('option'),
|
|
21
|
+
options: uniqueStringArray,
|
|
22
|
+
});
|
|
23
|
+
const pluginCogsValueTypeJsonSchema = z.union([
|
|
24
|
+
cogsValueTypeStringSchema,
|
|
25
|
+
cogsValueTypeNumberSchema,
|
|
26
|
+
cogsValueTypeBooleanSchema,
|
|
27
|
+
cogsValueTypeOptionSchema,
|
|
28
|
+
]);
|
|
29
|
+
const cogsValueTypeStringWithDefaultSchema = (options) => (options.defaultRequired
|
|
30
|
+
? z.strictObject({ type: z.literal('string'), default: z.string() })
|
|
31
|
+
: z.strictObject({ type: z.literal('string'), default: z.string().optional() }));
|
|
32
|
+
const cogsValueTypeNumberWithDefaultSchema = (options) => (options.defaultRequired
|
|
33
|
+
? z.strictObject({
|
|
34
|
+
type: z.literal('number'),
|
|
35
|
+
integer: z.boolean().optional(),
|
|
36
|
+
min: z.number().optional(),
|
|
37
|
+
max: z.number().optional(),
|
|
38
|
+
default: z.number(),
|
|
39
|
+
})
|
|
40
|
+
: z.strictObject({
|
|
41
|
+
type: z.literal('number'),
|
|
42
|
+
integer: z.boolean().optional(),
|
|
43
|
+
min: z.number().optional(),
|
|
44
|
+
max: z.number().optional(),
|
|
45
|
+
default: z.number().optional(),
|
|
46
|
+
}));
|
|
47
|
+
const cogsValueTypeBooleanWithDefaultSchema = (options) => (options.defaultRequired
|
|
48
|
+
? z.strictObject({ type: z.literal('boolean'), default: z.boolean() })
|
|
49
|
+
: z.strictObject({ type: z.literal('boolean'), default: z.boolean().optional() }));
|
|
50
|
+
export const cogsValueTypeOptionWithDefaultSchema = (options) => (options.defaultRequired
|
|
51
|
+
? z.strictObject({
|
|
52
|
+
type: z.literal('option'),
|
|
53
|
+
options: uniqueStringArray.optional(),
|
|
54
|
+
default: z.string().min(1),
|
|
55
|
+
})
|
|
56
|
+
: z.strictObject({
|
|
57
|
+
type: z.literal('option'),
|
|
58
|
+
options: uniqueStringArray.optional(),
|
|
59
|
+
default: z.string().min(1).optional(),
|
|
60
|
+
}));
|
|
61
|
+
const pluginCogsValueTypeWithDefaultJsonSchema = (options) => z.union([
|
|
62
|
+
cogsValueTypeStringWithDefaultSchema(options),
|
|
63
|
+
cogsValueTypeNumberWithDefaultSchema(options),
|
|
64
|
+
cogsValueTypeBooleanWithDefaultSchema(options),
|
|
65
|
+
cogsValueTypeOptionWithDefaultSchema(options),
|
|
66
|
+
]);
|
|
67
|
+
const pluginManifestConfigJsonSchema = z.strictObject({
|
|
68
|
+
name: z.string().min(1),
|
|
69
|
+
value: pluginCogsValueTypeWithDefaultJsonSchema({ defaultRequired: false }),
|
|
70
|
+
});
|
|
71
|
+
const pluginManifestEventJsonSchema = z.strictObject({
|
|
72
|
+
name: z.string().min(1),
|
|
73
|
+
value: pluginCogsValueTypeJsonSchema.optional(),
|
|
74
|
+
});
|
|
75
|
+
const pluginManifestStateJsonSchema = z.strictObject({
|
|
76
|
+
name: z.string().min(1),
|
|
77
|
+
value: pluginCogsValueTypeWithDefaultJsonSchema({ defaultRequired: true }),
|
|
78
|
+
writableFromCogs: z.literal(true).optional(),
|
|
79
|
+
writableFromClient: z.literal(true).optional(),
|
|
80
|
+
});
|
|
81
|
+
const validateNetworkHostPattern = (pattern) => {
|
|
82
|
+
try {
|
|
83
|
+
new NetworkHostPattern(pattern);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const cogsPluginManifestJsonSchema = z
|
|
91
|
+
.strictObject({
|
|
92
|
+
version: z.union([
|
|
93
|
+
z.templateLiteral([z.uint32()]),
|
|
94
|
+
z.templateLiteral([z.uint32(), z.literal('.'), z.uint32()]),
|
|
95
|
+
z.templateLiteral([z.uint32(), z.literal('.'), z.uint32(), z.literal('.'), z.uint32()]),
|
|
96
|
+
]),
|
|
97
|
+
name: z.string(),
|
|
98
|
+
minCogsVersion: z.templateLiteral([z.uint32(), z.literal('.'), z.uint32(), z.literal('.'), z.uint32()]).optional(),
|
|
99
|
+
description: z.string().optional(),
|
|
100
|
+
icon: z.string().optional(),
|
|
101
|
+
indexPath: z.string().optional(),
|
|
102
|
+
window: z
|
|
103
|
+
.object({
|
|
104
|
+
width: z.number().gt(0).int(),
|
|
105
|
+
height: z.number().gt(0).int(),
|
|
106
|
+
visible: z.boolean().optional(),
|
|
107
|
+
})
|
|
108
|
+
.optional(),
|
|
109
|
+
config: z.array(pluginManifestConfigJsonSchema).optional(),
|
|
110
|
+
events: z
|
|
111
|
+
.object({
|
|
112
|
+
fromCogs: z.array(pluginManifestEventJsonSchema).optional(),
|
|
113
|
+
toCogs: z.array(pluginManifestEventJsonSchema).optional(),
|
|
114
|
+
})
|
|
115
|
+
.optional(),
|
|
116
|
+
state: z.array(pluginManifestStateJsonSchema).optional(),
|
|
117
|
+
media: z
|
|
118
|
+
.object({
|
|
119
|
+
audio: z.literal(true).optional(),
|
|
120
|
+
video: z.literal(true).optional(),
|
|
121
|
+
images: z.literal(true).optional(),
|
|
122
|
+
})
|
|
123
|
+
.optional(),
|
|
124
|
+
store: z
|
|
125
|
+
.object({
|
|
126
|
+
items: z
|
|
127
|
+
.record(z.string(), z.strictObject({
|
|
128
|
+
persistValue: z.boolean().optional(),
|
|
129
|
+
}))
|
|
130
|
+
.optional(),
|
|
131
|
+
})
|
|
132
|
+
.optional(),
|
|
133
|
+
permissions: z
|
|
134
|
+
.strictObject({
|
|
135
|
+
network: z
|
|
136
|
+
.strictObject({
|
|
137
|
+
access: z
|
|
138
|
+
.array(z.strictObject({
|
|
139
|
+
hosts: z.array(z.string().refine(validateNetworkHostPattern, { error: 'Invalid network host pattern' })).min(1),
|
|
140
|
+
caCertificate: z.string().optional(),
|
|
141
|
+
}))
|
|
142
|
+
.optional(),
|
|
143
|
+
})
|
|
144
|
+
.optional(),
|
|
145
|
+
})
|
|
146
|
+
.optional(),
|
|
147
|
+
})
|
|
148
|
+
// Check that network access rules are only present if minCogsVersion is at least 5.11.0
|
|
149
|
+
.refine((manifest) => {
|
|
150
|
+
if (!manifest.permissions?.network?.access) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (manifest.minCogsVersion && semver.gte(manifest.minCogsVersion, '5.11.0')) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}, {
|
|
158
|
+
path: ['permissions', 'network', 'access'],
|
|
159
|
+
message: 'minCogsVersion must be at least 5.11.0',
|
|
160
|
+
});
|
|
161
|
+
const validate = cogsPluginManifestJsonSchema;
|
|
162
|
+
export function getPluginManifestErrors(manifest) {
|
|
163
|
+
const result = validate.safeParse(manifest);
|
|
164
|
+
if (result.success)
|
|
165
|
+
return null;
|
|
166
|
+
return result.error.issues.map((issue) => `${issue.path.join('.') || 'root'}: ${issue.message}`);
|
|
167
|
+
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Connect to COGS to build a custom Media Master",
|
|
4
4
|
"author": "Clockwork Dog <info@clockwork.dog>",
|
|
5
5
|
"homepage": "https://github.com/clockwork-dog/cogs-sdk/tree/main/packages/javascript",
|
|
6
|
-
"version": "3.0
|
|
6
|
+
"version": "3.1.0",
|
|
7
7
|
"keywords": [],
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -37,8 +37,10 @@
|
|
|
37
37
|
"cy:generate": "cypress run --e2e"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@clockworkdog/timesync": "^3.0
|
|
40
|
+
"@clockworkdog/timesync": "^3.1.0",
|
|
41
|
+
"ip-address": "^10.2.0",
|
|
41
42
|
"reconnecting-websocket": "^4.4.0",
|
|
43
|
+
"semver": "^7.8.5",
|
|
42
44
|
"zod": "^4.1.13"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
"@eslint/js": "^9.17.0",
|
|
47
49
|
"@types/jsdom": "^27",
|
|
48
50
|
"@types/node": "^22.10.2",
|
|
51
|
+
"@types/semver": "^7",
|
|
49
52
|
"cypress": "^14.5.4",
|
|
50
53
|
"eslint": "^9.17.0",
|
|
51
54
|
"eslint-config-prettier": "^9.1.0",
|