agl 1.0.17 → 21.0.2
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/.smartconfig.json +55 -0
- package/changelog.md +1026 -0
- package/cli.js +14 -0
- package/dist_serve/bundle.js +44778 -0
- package/dist_serve/index.html +13 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.authmodels.d.ts +126 -0
- package/dist_ts/classes.authmodels.js +1422 -0
- package/dist_ts/classes.authstore.d.ts +160 -0
- package/dist_ts/classes.authstore.js +2483 -0
- package/dist_ts/classes.browserresourcehost.d.ts +85 -0
- package/dist_ts/classes.browserresourcehost.js +912 -0
- package/dist_ts/classes.cli.d.ts +21 -0
- package/dist_ts/classes.cli.js +1082 -0
- package/dist_ts/classes.config.d.ts +23 -0
- package/dist_ts/classes.config.js +267 -0
- package/dist_ts/classes.controller.d.ts +523 -0
- package/dist_ts/classes.controller.js +10611 -0
- package/dist_ts/classes.controllermanagement.d.ts +32 -0
- package/dist_ts/classes.controllermanagement.js +222 -0
- package/dist_ts/classes.draftmanager.d.ts +30 -0
- package/dist_ts/classes.draftmanager.js +179 -0
- package/dist_ts/classes.embeddeddb.d.ts +30 -0
- package/dist_ts/classes.embeddeddb.js +159 -0
- package/dist_ts/classes.flexframedtransport.d.ts +56 -0
- package/dist_ts/classes.flexframedtransport.js +275 -0
- package/dist_ts/classes.flexmodels.private.d.ts +231 -0
- package/dist_ts/classes.flexmodels.private.js +985 -0
- package/dist_ts/classes.flexmodels.public.d.ts +207 -0
- package/dist_ts/classes.flexmodels.public.js +885 -0
- package/dist_ts/classes.flexprojectionreader.d.ts +89 -0
- package/dist_ts/classes.flexprojectionreader.js +580 -0
- package/dist_ts/classes.flexservice.d.ts +196 -0
- package/dist_ts/classes.flexservice.js +3704 -0
- package/dist_ts/classes.flexstore.d.ts +174 -0
- package/dist_ts/classes.flexstore.js +3400 -0
- package/dist_ts/classes.flexsupervisor.d.ts +105 -0
- package/dist_ts/classes.flexsupervisor.js +1191 -0
- package/dist_ts/classes.gitreversion.d.ts +431 -0
- package/dist_ts/classes.gitreversion.js +10592 -0
- package/dist_ts/classes.harnessoperationmanager.d.ts +113 -0
- package/dist_ts/classes.harnessoperationmanager.js +602 -0
- package/dist_ts/classes.opencodeclient.d.ts +146 -0
- package/dist_ts/classes.opencodeclient.js +3003 -0
- package/dist_ts/classes.opencodesupervisor.d.ts +60 -0
- package/dist_ts/classes.opencodesupervisor.js +1003 -0
- package/dist_ts/classes.passkeymanager.d.ts +34 -0
- package/dist_ts/classes.passkeymanager.js +456 -0
- package/dist_ts/classes.processinspection.d.ts +50 -0
- package/dist_ts/classes.processinspection.js +484 -0
- package/dist_ts/classes.resourcecoordinator.d.ts +64 -0
- package/dist_ts/classes.resourcecoordinator.js +294 -0
- package/dist_ts/classes.sessionintelligencereader.d.ts +13 -0
- package/dist_ts/classes.sessionintelligencereader.js +40 -0
- package/dist_ts/classes.systemmetricssampler.d.ts +53 -0
- package/dist_ts/classes.systemmetricssampler.js +168 -0
- package/dist_ts/classes.terminalmanager.d.ts +95 -0
- package/dist_ts/classes.terminalmanager.js +981 -0
- package/dist_ts/classes.terminalresourcehost.d.ts +20 -0
- package/dist_ts/classes.terminalresourcehost.js +84 -0
- package/dist_ts/classes.upgradecoordinator.d.ts +244 -0
- package/dist_ts/classes.upgradecoordinator.js +2155 -0
- package/dist_ts/classes.upgradetransaction.d.ts +92 -0
- package/dist_ts/classes.upgradetransaction.js +2125 -0
- package/dist_ts/classes.uploadmanager.d.ts +24 -0
- package/dist_ts/classes.uploadmanager.js +210 -0
- package/dist_ts/classes.validation.d.ts +91 -0
- package/dist_ts/classes.validation.js +1087 -0
- package/dist_ts/flexharness.child.d.ts +1 -0
- package/dist_ts/flexharness.child.js +28 -0
- package/dist_ts/functions.canonicaldirectory.d.ts +21 -0
- package/dist_ts/functions.canonicaldirectory.js +144 -0
- package/dist_ts/functions.controllerdataroot.d.ts +15 -0
- package/dist_ts/functions.controllerdataroot.js +76 -0
- package/dist_ts/functions.embeddeddb.d.ts +2 -0
- package/dist_ts/functions.embeddeddb.js +21 -0
- package/dist_ts/functions.flexcredentialstore.d.ts +16 -0
- package/dist_ts/functions.flexcredentialstore.js +93 -0
- package/dist_ts/functions.opencodeauth.d.ts +3 -0
- package/dist_ts/functions.opencodeauth.js +31 -0
- package/dist_ts/functions.runtimeenvironment.d.ts +2 -0
- package/dist_ts/functions.runtimeenvironment.js +79 -0
- package/dist_ts/functions.upgradefollower.d.ts +3 -0
- package/dist_ts/functions.upgradefollower.js +95 -0
- package/dist_ts/index.d.ts +24 -0
- package/dist_ts/index.js +25 -0
- package/dist_ts/interfaces.auth.d.ts +181 -0
- package/dist_ts/interfaces.auth.js +82 -0
- package/dist_ts/interfaces.config.d.ts +34 -0
- package/dist_ts/interfaces.config.js +2 -0
- package/dist_ts/interfaces.flexipc.d.ts +939 -0
- package/dist_ts/interfaces.flexipc.js +1492 -0
- package/dist_ts/interfaces.gitreversion.d.ts +246 -0
- package/dist_ts/interfaces.gitreversion.js +2 -0
- package/dist_ts/interfaces.lifecycle.d.ts +37 -0
- package/dist_ts/interfaces.lifecycle.js +2 -0
- package/dist_ts/interfaces.opencode.d.ts +101 -0
- package/dist_ts/interfaces.opencode.js +3 -0
- package/dist_ts/interfaces.projects.d.ts +156 -0
- package/dist_ts/interfaces.projects.js +5 -0
- package/dist_ts/plugins.d.ts +36 -0
- package/dist_ts/plugins.js +40 -0
- package/dist_ts_interfaces/index.d.ts +2 -0
- package/dist_ts_interfaces/index.js +3 -0
- package/dist_ts_interfaces/interfaces.d.ts +747 -0
- package/dist_ts_interfaces/interfaces.js +47 -0
- package/dist_ts_interfaces/plugins.d.ts +4 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +806 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_migration/classes.documentmigrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.documentmigrationrunner.js +24 -0
- package/dist_ts_migration/classes.flexmigrationrunner.d.ts +11 -0
- package/dist_ts_migration/classes.flexmigrationrunner.js +25 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.d.ts +10 -0
- package/dist_ts_migration/classes.flexpostharnessmigrationrunner.js +15 -0
- package/dist_ts_migration/classes.migrationrunner.d.ts +8 -0
- package/dist_ts_migration/classes.migrationrunner.js +34 -0
- package/dist_ts_migration/index.d.ts +12 -0
- package/dist_ts_migration/index.js +13 -0
- package/dist_ts_migration/v10_flexharnessv3migration.d.ts +11 -0
- package/dist_ts_migration/v10_flexharnessv3migration.js +23 -0
- package/dist_ts_migration/v13_flexprojectionorder.d.ts +11 -0
- package/dist_ts_migration/v13_flexprojectionorder.js +23 -0
- package/dist_ts_migration/v16_flexprovidercredentials.d.ts +22 -0
- package/dist_ts_migration/v16_flexprovidercredentials.js +149 -0
- package/dist_ts_migration/v18_resourcelayout.d.ts +22 -0
- package/dist_ts_migration/v18_resourcelayout.js +23 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.d.ts +6 -0
- package/dist_ts_migration/v18_resourcelayoutmigration.js +61 -0
- package/dist_ts_migration/v19_flexprojectmanagement.d.ts +11 -0
- package/dist_ts_migration/v19_flexprojectmanagement.js +21 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.d.ts +28 -0
- package/dist_ts_migration/v1_embeddeddatabaselocation.js +415 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.d.ts +7 -0
- package/dist_ts_migration/v22_projectfilesystemidentitymigration.js +105 -0
- package/dist_ts_migration/v2_controllerdataroot.d.ts +130 -0
- package/dist_ts_migration/v2_controllerdataroot.js +1887 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocumentmigration.js +100 -0
- package/dist_ts_migration/v6_protocoldocuments.d.ts +7 -0
- package/dist_ts_migration/v6_protocoldocuments.js +138 -0
- package/html/index.html +13 -0
- package/{LICENSE → license.md} +2 -2
- package/package.json +86 -48
- package/readme.md +480 -0
- package/readme.plan.md +188 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.authmodels.ts +1245 -0
- package/ts/classes.authstore.ts +3274 -0
- package/ts/classes.browserresourcehost.ts +1205 -0
- package/ts/classes.cli.ts +1254 -0
- package/ts/classes.config.ts +351 -0
- package/ts/classes.controller.ts +14426 -0
- package/ts/classes.controllermanagement.ts +330 -0
- package/ts/classes.draftmanager.ts +232 -0
- package/ts/classes.embeddeddb.ts +166 -0
- package/ts/classes.flexframedtransport.ts +283 -0
- package/ts/classes.flexmodels.private.ts +823 -0
- package/ts/classes.flexmodels.public.ts +732 -0
- package/ts/classes.flexprojectionreader.ts +816 -0
- package/ts/classes.flexservice.ts +4302 -0
- package/ts/classes.flexstore.ts +4565 -0
- package/ts/classes.flexsupervisor.ts +1453 -0
- package/ts/classes.gitreversion.ts +13353 -0
- package/ts/classes.harnessoperationmanager.ts +870 -0
- package/ts/classes.opencodeclient.ts +4110 -0
- package/ts/classes.opencodesupervisor.ts +1280 -0
- package/ts/classes.passkeymanager.ts +570 -0
- package/ts/classes.processinspection.ts +623 -0
- package/ts/classes.resourcecoordinator.ts +436 -0
- package/ts/classes.sessionintelligencereader.ts +52 -0
- package/ts/classes.systemmetricssampler.ts +215 -0
- package/ts/classes.terminalmanager.ts +1207 -0
- package/ts/classes.terminalresourcehost.ts +122 -0
- package/ts/classes.upgradecoordinator.ts +2675 -0
- package/ts/classes.upgradetransaction.ts +2900 -0
- package/ts/classes.uploadmanager.ts +240 -0
- package/ts/classes.validation.ts +1409 -0
- package/ts/flexharness.child.ts +35 -0
- package/ts/functions.canonicaldirectory.ts +213 -0
- package/ts/functions.controllerdataroot.ts +107 -0
- package/ts/functions.embeddeddb.ts +22 -0
- package/ts/functions.flexcredentialstore.ts +128 -0
- package/ts/functions.opencodeauth.ts +34 -0
- package/ts/functions.runtimeenvironment.ts +90 -0
- package/ts/functions.upgradefollower.ts +115 -0
- package/ts/index.ts +36 -6
- package/ts/interfaces.auth.ts +316 -0
- package/ts/interfaces.config.ts +37 -0
- package/ts/interfaces.flexipc.ts +2586 -0
- package/ts/interfaces.gitreversion.ts +336 -0
- package/ts/interfaces.lifecycle.ts +46 -0
- package/ts/interfaces.opencode.ts +153 -0
- package/ts/interfaces.projects.ts +182 -0
- package/ts/plugins.ts +64 -0
- package/ts_interfaces/index.ts +2 -0
- package/ts_interfaces/interfaces.ts +827 -0
- package/ts_interfaces/plugins.ts +15 -0
- package/ts_interfaces/requests.ts +1186 -0
- package/ts_migration/classes.documentmigrationrunner.ts +32 -0
- package/ts_migration/classes.flexmigrationrunner.ts +33 -0
- package/ts_migration/classes.flexpostharnessmigrationrunner.ts +22 -0
- package/ts_migration/classes.migrationrunner.ts +40 -0
- package/ts_migration/index.ts +12 -0
- package/ts_migration/v10_flexharnessv3migration.ts +35 -0
- package/ts_migration/v13_flexprojectionorder.ts +33 -0
- package/ts_migration/v16_flexprovidercredentials.ts +196 -0
- package/ts_migration/v18_resourcelayout.ts +55 -0
- package/ts_migration/v18_resourcelayoutmigration.ts +68 -0
- package/ts_migration/v19_flexprojectmanagement.ts +28 -0
- package/ts_migration/v1_embeddeddatabaselocation.ts +560 -0
- package/ts_migration/v22_projectfilesystemidentitymigration.ts +121 -0
- package/ts_migration/v2_controllerdataroot.ts +2349 -0
- package/ts_migration/v6_protocoldocumentmigration.ts +144 -0
- package/ts_migration/v6_protocoldocuments.ts +198 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/classes.controllersocketclient.ts +517 -0
- package/ts_web/classes.sessiondraftsync.ts +390 -0
- package/ts_web/elements.harnesscontrollerapp.ts +11494 -0
- package/ts_web/index.ts +2 -0
- package/ts_web/plugins.ts +31 -0
- package/.gitlab-ci.yml +0 -71
- package/.npmignore +0 -5
- package/README.md +0 -40
- package/dist/cli.js +0 -2
- package/dist/index.d.ts +0 -0
- package/dist/index.js +0 -9
- package/dist/mod_bundle/index.d.ts +0 -1
- package/dist/mod_bundle/index.js +0 -18
- package/dist/mod_bundle/mod.plugins.d.ts +0 -4
- package/dist/mod_bundle/mod.plugins.js +0 -11
- package/dist/mod_install/index.d.ts +0 -4
- package/dist/mod_install/index.js +0 -21
- package/dist/mod_install/mod.plugins.d.ts +0 -1
- package/dist/mod_install/mod.plugins.js +0 -7
- package/dist/mod_standard/index.d.ts +0 -4
- package/dist/mod_standard/index.js +0 -21
- package/dist/mod_standard/mod.browser.d.ts +0 -6
- package/dist/mod_standard/mod.browser.js +0 -14
- package/dist/mod_standard/mod.clean.d.ts +0 -4
- package/dist/mod_standard/mod.clean.js +0 -12
- package/dist/mod_standard/mod.compile.d.ts +0 -4
- package/dist/mod_standard/mod.compile.js +0 -21
- package/dist/mod_standard/mod.plugins.d.ts +0 -8
- package/dist/mod_standard/mod.plugins.js +0 -19
- package/dist/mod_standard/mod.test.d.ts +0 -4
- package/dist/mod_standard/mod.test.js +0 -12
- package/dist/plr.cli.d.ts +0 -3
- package/dist/plr.cli.js +0 -57
- package/dist/plr.config.d.ts +0 -11
- package/dist/plr.config.js +0 -26
- package/dist/plr.mods.d.ts +0 -17
- package/dist/plr.mods.js +0 -17
- package/dist/plr.paths.d.ts +0 -19
- package/dist/plr.paths.js +0 -26
- package/dist/plr.plugins.d.ts +0 -8
- package/dist/plr.plugins.js +0 -16
- package/npmextra.json +0 -10
- package/ts/mod_bundle/index.ts +0 -20
- package/ts/mod_bundle/mod.plugins.ts +0 -9
- package/ts/mod_install/index.ts +0 -20
- package/ts/mod_install/mod.plugins.ts +0 -1
- package/ts/mod_standard/index.ts +0 -23
- package/ts/mod_standard/mod.browser.ts +0 -17
- package/ts/mod_standard/mod.clean.ts +0 -13
- package/ts/mod_standard/mod.compile.ts +0 -23
- package/ts/mod_standard/mod.plugins.ts +0 -17
- package/ts/mod_standard/mod.test.ts +0 -13
- package/ts/plr.cli.ts +0 -60
- package/ts/plr.config.ts +0 -36
- package/ts/plr.mods.ts +0 -25
- package/ts/plr.paths.ts +0 -29
- package/ts/plr.plugins.ts +0 -17
- package/tslint.json +0 -3
- package/yarn.lock +0 -3464
|
@@ -0,0 +1,1409 @@
|
|
|
1
|
+
import type * as interfaces from '../ts_interfaces/index.js';
|
|
2
|
+
import {
|
|
3
|
+
controllerHarnessIds,
|
|
4
|
+
controllerMaxDraftAttachmentBytes,
|
|
5
|
+
controllerMaxDraftAttachmentTotalBytes,
|
|
6
|
+
controllerMaxDraftAttachments,
|
|
7
|
+
controllerMaxDraftTextBytes,
|
|
8
|
+
controllerRuntimeIdKey,
|
|
9
|
+
controllerSessionHarnessIds,
|
|
10
|
+
} from '../ts_interfaces/index.js';
|
|
11
|
+
|
|
12
|
+
type TObject = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
const isPlainObject = (valueArg: unknown): valueArg is TObject => {
|
|
15
|
+
if (typeof valueArg !== 'object' || valueArg === null || Array.isArray(valueArg)) return false;
|
|
16
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
17
|
+
return prototype === Object.prototype || prototype === null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const assertObject = (valueArg: unknown, nameArg: string): TObject => {
|
|
21
|
+
if (!isPlainObject(valueArg)) throw new Error(`${nameArg} must be an object.`);
|
|
22
|
+
return valueArg;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const assertKeys = (
|
|
26
|
+
valueArg: TObject,
|
|
27
|
+
requiredArg: readonly string[],
|
|
28
|
+
optionalArg: readonly string[] = [],
|
|
29
|
+
): void => {
|
|
30
|
+
const allowed = new Set([...requiredArg, ...optionalArg]);
|
|
31
|
+
for (const key of Object.keys(valueArg)) {
|
|
32
|
+
if (!allowed.has(key)) throw new Error(`Unexpected request field: ${key}`);
|
|
33
|
+
}
|
|
34
|
+
for (const key of requiredArg) {
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(valueArg, key)) {
|
|
36
|
+
throw new Error(`Missing request field: ${key}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const assertString = (
|
|
42
|
+
valueArg: unknown,
|
|
43
|
+
nameArg: string,
|
|
44
|
+
minimumArg: number,
|
|
45
|
+
maximumArg: number,
|
|
46
|
+
): string => {
|
|
47
|
+
if (typeof valueArg !== 'string' || valueArg.length < minimumArg || valueArg.length > maximumArg) {
|
|
48
|
+
throw new Error(`${nameArg} must contain ${minimumArg}-${maximumArg} characters.`);
|
|
49
|
+
}
|
|
50
|
+
return valueArg;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const assertEmptyRequest = (valueArg: unknown): Record<string, never> => {
|
|
54
|
+
const value = assertObject(valueArg, 'request');
|
|
55
|
+
assertKeys(value, []);
|
|
56
|
+
return value as Record<string, never>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const assertIdentifier = (valueArg: unknown, nameArg: string): string => {
|
|
60
|
+
const value = assertString(valueArg, nameArg, 1, 128);
|
|
61
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
62
|
+
throw new Error(`${nameArg} contains unsupported characters.`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const assertOpaqueIdentifier = (valueArg: unknown, nameArg: string): string => {
|
|
68
|
+
if (
|
|
69
|
+
typeof valueArg !== 'string'
|
|
70
|
+
|| valueArg.length === 0
|
|
71
|
+
|| Buffer.byteLength(valueArg, 'utf8') > 512
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(`${nameArg} must contain 1-512 UTF-8 bytes.`);
|
|
74
|
+
}
|
|
75
|
+
return valueArg;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function assertControllerRuntimeId<THarnessId extends interfaces.TControllerHarnessId>(
|
|
79
|
+
valueArg: unknown,
|
|
80
|
+
nameArg: string,
|
|
81
|
+
allowedHarnessIdsArg: readonly THarnessId[],
|
|
82
|
+
): interfaces.IControllerRuntimeId & { harnessId: THarnessId };
|
|
83
|
+
export function assertControllerRuntimeId(
|
|
84
|
+
valueArg: unknown,
|
|
85
|
+
nameArg: string,
|
|
86
|
+
allowedHarnessIdsArg?: readonly interfaces.TControllerHarnessId[],
|
|
87
|
+
): interfaces.IControllerRuntimeId;
|
|
88
|
+
export function assertControllerRuntimeId(
|
|
89
|
+
valueArg: unknown,
|
|
90
|
+
nameArg: string,
|
|
91
|
+
allowedHarnessIdsArg: readonly interfaces.TControllerHarnessId[] = controllerHarnessIds,
|
|
92
|
+
): interfaces.IControllerRuntimeId {
|
|
93
|
+
const value = assertObject(valueArg, nameArg);
|
|
94
|
+
assertKeys(value, ['harnessId', 'nativeId']);
|
|
95
|
+
if (
|
|
96
|
+
typeof value.harnessId !== 'string'
|
|
97
|
+
|| !allowedHarnessIdsArg.includes(value.harnessId as interfaces.TControllerHarnessId)
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(`${nameArg}.harnessId has the wrong runtime owner.`);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
harnessId: value.harnessId as interfaces.TControllerHarnessId,
|
|
103
|
+
nativeId: assertOpaqueIdentifier(value.nativeId, `${nameArg}.nativeId`),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const assertRelatedRuntimeIds = (
|
|
108
|
+
leftArg: interfaces.IControllerRuntimeId,
|
|
109
|
+
rightArg: interfaces.IControllerRuntimeId,
|
|
110
|
+
nameArg: string,
|
|
111
|
+
): void => {
|
|
112
|
+
if (leftArg.harnessId !== rightArg.harnessId) {
|
|
113
|
+
throw new Error(`${nameArg} must belong to the same harness.`);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const assertCeremonyId = (valueArg: unknown): string => {
|
|
118
|
+
const value = assertString(valueArg, 'ceremonyId', 43, 43);
|
|
119
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(value)) {
|
|
120
|
+
throw new Error('ceremonyId must be a 32-byte base64url value.');
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const assertWebAuthnResponse = <T>(valueArg: unknown, kindArg: 'registration' | 'authentication'): T => {
|
|
126
|
+
const value = assertObject(valueArg, `${kindArg} response`);
|
|
127
|
+
assertKeys(value, ['id', 'rawId', 'response', 'type'], [
|
|
128
|
+
'authenticatorAttachment',
|
|
129
|
+
'clientExtensionResults',
|
|
130
|
+
]);
|
|
131
|
+
assertString(value.id, 'credential id', 1, 2048);
|
|
132
|
+
assertString(value.rawId, 'credential rawId', 1, 2048);
|
|
133
|
+
if (value.type !== 'public-key') throw new Error('WebAuthn credential type must be public-key.');
|
|
134
|
+
assertObject(value.response, 'WebAuthn response payload');
|
|
135
|
+
let encoded: string;
|
|
136
|
+
try {
|
|
137
|
+
encoded = JSON.stringify(value);
|
|
138
|
+
} catch {
|
|
139
|
+
throw new Error('WebAuthn response must be serializable.');
|
|
140
|
+
}
|
|
141
|
+
if (encoded.length > 220_000) throw new Error('WebAuthn response is too large.');
|
|
142
|
+
return value as T;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const assertSetupBeginRequest = (
|
|
146
|
+
valueArg: unknown,
|
|
147
|
+
): interfaces.IReq_ControllerSetupBegin['request'] => {
|
|
148
|
+
const value = assertObject(valueArg, 'setup request');
|
|
149
|
+
assertKeys(value, ['setupCode']);
|
|
150
|
+
return { setupCode: assertString(value.setupCode, 'setupCode', 1, 256) };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const assertSetupFinishRequest = (
|
|
154
|
+
valueArg: unknown,
|
|
155
|
+
): interfaces.IReq_ControllerSetupFinish['request'] => {
|
|
156
|
+
const value = assertObject(valueArg, 'setup completion request');
|
|
157
|
+
assertKeys(value, ['ceremonyId', 'setupCode', 'response']);
|
|
158
|
+
return {
|
|
159
|
+
ceremonyId: assertCeremonyId(value.ceremonyId),
|
|
160
|
+
setupCode: assertString(value.setupCode, 'setupCode', 1, 256),
|
|
161
|
+
response: assertWebAuthnResponse<interfaces.IReq_ControllerSetupFinish['request']['response']>(
|
|
162
|
+
value.response,
|
|
163
|
+
'registration',
|
|
164
|
+
),
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const assertAuthenticationFinishRequest = (
|
|
169
|
+
valueArg: unknown,
|
|
170
|
+
): interfaces.IReq_ControllerAuthenticationFinish['request'] => {
|
|
171
|
+
const value = assertObject(valueArg, 'authentication completion request');
|
|
172
|
+
assertKeys(value, ['ceremonyId', 'response']);
|
|
173
|
+
return {
|
|
174
|
+
ceremonyId: assertCeremonyId(value.ceremonyId),
|
|
175
|
+
response: assertWebAuthnResponse<
|
|
176
|
+
interfaces.IReq_ControllerAuthenticationFinish['request']['response']
|
|
177
|
+
>(value.response, 'authentication'),
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const assertProjectListRequest = assertEmptyRequest;
|
|
182
|
+
|
|
183
|
+
const pathSegmentValid = (segmentArg: string): boolean => (
|
|
184
|
+
segmentArg.length > 0
|
|
185
|
+
&& segmentArg.length <= 255
|
|
186
|
+
&& segmentArg !== '.'
|
|
187
|
+
&& segmentArg !== '..'
|
|
188
|
+
&& !segmentArg.startsWith('.')
|
|
189
|
+
// eslint-disable-next-line no-control-regex
|
|
190
|
+
&& !/[\x00-\x1f\x7f/\\]/.test(segmentArg)
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
export const assertProjectRelativePath = (valueArg: unknown, nameArg: string): string => {
|
|
194
|
+
const value = assertString(valueArg, nameArg, 1, 1024);
|
|
195
|
+
const segments = value.split('/');
|
|
196
|
+
if (!segments.every(pathSegmentValid)) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`${nameArg} must be a relative path of visible directory names without dot navigation.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const assertProjectCreateRequest = (
|
|
205
|
+
valueArg: unknown,
|
|
206
|
+
): interfaces.IReq_ControllerProjectCreate['request'] => {
|
|
207
|
+
const value = assertObject(valueArg, 'project creation request');
|
|
208
|
+
assertKeys(value, ['path']);
|
|
209
|
+
const path = assertString(value.path, 'path', 1, 4096);
|
|
210
|
+
// eslint-disable-next-line no-control-regex
|
|
211
|
+
if (/[\x00-\x1f\x7f\\]/.test(path)) {
|
|
212
|
+
throw new Error('path contains unsupported characters.');
|
|
213
|
+
}
|
|
214
|
+
// Resolution against the projects root (for relative input) happens in the
|
|
215
|
+
// controller; see resolveProjectPathInput.
|
|
216
|
+
return { path };
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Explicitly typed absolute paths may name hidden directories; only path
|
|
220
|
+
// navigation and unusable characters are rejected.
|
|
221
|
+
const absoluteSegmentValid = (segmentArg: string): boolean => (
|
|
222
|
+
segmentArg.length > 0
|
|
223
|
+
&& segmentArg.length <= 255
|
|
224
|
+
&& segmentArg !== '.'
|
|
225
|
+
&& segmentArg !== '..'
|
|
226
|
+
// eslint-disable-next-line no-control-regex
|
|
227
|
+
&& !/[\x00-\x1f\x7f/\\]/.test(segmentArg)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
export const assertAbsoluteProjectPath = (valueArg: unknown, nameArg: string): string => {
|
|
231
|
+
const value = assertString(valueArg, nameArg, 2, 4096);
|
|
232
|
+
if (!value.startsWith('/') || !value.slice(1).split('/').every(absoluteSegmentValid)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`${nameArg} must be an absolute path of directory names without dot navigation.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Accepts what a human types and returns the absolute directory to register:
|
|
242
|
+
* an absolute path is taken as-is (any location on disk), a relative path is
|
|
243
|
+
* resolved against the projects root.
|
|
244
|
+
*/
|
|
245
|
+
export const resolveProjectPathInput = (
|
|
246
|
+
pathArg: string,
|
|
247
|
+
projectsRootArg: string,
|
|
248
|
+
): string => {
|
|
249
|
+
if (!pathArg.startsWith('/')) {
|
|
250
|
+
return `${projectsRootArg}/${assertProjectRelativePath(pathArg, 'path')}`;
|
|
251
|
+
}
|
|
252
|
+
let trimmed = pathArg;
|
|
253
|
+
while (trimmed.length > 1 && trimmed.endsWith('/')) {
|
|
254
|
+
trimmed = trimmed.slice(0, -1);
|
|
255
|
+
}
|
|
256
|
+
if (trimmed === projectsRootArg) {
|
|
257
|
+
return projectsRootArg;
|
|
258
|
+
}
|
|
259
|
+
return assertAbsoluteProjectPath(trimmed, 'path');
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export interface IProjectSuggestParse {
|
|
263
|
+
/**
|
|
264
|
+
* Directory whose entries are being completed: absolute when the query was
|
|
265
|
+
* absolute, else relative to the projects root ('' meaning the root).
|
|
266
|
+
*/
|
|
267
|
+
directoryPath: string;
|
|
268
|
+
prefix: string;
|
|
269
|
+
unmatchable: boolean;
|
|
270
|
+
/** The query was an absolute path; suggestions should be returned absolute. */
|
|
271
|
+
absolute: boolean;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const assertProjectSuggestRequest = (
|
|
275
|
+
valueArg: unknown,
|
|
276
|
+
): IProjectSuggestParse => {
|
|
277
|
+
const value = assertObject(valueArg, 'path suggestion request');
|
|
278
|
+
assertKeys(value, ['query']);
|
|
279
|
+
const query = assertString(value.query, 'query', 0, 4096);
|
|
280
|
+
const absolute = query.startsWith('/');
|
|
281
|
+
const lastSlash = query.lastIndexOf('/');
|
|
282
|
+
const directoryPath = absolute
|
|
283
|
+
? (lastSlash === 0 ? '/' : query.slice(0, lastSlash))
|
|
284
|
+
: (lastSlash < 0 ? '' : query.slice(0, lastSlash));
|
|
285
|
+
const prefix = lastSlash < 0 ? query : query.slice(lastSlash + 1);
|
|
286
|
+
if (absolute && directoryPath !== '/') {
|
|
287
|
+
assertAbsoluteProjectPath(directoryPath, 'query');
|
|
288
|
+
} else if (!absolute && directoryPath !== '') {
|
|
289
|
+
assertProjectRelativePath(directoryPath, 'query');
|
|
290
|
+
}
|
|
291
|
+
if (
|
|
292
|
+
prefix.length > 255
|
|
293
|
+
// eslint-disable-next-line no-control-regex
|
|
294
|
+
|| /[\x00-\x1f\x7f\\]/.test(prefix)
|
|
295
|
+
) {
|
|
296
|
+
throw new Error('query contains unsupported characters.');
|
|
297
|
+
}
|
|
298
|
+
// Hidden entries are never suggested; a dot prefix yields an empty result
|
|
299
|
+
// while typing rather than an error.
|
|
300
|
+
return { directoryPath, prefix, unmatchable: prefix.startsWith('.'), absolute };
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export const assertProjectRemoveRequest = (
|
|
304
|
+
valueArg: unknown,
|
|
305
|
+
): interfaces.IReq_ControllerProjectRemove['request'] => {
|
|
306
|
+
const value = assertObject(valueArg, 'project removal request');
|
|
307
|
+
assertKeys(value, ['projectId']);
|
|
308
|
+
return { projectId: assertIdentifier(value.projectId, 'projectId') };
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export const assertProviderListRequest = assertEmptyRequest;
|
|
312
|
+
export const assertProviderConnectionListRequest = assertEmptyRequest;
|
|
313
|
+
|
|
314
|
+
export const assertProviderLoginBeginRequest = (
|
|
315
|
+
valueArg: unknown,
|
|
316
|
+
): interfaces.IReq_ControllerProviderLoginBegin['request'] => {
|
|
317
|
+
const value = assertObject(valueArg, 'provider login request');
|
|
318
|
+
assertKeys(value, ['providerID']);
|
|
319
|
+
if (value.providerID !== 'openai') {
|
|
320
|
+
throw new Error('providerID must be openai.');
|
|
321
|
+
}
|
|
322
|
+
return { providerID: 'openai' };
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const assertProviderLoginGetRequest = (
|
|
326
|
+
valueArg: unknown,
|
|
327
|
+
): interfaces.IReq_ControllerProviderLoginGet['request'] => {
|
|
328
|
+
const value = assertObject(valueArg, 'provider login lookup request');
|
|
329
|
+
assertKeys(value, ['loginId']);
|
|
330
|
+
return { loginId: assertControllerRuntimeId(value.loginId, 'loginId', ['flex']) };
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export const assertProviderLoginCancelRequest = (
|
|
334
|
+
valueArg: unknown,
|
|
335
|
+
): interfaces.IReq_ControllerProviderLoginCancel['request'] => {
|
|
336
|
+
const value = assertObject(valueArg, 'provider login cancellation request');
|
|
337
|
+
assertKeys(value, ['loginId']);
|
|
338
|
+
return { loginId: assertControllerRuntimeId(value.loginId, 'loginId', ['flex']) };
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
export const assertProviderConnectionLogoutRequest = (
|
|
342
|
+
valueArg: unknown,
|
|
343
|
+
): interfaces.IReq_ControllerProviderConnectionLogout['request'] => {
|
|
344
|
+
const value = assertObject(valueArg, 'provider connection logout request');
|
|
345
|
+
assertKeys(value, ['providerConnectionId']);
|
|
346
|
+
return {
|
|
347
|
+
providerConnectionId: assertOpaqueIdentifier(
|
|
348
|
+
value.providerConnectionId,
|
|
349
|
+
'providerConnectionId',
|
|
350
|
+
),
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const assertProviderConnectionRateLimitsGetRequest = (
|
|
355
|
+
valueArg: unknown,
|
|
356
|
+
): interfaces.IReq_ControllerProviderConnectionRateLimitsGet['request'] => (
|
|
357
|
+
assertProviderConnectionLogoutRequest(valueArg)
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
export const assertProviderConnectionActivateOpenCodeRequest = (
|
|
361
|
+
valueArg: unknown,
|
|
362
|
+
): interfaces.IReq_ControllerProviderConnectionActivateOpenCode['request'] => (
|
|
363
|
+
assertProviderConnectionLogoutRequest(valueArg)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
export const assertProviderModelRefreshBeginRequest = (
|
|
367
|
+
valueArg: unknown,
|
|
368
|
+
): interfaces.IReq_ControllerProviderModelRefreshBegin['request'] => {
|
|
369
|
+
const value = assertObject(valueArg, 'provider model refresh request');
|
|
370
|
+
assertKeys(value, ['providerConnectionId']);
|
|
371
|
+
return {
|
|
372
|
+
providerConnectionId: assertOpaqueIdentifier(
|
|
373
|
+
value.providerConnectionId,
|
|
374
|
+
'providerConnectionId',
|
|
375
|
+
),
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
export const assertProviderModelRefreshGetRequest = (
|
|
380
|
+
valueArg: unknown,
|
|
381
|
+
): interfaces.IReq_ControllerProviderModelRefreshGet['request'] => {
|
|
382
|
+
const value = assertObject(valueArg, 'provider model refresh lookup request');
|
|
383
|
+
assertKeys(value, ['jobId']);
|
|
384
|
+
return { jobId: assertControllerRuntimeId(value.jobId, 'jobId', ['flex']) };
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export const assertSettingsUpdateRequest = (
|
|
388
|
+
valueArg: unknown,
|
|
389
|
+
): interfaces.IReq_ControllerSettingsUpdate['request'] => {
|
|
390
|
+
const value = assertObject(valueArg, 'settings update request');
|
|
391
|
+
assertKeys(value, [], ['defaultModels', 'autoAcceptPermissions']);
|
|
392
|
+
const result: interfaces.IReq_ControllerSettingsUpdate['request'] = {};
|
|
393
|
+
if ('autoAcceptPermissions' in value) {
|
|
394
|
+
if (typeof value.autoAcceptPermissions !== 'boolean') {
|
|
395
|
+
throw new Error('autoAcceptPermissions must be a boolean.');
|
|
396
|
+
}
|
|
397
|
+
result.autoAcceptPermissions = value.autoAcceptPermissions;
|
|
398
|
+
}
|
|
399
|
+
if (!('defaultModels' in value)) return result;
|
|
400
|
+
if (!Array.isArray(value.defaultModels) || value.defaultModels.length > 2) {
|
|
401
|
+
throw new Error('defaultModels must contain at most one model per session harness.');
|
|
402
|
+
}
|
|
403
|
+
const defaultModels = value.defaultModels.map((modelArg) => assertModelChoice(modelArg));
|
|
404
|
+
if (new Set(defaultModels.map((model) => model.harnessId)).size !== defaultModels.length) {
|
|
405
|
+
throw new Error('defaultModels must contain at most one model per session harness.');
|
|
406
|
+
}
|
|
407
|
+
return { ...result, defaultModels };
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const assertSessionListRequest = (
|
|
411
|
+
valueArg: unknown,
|
|
412
|
+
): interfaces.IReq_ControllerSessionList['request'] => {
|
|
413
|
+
const value = assertObject(valueArg, 'session list request');
|
|
414
|
+
assertKeys(value, ['projectId']);
|
|
415
|
+
return { projectId: assertIdentifier(value.projectId, 'projectId') };
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export const assertSlashListRequest = (
|
|
419
|
+
valueArg: unknown,
|
|
420
|
+
): interfaces.IReq_ControllerSlashList['request'] => {
|
|
421
|
+
const value = assertObject(valueArg, 'slash command list request');
|
|
422
|
+
assertKeys(value, ['projectId', 'sessionId']);
|
|
423
|
+
return {
|
|
424
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
425
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
export const assertSessionGetRequest = (
|
|
430
|
+
valueArg: unknown,
|
|
431
|
+
): interfaces.IReq_ControllerSessionGet['request'] => {
|
|
432
|
+
const value = assertObject(valueArg, 'session request');
|
|
433
|
+
assertKeys(value, ['projectId', 'sessionId']);
|
|
434
|
+
return {
|
|
435
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
436
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
export const assertSessionAuxiliaryGetRequest = (
|
|
441
|
+
valueArg: unknown,
|
|
442
|
+
): interfaces.IReq_ControllerSessionAuxiliaryGet['request'] => assertSessionGetRequest(valueArg);
|
|
443
|
+
|
|
444
|
+
export const assertSessionModelUpdateRequest = (
|
|
445
|
+
valueArg: unknown,
|
|
446
|
+
): interfaces.IReq_ControllerSessionModelUpdate['request'] => {
|
|
447
|
+
const value = assertObject(valueArg, 'session model update request');
|
|
448
|
+
assertKeys(value, ['projectId', 'sessionId', 'model']);
|
|
449
|
+
const sessionId = assertControllerRuntimeId(
|
|
450
|
+
value.sessionId,
|
|
451
|
+
'sessionId',
|
|
452
|
+
controllerSessionHarnessIds,
|
|
453
|
+
);
|
|
454
|
+
const model = assertModelChoice(value.model);
|
|
455
|
+
if (model.harnessId !== sessionId.harnessId) {
|
|
456
|
+
throw new Error('model must belong to the same harness as sessionId.');
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
460
|
+
sessionId,
|
|
461
|
+
model,
|
|
462
|
+
};
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
export const assertSessionScratchpadSaveRequest = (
|
|
466
|
+
valueArg: unknown,
|
|
467
|
+
): interfaces.IReq_ControllerSessionScratchpadSave['request'] => {
|
|
468
|
+
const value = assertObject(valueArg, 'session scratchpad save request');
|
|
469
|
+
assertKeys(value, ['projectId', 'sessionId', 'text', 'expectedRevision']);
|
|
470
|
+
const text = assertString(value.text, 'text', 0, 32_768);
|
|
471
|
+
if (Buffer.byteLength(text, 'utf8') > 128 * 1024) {
|
|
472
|
+
throw new Error('text exceeds the 128 KiB UTF-8 limit.');
|
|
473
|
+
}
|
|
474
|
+
if (!Number.isSafeInteger(value.expectedRevision) || (value.expectedRevision as number) < 0) {
|
|
475
|
+
throw new Error('expectedRevision must be a non-negative safe integer.');
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
479
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
480
|
+
text,
|
|
481
|
+
expectedRevision: value.expectedRevision as number,
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
export const assertSessionIntelligenceAskRequest = (
|
|
486
|
+
valueArg: unknown,
|
|
487
|
+
): interfaces.IReq_ControllerSessionIntelligenceAsk['request'] => {
|
|
488
|
+
const value = assertObject(valueArg, 'session intelligence request');
|
|
489
|
+
assertKeys(value, ['projectId', 'sessionId', 'question']);
|
|
490
|
+
const question = assertString(value.question, 'question', 1, 8_192);
|
|
491
|
+
if (!question.trim()) throw new Error('question must not be blank.');
|
|
492
|
+
if (Buffer.byteLength(question, 'utf8') > 32 * 1024) {
|
|
493
|
+
throw new Error('question exceeds the 32 KiB UTF-8 limit.');
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
497
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
498
|
+
question,
|
|
499
|
+
};
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export const assertSessionMessagesPageRequest = (
|
|
503
|
+
valueArg: unknown,
|
|
504
|
+
): interfaces.IReq_ControllerSessionMessagesPage['request'] => {
|
|
505
|
+
const value = assertObject(valueArg, 'session message page request');
|
|
506
|
+
assertKeys(value, ['projectId', 'sessionId', 'limit'], ['before']);
|
|
507
|
+
if (!Number.isSafeInteger(value.limit) || (value.limit as number) < 1 || (value.limit as number) > 50) {
|
|
508
|
+
throw new Error('limit must be an integer between 1 and 50.');
|
|
509
|
+
}
|
|
510
|
+
const before = value.before === undefined
|
|
511
|
+
? undefined
|
|
512
|
+
: assertString(value.before, 'before', 1, 4096);
|
|
513
|
+
return {
|
|
514
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
515
|
+
sessionId: assertControllerRuntimeId(
|
|
516
|
+
value.sessionId,
|
|
517
|
+
'sessionId',
|
|
518
|
+
controllerSessionHarnessIds,
|
|
519
|
+
),
|
|
520
|
+
limit: value.limit as number,
|
|
521
|
+
...(before === undefined ? {} : { before }),
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
export const assertSessionMessageGetRequest = (
|
|
526
|
+
valueArg: unknown,
|
|
527
|
+
): interfaces.IReq_ControllerSessionMessageGet['request'] => {
|
|
528
|
+
const value = assertObject(valueArg, 'exact session message request');
|
|
529
|
+
assertKeys(value, ['projectId', 'sessionId', 'messageId']);
|
|
530
|
+
const sessionId = assertControllerRuntimeId(
|
|
531
|
+
value.sessionId,
|
|
532
|
+
'sessionId',
|
|
533
|
+
controllerSessionHarnessIds,
|
|
534
|
+
);
|
|
535
|
+
const messageId = assertControllerRuntimeId(
|
|
536
|
+
value.messageId,
|
|
537
|
+
'messageId',
|
|
538
|
+
controllerSessionHarnessIds,
|
|
539
|
+
);
|
|
540
|
+
assertRelatedRuntimeIds(sessionId, messageId, 'messageId');
|
|
541
|
+
return {
|
|
542
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
543
|
+
sessionId,
|
|
544
|
+
messageId,
|
|
545
|
+
};
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
export const assertSessionCreateRequest = (
|
|
549
|
+
valueArg: unknown,
|
|
550
|
+
): interfaces.IReq_ControllerSessionCreate['request'] => {
|
|
551
|
+
const value = assertObject(valueArg, 'session creation request');
|
|
552
|
+
assertKeys(value, ['projectId', 'harnessId'], ['title', 'model']);
|
|
553
|
+
const projectId = assertIdentifier(value.projectId, 'projectId');
|
|
554
|
+
if (
|
|
555
|
+
typeof value.harnessId !== 'string'
|
|
556
|
+
|| !controllerSessionHarnessIds.includes(value.harnessId as interfaces.TControllerSessionHarnessId)
|
|
557
|
+
) {
|
|
558
|
+
throw new Error('harnessId must be opencode or flex.');
|
|
559
|
+
}
|
|
560
|
+
const harnessId = value.harnessId as interfaces.TControllerSessionHarnessId;
|
|
561
|
+
const model = value.model === undefined ? undefined : assertModelChoice(value.model);
|
|
562
|
+
if (model !== undefined && model.harnessId !== harnessId) {
|
|
563
|
+
throw new Error('model must belong to the requested harness.');
|
|
564
|
+
}
|
|
565
|
+
const title = value.title === undefined
|
|
566
|
+
? undefined
|
|
567
|
+
: assertString(value.title, 'title', 1, 200).trim();
|
|
568
|
+
if (value.title !== undefined && !title) throw new Error('title must not be blank.');
|
|
569
|
+
return {
|
|
570
|
+
projectId,
|
|
571
|
+
harnessId,
|
|
572
|
+
...(title === undefined ? {} : { title }),
|
|
573
|
+
...(model === undefined ? {} : { model }),
|
|
574
|
+
};
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
export const assertTempPasswordLoginRequest = (
|
|
578
|
+
valueArg: unknown,
|
|
579
|
+
): interfaces.IReq_ControllerTempPasswordLogin['request'] => {
|
|
580
|
+
const value = assertObject(valueArg, 'temporary password login request');
|
|
581
|
+
assertKeys(value, ['password']);
|
|
582
|
+
const password = assertString(value.password, 'password', 8, 256);
|
|
583
|
+
if (!/^[\x21-\x7e]+$/.test(password)) {
|
|
584
|
+
throw new Error('password contains unsupported characters.');
|
|
585
|
+
}
|
|
586
|
+
return { password };
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
export const assertAuthResumeRequest = (
|
|
590
|
+
valueArg: unknown,
|
|
591
|
+
): interfaces.IReq_ControllerAuthResume['request'] => {
|
|
592
|
+
const value = assertObject(valueArg, 'auth resume request');
|
|
593
|
+
assertKeys(value, ['token']);
|
|
594
|
+
const token = assertString(value.token, 'token', 43, 43);
|
|
595
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(token)) {
|
|
596
|
+
throw new Error('token contains unsupported characters.');
|
|
597
|
+
}
|
|
598
|
+
return { token };
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
export const assertVariantName = (valueArg: unknown): string => {
|
|
602
|
+
const variant = assertString(valueArg, 'variant', 1, 64);
|
|
603
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(variant)) {
|
|
604
|
+
throw new Error('variant contains unsupported characters.');
|
|
605
|
+
}
|
|
606
|
+
return variant;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export function assertModelChoice(
|
|
610
|
+
valueArg: unknown,
|
|
611
|
+
nameArg = 'model',
|
|
612
|
+
): interfaces.TControllerModelChoice {
|
|
613
|
+
const model = assertObject(valueArg, nameArg);
|
|
614
|
+
if (model.harnessId === 'opencode') {
|
|
615
|
+
assertKeys(model, ['harnessId', 'providerID', 'modelID'], ['variant']);
|
|
616
|
+
} else if (model.harnessId === 'flex') {
|
|
617
|
+
assertKeys(
|
|
618
|
+
model,
|
|
619
|
+
['harnessId', 'providerConnectionId', 'providerID', 'modelID'],
|
|
620
|
+
['variant'],
|
|
621
|
+
);
|
|
622
|
+
} else {
|
|
623
|
+
throw new Error(`${nameArg}.harnessId must be opencode or flex.`);
|
|
624
|
+
}
|
|
625
|
+
const variant = model.variant === undefined ? undefined : assertVariantName(model.variant);
|
|
626
|
+
const common = {
|
|
627
|
+
providerID: assertString(model.providerID, `${nameArg}.providerID`, 1, 256),
|
|
628
|
+
modelID: assertString(model.modelID, `${nameArg}.modelID`, 1, 256),
|
|
629
|
+
...(variant === undefined ? {} : { variant }),
|
|
630
|
+
};
|
|
631
|
+
if (model.harnessId === 'opencode') return { harnessId: 'opencode', ...common };
|
|
632
|
+
return {
|
|
633
|
+
harnessId: 'flex',
|
|
634
|
+
providerConnectionId: assertOpaqueIdentifier(
|
|
635
|
+
model.providerConnectionId,
|
|
636
|
+
`${nameArg}.providerConnectionId`,
|
|
637
|
+
),
|
|
638
|
+
...common,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const assertOptionalModelChoice = (
|
|
643
|
+
valueArg: unknown,
|
|
644
|
+
sessionIdArg: interfaces.IControllerRuntimeId,
|
|
645
|
+
): interfaces.TControllerModelChoice | undefined => {
|
|
646
|
+
if (valueArg === undefined) return undefined;
|
|
647
|
+
const model = assertModelChoice(valueArg);
|
|
648
|
+
if (model.harnessId !== sessionIdArg.harnessId) {
|
|
649
|
+
throw new Error('model must belong to the same harness as sessionId.');
|
|
650
|
+
}
|
|
651
|
+
return model;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const attachmentMediaTypePattern = /^[a-z0-9!#$&^_.+-]{1,64}\/[a-z0-9!#$&^_.+-]{1,64}$/i;
|
|
655
|
+
|
|
656
|
+
const sanitizeAttachmentName = (valueArg: unknown, indexArg: number): string => {
|
|
657
|
+
const raw = assertString(valueArg, `attachments[${indexArg}].name`, 1, 255);
|
|
658
|
+
// Basename only — uploads must never traverse out of their storage folder.
|
|
659
|
+
const basename = raw.split(/[/\\]/).pop() ?? '';
|
|
660
|
+
const cleaned = basename.replace(/[^A-Za-z0-9._ -]/g, '_').replace(/^\.+/, '').trim();
|
|
661
|
+
return cleaned || `file-${indexArg + 1}`;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const assertDraftAttachments = (
|
|
665
|
+
valueArg: unknown,
|
|
666
|
+
): interfaces.IControllerDraftAttachment[] => {
|
|
667
|
+
if (!Array.isArray(valueArg)) throw new Error('attachments must be an array.');
|
|
668
|
+
if (valueArg.length > controllerMaxDraftAttachments) {
|
|
669
|
+
throw new Error(`At most ${controllerMaxDraftAttachments} draft attachments are allowed.`);
|
|
670
|
+
}
|
|
671
|
+
let totalRawBytes = 0;
|
|
672
|
+
const ids = new Set<string>();
|
|
673
|
+
return valueArg.map((entry, index): interfaces.IControllerDraftAttachment => {
|
|
674
|
+
const attachment = assertObject(entry, `attachments[${index}]`);
|
|
675
|
+
assertKeys(attachment, ['id', 'name', 'mediaType', 'size', 'kind', 'dataBase64']);
|
|
676
|
+
const id = assertOpaqueIdentifier(attachment.id, `attachments[${index}].id`);
|
|
677
|
+
if (ids.has(id)) throw new Error('Attachment IDs must be unique.');
|
|
678
|
+
ids.add(id);
|
|
679
|
+
const mediaType = assertString(attachment.mediaType, `attachments[${index}].mediaType`, 1, 129);
|
|
680
|
+
if (!attachmentMediaTypePattern.test(mediaType)) {
|
|
681
|
+
throw new Error(`attachments[${index}].mediaType is not a valid media type.`);
|
|
682
|
+
}
|
|
683
|
+
if (
|
|
684
|
+
typeof attachment.dataBase64 !== 'string'
|
|
685
|
+
|| attachment.dataBase64.length > Math.ceil((controllerMaxDraftAttachmentBytes * 4) / 3) + 4
|
|
686
|
+
) throw new Error(`attachments[${index}].dataBase64 is invalid.`);
|
|
687
|
+
const dataBase64 = attachment.dataBase64;
|
|
688
|
+
const decoded = Buffer.from(dataBase64, 'base64');
|
|
689
|
+
if (decoded.toString('base64') !== dataBase64) {
|
|
690
|
+
throw new Error(`attachments[${index}].dataBase64 must be valid base64.`);
|
|
691
|
+
}
|
|
692
|
+
const rawBytes = decoded.byteLength;
|
|
693
|
+
if (rawBytes > controllerMaxDraftAttachmentBytes) {
|
|
694
|
+
throw new Error(`attachments[${index}] exceeds the per-file upload limit.`);
|
|
695
|
+
}
|
|
696
|
+
if (!Number.isSafeInteger(attachment.size) || Number(attachment.size) !== rawBytes) {
|
|
697
|
+
throw new Error(`attachments[${index}].size must match the decoded content size.`);
|
|
698
|
+
}
|
|
699
|
+
if (!['image', 'text', 'binary'].includes(String(attachment.kind))) {
|
|
700
|
+
throw new Error(`attachments[${index}].kind is invalid.`);
|
|
701
|
+
}
|
|
702
|
+
totalRawBytes += rawBytes;
|
|
703
|
+
if (totalRawBytes > controllerMaxDraftAttachmentTotalBytes) {
|
|
704
|
+
throw new Error('The combined attachment size exceeds the upload limit.');
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
id,
|
|
708
|
+
name: sanitizeAttachmentName(attachment.name, index),
|
|
709
|
+
mediaType,
|
|
710
|
+
size: rawBytes,
|
|
711
|
+
kind: attachment.kind as interfaces.IControllerDraftAttachment['kind'],
|
|
712
|
+
dataBase64,
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const assertDraftRevision = (valueArg: unknown, nameArg = 'draftRevision'): number => {
|
|
718
|
+
if (!Number.isSafeInteger(valueArg) || Number(valueArg) < 0) {
|
|
719
|
+
throw new Error(`${nameArg} must be a non-negative safe integer.`);
|
|
720
|
+
}
|
|
721
|
+
return Number(valueArg);
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
export const assertSessionDraftUpdateRequest = (
|
|
725
|
+
valueArg: unknown,
|
|
726
|
+
): interfaces.IReq_ControllerSessionDraftUpdate['request'] => {
|
|
727
|
+
const value = assertObject(valueArg, 'session draft update request');
|
|
728
|
+
assertKeys(
|
|
729
|
+
value,
|
|
730
|
+
['projectId', 'sessionId', 'expectedRevision'],
|
|
731
|
+
['text', 'attachments'],
|
|
732
|
+
);
|
|
733
|
+
if (value.text === undefined && value.attachments === undefined) {
|
|
734
|
+
throw new Error('A draft update must replace text, attachments, or both.');
|
|
735
|
+
}
|
|
736
|
+
let text: string | undefined;
|
|
737
|
+
if (value.text !== undefined) {
|
|
738
|
+
if (
|
|
739
|
+
typeof value.text !== 'string'
|
|
740
|
+
|| Buffer.byteLength(value.text, 'utf8') > controllerMaxDraftTextBytes
|
|
741
|
+
) throw new Error('text exceeds the draft UTF-8 byte limit.');
|
|
742
|
+
text = value.text;
|
|
743
|
+
}
|
|
744
|
+
const attachments = value.attachments === undefined
|
|
745
|
+
? undefined
|
|
746
|
+
: assertDraftAttachments(value.attachments);
|
|
747
|
+
return {
|
|
748
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
749
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
750
|
+
expectedRevision: assertDraftRevision(value.expectedRevision, 'expectedRevision'),
|
|
751
|
+
...(text === undefined ? {} : { text }),
|
|
752
|
+
...(attachments === undefined ? {} : { attachments }),
|
|
753
|
+
};
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
export const assertSessionSendRequest = (
|
|
757
|
+
valueArg: unknown,
|
|
758
|
+
): interfaces.IReq_ControllerSessionSend['request'] => {
|
|
759
|
+
const value = assertObject(valueArg, 'session message request');
|
|
760
|
+
assertKeys(value, ['projectId', 'sessionId', 'draftRevision'], ['model']);
|
|
761
|
+
const sessionId = assertControllerRuntimeId(
|
|
762
|
+
value.sessionId,
|
|
763
|
+
'sessionId',
|
|
764
|
+
controllerSessionHarnessIds,
|
|
765
|
+
);
|
|
766
|
+
const model = assertOptionalModelChoice(value.model, sessionId);
|
|
767
|
+
return {
|
|
768
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
769
|
+
sessionId,
|
|
770
|
+
draftRevision: assertDraftRevision(value.draftRevision),
|
|
771
|
+
...(model === undefined ? {} : { model }),
|
|
772
|
+
};
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
export const assertSlashExecuteRequest = (
|
|
776
|
+
valueArg: unknown,
|
|
777
|
+
): interfaces.IReq_ControllerSlashExecute['request'] => {
|
|
778
|
+
const value = assertObject(valueArg, 'slash command execution request');
|
|
779
|
+
assertKeys(value, ['projectId', 'sessionId', 'draftRevision'], ['model']);
|
|
780
|
+
const sessionId = assertControllerRuntimeId(
|
|
781
|
+
value.sessionId,
|
|
782
|
+
'sessionId',
|
|
783
|
+
controllerSessionHarnessIds,
|
|
784
|
+
);
|
|
785
|
+
const model = assertOptionalModelChoice(value.model, sessionId);
|
|
786
|
+
return {
|
|
787
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
788
|
+
sessionId,
|
|
789
|
+
draftRevision: assertDraftRevision(value.draftRevision),
|
|
790
|
+
...(model === undefined ? {} : { model }),
|
|
791
|
+
};
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
export const assertPermissionReplyRequest = (
|
|
795
|
+
valueArg: unknown,
|
|
796
|
+
): interfaces.IReq_ControllerPermissionReply['request'] => {
|
|
797
|
+
const value = assertObject(valueArg, 'permission reply request');
|
|
798
|
+
assertKeys(value, ['projectId', 'sessionId', 'requestId', 'reply'], ['message']);
|
|
799
|
+
if (value.reply !== 'once' && value.reply !== 'reject') {
|
|
800
|
+
throw new Error('reply must be once or reject. Persistent broad grants are not supported.');
|
|
801
|
+
}
|
|
802
|
+
const message = value.message === undefined
|
|
803
|
+
? undefined
|
|
804
|
+
: assertString(value.message, 'message', 1, 2048);
|
|
805
|
+
const sessionId = assertControllerRuntimeId(
|
|
806
|
+
value.sessionId,
|
|
807
|
+
'sessionId',
|
|
808
|
+
controllerSessionHarnessIds,
|
|
809
|
+
);
|
|
810
|
+
const requestId = assertControllerRuntimeId(
|
|
811
|
+
value.requestId,
|
|
812
|
+
'requestId',
|
|
813
|
+
controllerSessionHarnessIds,
|
|
814
|
+
);
|
|
815
|
+
assertRelatedRuntimeIds(sessionId, requestId, 'requestId');
|
|
816
|
+
return {
|
|
817
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
818
|
+
sessionId,
|
|
819
|
+
requestId,
|
|
820
|
+
reply: value.reply,
|
|
821
|
+
...(message === undefined ? {} : { message }),
|
|
822
|
+
};
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
export const assertSessionGroupsGetRequest = (
|
|
826
|
+
valueArg: unknown,
|
|
827
|
+
): interfaces.IReq_ControllerSessionGroupsGet['request'] => {
|
|
828
|
+
const value = assertObject(valueArg, 'session groups request');
|
|
829
|
+
assertKeys(value, ['projectId']);
|
|
830
|
+
return { projectId: assertIdentifier(value.projectId, 'projectId') };
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
export const assertSessionGroupsUpdateRequest = (
|
|
834
|
+
valueArg: unknown,
|
|
835
|
+
): interfaces.IReq_ControllerSessionGroupsUpdate['request'] => {
|
|
836
|
+
const value = assertObject(valueArg, 'session groups update request');
|
|
837
|
+
assertKeys(value, ['projectId', 'groups'], ['ungroupedSessionIds', 'expectedRevision']);
|
|
838
|
+
const projectId = assertIdentifier(value.projectId, 'projectId');
|
|
839
|
+
if (!Array.isArray(value.groups) || value.groups.length > 64) {
|
|
840
|
+
throw new Error('groups must be a list of at most 64 groups.');
|
|
841
|
+
}
|
|
842
|
+
const groupIds = new Set<string>();
|
|
843
|
+
const memberIds = new Set<string>();
|
|
844
|
+
const groups = value.groups.map((groupArg) => {
|
|
845
|
+
const group = assertObject(groupArg, 'group');
|
|
846
|
+
assertKeys(group, ['id', 'name', 'sessionIds']);
|
|
847
|
+
const id = assertIdentifier(group.id, 'group id');
|
|
848
|
+
if (groupIds.has(id)) throw new Error('Group IDs must be unique.');
|
|
849
|
+
groupIds.add(id);
|
|
850
|
+
const name = assertString(group.name, 'group name', 1, 128);
|
|
851
|
+
// eslint-disable-next-line no-control-regex
|
|
852
|
+
if (/[\x00-\x1f\x7f]/.test(name) || name.trim().length === 0) {
|
|
853
|
+
throw new Error('Group names must be a single line of printable text.');
|
|
854
|
+
}
|
|
855
|
+
if (!Array.isArray(group.sessionIds) || group.sessionIds.length > 512) {
|
|
856
|
+
throw new Error('A group holds at most 512 layout items.');
|
|
857
|
+
}
|
|
858
|
+
const sessionIds = group.sessionIds.map((sessionIdArg) => {
|
|
859
|
+
const sessionId = assertControllerRuntimeId(
|
|
860
|
+
sessionIdArg,
|
|
861
|
+
'layout item id',
|
|
862
|
+
controllerSessionHarnessIds,
|
|
863
|
+
);
|
|
864
|
+
const sessionKey = controllerRuntimeIdKey(sessionId);
|
|
865
|
+
if (memberIds.has(sessionKey)) {
|
|
866
|
+
throw new Error('A layout item can only belong to one group.');
|
|
867
|
+
}
|
|
868
|
+
memberIds.add(sessionKey);
|
|
869
|
+
return sessionId;
|
|
870
|
+
});
|
|
871
|
+
return { id, name: name.trim(), sessionIds };
|
|
872
|
+
});
|
|
873
|
+
let ungroupedSessionIds: interfaces.TControllerSessionId[] | undefined;
|
|
874
|
+
if (value.ungroupedSessionIds !== undefined) {
|
|
875
|
+
if (!Array.isArray(value.ungroupedSessionIds) || value.ungroupedSessionIds.length > 2048) {
|
|
876
|
+
throw new Error('ungroupedSessionIds must be a list of at most 2048 layout item IDs.');
|
|
877
|
+
}
|
|
878
|
+
const ungroupedIds = new Set<string>();
|
|
879
|
+
ungroupedSessionIds = value.ungroupedSessionIds.map((sessionIdArg) => {
|
|
880
|
+
const sessionId = assertControllerRuntimeId(
|
|
881
|
+
sessionIdArg,
|
|
882
|
+
'ungrouped layout item id',
|
|
883
|
+
controllerSessionHarnessIds,
|
|
884
|
+
);
|
|
885
|
+
const sessionKey = controllerRuntimeIdKey(sessionId);
|
|
886
|
+
if (memberIds.has(sessionKey) || ungroupedIds.has(sessionKey)) {
|
|
887
|
+
throw new Error('A layout item can only appear once in the session layout.');
|
|
888
|
+
}
|
|
889
|
+
ungroupedIds.add(sessionKey);
|
|
890
|
+
return sessionId;
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
let expectedRevision: number | undefined;
|
|
894
|
+
if (value.expectedRevision !== undefined) {
|
|
895
|
+
if (!Number.isSafeInteger(value.expectedRevision) || (value.expectedRevision as number) < 0) {
|
|
896
|
+
throw new Error('expectedRevision must be a non-negative safe integer.');
|
|
897
|
+
}
|
|
898
|
+
expectedRevision = value.expectedRevision as number;
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
projectId,
|
|
902
|
+
groups,
|
|
903
|
+
...(ungroupedSessionIds === undefined ? {} : { ungroupedSessionIds }),
|
|
904
|
+
...(expectedRevision === undefined ? {} : { expectedRevision }),
|
|
905
|
+
};
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
export const assertSessionYoloRequest = (
|
|
909
|
+
valueArg: unknown,
|
|
910
|
+
): interfaces.IReq_ControllerSessionYolo['request'] => {
|
|
911
|
+
const value = assertObject(valueArg, 'session yolo request');
|
|
912
|
+
assertKeys(value, ['projectId', 'sessionId', 'enabled']);
|
|
913
|
+
if (typeof value.enabled !== 'boolean') {
|
|
914
|
+
throw new Error('enabled must be a boolean.');
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
918
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', ['opencode', 'flex']),
|
|
919
|
+
enabled: value.enabled,
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
export const assertControllerSessionTitle = (valueArg: unknown): string => {
|
|
924
|
+
const title = assertString(valueArg, 'title', 1, 2048);
|
|
925
|
+
// eslint-disable-next-line no-control-regex
|
|
926
|
+
if (/[\x00-\x1f\x7f]/.test(title) || title.trim().length === 0) {
|
|
927
|
+
throw new Error('title must be a single line of printable text.');
|
|
928
|
+
}
|
|
929
|
+
return title.trim();
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
export const assertSessionRenameRequest = (
|
|
933
|
+
valueArg: unknown,
|
|
934
|
+
): interfaces.IReq_ControllerSessionRename['request'] => {
|
|
935
|
+
const value = assertObject(valueArg, 'session rename request');
|
|
936
|
+
assertKeys(value, ['projectId', 'sessionId', 'title']);
|
|
937
|
+
return {
|
|
938
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
939
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
940
|
+
title: assertControllerSessionTitle(value.title),
|
|
941
|
+
};
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
export const assertSessionArchiveRequest = (
|
|
945
|
+
valueArg: unknown,
|
|
946
|
+
): interfaces.IReq_ControllerSessionArchive['request'] => {
|
|
947
|
+
const value = assertObject(valueArg, 'session archive request');
|
|
948
|
+
assertKeys(value, ['projectId', 'sessionId']);
|
|
949
|
+
return {
|
|
950
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
951
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
const assertSingleLineText = (valueArg: unknown, nameArg: string, maximumArg: number): string => {
|
|
956
|
+
const text = assertString(valueArg, nameArg, 1, maximumArg);
|
|
957
|
+
// eslint-disable-next-line no-control-regex
|
|
958
|
+
if (/[\x00-\x1f\x7f]/.test(text) || text.trim().length === 0) {
|
|
959
|
+
throw new Error(`${nameArg} must be a single line of printable text.`);
|
|
960
|
+
}
|
|
961
|
+
return text.trim();
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const base64Pattern = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
965
|
+
|
|
966
|
+
export const assertTerminalListRequest = (
|
|
967
|
+
valueArg: unknown,
|
|
968
|
+
): interfaces.IReq_ControllerTerminalList['request'] => {
|
|
969
|
+
const value = assertObject(valueArg, 'terminal list request');
|
|
970
|
+
assertKeys(value, ['projectId']);
|
|
971
|
+
return { projectId: assertIdentifier(value.projectId, 'projectId') };
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
export const assertResourceListRequest = (
|
|
975
|
+
valueArg: unknown,
|
|
976
|
+
): interfaces.IReq_ControllerResourceList['request'] => {
|
|
977
|
+
const value = assertObject(valueArg, 'resource list request');
|
|
978
|
+
assertKeys(value, ['projectId']);
|
|
979
|
+
return { projectId: assertIdentifier(value.projectId, 'projectId') };
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
export const assertResourceCreateRequest = (
|
|
983
|
+
valueArg: unknown,
|
|
984
|
+
): interfaces.IReq_ControllerResourceCreate['request'] => {
|
|
985
|
+
const value = assertObject(valueArg, 'resource create request');
|
|
986
|
+
assertKeys(value, ['projectId', 'kind'], ['title']);
|
|
987
|
+
if (value.kind !== 'terminal' && value.kind !== 'browser') {
|
|
988
|
+
throw new Error('kind must be terminal or browser.');
|
|
989
|
+
}
|
|
990
|
+
const title = value.title === undefined
|
|
991
|
+
? undefined
|
|
992
|
+
: assertSingleLineText(value.title, 'title', 256);
|
|
993
|
+
return {
|
|
994
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
995
|
+
kind: value.kind,
|
|
996
|
+
...(title === undefined ? {} : { title }),
|
|
997
|
+
};
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
export const assertResourceRenameRequest = (
|
|
1001
|
+
valueArg: unknown,
|
|
1002
|
+
): interfaces.IReq_ControllerResourceRename['request'] => {
|
|
1003
|
+
const value = assertObject(valueArg, 'resource rename request');
|
|
1004
|
+
assertKeys(value, ['projectId', 'resourceId', 'title']);
|
|
1005
|
+
return {
|
|
1006
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1007
|
+
resourceId: assertIdentifier(value.resourceId, 'resourceId'),
|
|
1008
|
+
title: assertSingleLineText(value.title, 'title', 256),
|
|
1009
|
+
};
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
const assertAttachmentRevision = (valueArg: unknown): number => {
|
|
1013
|
+
if (!Number.isSafeInteger(valueArg) || (valueArg as number) < 0) {
|
|
1014
|
+
throw new Error('expectedAttachmentRevision must be a non-negative safe integer.');
|
|
1015
|
+
}
|
|
1016
|
+
return valueArg as number;
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
export const assertResourceAttachRequest = (
|
|
1020
|
+
valueArg: unknown,
|
|
1021
|
+
): interfaces.IReq_ControllerResourceAttach['request'] => {
|
|
1022
|
+
const value = assertObject(valueArg, 'resource attach request');
|
|
1023
|
+
assertKeys(value, ['projectId', 'resourceId', 'sessionId', 'expectedAttachmentRevision']);
|
|
1024
|
+
return {
|
|
1025
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1026
|
+
resourceId: assertIdentifier(value.resourceId, 'resourceId'),
|
|
1027
|
+
sessionId: assertControllerRuntimeId(value.sessionId, 'sessionId', controllerSessionHarnessIds),
|
|
1028
|
+
expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision),
|
|
1029
|
+
};
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
export const assertResourceDetachRequest = (
|
|
1033
|
+
valueArg: unknown,
|
|
1034
|
+
): interfaces.IReq_ControllerResourceDetach['request'] => {
|
|
1035
|
+
const value = assertObject(valueArg, 'resource detach request');
|
|
1036
|
+
assertKeys(value, ['projectId', 'resourceId', 'expectedAttachmentRevision']);
|
|
1037
|
+
return {
|
|
1038
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1039
|
+
resourceId: assertIdentifier(value.resourceId, 'resourceId'),
|
|
1040
|
+
expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision),
|
|
1041
|
+
};
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
export const assertResourceTargetRequest = (
|
|
1045
|
+
valueArg: unknown,
|
|
1046
|
+
nameArg: string,
|
|
1047
|
+
): { projectId: string; resourceId: string } => {
|
|
1048
|
+
const value = assertObject(valueArg, nameArg);
|
|
1049
|
+
assertKeys(value, ['projectId', 'resourceId']);
|
|
1050
|
+
return {
|
|
1051
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1052
|
+
resourceId: assertIdentifier(value.resourceId, 'resourceId'),
|
|
1053
|
+
};
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
const assertBrowserViewport = (
|
|
1057
|
+
valueArg: unknown,
|
|
1058
|
+
nameArg: string,
|
|
1059
|
+
): interfaces.IControllerBrowserViewport => {
|
|
1060
|
+
const value = assertObject(valueArg, nameArg);
|
|
1061
|
+
assertKeys(value, ['width', 'height', 'deviceScaleFactor']);
|
|
1062
|
+
if (
|
|
1063
|
+
!Number.isInteger(value.width) || (value.width as number) < 1 || (value.width as number) > 4096
|
|
1064
|
+
|| !Number.isInteger(value.height) || (value.height as number) < 1 || (value.height as number) > 4096
|
|
1065
|
+
|| typeof value.deviceScaleFactor !== 'number'
|
|
1066
|
+
|| !Number.isFinite(value.deviceScaleFactor)
|
|
1067
|
+
|| value.deviceScaleFactor < 0.25
|
|
1068
|
+
|| value.deviceScaleFactor > 4
|
|
1069
|
+
) throw new Error(`${nameArg} is invalid.`);
|
|
1070
|
+
return value as unknown as interfaces.IControllerBrowserViewport;
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
const assertBrowserInputBase = (
|
|
1074
|
+
valueArg: TObject,
|
|
1075
|
+
nameArg: string,
|
|
1076
|
+
): interfaces.IControllerBrowserInputBase => {
|
|
1077
|
+
if (
|
|
1078
|
+
!Number.isSafeInteger(valueArg.generation) || (valueArg.generation as number) < 0
|
|
1079
|
+
|| !Number.isSafeInteger(valueArg.viewportRevision) || (valueArg.viewportRevision as number) < 0
|
|
1080
|
+
) throw new Error(`${nameArg} has an invalid frame identity.`);
|
|
1081
|
+
return {
|
|
1082
|
+
tabId: assertIdentifier(valueArg.tabId, `${nameArg}.tabId`),
|
|
1083
|
+
generation: valueArg.generation as number,
|
|
1084
|
+
viewportRevision: valueArg.viewportRevision as number,
|
|
1085
|
+
};
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
const assertBrowserModifiers = (
|
|
1089
|
+
valueArg: unknown,
|
|
1090
|
+
nameArg: string,
|
|
1091
|
+
): interfaces.IControllerBrowserModifierState | undefined => {
|
|
1092
|
+
if (valueArg === undefined) return undefined;
|
|
1093
|
+
const value = assertObject(valueArg, nameArg);
|
|
1094
|
+
assertKeys(value, [], ['alt', 'control', 'meta', 'shift']);
|
|
1095
|
+
for (const key of Object.keys(value)) {
|
|
1096
|
+
if (typeof value[key] !== 'boolean') throw new Error(`${nameArg}.${key} must be boolean.`);
|
|
1097
|
+
}
|
|
1098
|
+
return value as interfaces.IControllerBrowserModifierState;
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
const assertFiniteNumber = (valueArg: unknown, nameArg: string): number => {
|
|
1102
|
+
if (typeof valueArg !== 'number' || !Number.isFinite(valueArg)) {
|
|
1103
|
+
throw new Error(`${nameArg} must be finite.`);
|
|
1104
|
+
}
|
|
1105
|
+
return valueArg;
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
export const assertBrowserViewOpenRequest = (
|
|
1109
|
+
valueArg: unknown,
|
|
1110
|
+
): interfaces.IReq_ControllerBrowserViewOpen['request'] => {
|
|
1111
|
+
const value = assertObject(valueArg, 'browser view open request');
|
|
1112
|
+
assertKeys(value, ['projectId', 'resourceId', 'expectedAttachmentRevision']);
|
|
1113
|
+
return {
|
|
1114
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1115
|
+
resourceId: assertIdentifier(value.resourceId, 'resourceId'),
|
|
1116
|
+
expectedAttachmentRevision: assertAttachmentRevision(value.expectedAttachmentRevision),
|
|
1117
|
+
};
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
export const assertBrowserViewCloseRequest = (
|
|
1121
|
+
valueArg: unknown,
|
|
1122
|
+
): interfaces.IReq_ControllerBrowserViewClose['request'] => {
|
|
1123
|
+
const value = assertObject(valueArg, 'browser view close request');
|
|
1124
|
+
assertKeys(value, ['viewId']);
|
|
1125
|
+
return { viewId: assertIdentifier(value.viewId, 'viewId') };
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
export const assertBrowserViewActivateRequest = (
|
|
1129
|
+
valueArg: unknown,
|
|
1130
|
+
): interfaces.IReq_ControllerBrowserViewActivate['request'] => {
|
|
1131
|
+
const value = assertObject(valueArg, 'browser view activation request');
|
|
1132
|
+
assertKeys(value, ['viewId']);
|
|
1133
|
+
return { viewId: assertIdentifier(value.viewId, 'viewId') };
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
export const assertBrowserViewOperateRequest = (
|
|
1137
|
+
valueArg: unknown,
|
|
1138
|
+
): interfaces.IReq_ControllerBrowserViewOperate['request'] => {
|
|
1139
|
+
const value = assertObject(valueArg, 'browser view operation request');
|
|
1140
|
+
assertKeys(value, ['viewId', 'operation']);
|
|
1141
|
+
const operation = assertObject(value.operation, 'operation');
|
|
1142
|
+
if (typeof operation.type !== 'string') throw new Error('operation.type is invalid.');
|
|
1143
|
+
if (operation.type === 'acknowledgeFrame') {
|
|
1144
|
+
assertKeys(operation, ['type', 'acknowledgement']);
|
|
1145
|
+
const acknowledgement = assertObject(operation.acknowledgement, 'acknowledgement');
|
|
1146
|
+
assertKeys(acknowledgement, ['tabId', 'sequence', 'generation', 'viewportRevision']);
|
|
1147
|
+
if (!Number.isSafeInteger(acknowledgement.sequence) || (acknowledgement.sequence as number) < 0) {
|
|
1148
|
+
throw new Error('acknowledgement.sequence is invalid.');
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1152
|
+
operation: {
|
|
1153
|
+
type: 'acknowledgeFrame',
|
|
1154
|
+
acknowledgement: {
|
|
1155
|
+
...assertBrowserInputBase(acknowledgement, 'acknowledgement'),
|
|
1156
|
+
sequence: acknowledgement.sequence as number,
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
if (operation.type === 'setViewport') {
|
|
1162
|
+
assertKeys(operation, ['type', 'viewport']);
|
|
1163
|
+
return {
|
|
1164
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1165
|
+
operation: { type: 'setViewport', viewport: assertBrowserViewport(operation.viewport, 'viewport') },
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
if (operation.type === 'dispatchMouse') {
|
|
1169
|
+
assertKeys(operation, ['type', 'input']);
|
|
1170
|
+
const input = assertObject(operation.input, 'input');
|
|
1171
|
+
assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'type', 'x', 'y'], [
|
|
1172
|
+
'button', 'buttons', 'clickCount', 'modifiers',
|
|
1173
|
+
]);
|
|
1174
|
+
if (!['move', 'down', 'up'].includes(String(input.type))) throw new Error('input.type is invalid.');
|
|
1175
|
+
if (input.button !== undefined && !['none', 'left', 'middle', 'right', 'back', 'forward'].includes(String(input.button))) {
|
|
1176
|
+
throw new Error('input.button is invalid.');
|
|
1177
|
+
}
|
|
1178
|
+
const modifiers = assertBrowserModifiers(input.modifiers, 'input.modifiers');
|
|
1179
|
+
return {
|
|
1180
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1181
|
+
operation: { type: 'dispatchMouse', input: {
|
|
1182
|
+
...assertBrowserInputBase(input, 'input'),
|
|
1183
|
+
type: input.type as 'move' | 'down' | 'up',
|
|
1184
|
+
x: assertFiniteNumber(input.x, 'input.x'),
|
|
1185
|
+
y: assertFiniteNumber(input.y, 'input.y'),
|
|
1186
|
+
...(input.button === undefined ? {} : { button: input.button as interfaces.IControllerBrowserMouseInput['button'] }),
|
|
1187
|
+
...(input.buttons === undefined ? {} : { buttons: assertFiniteNumber(input.buttons, 'input.buttons') }),
|
|
1188
|
+
...(input.clickCount === undefined ? {} : { clickCount: assertFiniteNumber(input.clickCount, 'input.clickCount') }),
|
|
1189
|
+
...(modifiers === undefined ? {} : { modifiers }),
|
|
1190
|
+
} },
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
if (operation.type === 'dispatchWheel') {
|
|
1194
|
+
assertKeys(operation, ['type', 'input']);
|
|
1195
|
+
const input = assertObject(operation.input, 'input');
|
|
1196
|
+
assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'x', 'y', 'deltaX', 'deltaY'], ['modifiers']);
|
|
1197
|
+
const modifiers = assertBrowserModifiers(input.modifiers, 'input.modifiers');
|
|
1198
|
+
return {
|
|
1199
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1200
|
+
operation: { type: 'dispatchWheel', input: {
|
|
1201
|
+
...assertBrowserInputBase(input, 'input'),
|
|
1202
|
+
x: assertFiniteNumber(input.x, 'input.x'),
|
|
1203
|
+
y: assertFiniteNumber(input.y, 'input.y'),
|
|
1204
|
+
deltaX: assertFiniteNumber(input.deltaX, 'input.deltaX'),
|
|
1205
|
+
deltaY: assertFiniteNumber(input.deltaY, 'input.deltaY'),
|
|
1206
|
+
...(modifiers === undefined ? {} : { modifiers }),
|
|
1207
|
+
} },
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
if (operation.type === 'dispatchKey') {
|
|
1211
|
+
assertKeys(operation, ['type', 'input']);
|
|
1212
|
+
const input = assertObject(operation.input, 'input');
|
|
1213
|
+
assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'type', 'key'], [
|
|
1214
|
+
'code', 'text', 'unmodifiedText', 'windowsVirtualKeyCode', 'nativeVirtualKeyCode',
|
|
1215
|
+
'autoRepeat', 'isKeypad', 'location', 'modifiers',
|
|
1216
|
+
]);
|
|
1217
|
+
if (input.type !== 'down' && input.type !== 'up') throw new Error('input.type is invalid.');
|
|
1218
|
+
const optionalString = (keyArg: 'code' | 'text' | 'unmodifiedText') => input[keyArg] === undefined
|
|
1219
|
+
? undefined
|
|
1220
|
+
: assertString(input[keyArg], `input.${keyArg}`, 0, 256);
|
|
1221
|
+
const optionalInteger = (keyArg: 'windowsVirtualKeyCode' | 'nativeVirtualKeyCode' | 'location') => {
|
|
1222
|
+
const candidate = input[keyArg];
|
|
1223
|
+
if (candidate === undefined) return undefined;
|
|
1224
|
+
if (!Number.isSafeInteger(candidate) || (candidate as number) < 0) throw new Error(`input.${keyArg} is invalid.`);
|
|
1225
|
+
return candidate as number;
|
|
1226
|
+
};
|
|
1227
|
+
const modifiers = assertBrowserModifiers(input.modifiers, 'input.modifiers');
|
|
1228
|
+
return {
|
|
1229
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1230
|
+
operation: { type: 'dispatchKey', input: {
|
|
1231
|
+
...assertBrowserInputBase(input, 'input'),
|
|
1232
|
+
type: input.type,
|
|
1233
|
+
key: assertString(input.key, 'input.key', 1, 256),
|
|
1234
|
+
...(['code', 'text', 'unmodifiedText'] as const).reduce<Record<string, string>>((result, key) => {
|
|
1235
|
+
const candidate = optionalString(key);
|
|
1236
|
+
if (candidate !== undefined) result[key] = candidate;
|
|
1237
|
+
return result;
|
|
1238
|
+
}, {}),
|
|
1239
|
+
...(['windowsVirtualKeyCode', 'nativeVirtualKeyCode', 'location'] as const)
|
|
1240
|
+
.reduce<Record<string, number>>((result, key) => {
|
|
1241
|
+
const candidate = optionalInteger(key);
|
|
1242
|
+
if (candidate !== undefined) result[key] = candidate;
|
|
1243
|
+
return result;
|
|
1244
|
+
}, {}),
|
|
1245
|
+
...(typeof input.autoRepeat === 'boolean' ? { autoRepeat: input.autoRepeat } : {}),
|
|
1246
|
+
...(typeof input.isKeypad === 'boolean' ? { isKeypad: input.isKeypad } : {}),
|
|
1247
|
+
...(modifiers === undefined ? {} : { modifiers }),
|
|
1248
|
+
} },
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
if (operation.type === 'insertText') {
|
|
1252
|
+
assertKeys(operation, ['type', 'input']);
|
|
1253
|
+
const input = assertObject(operation.input, 'input');
|
|
1254
|
+
assertKeys(input, ['tabId', 'generation', 'viewportRevision', 'text']);
|
|
1255
|
+
return {
|
|
1256
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1257
|
+
operation: { type: 'insertText', input: {
|
|
1258
|
+
...assertBrowserInputBase(input, 'input'),
|
|
1259
|
+
text: assertString(input.text, 'input.text', 1, 16 * 1024),
|
|
1260
|
+
} },
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
if (operation.type === 'navigate') {
|
|
1264
|
+
assertKeys(operation, ['type', 'url'], ['tabId', 'timeoutMs']);
|
|
1265
|
+
return {
|
|
1266
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1267
|
+
operation: {
|
|
1268
|
+
type: 'navigate',
|
|
1269
|
+
url: assertString(operation.url, 'operation.url', 1, 8192),
|
|
1270
|
+
...(operation.tabId === undefined ? {} : { tabId: assertIdentifier(operation.tabId, 'operation.tabId') }),
|
|
1271
|
+
...(operation.timeoutMs === undefined ? {} : { timeoutMs: assertFiniteNumber(operation.timeoutMs, 'operation.timeoutMs') }),
|
|
1272
|
+
},
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
if (operation.type === 'createTab') {
|
|
1276
|
+
assertKeys(operation, ['type'], ['url', 'activate']);
|
|
1277
|
+
if (operation.activate !== undefined && typeof operation.activate !== 'boolean') throw new Error('operation.activate is invalid.');
|
|
1278
|
+
return {
|
|
1279
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1280
|
+
operation: {
|
|
1281
|
+
type: 'createTab',
|
|
1282
|
+
...(operation.url === undefined ? {} : { url: assertString(operation.url, 'operation.url', 1, 8192) }),
|
|
1283
|
+
...(operation.activate === undefined ? {} : { activate: operation.activate }),
|
|
1284
|
+
},
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
if (operation.type === 'activateTab' || operation.type === 'closeTab') {
|
|
1288
|
+
assertKeys(operation, ['type', 'tabId']);
|
|
1289
|
+
return {
|
|
1290
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1291
|
+
operation: { type: operation.type, tabId: assertIdentifier(operation.tabId, 'operation.tabId') },
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
if (operation.type === 'back' || operation.type === 'forward' || operation.type === 'reload') {
|
|
1295
|
+
assertKeys(operation, ['type'], ['tabId']);
|
|
1296
|
+
return {
|
|
1297
|
+
viewId: assertIdentifier(value.viewId, 'viewId'),
|
|
1298
|
+
operation: {
|
|
1299
|
+
type: operation.type,
|
|
1300
|
+
...(operation.tabId === undefined ? {} : { tabId: assertIdentifier(operation.tabId, 'operation.tabId') }),
|
|
1301
|
+
},
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
throw new Error('operation.type is unsupported.');
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
export const assertTerminalCreateRequest = (
|
|
1308
|
+
valueArg: unknown,
|
|
1309
|
+
): interfaces.IReq_ControllerTerminalCreate['request'] => {
|
|
1310
|
+
const value = assertObject(valueArg, 'terminal create request');
|
|
1311
|
+
assertKeys(value, ['projectId'], ['title']);
|
|
1312
|
+
const title = value.title === undefined
|
|
1313
|
+
? undefined
|
|
1314
|
+
: assertSingleLineText(value.title, 'title', 256);
|
|
1315
|
+
return {
|
|
1316
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1317
|
+
...(title === undefined ? {} : { title }),
|
|
1318
|
+
};
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
export const assertTerminalRenameRequest = (
|
|
1322
|
+
valueArg: unknown,
|
|
1323
|
+
): interfaces.IReq_ControllerTerminalRename['request'] => {
|
|
1324
|
+
const value = assertObject(valueArg, 'terminal rename request');
|
|
1325
|
+
assertKeys(value, ['projectId', 'terminalId', 'title']);
|
|
1326
|
+
return {
|
|
1327
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1328
|
+
terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']),
|
|
1329
|
+
title: assertSingleLineText(value.title, 'title', 256),
|
|
1330
|
+
};
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
export const assertTerminalTargetRequest = (
|
|
1334
|
+
valueArg: unknown,
|
|
1335
|
+
nameArg: string,
|
|
1336
|
+
): { projectId: string; terminalId: interfaces.TControllerTerminalId } => {
|
|
1337
|
+
const value = assertObject(valueArg, nameArg);
|
|
1338
|
+
assertKeys(value, ['projectId', 'terminalId']);
|
|
1339
|
+
return {
|
|
1340
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1341
|
+
terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']),
|
|
1342
|
+
};
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
export const assertTerminalInputRequest = (
|
|
1346
|
+
valueArg: unknown,
|
|
1347
|
+
): interfaces.IReq_ControllerTerminalInput['request'] => {
|
|
1348
|
+
const value = assertObject(valueArg, 'terminal input request');
|
|
1349
|
+
assertKeys(value, ['projectId', 'terminalId', 'dataBase64']);
|
|
1350
|
+
const dataBase64 = assertString(value.dataBase64, 'dataBase64', 1, 16 * 1024);
|
|
1351
|
+
if (!base64Pattern.test(dataBase64)) {
|
|
1352
|
+
throw new Error('dataBase64 must be valid base64.');
|
|
1353
|
+
}
|
|
1354
|
+
return {
|
|
1355
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1356
|
+
terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']),
|
|
1357
|
+
dataBase64,
|
|
1358
|
+
};
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
export const assertTerminalResizeRequest = (
|
|
1362
|
+
valueArg: unknown,
|
|
1363
|
+
): interfaces.IReq_ControllerTerminalResize['request'] => {
|
|
1364
|
+
const value = assertObject(valueArg, 'terminal resize request');
|
|
1365
|
+
assertKeys(value, ['projectId', 'terminalId', 'rows', 'cols']);
|
|
1366
|
+
const assertDimension = (candidate: unknown, nameArg: string): number => {
|
|
1367
|
+
if (!Number.isSafeInteger(candidate) || (candidate as number) < 2 || (candidate as number) > 500) {
|
|
1368
|
+
throw new Error(`${nameArg} must be an integer between 2 and 500.`);
|
|
1369
|
+
}
|
|
1370
|
+
return candidate as number;
|
|
1371
|
+
};
|
|
1372
|
+
return {
|
|
1373
|
+
projectId: assertIdentifier(value.projectId, 'projectId'),
|
|
1374
|
+
terminalId: assertControllerRuntimeId(value.terminalId, 'terminalId', ['controller']),
|
|
1375
|
+
rows: assertDimension(value.rows, 'rows'),
|
|
1376
|
+
cols: assertDimension(value.cols, 'cols'),
|
|
1377
|
+
};
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
export const assertQuestionReplyRequest = (
|
|
1381
|
+
valueArg: unknown,
|
|
1382
|
+
): interfaces.IReq_ControllerQuestionReply['request'] => {
|
|
1383
|
+
const value = assertObject(valueArg, 'question reply request');
|
|
1384
|
+
assertKeys(value, ['projectId', 'sessionId', 'requestId', 'answers']);
|
|
1385
|
+
const projectId = assertIdentifier(value.projectId, 'projectId');
|
|
1386
|
+
const sessionId = assertControllerRuntimeId(value.sessionId, 'sessionId', ['opencode']);
|
|
1387
|
+
const requestId = assertControllerRuntimeId(value.requestId, 'requestId', ['opencode']);
|
|
1388
|
+
assertRelatedRuntimeIds(sessionId, requestId, 'requestId');
|
|
1389
|
+
if (value.answers === null) {
|
|
1390
|
+
return { projectId, sessionId, requestId, answers: null };
|
|
1391
|
+
}
|
|
1392
|
+
if (!Array.isArray(value.answers) || value.answers.length === 0 || value.answers.length > 16) {
|
|
1393
|
+
throw new Error('answers must be null or contain 1-16 per-question answer lists.');
|
|
1394
|
+
}
|
|
1395
|
+
const answers = value.answers.map((answerList) => {
|
|
1396
|
+
if (!Array.isArray(answerList) || answerList.length === 0 || answerList.length > 16) {
|
|
1397
|
+
throw new Error('Each question needs 1-16 selected answers.');
|
|
1398
|
+
}
|
|
1399
|
+
return answerList.map((answer) => {
|
|
1400
|
+
const text = assertString(answer, 'answer', 1, 4096);
|
|
1401
|
+
// eslint-disable-next-line no-control-regex
|
|
1402
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(text)) {
|
|
1403
|
+
throw new Error('answer contains unsupported control characters.');
|
|
1404
|
+
}
|
|
1405
|
+
return text;
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
return { projectId, sessionId, requestId, answers };
|
|
1409
|
+
};
|