@clockworkdog/cogs-client 3.0.4 → 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 +4606 -1859
- package/dist/browser/index.umd.js +13 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- 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';
|
|
@@ -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",
|