@camstack/core 0.1.18 → 0.1.20
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/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts +10 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.d.ts.map +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +53 -4
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js.map +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +53 -4
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs.map +1 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts +25 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.d.ts.map +1 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js +60 -11
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.js.map +1 -1
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs +60 -11
- package/dist/builtins/turn-orchestrator/turn-orchestrator.addon.mjs.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { BaseAddon, ProviderRegistration } from '@camstack/types';
|
|
2
|
-
|
|
2
|
+
interface RemoteAccessOrchestratorConfig {
|
|
3
|
+
/**
|
|
4
|
+
* addonIds the operator has explicitly Started. Auto-respawned on
|
|
5
|
+
* boot so a tunnel set up once stays up across hub restarts.
|
|
6
|
+
*/
|
|
7
|
+
readonly enabledProviders: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
export declare class RemoteAccessOrchestratorAddon extends BaseAddon<RemoteAccessOrchestratorConfig> {
|
|
3
10
|
constructor();
|
|
4
11
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
12
|
+
private autoStartEnabledProviders;
|
|
13
|
+
private markEnabled;
|
|
5
14
|
private resolveImpl;
|
|
6
15
|
private listProviders;
|
|
7
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-access-orchestrator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"remote-access-orchestrator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,SAAS,EAIT,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAA;AAiBxB,UAAU,8BAA8B;IACtC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7C;AAED,qBAAa,6BAA8B,SAAQ,SAAS,CAAC,8BAA8B,CAAC;;cAK1E,YAAY,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;YAgCjD,yBAAyB;YAiCzB,WAAW;IAQzB,OAAO,CAAC,WAAW;YAQL,aAAa;CAmC5B;AAED,eAAe,6BAA6B,CAAA"}
|
|
@@ -9,10 +9,18 @@ let _camstack_types = require("@camstack/types");
|
|
|
9
9
|
* Remote-access orchestrator — singleton facade over the
|
|
10
10
|
* `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).
|
|
11
11
|
* Mirrors the auth-orchestrator and backup-orchestrator patterns.
|
|
12
|
+
*
|
|
13
|
+
* Persistence + autostart contract:
|
|
14
|
+
* The orchestrator owns the "operator wants this provider running"
|
|
15
|
+
* intent — a `enabledProviders: string[]` slice in its addon-store
|
|
16
|
+
* blob (BaseAddon.config). On boot we iterate the list and call
|
|
17
|
+
* `provider.start()` for each enabled entry. `startProvider` /
|
|
18
|
+
* `stopProvider` mutate this list so a Start press persists across
|
|
19
|
+
* restarts. Same shape as turn-orchestrator's setProviderEnabled.
|
|
12
20
|
*/
|
|
13
21
|
var RemoteAccessOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
14
22
|
constructor() {
|
|
15
|
-
super({});
|
|
23
|
+
super({ enabledProviders: [] });
|
|
16
24
|
}
|
|
17
25
|
async onInitialize() {
|
|
18
26
|
const provider = {
|
|
@@ -20,25 +28,66 @@ var RemoteAccessOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
|
20
28
|
startProvider: async ({ addonId }) => {
|
|
21
29
|
const impl = this.resolveImpl(addonId);
|
|
22
30
|
if (!impl?.start) throw new Error(`Remote-access provider "${addonId}" does not support start`);
|
|
23
|
-
|
|
31
|
+
const endpoint = await impl.start();
|
|
32
|
+
await this.markEnabled(addonId, true);
|
|
33
|
+
return endpoint;
|
|
24
34
|
},
|
|
25
35
|
stopProvider: async ({ addonId }) => {
|
|
26
36
|
const impl = this.resolveImpl(addonId);
|
|
27
37
|
if (impl?.stop) await impl.stop();
|
|
38
|
+
await this.markEnabled(addonId, false);
|
|
28
39
|
return { success: true };
|
|
29
40
|
}
|
|
30
41
|
};
|
|
31
|
-
this.ctx.logger.info("Remote-access orchestrator initialized");
|
|
42
|
+
this.ctx.logger.info("Remote-access orchestrator initialized", { meta: { enabledCount: this.config.enabledProviders.length } });
|
|
43
|
+
setImmediate(() => {
|
|
44
|
+
this.autoStartEnabledProviders();
|
|
45
|
+
});
|
|
32
46
|
return [{
|
|
33
47
|
capability: _camstack_types.remoteAccessCapability,
|
|
34
48
|
provider
|
|
35
49
|
}];
|
|
36
50
|
}
|
|
51
|
+
async autoStartEnabledProviders() {
|
|
52
|
+
const ids = this.config.enabledProviders;
|
|
53
|
+
if (ids.length === 0) return;
|
|
54
|
+
this.ctx.logger.info("Auto-starting enabled remote-access providers", { meta: { addonIds: [...ids] } });
|
|
55
|
+
for (const addonId of ids) try {
|
|
56
|
+
const impl = this.resolveImpl(addonId);
|
|
57
|
+
if (!impl?.start) {
|
|
58
|
+
this.ctx.logger.warn("autostart: provider not ready or unsupported", { meta: {
|
|
59
|
+
addonId,
|
|
60
|
+
hasImpl: !!impl,
|
|
61
|
+
hasStart: !!impl?.start
|
|
62
|
+
} });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const endpoint = await impl.start();
|
|
66
|
+
this.ctx.logger.info("autostart: provider started", { meta: {
|
|
67
|
+
addonId,
|
|
68
|
+
url: endpoint.url
|
|
69
|
+
} });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.ctx.logger.error("autostart: provider start failed", { meta: {
|
|
72
|
+
addonId,
|
|
73
|
+
error: err instanceof Error ? err.message : String(err)
|
|
74
|
+
} });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async markEnabled(addonId, enabled) {
|
|
78
|
+
const current = new Set(this.config.enabledProviders);
|
|
79
|
+
const wasEnabled = current.has(addonId);
|
|
80
|
+
if (enabled) current.add(addonId);
|
|
81
|
+
else current.delete(addonId);
|
|
82
|
+
if (wasEnabled === enabled) return;
|
|
83
|
+
await this.updateGlobalSettings({ enabledProviders: [...current] });
|
|
84
|
+
}
|
|
37
85
|
resolveImpl(addonId) {
|
|
38
86
|
return (this.capabilities?.getCollectionEntries("network-access") ?? []).find(([id]) => id === addonId)?.[1] ?? null;
|
|
39
87
|
}
|
|
40
88
|
async listProviders() {
|
|
41
89
|
const entries = this.capabilities?.getCollectionEntries("network-access") ?? [];
|
|
90
|
+
const enabled = new Set(this.config.enabledProviders);
|
|
42
91
|
const out = [];
|
|
43
92
|
for (const [addonId, impl] of entries) {
|
|
44
93
|
let connected = false;
|
|
@@ -56,7 +105,7 @@ var RemoteAccessOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
|
56
105
|
addonId,
|
|
57
106
|
displayName: impl.displayName ?? addonId,
|
|
58
107
|
kind: impl.kind ?? (addonId.includes("cloudflare") ? "cloudflare-tunnel" : "other"),
|
|
59
|
-
enabled:
|
|
108
|
+
enabled: enabled.has(addonId),
|
|
60
109
|
connected,
|
|
61
110
|
endpoint,
|
|
62
111
|
...error !== void 0 ? { error } : {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-access-orchestrator.addon.js","names":[],"sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"sourcesContent":["/**\n * Remote-access orchestrator — singleton facade over the\n * `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).\n * Mirrors the auth-orchestrator and backup-orchestrator patterns.\n */\nimport {\n BaseAddon,\n remoteAccessCapability,\n type IRemoteAccessOrchestrator,\n type RemoteAccessProviderInfo,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface NetworkAccessLike {\n start?: () => Promise<{ url: string; hostname: string; port: number; protocol: 'http' | 'https' }>\n stop?: () => Promise<void>\n getStatus?: () => Promise<{\n connected: boolean\n endpoint: { url: string; hostname: string; port: number; protocol: 'http' | 'https' } | null\n error?: string\n }>\n}\n\ninterface NetworkAccessRegistrationMeta {\n readonly displayName?: string\n readonly kind?: RemoteAccessProviderInfo['kind']\n}\n\nexport class RemoteAccessOrchestratorAddon extends BaseAddon<
|
|
1
|
+
{"version":3,"file":"remote-access-orchestrator.addon.js","names":[],"sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"sourcesContent":["/**\n * Remote-access orchestrator — singleton facade over the\n * `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).\n * Mirrors the auth-orchestrator and backup-orchestrator patterns.\n *\n * Persistence + autostart contract:\n * The orchestrator owns the \"operator wants this provider running\"\n * intent — a `enabledProviders: string[]` slice in its addon-store\n * blob (BaseAddon.config). On boot we iterate the list and call\n * `provider.start()` for each enabled entry. `startProvider` /\n * `stopProvider` mutate this list so a Start press persists across\n * restarts. Same shape as turn-orchestrator's setProviderEnabled.\n */\nimport {\n BaseAddon,\n remoteAccessCapability,\n type IRemoteAccessOrchestrator,\n type RemoteAccessProviderInfo,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface NetworkAccessLike {\n start?: () => Promise<{ url: string; hostname: string; port: number; protocol: 'http' | 'https' }>\n stop?: () => Promise<void>\n getStatus?: () => Promise<{\n connected: boolean\n endpoint: { url: string; hostname: string; port: number; protocol: 'http' | 'https' } | null\n error?: string\n }>\n}\n\ninterface NetworkAccessRegistrationMeta {\n readonly displayName?: string\n readonly kind?: RemoteAccessProviderInfo['kind']\n}\n\ninterface RemoteAccessOrchestratorConfig {\n /**\n * addonIds the operator has explicitly Started. Auto-respawned on\n * boot so a tunnel set up once stays up across hub restarts.\n */\n readonly enabledProviders: readonly string[]\n}\n\nexport class RemoteAccessOrchestratorAddon extends BaseAddon<RemoteAccessOrchestratorConfig> {\n constructor() {\n super({ enabledProviders: [] })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n const provider: IRemoteAccessOrchestrator = {\n listProviders: async () => this.listProviders(),\n startProvider: async ({ addonId }) => {\n const impl = this.resolveImpl(addonId)\n if (!impl?.start) throw new Error(`Remote-access provider \"${addonId}\" does not support start`)\n const endpoint = await impl.start()\n // Persist intent — next boot will auto-respawn this provider.\n await this.markEnabled(addonId, true)\n return endpoint\n },\n stopProvider: async ({ addonId }) => {\n const impl = this.resolveImpl(addonId)\n if (impl?.stop) await impl.stop()\n // Clear intent — boot must NOT respawn this on next start.\n await this.markEnabled(addonId, false)\n return { success: true as const }\n },\n } satisfies IRemoteAccessOrchestrator\n this.ctx.logger.info('Remote-access orchestrator initialized', {\n meta: { enabledCount: this.config.enabledProviders.length },\n })\n // Defer autostart to next tick so the orchestrator's own provider\n // registration completes first. `resolveImpl` reads from the\n // capabilities registry which only sees in-process / cluster-mirrored\n // providers once they've ALSO registered — small delay gives the\n // cluster bridge time to discover them on cold boot. Errors are\n // logged but never block init.\n setImmediate(() => { this.autoStartEnabledProviders() })\n return [{ capability: remoteAccessCapability, provider }]\n }\n\n private async autoStartEnabledProviders(): Promise<void> {\n const ids = this.config.enabledProviders\n if (ids.length === 0) return\n this.ctx.logger.info('Auto-starting enabled remote-access providers', {\n meta: { addonIds: [...ids] },\n })\n for (const addonId of ids) {\n try {\n const impl = this.resolveImpl(addonId)\n if (!impl?.start) {\n // Provider isn't loaded yet (worker bridge still hydrating)\n // OR it doesn't implement start. Log + skip; the operator can\n // retry from the UI when the addon is ready.\n this.ctx.logger.warn('autostart: provider not ready or unsupported', {\n meta: { addonId, hasImpl: !!impl, hasStart: !!impl?.start },\n })\n continue\n }\n const endpoint = await impl.start()\n this.ctx.logger.info('autostart: provider started', {\n meta: { addonId, url: endpoint.url },\n })\n } catch (err) {\n this.ctx.logger.error('autostart: provider start failed', {\n meta: {\n addonId,\n error: err instanceof Error ? err.message : String(err),\n },\n })\n }\n }\n }\n\n private async markEnabled(addonId: string, enabled: boolean): Promise<void> {\n const current = new Set(this.config.enabledProviders)\n const wasEnabled = current.has(addonId)\n if (enabled) current.add(addonId); else current.delete(addonId)\n if (wasEnabled === enabled) return\n await this.updateGlobalSettings({ enabledProviders: [...current] })\n }\n\n private resolveImpl(addonId: string): (NetworkAccessLike & NetworkAccessRegistrationMeta) | null {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike & NetworkAccessRegistrationMeta>(\n 'network-access',\n ) ?? []\n const found = entries.find(([id]) => id === addonId)\n return found?.[1] ?? null\n }\n\n private async listProviders(): Promise<readonly RemoteAccessProviderInfo[]> {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike & NetworkAccessRegistrationMeta>(\n 'network-access',\n ) ?? []\n const enabled = new Set(this.config.enabledProviders)\n const out: RemoteAccessProviderInfo[] = []\n for (const [addonId, impl] of entries) {\n let connected = false\n let endpoint: RemoteAccessProviderInfo['endpoint'] = null\n let error: string | undefined\n if (impl.getStatus) {\n try {\n const s = await impl.getStatus()\n connected = s.connected\n endpoint = s.endpoint\n error = s.error\n } catch (err) {\n error = err instanceof Error ? err.message : String(err)\n }\n }\n out.push({\n addonId,\n displayName: impl.displayName ?? addonId,\n kind: impl.kind ?? (addonId.includes('cloudflare') ? 'cloudflare-tunnel' as const : 'other' as const),\n // `enabled` is now the operator's persisted intent — orthogonal\n // to `connected` (which reflects the live tunnel state). Boot\n // tries to bring enabled→connected automatically.\n enabled: enabled.has(addonId),\n connected,\n endpoint,\n ...(error !== undefined ? { error } : {}),\n })\n }\n return out\n }\n}\n\nexport default RemoteAccessOrchestratorAddon\n"],"mappings":"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"}
|
|
@@ -4,10 +4,18 @@ import { BaseAddon, remoteAccessCapability } from "@camstack/types";
|
|
|
4
4
|
* Remote-access orchestrator — singleton facade over the
|
|
5
5
|
* `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).
|
|
6
6
|
* Mirrors the auth-orchestrator and backup-orchestrator patterns.
|
|
7
|
+
*
|
|
8
|
+
* Persistence + autostart contract:
|
|
9
|
+
* The orchestrator owns the "operator wants this provider running"
|
|
10
|
+
* intent — a `enabledProviders: string[]` slice in its addon-store
|
|
11
|
+
* blob (BaseAddon.config). On boot we iterate the list and call
|
|
12
|
+
* `provider.start()` for each enabled entry. `startProvider` /
|
|
13
|
+
* `stopProvider` mutate this list so a Start press persists across
|
|
14
|
+
* restarts. Same shape as turn-orchestrator's setProviderEnabled.
|
|
7
15
|
*/
|
|
8
16
|
var RemoteAccessOrchestratorAddon = class extends BaseAddon {
|
|
9
17
|
constructor() {
|
|
10
|
-
super({});
|
|
18
|
+
super({ enabledProviders: [] });
|
|
11
19
|
}
|
|
12
20
|
async onInitialize() {
|
|
13
21
|
const provider = {
|
|
@@ -15,25 +23,66 @@ var RemoteAccessOrchestratorAddon = class extends BaseAddon {
|
|
|
15
23
|
startProvider: async ({ addonId }) => {
|
|
16
24
|
const impl = this.resolveImpl(addonId);
|
|
17
25
|
if (!impl?.start) throw new Error(`Remote-access provider "${addonId}" does not support start`);
|
|
18
|
-
|
|
26
|
+
const endpoint = await impl.start();
|
|
27
|
+
await this.markEnabled(addonId, true);
|
|
28
|
+
return endpoint;
|
|
19
29
|
},
|
|
20
30
|
stopProvider: async ({ addonId }) => {
|
|
21
31
|
const impl = this.resolveImpl(addonId);
|
|
22
32
|
if (impl?.stop) await impl.stop();
|
|
33
|
+
await this.markEnabled(addonId, false);
|
|
23
34
|
return { success: true };
|
|
24
35
|
}
|
|
25
36
|
};
|
|
26
|
-
this.ctx.logger.info("Remote-access orchestrator initialized");
|
|
37
|
+
this.ctx.logger.info("Remote-access orchestrator initialized", { meta: { enabledCount: this.config.enabledProviders.length } });
|
|
38
|
+
setImmediate(() => {
|
|
39
|
+
this.autoStartEnabledProviders();
|
|
40
|
+
});
|
|
27
41
|
return [{
|
|
28
42
|
capability: remoteAccessCapability,
|
|
29
43
|
provider
|
|
30
44
|
}];
|
|
31
45
|
}
|
|
46
|
+
async autoStartEnabledProviders() {
|
|
47
|
+
const ids = this.config.enabledProviders;
|
|
48
|
+
if (ids.length === 0) return;
|
|
49
|
+
this.ctx.logger.info("Auto-starting enabled remote-access providers", { meta: { addonIds: [...ids] } });
|
|
50
|
+
for (const addonId of ids) try {
|
|
51
|
+
const impl = this.resolveImpl(addonId);
|
|
52
|
+
if (!impl?.start) {
|
|
53
|
+
this.ctx.logger.warn("autostart: provider not ready or unsupported", { meta: {
|
|
54
|
+
addonId,
|
|
55
|
+
hasImpl: !!impl,
|
|
56
|
+
hasStart: !!impl?.start
|
|
57
|
+
} });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const endpoint = await impl.start();
|
|
61
|
+
this.ctx.logger.info("autostart: provider started", { meta: {
|
|
62
|
+
addonId,
|
|
63
|
+
url: endpoint.url
|
|
64
|
+
} });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
this.ctx.logger.error("autostart: provider start failed", { meta: {
|
|
67
|
+
addonId,
|
|
68
|
+
error: err instanceof Error ? err.message : String(err)
|
|
69
|
+
} });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async markEnabled(addonId, enabled) {
|
|
73
|
+
const current = new Set(this.config.enabledProviders);
|
|
74
|
+
const wasEnabled = current.has(addonId);
|
|
75
|
+
if (enabled) current.add(addonId);
|
|
76
|
+
else current.delete(addonId);
|
|
77
|
+
if (wasEnabled === enabled) return;
|
|
78
|
+
await this.updateGlobalSettings({ enabledProviders: [...current] });
|
|
79
|
+
}
|
|
32
80
|
resolveImpl(addonId) {
|
|
33
81
|
return (this.capabilities?.getCollectionEntries("network-access") ?? []).find(([id]) => id === addonId)?.[1] ?? null;
|
|
34
82
|
}
|
|
35
83
|
async listProviders() {
|
|
36
84
|
const entries = this.capabilities?.getCollectionEntries("network-access") ?? [];
|
|
85
|
+
const enabled = new Set(this.config.enabledProviders);
|
|
37
86
|
const out = [];
|
|
38
87
|
for (const [addonId, impl] of entries) {
|
|
39
88
|
let connected = false;
|
|
@@ -51,7 +100,7 @@ var RemoteAccessOrchestratorAddon = class extends BaseAddon {
|
|
|
51
100
|
addonId,
|
|
52
101
|
displayName: impl.displayName ?? addonId,
|
|
53
102
|
kind: impl.kind ?? (addonId.includes("cloudflare") ? "cloudflare-tunnel" : "other"),
|
|
54
|
-
enabled:
|
|
103
|
+
enabled: enabled.has(addonId),
|
|
55
104
|
connected,
|
|
56
105
|
endpoint,
|
|
57
106
|
...error !== void 0 ? { error } : {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-access-orchestrator.addon.mjs","names":[],"sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"sourcesContent":["/**\n * Remote-access orchestrator — singleton facade over the\n * `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).\n * Mirrors the auth-orchestrator and backup-orchestrator patterns.\n */\nimport {\n BaseAddon,\n remoteAccessCapability,\n type IRemoteAccessOrchestrator,\n type RemoteAccessProviderInfo,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface NetworkAccessLike {\n start?: () => Promise<{ url: string; hostname: string; port: number; protocol: 'http' | 'https' }>\n stop?: () => Promise<void>\n getStatus?: () => Promise<{\n connected: boolean\n endpoint: { url: string; hostname: string; port: number; protocol: 'http' | 'https' } | null\n error?: string\n }>\n}\n\ninterface NetworkAccessRegistrationMeta {\n readonly displayName?: string\n readonly kind?: RemoteAccessProviderInfo['kind']\n}\n\nexport class RemoteAccessOrchestratorAddon extends BaseAddon<
|
|
1
|
+
{"version":3,"file":"remote-access-orchestrator.addon.mjs","names":[],"sources":["../../../src/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.ts"],"sourcesContent":["/**\n * Remote-access orchestrator — singleton facade over the\n * `network-access` collection (Cloudflare Tunnel, ngrok, Tailscale, …).\n * Mirrors the auth-orchestrator and backup-orchestrator patterns.\n *\n * Persistence + autostart contract:\n * The orchestrator owns the \"operator wants this provider running\"\n * intent — a `enabledProviders: string[]` slice in its addon-store\n * blob (BaseAddon.config). On boot we iterate the list and call\n * `provider.start()` for each enabled entry. `startProvider` /\n * `stopProvider` mutate this list so a Start press persists across\n * restarts. Same shape as turn-orchestrator's setProviderEnabled.\n */\nimport {\n BaseAddon,\n remoteAccessCapability,\n type IRemoteAccessOrchestrator,\n type RemoteAccessProviderInfo,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface NetworkAccessLike {\n start?: () => Promise<{ url: string; hostname: string; port: number; protocol: 'http' | 'https' }>\n stop?: () => Promise<void>\n getStatus?: () => Promise<{\n connected: boolean\n endpoint: { url: string; hostname: string; port: number; protocol: 'http' | 'https' } | null\n error?: string\n }>\n}\n\ninterface NetworkAccessRegistrationMeta {\n readonly displayName?: string\n readonly kind?: RemoteAccessProviderInfo['kind']\n}\n\ninterface RemoteAccessOrchestratorConfig {\n /**\n * addonIds the operator has explicitly Started. Auto-respawned on\n * boot so a tunnel set up once stays up across hub restarts.\n */\n readonly enabledProviders: readonly string[]\n}\n\nexport class RemoteAccessOrchestratorAddon extends BaseAddon<RemoteAccessOrchestratorConfig> {\n constructor() {\n super({ enabledProviders: [] })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n const provider: IRemoteAccessOrchestrator = {\n listProviders: async () => this.listProviders(),\n startProvider: async ({ addonId }) => {\n const impl = this.resolveImpl(addonId)\n if (!impl?.start) throw new Error(`Remote-access provider \"${addonId}\" does not support start`)\n const endpoint = await impl.start()\n // Persist intent — next boot will auto-respawn this provider.\n await this.markEnabled(addonId, true)\n return endpoint\n },\n stopProvider: async ({ addonId }) => {\n const impl = this.resolveImpl(addonId)\n if (impl?.stop) await impl.stop()\n // Clear intent — boot must NOT respawn this on next start.\n await this.markEnabled(addonId, false)\n return { success: true as const }\n },\n } satisfies IRemoteAccessOrchestrator\n this.ctx.logger.info('Remote-access orchestrator initialized', {\n meta: { enabledCount: this.config.enabledProviders.length },\n })\n // Defer autostart to next tick so the orchestrator's own provider\n // registration completes first. `resolveImpl` reads from the\n // capabilities registry which only sees in-process / cluster-mirrored\n // providers once they've ALSO registered — small delay gives the\n // cluster bridge time to discover them on cold boot. Errors are\n // logged but never block init.\n setImmediate(() => { this.autoStartEnabledProviders() })\n return [{ capability: remoteAccessCapability, provider }]\n }\n\n private async autoStartEnabledProviders(): Promise<void> {\n const ids = this.config.enabledProviders\n if (ids.length === 0) return\n this.ctx.logger.info('Auto-starting enabled remote-access providers', {\n meta: { addonIds: [...ids] },\n })\n for (const addonId of ids) {\n try {\n const impl = this.resolveImpl(addonId)\n if (!impl?.start) {\n // Provider isn't loaded yet (worker bridge still hydrating)\n // OR it doesn't implement start. Log + skip; the operator can\n // retry from the UI when the addon is ready.\n this.ctx.logger.warn('autostart: provider not ready or unsupported', {\n meta: { addonId, hasImpl: !!impl, hasStart: !!impl?.start },\n })\n continue\n }\n const endpoint = await impl.start()\n this.ctx.logger.info('autostart: provider started', {\n meta: { addonId, url: endpoint.url },\n })\n } catch (err) {\n this.ctx.logger.error('autostart: provider start failed', {\n meta: {\n addonId,\n error: err instanceof Error ? err.message : String(err),\n },\n })\n }\n }\n }\n\n private async markEnabled(addonId: string, enabled: boolean): Promise<void> {\n const current = new Set(this.config.enabledProviders)\n const wasEnabled = current.has(addonId)\n if (enabled) current.add(addonId); else current.delete(addonId)\n if (wasEnabled === enabled) return\n await this.updateGlobalSettings({ enabledProviders: [...current] })\n }\n\n private resolveImpl(addonId: string): (NetworkAccessLike & NetworkAccessRegistrationMeta) | null {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike & NetworkAccessRegistrationMeta>(\n 'network-access',\n ) ?? []\n const found = entries.find(([id]) => id === addonId)\n return found?.[1] ?? null\n }\n\n private async listProviders(): Promise<readonly RemoteAccessProviderInfo[]> {\n const entries = this.capabilities?.getCollectionEntries<NetworkAccessLike & NetworkAccessRegistrationMeta>(\n 'network-access',\n ) ?? []\n const enabled = new Set(this.config.enabledProviders)\n const out: RemoteAccessProviderInfo[] = []\n for (const [addonId, impl] of entries) {\n let connected = false\n let endpoint: RemoteAccessProviderInfo['endpoint'] = null\n let error: string | undefined\n if (impl.getStatus) {\n try {\n const s = await impl.getStatus()\n connected = s.connected\n endpoint = s.endpoint\n error = s.error\n } catch (err) {\n error = err instanceof Error ? err.message : String(err)\n }\n }\n out.push({\n addonId,\n displayName: impl.displayName ?? addonId,\n kind: impl.kind ?? (addonId.includes('cloudflare') ? 'cloudflare-tunnel' as const : 'other' as const),\n // `enabled` is now the operator's persisted intent — orthogonal\n // to `connected` (which reflects the live tunnel state). Boot\n // tries to bring enabled→connected automatically.\n enabled: enabled.has(addonId),\n connected,\n endpoint,\n ...(error !== undefined ? { error } : {}),\n })\n }\n return out\n }\n}\n\nexport default RemoteAccessOrchestratorAddon\n"],"mappings":"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"}
|
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import { BaseAddon, ProviderRegistration } from '@camstack/types';
|
|
2
|
-
|
|
2
|
+
interface TurnOrchestratorConfig {
|
|
3
|
+
/**
|
|
4
|
+
* addonIds the operator has explicitly enabled. Persisted in the
|
|
5
|
+
* addon-store; first-launch defaults to "every provider enabled" so
|
|
6
|
+
* a freshly-installed TURN addon participates immediately (the
|
|
7
|
+
* operator can disable from the UI). See `effectivelyEnabled`.
|
|
8
|
+
*/
|
|
9
|
+
readonly enabledProviders: readonly string[];
|
|
10
|
+
/**
|
|
11
|
+
* Sentinel: true after `enabledProviders` has been written at least
|
|
12
|
+
* once. Distinguishes "operator hasn't touched the toggles yet —
|
|
13
|
+
* default-enable all" from "operator cleared the list — keep it
|
|
14
|
+
* empty". Same shape as local-network's bootSeeded.
|
|
15
|
+
*/
|
|
16
|
+
readonly enabledProvidersSeeded: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare class TurnOrchestratorAddon extends BaseAddon<TurnOrchestratorConfig> {
|
|
3
19
|
constructor();
|
|
4
20
|
protected onInitialize(): Promise<ProviderRegistration[]>;
|
|
5
21
|
private getEntries;
|
|
22
|
+
/**
|
|
23
|
+
* Operator-effective enabled set. Before the first toggle, default
|
|
24
|
+
* to "every registered provider is enabled" so a fresh install of a
|
|
25
|
+
* TURN addon participates without manual intervention. After the
|
|
26
|
+
* operator clicks anything, the persisted list is authoritative.
|
|
27
|
+
*/
|
|
28
|
+
private effectivelyEnabled;
|
|
29
|
+
private markEnabled;
|
|
6
30
|
private listProviders;
|
|
7
31
|
private getAllServers;
|
|
8
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-orchestrator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/turn-orchestrator/turn-orchestrator.addon.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"turn-orchestrator.addon.d.ts","sourceRoot":"","sources":["../../../src/builtins/turn-orchestrator/turn-orchestrator.addon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,SAAS,EAIT,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAA;AAQxB,UAAU,sBAAsB;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAA;CACzC;AAED,qBAAa,qBAAsB,SAAQ,SAAS,CAAC,sBAAsB,CAAC;;cAK1D,YAAY,IAAI,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAkB/D,OAAO,CAAC,UAAU;IAIlB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;YAOZ,WAAW;YAYX,aAAa;YA0Cb,aAAa;CAiB5B;AAED,eAAe,qBAAqB,CAAA"}
|
|
@@ -8,26 +8,40 @@ let _camstack_types = require("@camstack/types");
|
|
|
8
8
|
/**
|
|
9
9
|
* TURN orchestrator — singleton facade over the `turn-provider`
|
|
10
10
|
* collection (Cloudflare TURN, coturn, Twilio, …). Mirrors the
|
|
11
|
-
*
|
|
11
|
+
* remote-access-orchestrator persistence + autostart contract:
|
|
12
|
+
* - `enabledProviders: string[]` in addon-store is the operator's
|
|
13
|
+
* intent. Boot reads it back; the Enable/Disable toggle in the
|
|
14
|
+
* admin UI writes it.
|
|
15
|
+
* - `getAllServers()` (consumed by the WebRTC layer) aggregates ICE
|
|
16
|
+
* servers ONLY from enabled providers.
|
|
17
|
+
* - `listProviders()` surfaces the persisted `enabled` flag so the
|
|
18
|
+
* UI can render the correct toggle state on first paint.
|
|
12
19
|
*
|
|
13
20
|
* Provides:
|
|
14
21
|
* - listProviders() — admin UI inventory
|
|
15
|
-
* - getAllServers() — combined ICE server list across enabled
|
|
16
|
-
*
|
|
22
|
+
* - getAllServers() — combined ICE server list across enabled providers
|
|
23
|
+
* - setProviderEnabled() — toggle a provider on/off (persisted)
|
|
17
24
|
*/
|
|
18
25
|
var TurnOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
19
26
|
constructor() {
|
|
20
|
-
super({
|
|
27
|
+
super({
|
|
28
|
+
enabledProviders: [],
|
|
29
|
+
enabledProvidersSeeded: false
|
|
30
|
+
});
|
|
21
31
|
}
|
|
22
32
|
async onInitialize() {
|
|
23
33
|
const provider = {
|
|
24
34
|
listProviders: async () => this.listProviders(),
|
|
25
35
|
getAllServers: async () => this.getAllServers(),
|
|
26
|
-
setProviderEnabled: async () => {
|
|
36
|
+
setProviderEnabled: async ({ addonId, enabled }) => {
|
|
37
|
+
await this.markEnabled(addonId, enabled);
|
|
27
38
|
return { success: true };
|
|
28
39
|
}
|
|
29
40
|
};
|
|
30
|
-
this.ctx.logger.info("TURN orchestrator initialized"
|
|
41
|
+
this.ctx.logger.info("TURN orchestrator initialized", { meta: {
|
|
42
|
+
enabledCount: this.config.enabledProviders.length,
|
|
43
|
+
seeded: this.config.enabledProvidersSeeded
|
|
44
|
+
} });
|
|
31
45
|
return [{
|
|
32
46
|
capability: _camstack_types.turnOrchestratorCapability,
|
|
33
47
|
provider
|
|
@@ -36,13 +50,42 @@ var TurnOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
|
36
50
|
getEntries() {
|
|
37
51
|
return this.capabilities?.getCollectionEntries("turn-provider") ?? [];
|
|
38
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Operator-effective enabled set. Before the first toggle, default
|
|
55
|
+
* to "every registered provider is enabled" so a fresh install of a
|
|
56
|
+
* TURN addon participates without manual intervention. After the
|
|
57
|
+
* operator clicks anything, the persisted list is authoritative.
|
|
58
|
+
*/
|
|
59
|
+
effectivelyEnabled() {
|
|
60
|
+
if (this.config.enabledProvidersSeeded) return new Set(this.config.enabledProviders);
|
|
61
|
+
return new Set(this.getEntries().map(([id]) => id));
|
|
62
|
+
}
|
|
63
|
+
async markEnabled(addonId, enabled) {
|
|
64
|
+
const current = this.effectivelyEnabled();
|
|
65
|
+
const wasEnabled = current.has(addonId);
|
|
66
|
+
if (enabled) current.add(addonId);
|
|
67
|
+
else current.delete(addonId);
|
|
68
|
+
if (wasEnabled === enabled && this.config.enabledProvidersSeeded) return;
|
|
69
|
+
await this.updateGlobalSettings({
|
|
70
|
+
enabledProviders: [...current],
|
|
71
|
+
enabledProvidersSeeded: true
|
|
72
|
+
});
|
|
73
|
+
}
|
|
39
74
|
async listProviders() {
|
|
75
|
+
const enabled = this.effectivelyEnabled();
|
|
40
76
|
const out = [];
|
|
41
77
|
for (const [addonId, impl] of this.getEntries()) {
|
|
42
78
|
let serverCount = 0;
|
|
79
|
+
let urls = [];
|
|
43
80
|
let error;
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
const isEnabled = enabled.has(addonId);
|
|
82
|
+
if (isEnabled && impl.getTurnServers) try {
|
|
83
|
+
const servers = await impl.getTurnServers();
|
|
84
|
+
serverCount = servers.length;
|
|
85
|
+
const flat = [];
|
|
86
|
+
for (const s of servers) if (Array.isArray(s.urls)) flat.push(...s.urls);
|
|
87
|
+
else if (typeof s.urls === "string") flat.push(s.urls);
|
|
88
|
+
urls = flat;
|
|
46
89
|
} catch (err) {
|
|
47
90
|
error = err instanceof Error ? err.message : String(err);
|
|
48
91
|
}
|
|
@@ -50,22 +93,28 @@ var TurnOrchestratorAddon = class extends _camstack_types.BaseAddon {
|
|
|
50
93
|
addonId,
|
|
51
94
|
displayName: impl.displayName ?? addonId,
|
|
52
95
|
kind: impl.kind ?? (addonId.includes("cloudflare") ? "cloudflare" : addonId.includes("coturn") ? "coturn" : "other"),
|
|
53
|
-
enabled:
|
|
96
|
+
enabled: isEnabled,
|
|
54
97
|
serverCount,
|
|
98
|
+
urls,
|
|
55
99
|
...error !== void 0 ? { error } : {}
|
|
56
100
|
});
|
|
57
101
|
}
|
|
58
102
|
return out;
|
|
59
103
|
}
|
|
60
104
|
async getAllServers() {
|
|
105
|
+
const enabled = this.effectivelyEnabled();
|
|
61
106
|
const out = [];
|
|
62
|
-
for (const [, impl] of this.getEntries()) {
|
|
107
|
+
for (const [addonId, impl] of this.getEntries()) {
|
|
108
|
+
if (!enabled.has(addonId)) continue;
|
|
63
109
|
if (!impl.getTurnServers) continue;
|
|
64
110
|
try {
|
|
65
111
|
const servers = await impl.getTurnServers();
|
|
66
112
|
out.push(...servers);
|
|
67
113
|
} catch (err) {
|
|
68
|
-
this.ctx.logger.warn("TURN provider failed to enumerate servers", { meta: {
|
|
114
|
+
this.ctx.logger.warn("TURN provider failed to enumerate servers", { meta: {
|
|
115
|
+
addonId,
|
|
116
|
+
error: err instanceof Error ? err.message : String(err)
|
|
117
|
+
} });
|
|
69
118
|
}
|
|
70
119
|
}
|
|
71
120
|
return out;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-orchestrator.addon.js","names":[],"sources":["../../../src/builtins/turn-orchestrator/turn-orchestrator.addon.ts"],"sourcesContent":["/**\n * TURN orchestrator — singleton facade over the `turn-provider`\n * collection (Cloudflare TURN, coturn, Twilio, …). Mirrors the\n *
|
|
1
|
+
{"version":3,"file":"turn-orchestrator.addon.js","names":[],"sources":["../../../src/builtins/turn-orchestrator/turn-orchestrator.addon.ts"],"sourcesContent":["/**\n * TURN orchestrator — singleton facade over the `turn-provider`\n * collection (Cloudflare TURN, coturn, Twilio, …). Mirrors the\n * remote-access-orchestrator persistence + autostart contract:\n * - `enabledProviders: string[]` in addon-store is the operator's\n * intent. Boot reads it back; the Enable/Disable toggle in the\n * admin UI writes it.\n * - `getAllServers()` (consumed by the WebRTC layer) aggregates ICE\n * servers ONLY from enabled providers.\n * - `listProviders()` surfaces the persisted `enabled` flag so the\n * UI can render the correct toggle state on first paint.\n *\n * Provides:\n * - listProviders() — admin UI inventory\n * - getAllServers() — combined ICE server list across enabled providers\n * - setProviderEnabled() — toggle a provider on/off (persisted)\n */\nimport {\n BaseAddon,\n turnOrchestratorCapability,\n type ITurnOrchestratorProvider,\n type TurnProviderInfo,\n type ProviderRegistration,\n} from '@camstack/types'\n\ninterface TurnProviderLike {\n getTurnServers?: () => Promise<readonly { urls: string | string[]; username?: string; credential?: string }[]>\n readonly displayName?: string\n readonly kind?: TurnProviderInfo['kind']\n}\n\ninterface TurnOrchestratorConfig {\n /**\n * addonIds the operator has explicitly enabled. Persisted in the\n * addon-store; first-launch defaults to \"every provider enabled\" so\n * a freshly-installed TURN addon participates immediately (the\n * operator can disable from the UI). See `effectivelyEnabled`.\n */\n readonly enabledProviders: readonly string[]\n /**\n * Sentinel: true after `enabledProviders` has been written at least\n * once. Distinguishes \"operator hasn't touched the toggles yet —\n * default-enable all\" from \"operator cleared the list — keep it\n * empty\". Same shape as local-network's bootSeeded.\n */\n readonly enabledProvidersSeeded: boolean\n}\n\nexport class TurnOrchestratorAddon extends BaseAddon<TurnOrchestratorConfig> {\n constructor() {\n super({ enabledProviders: [], enabledProvidersSeeded: false })\n }\n\n protected async onInitialize(): Promise<ProviderRegistration[]> {\n const provider: ITurnOrchestratorProvider = {\n listProviders: async () => this.listProviders(),\n getAllServers: async () => this.getAllServers(),\n setProviderEnabled: async ({ addonId, enabled }) => {\n await this.markEnabled(addonId, enabled)\n return { success: true as const }\n },\n }\n this.ctx.logger.info('TURN orchestrator initialized', {\n meta: {\n enabledCount: this.config.enabledProviders.length,\n seeded: this.config.enabledProvidersSeeded,\n },\n })\n return [{ capability: turnOrchestratorCapability, provider }]\n }\n\n private getEntries(): readonly (readonly [string, TurnProviderLike])[] {\n return this.capabilities?.getCollectionEntries<TurnProviderLike>('turn-provider') ?? []\n }\n\n /**\n * Operator-effective enabled set. Before the first toggle, default\n * to \"every registered provider is enabled\" so a fresh install of a\n * TURN addon participates without manual intervention. After the\n * operator clicks anything, the persisted list is authoritative.\n */\n private effectivelyEnabled(): Set<string> {\n if (this.config.enabledProvidersSeeded) {\n return new Set(this.config.enabledProviders)\n }\n return new Set(this.getEntries().map(([id]) => id))\n }\n\n private async markEnabled(addonId: string, enabled: boolean): Promise<void> {\n const current = this.effectivelyEnabled()\n const wasEnabled = current.has(addonId)\n if (enabled) current.add(addonId); else current.delete(addonId)\n // Always persist with seeded=true so the operator's choice locks in.\n if (wasEnabled === enabled && this.config.enabledProvidersSeeded) return\n await this.updateGlobalSettings({\n enabledProviders: [...current],\n enabledProvidersSeeded: true,\n })\n }\n\n private async listProviders(): Promise<readonly TurnProviderInfo[]> {\n const enabled = this.effectivelyEnabled()\n const out: TurnProviderInfo[] = []\n for (const [addonId, impl] of this.getEntries()) {\n let serverCount = 0\n let urls: readonly string[] = []\n let error: string | undefined\n const isEnabled = enabled.has(addonId)\n // Only fetch live server count for enabled providers — disabled\n // providers shouldn't be making outbound API calls just to render\n // an admin row.\n if (isEnabled && impl.getTurnServers) {\n try {\n const servers = await impl.getTurnServers()\n serverCount = servers.length\n // Flatten the urls field of every ICE-server entry into one\n // list — `{ urls: ['stun:a', 'stun:b'] }` becomes `['stun:a',\n // 'stun:b']`; `{ urls: 'turn:x' }` becomes `['turn:x']`. The\n // admin UI's compact per-provider chip list iterates this.\n const flat: string[] = []\n for (const s of servers) {\n if (Array.isArray(s.urls)) flat.push(...s.urls)\n else if (typeof s.urls === 'string') flat.push(s.urls)\n }\n urls = flat\n } catch (err) {\n error = err instanceof Error ? err.message : String(err)\n }\n }\n out.push({\n addonId,\n displayName: impl.displayName ?? addonId,\n kind: impl.kind ?? (addonId.includes('cloudflare') ? 'cloudflare' as const : addonId.includes('coturn') ? 'coturn' as const : 'other' as const),\n enabled: isEnabled,\n serverCount,\n urls,\n ...(error !== undefined ? { error } : {}),\n })\n }\n return out\n }\n\n private async getAllServers(): Promise<readonly { urls: string | string[]; username?: string; credential?: string }[]> {\n const enabled = this.effectivelyEnabled()\n const out: { urls: string | string[]; username?: string; credential?: string }[] = []\n for (const [addonId, impl] of this.getEntries()) {\n if (!enabled.has(addonId)) continue\n if (!impl.getTurnServers) continue\n try {\n const servers = await impl.getTurnServers()\n out.push(...servers)\n } catch (err) {\n this.ctx.logger.warn('TURN provider failed to enumerate servers', {\n meta: { addonId, error: err instanceof Error ? err.message : String(err) },\n })\n }\n }\n return out\n }\n}\n\nexport default TurnOrchestratorAddon\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAgDA,IAAa,wBAAb,cAA2C,gBAAA,UAAkC;CAC3E,cAAc;EACZ,MAAM;GAAE,kBAAkB,EAAE;GAAE,wBAAwB;GAAO,CAAC;;CAGhE,MAAgB,eAAgD;EAC9D,MAAM,WAAsC;GAC1C,eAAe,YAAY,KAAK,eAAe;GAC/C,eAAe,YAAY,KAAK,eAAe;GAC/C,oBAAoB,OAAO,EAAE,SAAS,cAAc;IAClD,MAAM,KAAK,YAAY,SAAS,QAAQ;IACxC,OAAO,EAAE,SAAS,MAAe;;GAEpC;EACD,KAAK,IAAI,OAAO,KAAK,iCAAiC,EACpD,MAAM;GACJ,cAAc,KAAK,OAAO,iBAAiB;GAC3C,QAAQ,KAAK,OAAO;GACrB,EACF,CAAC;EACF,OAAO,CAAC;GAAE,YAAY,gBAAA;GAA4B;GAAU,CAAC;;CAG/D,aAAuE;EACrE,OAAO,KAAK,cAAc,qBAAuC,gBAAgB,IAAI,EAAE;;;;;;;;CASzF,qBAA0C;EACxC,IAAI,KAAK,OAAO,wBACd,OAAO,IAAI,IAAI,KAAK,OAAO,iBAAiB;EAE9C,OAAO,IAAI,IAAI,KAAK,YAAY,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC;;CAGrD,MAAc,YAAY,SAAiB,SAAiC;EAC1E,MAAM,UAAU,KAAK,oBAAoB;EACzC,MAAM,aAAa,QAAQ,IAAI,QAAQ;EACvC,IAAI,SAAS,QAAQ,IAAI,QAAQ;OAAO,QAAQ,OAAO,QAAQ;EAE/D,IAAI,eAAe,WAAW,KAAK,OAAO,wBAAwB;EAClE,MAAM,KAAK,qBAAqB;GAC9B,kBAAkB,CAAC,GAAG,QAAQ;GAC9B,wBAAwB;GACzB,CAAC;;CAGJ,MAAc,gBAAsD;EAClE,MAAM,UAAU,KAAK,oBAAoB;EACzC,MAAM,MAA0B,EAAE;EAClC,KAAK,MAAM,CAAC,SAAS,SAAS,KAAK,YAAY,EAAE;GAC/C,IAAI,cAAc;GAClB,IAAI,OAA0B,EAAE;GAChC,IAAI;GACJ,MAAM,YAAY,QAAQ,IAAI,QAAQ;GAItC,IAAI,aAAa,KAAK,gBACpB,IAAI;IACF,MAAM,UAAU,MAAM,KAAK,gBAAgB;IAC3C,cAAc,QAAQ;IAKtB,MAAM,OAAiB,EAAE;IACzB,KAAK,MAAM,KAAK,SACd,IAAI,MAAM,QAAQ,EAAE,KAAK,EAAE,KAAK,KAAK,GAAG,EAAE,KAAK;SAC1C,IAAI,OAAO,EAAE,SAAS,UAAU,KAAK,KAAK,EAAE,KAAK;IAExD,OAAO;YACA,KAAK;IACZ,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;;GAG5D,IAAI,KAAK;IACP;IACA,aAAa,KAAK,eAAe;IACjC,MAAM,KAAK,SAAS,QAAQ,SAAS,aAAa,GAAG,eAAwB,QAAQ,SAAS,SAAS,GAAG,WAAoB;IAC9H,SAAS;IACT;IACA;IACA,GAAI,UAAU,KAAA,IAAY,EAAE,OAAO,GAAG,EAAE;IACzC,CAAC;;EAEJ,OAAO;;CAGT,MAAc,gBAAyG;EACrH,MAAM,UAAU,KAAK,oBAAoB;EACzC,MAAM,MAA6E,EAAE;EACrF,KAAK,MAAM,CAAC,SAAS,SAAS,KAAK,YAAY,EAAE;GAC/C,IAAI,CAAC,QAAQ,IAAI,QAAQ,EAAE;GAC3B,IAAI,CAAC,KAAK,gBAAgB;GAC1B,IAAI;IACF,MAAM,UAAU,MAAM,KAAK,gBAAgB;IAC3C,IAAI,KAAK,GAAG,QAAQ;YACb,KAAK;IACZ,KAAK,IAAI,OAAO,KAAK,6CAA6C,EAChE,MAAM;KAAE;KAAS,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;KAAE,EAC3E,CAAC;;;EAGN,OAAO"}
|
|
@@ -3,26 +3,40 @@ import { BaseAddon, turnOrchestratorCapability } from "@camstack/types";
|
|
|
3
3
|
/**
|
|
4
4
|
* TURN orchestrator — singleton facade over the `turn-provider`
|
|
5
5
|
* collection (Cloudflare TURN, coturn, Twilio, …). Mirrors the
|
|
6
|
-
*
|
|
6
|
+
* remote-access-orchestrator persistence + autostart contract:
|
|
7
|
+
* - `enabledProviders: string[]` in addon-store is the operator's
|
|
8
|
+
* intent. Boot reads it back; the Enable/Disable toggle in the
|
|
9
|
+
* admin UI writes it.
|
|
10
|
+
* - `getAllServers()` (consumed by the WebRTC layer) aggregates ICE
|
|
11
|
+
* servers ONLY from enabled providers.
|
|
12
|
+
* - `listProviders()` surfaces the persisted `enabled` flag so the
|
|
13
|
+
* UI can render the correct toggle state on first paint.
|
|
7
14
|
*
|
|
8
15
|
* Provides:
|
|
9
16
|
* - listProviders() — admin UI inventory
|
|
10
|
-
* - getAllServers() — combined ICE server list across enabled
|
|
11
|
-
*
|
|
17
|
+
* - getAllServers() — combined ICE server list across enabled providers
|
|
18
|
+
* - setProviderEnabled() — toggle a provider on/off (persisted)
|
|
12
19
|
*/
|
|
13
20
|
var TurnOrchestratorAddon = class extends BaseAddon {
|
|
14
21
|
constructor() {
|
|
15
|
-
super({
|
|
22
|
+
super({
|
|
23
|
+
enabledProviders: [],
|
|
24
|
+
enabledProvidersSeeded: false
|
|
25
|
+
});
|
|
16
26
|
}
|
|
17
27
|
async onInitialize() {
|
|
18
28
|
const provider = {
|
|
19
29
|
listProviders: async () => this.listProviders(),
|
|
20
30
|
getAllServers: async () => this.getAllServers(),
|
|
21
|
-
setProviderEnabled: async () => {
|
|
31
|
+
setProviderEnabled: async ({ addonId, enabled }) => {
|
|
32
|
+
await this.markEnabled(addonId, enabled);
|
|
22
33
|
return { success: true };
|
|
23
34
|
}
|
|
24
35
|
};
|
|
25
|
-
this.ctx.logger.info("TURN orchestrator initialized"
|
|
36
|
+
this.ctx.logger.info("TURN orchestrator initialized", { meta: {
|
|
37
|
+
enabledCount: this.config.enabledProviders.length,
|
|
38
|
+
seeded: this.config.enabledProvidersSeeded
|
|
39
|
+
} });
|
|
26
40
|
return [{
|
|
27
41
|
capability: turnOrchestratorCapability,
|
|
28
42
|
provider
|
|
@@ -31,13 +45,42 @@ var TurnOrchestratorAddon = class extends BaseAddon {
|
|
|
31
45
|
getEntries() {
|
|
32
46
|
return this.capabilities?.getCollectionEntries("turn-provider") ?? [];
|
|
33
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Operator-effective enabled set. Before the first toggle, default
|
|
50
|
+
* to "every registered provider is enabled" so a fresh install of a
|
|
51
|
+
* TURN addon participates without manual intervention. After the
|
|
52
|
+
* operator clicks anything, the persisted list is authoritative.
|
|
53
|
+
*/
|
|
54
|
+
effectivelyEnabled() {
|
|
55
|
+
if (this.config.enabledProvidersSeeded) return new Set(this.config.enabledProviders);
|
|
56
|
+
return new Set(this.getEntries().map(([id]) => id));
|
|
57
|
+
}
|
|
58
|
+
async markEnabled(addonId, enabled) {
|
|
59
|
+
const current = this.effectivelyEnabled();
|
|
60
|
+
const wasEnabled = current.has(addonId);
|
|
61
|
+
if (enabled) current.add(addonId);
|
|
62
|
+
else current.delete(addonId);
|
|
63
|
+
if (wasEnabled === enabled && this.config.enabledProvidersSeeded) return;
|
|
64
|
+
await this.updateGlobalSettings({
|
|
65
|
+
enabledProviders: [...current],
|
|
66
|
+
enabledProvidersSeeded: true
|
|
67
|
+
});
|
|
68
|
+
}
|
|
34
69
|
async listProviders() {
|
|
70
|
+
const enabled = this.effectivelyEnabled();
|
|
35
71
|
const out = [];
|
|
36
72
|
for (const [addonId, impl] of this.getEntries()) {
|
|
37
73
|
let serverCount = 0;
|
|
74
|
+
let urls = [];
|
|
38
75
|
let error;
|
|
39
|
-
|
|
40
|
-
|
|
76
|
+
const isEnabled = enabled.has(addonId);
|
|
77
|
+
if (isEnabled && impl.getTurnServers) try {
|
|
78
|
+
const servers = await impl.getTurnServers();
|
|
79
|
+
serverCount = servers.length;
|
|
80
|
+
const flat = [];
|
|
81
|
+
for (const s of servers) if (Array.isArray(s.urls)) flat.push(...s.urls);
|
|
82
|
+
else if (typeof s.urls === "string") flat.push(s.urls);
|
|
83
|
+
urls = flat;
|
|
41
84
|
} catch (err) {
|
|
42
85
|
error = err instanceof Error ? err.message : String(err);
|
|
43
86
|
}
|
|
@@ -45,22 +88,28 @@ var TurnOrchestratorAddon = class extends BaseAddon {
|
|
|
45
88
|
addonId,
|
|
46
89
|
displayName: impl.displayName ?? addonId,
|
|
47
90
|
kind: impl.kind ?? (addonId.includes("cloudflare") ? "cloudflare" : addonId.includes("coturn") ? "coturn" : "other"),
|
|
48
|
-
enabled:
|
|
91
|
+
enabled: isEnabled,
|
|
49
92
|
serverCount,
|
|
93
|
+
urls,
|
|
50
94
|
...error !== void 0 ? { error } : {}
|
|
51
95
|
});
|
|
52
96
|
}
|
|
53
97
|
return out;
|
|
54
98
|
}
|
|
55
99
|
async getAllServers() {
|
|
100
|
+
const enabled = this.effectivelyEnabled();
|
|
56
101
|
const out = [];
|
|
57
|
-
for (const [, impl] of this.getEntries()) {
|
|
102
|
+
for (const [addonId, impl] of this.getEntries()) {
|
|
103
|
+
if (!enabled.has(addonId)) continue;
|
|
58
104
|
if (!impl.getTurnServers) continue;
|
|
59
105
|
try {
|
|
60
106
|
const servers = await impl.getTurnServers();
|
|
61
107
|
out.push(...servers);
|
|
62
108
|
} catch (err) {
|
|
63
|
-
this.ctx.logger.warn("TURN provider failed to enumerate servers", { meta: {
|
|
109
|
+
this.ctx.logger.warn("TURN provider failed to enumerate servers", { meta: {
|
|
110
|
+
addonId,
|
|
111
|
+
error: err instanceof Error ? err.message : String(err)
|
|
112
|
+
} });
|
|
64
113
|
}
|
|
65
114
|
}
|
|
66
115
|
return out;
|