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,1245 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
controllerAuditEventTypes,
|
|
4
|
+
type IControllerAuditEvent,
|
|
5
|
+
type IControllerAuthDocument,
|
|
6
|
+
type IControllerTempPasswordDocument,
|
|
7
|
+
type ISetupAuthorityState,
|
|
8
|
+
type IStoredPasskeyCredential,
|
|
9
|
+
type IWebAuthnCeremonyDocument,
|
|
10
|
+
} from './interfaces.auth.js';
|
|
11
|
+
import type { IControllerRuntimeConfig } from './interfaces.config.js';
|
|
12
|
+
import {
|
|
13
|
+
controllerFilesystemIdentityMaximumComponents,
|
|
14
|
+
type IControllerFilesystemIdentity,
|
|
15
|
+
type IControllerProjectDocument,
|
|
16
|
+
type IControllerResourceDocument,
|
|
17
|
+
type IControllerSessionStateDocument,
|
|
18
|
+
type IControllerSessionGroupsDocument,
|
|
19
|
+
type IControllerSettingsDocument,
|
|
20
|
+
} from './interfaces.projects.js';
|
|
21
|
+
import {
|
|
22
|
+
controllerRuntimeIdKey,
|
|
23
|
+
type TControllerModelChoice,
|
|
24
|
+
type IControllerRuntimeId,
|
|
25
|
+
type IControllerSessionGroup,
|
|
26
|
+
type TControllerSessionId,
|
|
27
|
+
type TControllerHarnessId,
|
|
28
|
+
} from '../ts_interfaces/index.js';
|
|
29
|
+
|
|
30
|
+
const base64UrlPattern = /^[A-Za-z0-9_-]+$/;
|
|
31
|
+
const credentialTransports = new Set([
|
|
32
|
+
'ble',
|
|
33
|
+
'cable',
|
|
34
|
+
'hybrid',
|
|
35
|
+
'internal',
|
|
36
|
+
'nfc',
|
|
37
|
+
'smart-card',
|
|
38
|
+
'usb',
|
|
39
|
+
]);
|
|
40
|
+
const auditEventTypes: ReadonlySet<string> = new Set(controllerAuditEventTypes);
|
|
41
|
+
const auditOutcomes = new Set(['attempted', 'succeeded', 'denied', 'failed']);
|
|
42
|
+
const auditRetentionSeconds = 90 * 24 * 60 * 60;
|
|
43
|
+
|
|
44
|
+
const isPlainObject = (valueArg: unknown): valueArg is Record<string, unknown> =>
|
|
45
|
+
typeof valueArg === 'object'
|
|
46
|
+
&& valueArg !== null
|
|
47
|
+
&& !Array.isArray(valueArg)
|
|
48
|
+
&& (
|
|
49
|
+
Object.getPrototypeOf(valueArg) === Object.prototype
|
|
50
|
+
|| Object.getPrototypeOf(valueArg) === null
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const hasExactKeys = (
|
|
54
|
+
valueArg: Record<string, unknown>,
|
|
55
|
+
requiredKeysArg: readonly string[],
|
|
56
|
+
optionalKeysArg: readonly string[] = [],
|
|
57
|
+
): boolean => {
|
|
58
|
+
const keys = Object.keys(valueArg);
|
|
59
|
+
return requiredKeysArg.every((key) => Object.hasOwn(valueArg, key))
|
|
60
|
+
&& keys.every((key) => requiredKeysArg.includes(key) || optionalKeysArg.includes(key));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isNonEmptyString = (valueArg: unknown): valueArg is string =>
|
|
64
|
+
typeof valueArg === 'string' && valueArg.length > 0;
|
|
65
|
+
|
|
66
|
+
const isRuntimeId = (
|
|
67
|
+
valueArg: unknown,
|
|
68
|
+
allowedHarnessIdsArg: readonly TControllerHarnessId[],
|
|
69
|
+
): valueArg is IControllerRuntimeId =>
|
|
70
|
+
isPlainObject(valueArg)
|
|
71
|
+
&& hasExactKeys(valueArg, ['harnessId', 'nativeId'])
|
|
72
|
+
&& allowedHarnessIdsArg.includes(valueArg.harnessId as TControllerHarnessId)
|
|
73
|
+
&& isNonEmptyString(valueArg.nativeId)
|
|
74
|
+
&& Buffer.byteLength(valueArg.nativeId, 'utf8') <= 512;
|
|
75
|
+
|
|
76
|
+
const isBase64Url = (valueArg: unknown, byteLengthArg?: number): valueArg is string => {
|
|
77
|
+
if (!isNonEmptyString(valueArg) || !base64UrlPattern.test(valueArg)) return false;
|
|
78
|
+
if (byteLengthArg === undefined) return true;
|
|
79
|
+
try {
|
|
80
|
+
return Buffer.from(valueArg, 'base64url').byteLength === byteLengthArg;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isValidDate = (valueArg: unknown): valueArg is Date =>
|
|
87
|
+
valueArg instanceof Date && Number.isFinite(valueArg.getTime());
|
|
88
|
+
|
|
89
|
+
const isValidPort = (valueArg: unknown): valueArg is number =>
|
|
90
|
+
Number.isSafeInteger(valueArg) && (valueArg as number) >= 1 && (valueArg as number) <= 65535;
|
|
91
|
+
|
|
92
|
+
const isRuntimeConfig = (valueArg: unknown): valueArg is IControllerRuntimeConfig => {
|
|
93
|
+
if (!isPlainObject(valueArg)) return false;
|
|
94
|
+
if (
|
|
95
|
+
// projectsRoot is optional only because pre-v2 documents lack it; once
|
|
96
|
+
// present it must be a canonical absolute path like the workspace.
|
|
97
|
+
!hasExactKeys(valueArg, [
|
|
98
|
+
'controllerPort',
|
|
99
|
+
'publicOrigin',
|
|
100
|
+
'rpId',
|
|
101
|
+
'workspaceDirectory',
|
|
102
|
+
'opencodePort',
|
|
103
|
+
'tlsMode',
|
|
104
|
+
], ['projectsRoot'])
|
|
105
|
+
|| (
|
|
106
|
+
Object.hasOwn(valueArg, 'projectsRoot')
|
|
107
|
+
&& (
|
|
108
|
+
!isNonEmptyString(valueArg.projectsRoot)
|
|
109
|
+
|| !plugins.path.isAbsolute(valueArg.projectsRoot)
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|| !isValidPort(valueArg.controllerPort)
|
|
113
|
+
|| !isValidPort(valueArg.opencodePort)
|
|
114
|
+
|| valueArg.controllerPort === valueArg.opencodePort
|
|
115
|
+
|| !isNonEmptyString(valueArg.publicOrigin)
|
|
116
|
+
|| !isNonEmptyString(valueArg.rpId)
|
|
117
|
+
|| !isNonEmptyString(valueArg.workspaceDirectory)
|
|
118
|
+
|| !plugins.path.isAbsolute(valueArg.workspaceDirectory)
|
|
119
|
+
|| !['local', 'proxy'].includes(valueArg.tlsMode as string)
|
|
120
|
+
) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const origin = new URL(valueArg.publicOrigin);
|
|
126
|
+
const hostname = origin.hostname.toLowerCase().replace(/\.$/, '');
|
|
127
|
+
const rpId = valueArg.rpId.toLowerCase().replace(/\.$/, '');
|
|
128
|
+
if (
|
|
129
|
+
origin.origin !== valueArg.publicOrigin
|
|
130
|
+
|| (origin.protocol !== 'http:' && origin.protocol !== 'https:')
|
|
131
|
+
|| origin.username.length > 0
|
|
132
|
+
|| origin.password.length > 0
|
|
133
|
+
|| origin.pathname !== '/'
|
|
134
|
+
|| origin.search.length > 0
|
|
135
|
+
|| origin.hash.length > 0
|
|
136
|
+
|| valueArg.rpId !== rpId
|
|
137
|
+
|| rpId.length === 0
|
|
138
|
+
|| rpId.includes('/')
|
|
139
|
+
|| rpId.includes(':')
|
|
140
|
+
|| /\s/.test(rpId)
|
|
141
|
+
|| (hostname !== rpId && !hostname.endsWith(`.${rpId}`))
|
|
142
|
+
) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (valueArg.tlsMode === 'local') {
|
|
146
|
+
return origin.protocol === 'http:' && hostname === 'localhost';
|
|
147
|
+
}
|
|
148
|
+
return origin.protocol === 'https:';
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const isSetupAuthorityState = (valueArg: unknown): valueArg is ISetupAuthorityState => {
|
|
155
|
+
if (!isPlainObject(valueArg)) return false;
|
|
156
|
+
return hasExactKeys(valueArg, ['generation', 'hash', 'expiresAt'])
|
|
157
|
+
&& isBase64Url(valueArg.generation, 32)
|
|
158
|
+
&& isBase64Url(valueArg.hash, 32)
|
|
159
|
+
&& isValidDate(valueArg.expiresAt);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const isStoredCredential = (valueArg: unknown): valueArg is IStoredPasskeyCredential => {
|
|
163
|
+
if (!isPlainObject(valueArg)) return false;
|
|
164
|
+
return hasExactKeys(valueArg, [
|
|
165
|
+
'id',
|
|
166
|
+
'publicKey',
|
|
167
|
+
'counter',
|
|
168
|
+
'transports',
|
|
169
|
+
'deviceType',
|
|
170
|
+
'backedUp',
|
|
171
|
+
'createdAt',
|
|
172
|
+
'counterUpdateId',
|
|
173
|
+
])
|
|
174
|
+
&& isBase64Url(valueArg.id)
|
|
175
|
+
&& isBase64Url(valueArg.publicKey)
|
|
176
|
+
&& Number.isSafeInteger(valueArg.counter)
|
|
177
|
+
&& (valueArg.counter as number) >= 0
|
|
178
|
+
&& Array.isArray(valueArg.transports)
|
|
179
|
+
&& valueArg.transports.every((entry) => credentialTransports.has(entry as string))
|
|
180
|
+
&& (valueArg.deviceType === 'singleDevice' || valueArg.deviceType === 'multiDevice')
|
|
181
|
+
&& typeof valueArg.backedUp === 'boolean'
|
|
182
|
+
&& isValidDate(valueArg.createdAt)
|
|
183
|
+
&& isBase64Url(valueArg.counterUpdateId, 32);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const assertControllerAuthDocument = (
|
|
187
|
+
valueArg: unknown,
|
|
188
|
+
): asserts valueArg is IControllerAuthDocument => {
|
|
189
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller authentication document.');
|
|
190
|
+
if (
|
|
191
|
+
!hasExactKeys(valueArg, [
|
|
192
|
+
'id',
|
|
193
|
+
'config',
|
|
194
|
+
'authState',
|
|
195
|
+
'setup',
|
|
196
|
+
'webauthnUserId',
|
|
197
|
+
'credentials',
|
|
198
|
+
])
|
|
199
|
+
|| !isRuntimeConfig(valueArg.config)
|
|
200
|
+
|| valueArg.id !== `controller:${valueArg.config.controllerPort}`
|
|
201
|
+
|| !isBase64Url(valueArg.webauthnUserId, 32)
|
|
202
|
+
|| !Array.isArray(valueArg.credentials)
|
|
203
|
+
|| !valueArg.credentials.every(isStoredCredential)
|
|
204
|
+
|| new Set(valueArg.credentials.map((credential) => credential.id)).size
|
|
205
|
+
!== valueArg.credentials.length
|
|
206
|
+
) {
|
|
207
|
+
throw new Error('Invalid controller authentication document.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (valueArg.credentials.length === 0) {
|
|
211
|
+
if (
|
|
212
|
+
valueArg.authState !== 'setupRequired'
|
|
213
|
+
|| (valueArg.setup !== null && !isSetupAuthorityState(valueArg.setup))
|
|
214
|
+
) {
|
|
215
|
+
throw new Error('Invalid controller setup state.');
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (valueArg.authState !== 'ready' || valueArg.setup !== null) {
|
|
221
|
+
throw new Error('Invalid enrolled controller authentication state.');
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const assertCeremonyStateFields = (valueArg: Record<string, unknown>): boolean => {
|
|
226
|
+
if (valueArg.state === 'pending') {
|
|
227
|
+
return !Object.hasOwn(valueArg, 'consumedAt') && !Object.hasOwn(valueArg, 'consumedNonce');
|
|
228
|
+
}
|
|
229
|
+
return valueArg.state === 'consumed'
|
|
230
|
+
&& isValidDate(valueArg.consumedAt)
|
|
231
|
+
&& isBase64Url(valueArg.consumedNonce, 32);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const assertWebAuthnCeremonyDocument = (
|
|
235
|
+
valueArg: unknown,
|
|
236
|
+
): asserts valueArg is IWebAuthnCeremonyDocument => {
|
|
237
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid WebAuthn ceremony document.');
|
|
238
|
+
const optionalKeys = valueArg.kind === 'setup'
|
|
239
|
+
? ['consumedAt', 'consumedNonce']
|
|
240
|
+
: ['consumedAt', 'consumedNonce'];
|
|
241
|
+
const requiredKeys = [
|
|
242
|
+
'id',
|
|
243
|
+
'kind',
|
|
244
|
+
'state',
|
|
245
|
+
'peerId',
|
|
246
|
+
'challenge',
|
|
247
|
+
'origin',
|
|
248
|
+
'rpId',
|
|
249
|
+
'userId',
|
|
250
|
+
'createdAt',
|
|
251
|
+
'expiresAt',
|
|
252
|
+
...(valueArg.kind === 'setup' ? ['setupGeneration', 'setupHash'] : []),
|
|
253
|
+
];
|
|
254
|
+
if (
|
|
255
|
+
!hasExactKeys(valueArg, requiredKeys, optionalKeys)
|
|
256
|
+
|| !isBase64Url(valueArg.id, 32)
|
|
257
|
+
|| (valueArg.kind !== 'setup' && valueArg.kind !== 'authentication')
|
|
258
|
+
|| !isNonEmptyString(valueArg.peerId)
|
|
259
|
+
|| valueArg.peerId.length > 512
|
|
260
|
+
|| !isBase64Url(valueArg.challenge)
|
|
261
|
+
|| !isNonEmptyString(valueArg.origin)
|
|
262
|
+
|| !isNonEmptyString(valueArg.rpId)
|
|
263
|
+
|| !isBase64Url(valueArg.userId, 32)
|
|
264
|
+
|| !isValidDate(valueArg.createdAt)
|
|
265
|
+
|| !isValidDate(valueArg.expiresAt)
|
|
266
|
+
|| valueArg.expiresAt.getTime() <= valueArg.createdAt.getTime()
|
|
267
|
+
|| !assertCeremonyStateFields(valueArg)
|
|
268
|
+
) {
|
|
269
|
+
throw new Error('Invalid WebAuthn ceremony document.');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let origin: URL;
|
|
273
|
+
try {
|
|
274
|
+
origin = new URL(valueArg.origin);
|
|
275
|
+
} catch {
|
|
276
|
+
throw new Error('Invalid WebAuthn ceremony origin.');
|
|
277
|
+
}
|
|
278
|
+
const hostname = origin.hostname.toLowerCase().replace(/\.$/, '');
|
|
279
|
+
if (
|
|
280
|
+
origin.origin !== valueArg.origin
|
|
281
|
+
|| (origin.protocol !== 'http:' && origin.protocol !== 'https:')
|
|
282
|
+
|| (hostname !== valueArg.rpId && !hostname.endsWith(`.${valueArg.rpId}`))
|
|
283
|
+
) {
|
|
284
|
+
throw new Error('Invalid WebAuthn ceremony origin or RP ID.');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
valueArg.kind === 'setup'
|
|
289
|
+
&& (!isBase64Url(valueArg.setupGeneration, 32) || !isBase64Url(valueArg.setupHash, 32))
|
|
290
|
+
) {
|
|
291
|
+
throw new Error('Invalid WebAuthn setup ceremony authority.');
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export const assertControllerAuditEventDocument = (
|
|
296
|
+
valueArg: unknown,
|
|
297
|
+
): asserts valueArg is IControllerAuditEvent => {
|
|
298
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller audit event.');
|
|
299
|
+
if (
|
|
300
|
+
!hasExactKeys(
|
|
301
|
+
valueArg,
|
|
302
|
+
['id', 'controllerId', 'timestamp', 'type', 'outcome'],
|
|
303
|
+
['operationId', 'peerId', 'credentialId', 'sessionId', 'requestId'],
|
|
304
|
+
)
|
|
305
|
+
|| !isBase64Url(valueArg.id, 32)
|
|
306
|
+
|| !isNonEmptyString(valueArg.controllerId)
|
|
307
|
+
|| !/^controller:[1-9][0-9]{0,4}$/.test(valueArg.controllerId)
|
|
308
|
+
|| Number(valueArg.controllerId.slice('controller:'.length)) > 65535
|
|
309
|
+
|| !isValidDate(valueArg.timestamp)
|
|
310
|
+
|| !auditEventTypes.has(valueArg.type as string)
|
|
311
|
+
|| !auditOutcomes.has(valueArg.outcome as string)
|
|
312
|
+
|| (
|
|
313
|
+
Object.hasOwn(valueArg, 'operationId')
|
|
314
|
+
&& !isBase64Url(valueArg.operationId, valueArg.type === 'resource.browser.operation' ? 18 : 24)
|
|
315
|
+
)
|
|
316
|
+
) {
|
|
317
|
+
throw new Error('Invalid controller audit event.');
|
|
318
|
+
}
|
|
319
|
+
for (const optionalKey of [
|
|
320
|
+
'peerId',
|
|
321
|
+
'credentialId',
|
|
322
|
+
'sessionId',
|
|
323
|
+
'requestId',
|
|
324
|
+
] as const) {
|
|
325
|
+
if (
|
|
326
|
+
Object.hasOwn(valueArg, optionalKey)
|
|
327
|
+
&& !(
|
|
328
|
+
// Protocol v5 audit rows are retained until TTL expiry; all new writes
|
|
329
|
+
// are qualified by IRecordAuditEventInput.
|
|
330
|
+
(isNonEmptyString(valueArg[optionalKey]) && valueArg[optionalKey].length <= 512)
|
|
331
|
+
|| isRuntimeId(valueArg[optionalKey], ['opencode', 'flex', 'controller'])
|
|
332
|
+
)
|
|
333
|
+
) {
|
|
334
|
+
throw new Error('Invalid controller audit event identifier.');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// A project name is a display path: relative to the projects root for
|
|
340
|
+
// directories inside it, else the absolute directory path. Absolute names may
|
|
341
|
+
// contain hidden (dot-prefixed) segments because they were typed explicitly;
|
|
342
|
+
// relative names stay restricted to visible directory names.
|
|
343
|
+
export const isProjectRelativePathName = (valueArg: unknown): valueArg is string => {
|
|
344
|
+
if (typeof valueArg !== 'string' || valueArg.length < 1 || valueArg.length > 4096) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
const absolute = valueArg.startsWith('/');
|
|
348
|
+
const segments = absolute ? valueArg.slice(1).split('/') : valueArg.split('/');
|
|
349
|
+
return segments.every((segment) => (
|
|
350
|
+
segment.length > 0
|
|
351
|
+
&& segment.length <= 255
|
|
352
|
+
&& segment !== '.'
|
|
353
|
+
&& segment !== '..'
|
|
354
|
+
&& (absolute || !segment.startsWith('.'))
|
|
355
|
+
// eslint-disable-next-line no-control-regex
|
|
356
|
+
&& !/[\x00-\x1f\x7f\\]/.test(segment)
|
|
357
|
+
));
|
|
358
|
+
};
|
|
359
|
+
const controllerIdPattern = /^controller:[1-9][0-9]{0,4}$/;
|
|
360
|
+
|
|
361
|
+
const isControllerId = (valueArg: unknown): valueArg is string =>
|
|
362
|
+
isNonEmptyString(valueArg)
|
|
363
|
+
&& controllerIdPattern.test(valueArg)
|
|
364
|
+
&& Number(valueArg.slice('controller:'.length)) <= 65535;
|
|
365
|
+
|
|
366
|
+
const isFilesystemIdentityPart = (valueArg: unknown): valueArg is string => (
|
|
367
|
+
typeof valueArg === 'string'
|
|
368
|
+
&& /^(?:0|[1-9][0-9]{0,31})$/u.test(valueArg)
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const isFilesystemIdentity = (
|
|
372
|
+
valueArg: unknown,
|
|
373
|
+
pathArg: string,
|
|
374
|
+
targetTypeArg: 'directory' | 'file',
|
|
375
|
+
): valueArg is IControllerFilesystemIdentity => {
|
|
376
|
+
if (!isPlainObject(valueArg) || !hasExactKeys(valueArg, ['ancestry'])) return false;
|
|
377
|
+
if (
|
|
378
|
+
!Array.isArray(valueArg.ancestry)
|
|
379
|
+
|| valueArg.ancestry.length < 1
|
|
380
|
+
|| valueArg.ancestry.length > controllerFilesystemIdentityMaximumComponents
|
|
381
|
+
) return false;
|
|
382
|
+
const root = plugins.path.parse(pathArg).root;
|
|
383
|
+
const relative = pathArg.slice(root.length);
|
|
384
|
+
const expectedLength = 1 + relative.split(plugins.path.sep).filter(Boolean).length;
|
|
385
|
+
const ancestry = valueArg.ancestry;
|
|
386
|
+
if (ancestry.length !== expectedLength) return false;
|
|
387
|
+
return ancestry.every((component, index) => (
|
|
388
|
+
isPlainObject(component)
|
|
389
|
+
&& hasExactKeys(component, ['device', 'inode', 'type'])
|
|
390
|
+
&& isFilesystemIdentityPart(component.device)
|
|
391
|
+
&& isFilesystemIdentityPart(component.inode)
|
|
392
|
+
&& component.type === (index === ancestry.length - 1 ? targetTypeArg : 'directory')
|
|
393
|
+
));
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export function assertControllerProjectDocument(
|
|
397
|
+
valueArg: unknown,
|
|
398
|
+
): asserts valueArg is IControllerProjectDocument {
|
|
399
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller project document.');
|
|
400
|
+
if (
|
|
401
|
+
!hasExactKeys(
|
|
402
|
+
valueArg,
|
|
403
|
+
['id', 'controllerId', 'name', 'directory', 'directoryIdentityState', 'createdAt'],
|
|
404
|
+
['directoryIdentity', 'removalStartedAt', 'removedAt'],
|
|
405
|
+
)
|
|
406
|
+
|| !isBase64Url(valueArg.id, 16)
|
|
407
|
+
|| !isControllerId(valueArg.controllerId)
|
|
408
|
+
|| !isProjectRelativePathName(valueArg.name)
|
|
409
|
+
|| !isNonEmptyString(valueArg.directory)
|
|
410
|
+
|| !plugins.path.isAbsolute(valueArg.directory)
|
|
411
|
+
|| Buffer.byteLength(valueArg.directory, 'utf8') > 4096
|
|
412
|
+
|| (
|
|
413
|
+
valueArg.directoryIdentityState !== 'bound'
|
|
414
|
+
&& valueArg.directoryIdentityState !== 'unbound'
|
|
415
|
+
)
|
|
416
|
+
|| (
|
|
417
|
+
valueArg.directoryIdentityState === 'bound'
|
|
418
|
+
&& !isFilesystemIdentity(valueArg.directoryIdentity, valueArg.directory, 'directory')
|
|
419
|
+
)
|
|
420
|
+
|| (
|
|
421
|
+
valueArg.directoryIdentityState === 'unbound'
|
|
422
|
+
&& Object.hasOwn(valueArg, 'directoryIdentity')
|
|
423
|
+
)
|
|
424
|
+
|| !isValidDate(valueArg.createdAt)
|
|
425
|
+
|| (Object.hasOwn(valueArg, 'removalStartedAt') && !isValidDate(valueArg.removalStartedAt))
|
|
426
|
+
|| (Object.hasOwn(valueArg, 'removedAt') && !isValidDate(valueArg.removedAt))
|
|
427
|
+
|| (
|
|
428
|
+
Object.hasOwn(valueArg, 'removalStartedAt')
|
|
429
|
+
&& Object.hasOwn(valueArg, 'removedAt')
|
|
430
|
+
)
|
|
431
|
+
) {
|
|
432
|
+
throw new Error('Invalid controller project document.');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const isModelChoice = (valueArg: unknown): valueArg is TControllerModelChoice => {
|
|
437
|
+
if (!isPlainObject(valueArg)) return false;
|
|
438
|
+
const hasValidVariant = !Object.hasOwn(valueArg, 'variant')
|
|
439
|
+
|| (
|
|
440
|
+
isNonEmptyString(valueArg.variant)
|
|
441
|
+
&& valueArg.variant.length <= 64
|
|
442
|
+
&& /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(valueArg.variant)
|
|
443
|
+
);
|
|
444
|
+
if (
|
|
445
|
+
!hasValidVariant
|
|
446
|
+
|| !isNonEmptyString(valueArg.providerID)
|
|
447
|
+
|| valueArg.providerID.length > 256
|
|
448
|
+
|| !isNonEmptyString(valueArg.modelID)
|
|
449
|
+
|| valueArg.modelID.length > 256
|
|
450
|
+
) {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
if (valueArg.harnessId === 'opencode') {
|
|
454
|
+
return hasExactKeys(valueArg, ['harnessId', 'providerID', 'modelID'], ['variant']);
|
|
455
|
+
}
|
|
456
|
+
return valueArg.harnessId === 'flex'
|
|
457
|
+
&& hasExactKeys(
|
|
458
|
+
valueArg,
|
|
459
|
+
['harnessId', 'providerConnectionId', 'providerID', 'modelID'],
|
|
460
|
+
['variant'],
|
|
461
|
+
)
|
|
462
|
+
&& isNonEmptyString(valueArg.providerConnectionId)
|
|
463
|
+
&& Buffer.byteLength(valueArg.providerConnectionId, 'utf8') <= 512;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
export function assertControllerSettingsDocument(
|
|
467
|
+
valueArg: unknown,
|
|
468
|
+
): asserts valueArg is IControllerSettingsDocument {
|
|
469
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller settings document.');
|
|
470
|
+
if (
|
|
471
|
+
!hasExactKeys(
|
|
472
|
+
valueArg,
|
|
473
|
+
['id', 'controllerId', 'defaultModels'],
|
|
474
|
+
['autoAcceptPermissions', 'selectedOpenCodeProviderConnectionId'],
|
|
475
|
+
)
|
|
476
|
+
|| !isControllerId(valueArg.controllerId)
|
|
477
|
+
|| valueArg.id !== `${valueArg.controllerId}:settings`
|
|
478
|
+
|| !Array.isArray(valueArg.defaultModels)
|
|
479
|
+
|| valueArg.defaultModels.length > 2
|
|
480
|
+
|| !valueArg.defaultModels.every(isModelChoice)
|
|
481
|
+
|| new Set(valueArg.defaultModels.map(
|
|
482
|
+
(model) => (model as TControllerModelChoice).harnessId,
|
|
483
|
+
)).size
|
|
484
|
+
!== valueArg.defaultModels.length
|
|
485
|
+
// Disabled is represented by an absent key, never by a stored false.
|
|
486
|
+
|| (Object.hasOwn(valueArg, 'autoAcceptPermissions') && valueArg.autoAcceptPermissions !== true)
|
|
487
|
+
|| (valueArg.selectedOpenCodeProviderConnectionId !== undefined && (
|
|
488
|
+
!isNonEmptyString(valueArg.selectedOpenCodeProviderConnectionId)
|
|
489
|
+
|| Buffer.byteLength(valueArg.selectedOpenCodeProviderConnectionId, 'utf8') > 512
|
|
490
|
+
))
|
|
491
|
+
) {
|
|
492
|
+
throw new Error('Invalid controller settings document.');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_auth' })
|
|
497
|
+
@plugins.smartdata.exactPersistence({
|
|
498
|
+
assertDocument: assertControllerAuthDocument,
|
|
499
|
+
})
|
|
500
|
+
export class ControllerAuthModel
|
|
501
|
+
extends plugins.smartdata.SmartDataDbDoc<ControllerAuthModel, IControllerAuthDocument> {
|
|
502
|
+
declare static exact: plugins.smartdata.TExact<ControllerAuthModel>;
|
|
503
|
+
|
|
504
|
+
@plugins.smartdata.unI()
|
|
505
|
+
public id!: string;
|
|
506
|
+
|
|
507
|
+
@plugins.smartdata.svDb()
|
|
508
|
+
public config!: IControllerRuntimeConfig;
|
|
509
|
+
|
|
510
|
+
@plugins.smartdata.svDb()
|
|
511
|
+
public authState!: IControllerAuthDocument['authState'];
|
|
512
|
+
|
|
513
|
+
@plugins.smartdata.svDb()
|
|
514
|
+
public setup!: ISetupAuthorityState | null;
|
|
515
|
+
|
|
516
|
+
@plugins.smartdata.svDb()
|
|
517
|
+
public webauthnUserId!: string;
|
|
518
|
+
|
|
519
|
+
@plugins.smartdata.svDb()
|
|
520
|
+
public credentials!: IStoredPasskeyCredential[];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_webauthn_ceremonies' })
|
|
524
|
+
@plugins.smartdata.exactPersistence({
|
|
525
|
+
assertDocument: assertWebAuthnCeremonyDocument,
|
|
526
|
+
})
|
|
527
|
+
export class WebAuthnCeremonyModel
|
|
528
|
+
extends plugins.smartdata.SmartDataDbDoc<WebAuthnCeremonyModel, IWebAuthnCeremonyDocument> {
|
|
529
|
+
declare static exact: plugins.smartdata.TExact<WebAuthnCeremonyModel>;
|
|
530
|
+
|
|
531
|
+
@plugins.smartdata.unI()
|
|
532
|
+
public id!: string;
|
|
533
|
+
|
|
534
|
+
@plugins.smartdata.svDb()
|
|
535
|
+
public kind!: IWebAuthnCeremonyDocument['kind'];
|
|
536
|
+
|
|
537
|
+
@plugins.smartdata.svDb()
|
|
538
|
+
public state!: IWebAuthnCeremonyDocument['state'];
|
|
539
|
+
|
|
540
|
+
@plugins.smartdata.svDb()
|
|
541
|
+
public peerId!: string;
|
|
542
|
+
|
|
543
|
+
@plugins.smartdata.svDb()
|
|
544
|
+
public challenge!: string;
|
|
545
|
+
|
|
546
|
+
@plugins.smartdata.svDb()
|
|
547
|
+
public origin!: string;
|
|
548
|
+
|
|
549
|
+
@plugins.smartdata.svDb()
|
|
550
|
+
public rpId!: string;
|
|
551
|
+
|
|
552
|
+
@plugins.smartdata.svDb()
|
|
553
|
+
public userId!: string;
|
|
554
|
+
|
|
555
|
+
@plugins.smartdata.svDb()
|
|
556
|
+
public setupGeneration?: string;
|
|
557
|
+
|
|
558
|
+
@plugins.smartdata.svDb()
|
|
559
|
+
public setupHash?: string;
|
|
560
|
+
|
|
561
|
+
@plugins.smartdata.svDb()
|
|
562
|
+
public createdAt!: Date;
|
|
563
|
+
|
|
564
|
+
@plugins.smartdata.index({ expireAfterSeconds: 0 })
|
|
565
|
+
public expiresAt!: Date;
|
|
566
|
+
|
|
567
|
+
@plugins.smartdata.svDb()
|
|
568
|
+
public consumedAt?: Date;
|
|
569
|
+
|
|
570
|
+
@plugins.smartdata.svDb()
|
|
571
|
+
public consumedNonce?: string;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_projects' })
|
|
575
|
+
@plugins.smartdata.exactPersistence({
|
|
576
|
+
assertDocument: assertControllerProjectDocument,
|
|
577
|
+
})
|
|
578
|
+
export class ControllerProjectModel
|
|
579
|
+
extends plugins.smartdata.SmartDataDbDoc<ControllerProjectModel, IControllerProjectDocument> {
|
|
580
|
+
declare static exact: plugins.smartdata.TExact<ControllerProjectModel>;
|
|
581
|
+
|
|
582
|
+
@plugins.smartdata.unI()
|
|
583
|
+
public id!: string;
|
|
584
|
+
|
|
585
|
+
@plugins.smartdata.svDb()
|
|
586
|
+
public controllerId!: string;
|
|
587
|
+
|
|
588
|
+
// `name` shadows a base-class property; the initializer makes that explicit.
|
|
589
|
+
@plugins.smartdata.svDb()
|
|
590
|
+
public name: string = '';
|
|
591
|
+
|
|
592
|
+
@plugins.smartdata.svDb()
|
|
593
|
+
public directory!: string;
|
|
594
|
+
|
|
595
|
+
@plugins.smartdata.svDb()
|
|
596
|
+
public directoryIdentityState!: IControllerProjectDocument['directoryIdentityState'];
|
|
597
|
+
|
|
598
|
+
@plugins.smartdata.svDb()
|
|
599
|
+
public directoryIdentity?: IControllerProjectDocument['directoryIdentity'];
|
|
600
|
+
|
|
601
|
+
@plugins.smartdata.svDb()
|
|
602
|
+
public createdAt!: Date;
|
|
603
|
+
|
|
604
|
+
@plugins.smartdata.svDb()
|
|
605
|
+
public removalStartedAt?: Date;
|
|
606
|
+
|
|
607
|
+
// Deregistered projects expire from the collection after 90 days; the
|
|
608
|
+
// directory on disk is never touched.
|
|
609
|
+
@plugins.smartdata.index({ expireAfterSeconds: 90 * 24 * 60 * 60 })
|
|
610
|
+
public removedAt?: Date;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_settings' })
|
|
614
|
+
@plugins.smartdata.exactPersistence({
|
|
615
|
+
assertDocument: assertControllerSettingsDocument,
|
|
616
|
+
})
|
|
617
|
+
export class ControllerSettingsModel
|
|
618
|
+
extends plugins.smartdata.SmartDataDbDoc<ControllerSettingsModel, IControllerSettingsDocument> {
|
|
619
|
+
declare static exact: plugins.smartdata.TExact<ControllerSettingsModel>;
|
|
620
|
+
|
|
621
|
+
@plugins.smartdata.unI()
|
|
622
|
+
public id!: string;
|
|
623
|
+
|
|
624
|
+
@plugins.smartdata.svDb()
|
|
625
|
+
public controllerId!: string;
|
|
626
|
+
|
|
627
|
+
@plugins.smartdata.svDb()
|
|
628
|
+
public defaultModels!: TControllerModelChoice[];
|
|
629
|
+
|
|
630
|
+
@plugins.smartdata.svDb()
|
|
631
|
+
public autoAcceptPermissions?: true;
|
|
632
|
+
|
|
633
|
+
@plugins.smartdata.svDb()
|
|
634
|
+
public selectedOpenCodeProviderConnectionId?: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const maxSessionGroups = 64;
|
|
638
|
+
const maxSessionsPerGroup = 512;
|
|
639
|
+
const maxUngroupedSessions = 2048;
|
|
640
|
+
const groupIdentifierPattern = /^[A-Za-z0-9_-]{1,128}$/;
|
|
641
|
+
|
|
642
|
+
export const isSessionGroupList = (
|
|
643
|
+
valueArg: unknown,
|
|
644
|
+
): valueArg is IControllerSessionGroupsDocument['groups'] => {
|
|
645
|
+
if (!Array.isArray(valueArg) || valueArg.length > maxSessionGroups) return false;
|
|
646
|
+
const groupIds = new Set<string>();
|
|
647
|
+
const memberIds = new Set<string>();
|
|
648
|
+
for (const group of valueArg) {
|
|
649
|
+
if (
|
|
650
|
+
!isPlainObject(group)
|
|
651
|
+
|| !hasExactKeys(group, ['id', 'name', 'sessionIds'])
|
|
652
|
+
|| !isNonEmptyString(group.id)
|
|
653
|
+
|| !groupIdentifierPattern.test(group.id)
|
|
654
|
+
|| groupIds.has(group.id)
|
|
655
|
+
|| !isNonEmptyString(group.name)
|
|
656
|
+
|| group.name.length > 128
|
|
657
|
+
// eslint-disable-next-line no-control-regex
|
|
658
|
+
|| /[\x00-\x1f\x7f]/.test(group.name)
|
|
659
|
+
|| !Array.isArray(group.sessionIds)
|
|
660
|
+
|| group.sessionIds.length > maxSessionsPerGroup
|
|
661
|
+
) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
groupIds.add(group.id);
|
|
665
|
+
for (const sessionId of group.sessionIds) {
|
|
666
|
+
if (
|
|
667
|
+
!isRuntimeId(sessionId, ['opencode', 'flex'])
|
|
668
|
+
// A session belongs to at most one group; duplicates are invalid.
|
|
669
|
+
|| memberIds.has(controllerRuntimeIdKey(sessionId))
|
|
670
|
+
) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
memberIds.add(controllerRuntimeIdKey(sessionId));
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return true;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
export const isUngroupedSessionList = (
|
|
680
|
+
valueArg: unknown,
|
|
681
|
+
groupedSessionIdsArg: ReadonlySet<string> = new Set(),
|
|
682
|
+
): valueArg is TControllerSessionId[] => {
|
|
683
|
+
if (!Array.isArray(valueArg) || valueArg.length > maxUngroupedSessions) return false;
|
|
684
|
+
const sessionIds = new Set<string>();
|
|
685
|
+
for (const sessionId of valueArg) {
|
|
686
|
+
const key = isRuntimeId(sessionId, ['opencode', 'flex'])
|
|
687
|
+
? controllerRuntimeIdKey(sessionId)
|
|
688
|
+
: undefined;
|
|
689
|
+
if (
|
|
690
|
+
key === undefined
|
|
691
|
+
|| groupedSessionIdsArg.has(key)
|
|
692
|
+
|| sessionIds.has(key)
|
|
693
|
+
) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
sessionIds.add(key);
|
|
697
|
+
}
|
|
698
|
+
return true;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
export function assertControllerSessionGroupsDocument(
|
|
702
|
+
valueArg: unknown,
|
|
703
|
+
): asserts valueArg is IControllerSessionGroupsDocument {
|
|
704
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller session groups document.');
|
|
705
|
+
if (
|
|
706
|
+
!hasExactKeys(
|
|
707
|
+
valueArg,
|
|
708
|
+
['id', 'controllerId', 'projectId', 'groups'],
|
|
709
|
+
['ungroupedSessionIds', 'revision'],
|
|
710
|
+
)
|
|
711
|
+
|| !isControllerId(valueArg.controllerId)
|
|
712
|
+
|| !isNonEmptyString(valueArg.projectId)
|
|
713
|
+
|| !groupIdentifierPattern.test(valueArg.projectId)
|
|
714
|
+
|| valueArg.id !== `${valueArg.controllerId}:groups:${valueArg.projectId}`
|
|
715
|
+
|| !isSessionGroupList(valueArg.groups)
|
|
716
|
+
) {
|
|
717
|
+
throw new Error('Invalid controller session groups document.');
|
|
718
|
+
}
|
|
719
|
+
const groupedSessionIds = new Set(
|
|
720
|
+
valueArg.groups.flatMap((group) => group.sessionIds.map(controllerRuntimeIdKey)),
|
|
721
|
+
);
|
|
722
|
+
const hasUngroupedSessionIds = valueArg.ungroupedSessionIds !== undefined;
|
|
723
|
+
const hasRevision = valueArg.revision !== undefined;
|
|
724
|
+
if (
|
|
725
|
+
hasUngroupedSessionIds !== hasRevision
|
|
726
|
+
||
|
|
727
|
+
(
|
|
728
|
+
hasRevision
|
|
729
|
+
&& (!Number.isSafeInteger(valueArg.revision) || (valueArg.revision as number) < 0)
|
|
730
|
+
)
|
|
731
|
+
||
|
|
732
|
+
hasUngroupedSessionIds
|
|
733
|
+
&& !isUngroupedSessionList(valueArg.ungroupedSessionIds, groupedSessionIds)
|
|
734
|
+
) {
|
|
735
|
+
throw new Error('Invalid controller session groups document.');
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_session_groups' })
|
|
740
|
+
@plugins.smartdata.exactPersistence({
|
|
741
|
+
assertDocument: assertControllerSessionGroupsDocument,
|
|
742
|
+
})
|
|
743
|
+
export class ControllerSessionGroupsModel
|
|
744
|
+
extends plugins.smartdata.SmartDataDbDoc<ControllerSessionGroupsModel, IControllerSessionGroupsDocument> {
|
|
745
|
+
declare static exact: plugins.smartdata.TExact<ControllerSessionGroupsModel>;
|
|
746
|
+
|
|
747
|
+
@plugins.smartdata.unI()
|
|
748
|
+
public id!: string;
|
|
749
|
+
|
|
750
|
+
@plugins.smartdata.svDb()
|
|
751
|
+
public controllerId!: string;
|
|
752
|
+
|
|
753
|
+
@plugins.smartdata.svDb()
|
|
754
|
+
public projectId!: string;
|
|
755
|
+
|
|
756
|
+
@plugins.smartdata.svDb()
|
|
757
|
+
public groups!: IControllerSessionGroupsDocument['groups'];
|
|
758
|
+
|
|
759
|
+
@plugins.smartdata.svDb()
|
|
760
|
+
public ungroupedSessionIds?: IControllerSessionGroupsDocument['ungroupedSessionIds'];
|
|
761
|
+
|
|
762
|
+
@plugins.smartdata.svDb()
|
|
763
|
+
public revision?: number;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const resourceIdPattern = /^[A-Za-z0-9_-]{22}$/;
|
|
767
|
+
const resourceAuthorityPattern = /^[A-Za-z0-9_-]{43}$/;
|
|
768
|
+
|
|
769
|
+
export function assertControllerResourceDocument(
|
|
770
|
+
valueArg: unknown,
|
|
771
|
+
): asserts valueArg is IControllerResourceDocument {
|
|
772
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller resource document.');
|
|
773
|
+
if (
|
|
774
|
+
!hasExactKeys(
|
|
775
|
+
valueArg,
|
|
776
|
+
[
|
|
777
|
+
'id',
|
|
778
|
+
'controllerId',
|
|
779
|
+
'projectId',
|
|
780
|
+
'kind',
|
|
781
|
+
'title',
|
|
782
|
+
'attachmentAuthorityId',
|
|
783
|
+
'attachmentRevision',
|
|
784
|
+
'sessionId',
|
|
785
|
+
'lifecycle',
|
|
786
|
+
'createdAt',
|
|
787
|
+
'updatedAt',
|
|
788
|
+
'updateId',
|
|
789
|
+
],
|
|
790
|
+
['pendingAttachment', 'terminal', 'retiringAt', 'retiredAt'],
|
|
791
|
+
)
|
|
792
|
+
|| !resourceIdPattern.test(valueArg.id as string)
|
|
793
|
+
|| !isControllerId(valueArg.controllerId)
|
|
794
|
+
|| !isNonEmptyString(valueArg.projectId)
|
|
795
|
+
|| !groupIdentifierPattern.test(valueArg.projectId)
|
|
796
|
+
|| (valueArg.kind !== 'terminal' && valueArg.kind !== 'browser')
|
|
797
|
+
|| !isNonEmptyString(valueArg.title)
|
|
798
|
+
|| Buffer.byteLength(valueArg.title, 'utf8') > 256
|
|
799
|
+
// eslint-disable-next-line no-control-regex
|
|
800
|
+
|| /[\x00-\x1f\x7f]/.test(valueArg.title as string)
|
|
801
|
+
|| !resourceAuthorityPattern.test(valueArg.attachmentAuthorityId as string)
|
|
802
|
+
|| !Number.isSafeInteger(valueArg.attachmentRevision)
|
|
803
|
+
|| (valueArg.attachmentRevision as number) < 0
|
|
804
|
+
|| (valueArg.sessionId !== null && !isRuntimeId(valueArg.sessionId, ['opencode', 'flex']))
|
|
805
|
+
|| !['active', 'retiring', 'retired'].includes(valueArg.lifecycle as string)
|
|
806
|
+
|| !isValidDate(valueArg.createdAt)
|
|
807
|
+
|| !isValidDate(valueArg.updatedAt)
|
|
808
|
+
|| valueArg.updatedAt.getTime() < valueArg.createdAt.getTime()
|
|
809
|
+
|| !isBase64Url(valueArg.updateId, 32)
|
|
810
|
+
) {
|
|
811
|
+
throw new Error('Invalid controller resource document.');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (valueArg.pendingAttachment !== undefined) {
|
|
815
|
+
const pending = valueArg.pendingAttachment;
|
|
816
|
+
if (
|
|
817
|
+
!isPlainObject(pending)
|
|
818
|
+
|| !hasExactKeys(pending, [
|
|
819
|
+
'operationId',
|
|
820
|
+
'attachmentRevision',
|
|
821
|
+
'sessionId',
|
|
822
|
+
'requestedAt',
|
|
823
|
+
])
|
|
824
|
+
|| !isBase64Url(pending.operationId, 16)
|
|
825
|
+
|| pending.attachmentRevision !== (valueArg.attachmentRevision as number) + 1
|
|
826
|
+
|| (pending.sessionId !== null && !isRuntimeId(pending.sessionId, ['opencode', 'flex']))
|
|
827
|
+
|| !isValidDate(pending.requestedAt)
|
|
828
|
+
|| valueArg.lifecycle !== 'active'
|
|
829
|
+
) {
|
|
830
|
+
throw new Error('Invalid pending controller resource attachment.');
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (valueArg.kind === 'terminal') {
|
|
835
|
+
if (!isPlainObject(valueArg.terminal) || !hasExactKeys(
|
|
836
|
+
valueArg.terminal,
|
|
837
|
+
['command', 'args', 'cwd'],
|
|
838
|
+
['stoppedAt', 'lastExitCode'],
|
|
839
|
+
) || !isNonEmptyString(valueArg.terminal.command)
|
|
840
|
+
|| !plugins.path.isAbsolute(valueArg.terminal.command)
|
|
841
|
+
|| !Array.isArray(valueArg.terminal.args)
|
|
842
|
+
|| valueArg.terminal.args.length > 32
|
|
843
|
+
|| valueArg.terminal.args.some((arg) => (
|
|
844
|
+
typeof arg !== 'string' || Buffer.byteLength(arg, 'utf8') > 4096
|
|
845
|
+
))
|
|
846
|
+
|| !isNonEmptyString(valueArg.terminal.cwd)
|
|
847
|
+
|| !plugins.path.isAbsolute(valueArg.terminal.cwd)
|
|
848
|
+
|| (valueArg.terminal.stoppedAt !== undefined && !isValidDate(valueArg.terminal.stoppedAt))
|
|
849
|
+
|| (valueArg.terminal.lastExitCode !== undefined
|
|
850
|
+
&& (!Number.isSafeInteger(valueArg.terminal.lastExitCode)
|
|
851
|
+
|| Math.abs(valueArg.terminal.lastExitCode as number) > 255))) {
|
|
852
|
+
throw new Error('Invalid controller terminal resource metadata.');
|
|
853
|
+
}
|
|
854
|
+
} else if (valueArg.terminal !== undefined) {
|
|
855
|
+
throw new Error('Browser resources cannot retain terminal metadata.');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const hasRetiringAt = valueArg.retiringAt !== undefined;
|
|
859
|
+
const hasRetiredAt = valueArg.retiredAt !== undefined;
|
|
860
|
+
if (
|
|
861
|
+
(hasRetiringAt && !isValidDate(valueArg.retiringAt))
|
|
862
|
+
|| (hasRetiredAt && !isValidDate(valueArg.retiredAt))
|
|
863
|
+
|| (valueArg.lifecycle === 'active' && (hasRetiringAt || hasRetiredAt))
|
|
864
|
+
|| (valueArg.lifecycle === 'retiring' && (!hasRetiringAt || hasRetiredAt))
|
|
865
|
+
|| (valueArg.lifecycle === 'retired' && (!hasRetiringAt || !hasRetiredAt))
|
|
866
|
+
) {
|
|
867
|
+
throw new Error('Invalid controller resource lifecycle.');
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_resources' })
|
|
872
|
+
@plugins.smartdata.exactPersistence({
|
|
873
|
+
assertDocument: assertControllerResourceDocument,
|
|
874
|
+
})
|
|
875
|
+
export class ControllerResourceModel
|
|
876
|
+
extends plugins.smartdata.SmartDataDbDoc<ControllerResourceModel, IControllerResourceDocument> {
|
|
877
|
+
declare static exact: plugins.smartdata.TExact<ControllerResourceModel>;
|
|
878
|
+
|
|
879
|
+
@plugins.smartdata.unI()
|
|
880
|
+
public id!: string;
|
|
881
|
+
|
|
882
|
+
@plugins.smartdata.svDb()
|
|
883
|
+
public controllerId!: string;
|
|
884
|
+
|
|
885
|
+
@plugins.smartdata.svDb()
|
|
886
|
+
public projectId!: string;
|
|
887
|
+
|
|
888
|
+
@plugins.smartdata.svDb()
|
|
889
|
+
public kind!: IControllerResourceDocument['kind'];
|
|
890
|
+
|
|
891
|
+
@plugins.smartdata.svDb()
|
|
892
|
+
public title: string = '';
|
|
893
|
+
|
|
894
|
+
@plugins.smartdata.svDb()
|
|
895
|
+
public attachmentAuthorityId!: string;
|
|
896
|
+
|
|
897
|
+
@plugins.smartdata.svDb()
|
|
898
|
+
public attachmentRevision!: number;
|
|
899
|
+
|
|
900
|
+
@plugins.smartdata.svDb()
|
|
901
|
+
public sessionId!: IControllerResourceDocument['sessionId'];
|
|
902
|
+
|
|
903
|
+
@plugins.smartdata.svDb()
|
|
904
|
+
public pendingAttachment?: IControllerResourceDocument['pendingAttachment'];
|
|
905
|
+
|
|
906
|
+
@plugins.smartdata.svDb()
|
|
907
|
+
public lifecycle!: IControllerResourceDocument['lifecycle'];
|
|
908
|
+
|
|
909
|
+
@plugins.smartdata.svDb()
|
|
910
|
+
public terminal?: IControllerResourceDocument['terminal'];
|
|
911
|
+
|
|
912
|
+
@plugins.smartdata.svDb()
|
|
913
|
+
public createdAt!: Date;
|
|
914
|
+
|
|
915
|
+
@plugins.smartdata.svDb()
|
|
916
|
+
public updatedAt!: Date;
|
|
917
|
+
|
|
918
|
+
@plugins.smartdata.svDb()
|
|
919
|
+
public retiringAt?: Date;
|
|
920
|
+
|
|
921
|
+
@plugins.smartdata.index({ expireAfterSeconds: 90 * 24 * 60 * 60 })
|
|
922
|
+
public retiredAt?: Date;
|
|
923
|
+
|
|
924
|
+
@plugins.smartdata.svDb()
|
|
925
|
+
public updateId!: string;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const maxSessionStateExchanges = 8;
|
|
929
|
+
const maxSessionStateExchangeBytes = 256 * 1024;
|
|
930
|
+
|
|
931
|
+
export const controllerSessionStateId = (
|
|
932
|
+
controllerIdArg: string,
|
|
933
|
+
projectIdArg: string,
|
|
934
|
+
sessionIdArg: IControllerRuntimeId,
|
|
935
|
+
): string => `sessionstate:v1:${plugins.crypto
|
|
936
|
+
.createHash('sha256')
|
|
937
|
+
.update(JSON.stringify([
|
|
938
|
+
controllerIdArg,
|
|
939
|
+
projectIdArg,
|
|
940
|
+
sessionIdArg.harnessId,
|
|
941
|
+
sessionIdArg.nativeId,
|
|
942
|
+
]))
|
|
943
|
+
.digest('base64url')}`;
|
|
944
|
+
|
|
945
|
+
export function assertControllerSessionStateDocument(
|
|
946
|
+
valueArg: unknown,
|
|
947
|
+
): asserts valueArg is IControllerSessionStateDocument {
|
|
948
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller session state document.');
|
|
949
|
+
if (
|
|
950
|
+
!hasExactKeys(
|
|
951
|
+
valueArg,
|
|
952
|
+
[
|
|
953
|
+
'id',
|
|
954
|
+
'controllerId',
|
|
955
|
+
'projectId',
|
|
956
|
+
'sessionId',
|
|
957
|
+
'scratchpad',
|
|
958
|
+
'intelligenceExchanges',
|
|
959
|
+
'updateId',
|
|
960
|
+
],
|
|
961
|
+
['modelChoice', 'flexProjectManagement', 'deletedAt'],
|
|
962
|
+
)
|
|
963
|
+
|| !isControllerId(valueArg.controllerId)
|
|
964
|
+
|| !isNonEmptyString(valueArg.projectId)
|
|
965
|
+
|| !groupIdentifierPattern.test(valueArg.projectId)
|
|
966
|
+
|| !isRuntimeId(valueArg.sessionId, ['opencode', 'flex'])
|
|
967
|
+
|| (valueArg.modelChoice !== undefined && (
|
|
968
|
+
!isModelChoice(valueArg.modelChoice)
|
|
969
|
+
|| valueArg.modelChoice.harnessId !== valueArg.sessionId.harnessId
|
|
970
|
+
))
|
|
971
|
+
|| valueArg.id !== controllerSessionStateId(
|
|
972
|
+
valueArg.controllerId,
|
|
973
|
+
valueArg.projectId,
|
|
974
|
+
valueArg.sessionId,
|
|
975
|
+
)
|
|
976
|
+
|| !isBase64Url(valueArg.updateId, 32)
|
|
977
|
+
|| (Object.hasOwn(valueArg, 'deletedAt') && !isValidDate(valueArg.deletedAt))
|
|
978
|
+
|| !isPlainObject(valueArg.scratchpad)
|
|
979
|
+
|| !hasExactKeys(
|
|
980
|
+
valueArg.scratchpad,
|
|
981
|
+
['text', 'revision'],
|
|
982
|
+
['updatedAt', 'updatedBy'],
|
|
983
|
+
)
|
|
984
|
+
|| typeof valueArg.scratchpad.text !== 'string'
|
|
985
|
+
|| valueArg.scratchpad.text.length > 32_768
|
|
986
|
+
|| Buffer.byteLength(valueArg.scratchpad.text, 'utf8') > 128 * 1024
|
|
987
|
+
|| !Number.isSafeInteger(valueArg.scratchpad.revision)
|
|
988
|
+
|| (valueArg.scratchpad.revision as number) < 0
|
|
989
|
+
) {
|
|
990
|
+
throw new Error('Invalid controller session state document.');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const hasUpdatedAt = Object.hasOwn(valueArg.scratchpad, 'updatedAt');
|
|
994
|
+
const hasUpdatedBy = Object.hasOwn(valueArg.scratchpad, 'updatedBy');
|
|
995
|
+
if (
|
|
996
|
+
hasUpdatedAt !== hasUpdatedBy
|
|
997
|
+
|| (hasUpdatedAt && !isValidDate(valueArg.scratchpad.updatedAt))
|
|
998
|
+
|| (
|
|
999
|
+
hasUpdatedBy
|
|
1000
|
+
&& valueArg.scratchpad.updatedBy !== 'user'
|
|
1001
|
+
&& valueArg.scratchpad.updatedBy !== 'intelligence'
|
|
1002
|
+
&& valueArg.scratchpad.updatedBy !== 'agent'
|
|
1003
|
+
&& valueArg.scratchpad.updatedBy !== 'application'
|
|
1004
|
+
)
|
|
1005
|
+
|| ((valueArg.scratchpad.revision as number) === 0 && hasUpdatedAt)
|
|
1006
|
+
|| ((valueArg.scratchpad.revision as number) > 0 && !hasUpdatedAt)
|
|
1007
|
+
|| !Array.isArray(valueArg.intelligenceExchanges)
|
|
1008
|
+
|| valueArg.intelligenceExchanges.length > maxSessionStateExchanges
|
|
1009
|
+
|| Buffer.byteLength(JSON.stringify(valueArg.intelligenceExchanges), 'utf8')
|
|
1010
|
+
> maxSessionStateExchangeBytes
|
|
1011
|
+
) {
|
|
1012
|
+
throw new Error('Invalid controller session state document.');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const exchangeIds = new Set<string>();
|
|
1016
|
+
for (const exchange of valueArg.intelligenceExchanges) {
|
|
1017
|
+
if (
|
|
1018
|
+
!isPlainObject(exchange)
|
|
1019
|
+
|| !hasExactKeys(
|
|
1020
|
+
exchange,
|
|
1021
|
+
['id', 'question', 'status', 'createdAt'],
|
|
1022
|
+
[
|
|
1023
|
+
'answer',
|
|
1024
|
+
'error',
|
|
1025
|
+
'model',
|
|
1026
|
+
'scratchpadConflict',
|
|
1027
|
+
'scratchpadRevisionAtAdmission',
|
|
1028
|
+
'temporarySessionId',
|
|
1029
|
+
'completedAt',
|
|
1030
|
+
],
|
|
1031
|
+
)
|
|
1032
|
+
|| !isBase64Url(exchange.id, 16)
|
|
1033
|
+
|| exchangeIds.has(exchange.id)
|
|
1034
|
+
|| !isNonEmptyString(exchange.question)
|
|
1035
|
+
|| exchange.question.length > 8_192
|
|
1036
|
+
|| Buffer.byteLength(exchange.question, 'utf8') > 32 * 1024
|
|
1037
|
+
|| !['running', 'completed', 'error'].includes(exchange.status as string)
|
|
1038
|
+
|| !isValidDate(exchange.createdAt)
|
|
1039
|
+
|| (Object.hasOwn(exchange, 'completedAt') && !isValidDate(exchange.completedAt))
|
|
1040
|
+
|| (Object.hasOwn(exchange, 'answer')
|
|
1041
|
+
&& (typeof exchange.answer !== 'string'
|
|
1042
|
+
|| exchange.answer.length > 32_768
|
|
1043
|
+
|| Buffer.byteLength(exchange.answer, 'utf8') > 128 * 1024))
|
|
1044
|
+
|| (Object.hasOwn(exchange, 'error')
|
|
1045
|
+
&& (typeof exchange.error !== 'string'
|
|
1046
|
+
|| exchange.error.length < 1
|
|
1047
|
+
|| Buffer.byteLength(exchange.error, 'utf8') > 16 * 1024))
|
|
1048
|
+
|| (Object.hasOwn(exchange, 'model')
|
|
1049
|
+
&& (!isNonEmptyString(exchange.model)
|
|
1050
|
+
|| Buffer.byteLength(exchange.model, 'utf8') > 512))
|
|
1051
|
+
|| (Object.hasOwn(exchange, 'scratchpadConflict') && exchange.scratchpadConflict !== true)
|
|
1052
|
+
|| (Object.hasOwn(exchange, 'scratchpadRevisionAtAdmission') && (
|
|
1053
|
+
!Number.isSafeInteger(exchange.scratchpadRevisionAtAdmission)
|
|
1054
|
+
|| (exchange.scratchpadRevisionAtAdmission as number) < 0
|
|
1055
|
+
))
|
|
1056
|
+
|| (Object.hasOwn(exchange, 'temporarySessionId') && (
|
|
1057
|
+
!isNonEmptyString(exchange.temporarySessionId)
|
|
1058
|
+
|| Buffer.byteLength(exchange.temporarySessionId, 'utf8') > 512
|
|
1059
|
+
))
|
|
1060
|
+
) {
|
|
1061
|
+
throw new Error('Invalid controller session intelligence exchange.');
|
|
1062
|
+
}
|
|
1063
|
+
exchangeIds.add(exchange.id);
|
|
1064
|
+
if (
|
|
1065
|
+
(exchange.status === 'running' && (
|
|
1066
|
+
Object.hasOwn(exchange, 'answer')
|
|
1067
|
+
|| Object.hasOwn(exchange, 'error')
|
|
1068
|
+
|| Object.hasOwn(exchange, 'completedAt')
|
|
1069
|
+
|| Object.hasOwn(exchange, 'scratchpadConflict')
|
|
1070
|
+
|| !Number.isSafeInteger(exchange.scratchpadRevisionAtAdmission)
|
|
1071
|
+
))
|
|
1072
|
+
|| (exchange.status === 'completed' && (
|
|
1073
|
+
typeof exchange.answer !== 'string'
|
|
1074
|
+
|| !isValidDate(exchange.completedAt)
|
|
1075
|
+
|| Object.hasOwn(exchange, 'error')
|
|
1076
|
+
|| Object.hasOwn(exchange, 'scratchpadRevisionAtAdmission')
|
|
1077
|
+
))
|
|
1078
|
+
|| (exchange.status === 'error' && (
|
|
1079
|
+
!isNonEmptyString(exchange.error)
|
|
1080
|
+
|| !isValidDate(exchange.completedAt)
|
|
1081
|
+
|| Object.hasOwn(exchange, 'answer')
|
|
1082
|
+
|| Object.hasOwn(exchange, 'scratchpadConflict')
|
|
1083
|
+
|| Object.hasOwn(exchange, 'scratchpadRevisionAtAdmission')
|
|
1084
|
+
))
|
|
1085
|
+
) {
|
|
1086
|
+
throw new Error('Invalid controller session intelligence exchange state.');
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (valueArg.flexProjectManagement !== undefined) {
|
|
1091
|
+
const projectManagement = valueArg.flexProjectManagement;
|
|
1092
|
+
plugins.flexharness.assertFlexProjectManagementRecord(projectManagement);
|
|
1093
|
+
const isTombstone = 'deletedAt' in projectManagement;
|
|
1094
|
+
const deletedAt = valueArg.deletedAt as Date | undefined;
|
|
1095
|
+
if (
|
|
1096
|
+
valueArg.sessionId.harnessId !== 'flex'
|
|
1097
|
+
|| isTombstone !== (deletedAt !== undefined)
|
|
1098
|
+
|| (
|
|
1099
|
+
!isTombstone
|
|
1100
|
+
&& projectManagement.scratchpad !== valueArg.scratchpad.text
|
|
1101
|
+
)
|
|
1102
|
+
|| (
|
|
1103
|
+
isTombstone
|
|
1104
|
+
&& projectManagement.deletedAt !== deletedAt?.toISOString()
|
|
1105
|
+
)
|
|
1106
|
+
) {
|
|
1107
|
+
throw new Error('Invalid controller Flex project-management state.');
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (
|
|
1112
|
+
valueArg.deletedAt !== undefined
|
|
1113
|
+
&& (
|
|
1114
|
+
valueArg.scratchpad.text !== ''
|
|
1115
|
+
|| valueArg.intelligenceExchanges.length !== 0
|
|
1116
|
+
|| valueArg.modelChoice !== undefined
|
|
1117
|
+
)
|
|
1118
|
+
) {
|
|
1119
|
+
throw new Error('Deleted controller session state must not retain content.');
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_session_states' })
|
|
1124
|
+
@plugins.smartdata.exactPersistence({
|
|
1125
|
+
assertDocument: assertControllerSessionStateDocument,
|
|
1126
|
+
})
|
|
1127
|
+
export class ControllerSessionStateModel
|
|
1128
|
+
extends plugins.smartdata.SmartDataDbDoc<ControllerSessionStateModel, IControllerSessionStateDocument> {
|
|
1129
|
+
declare static exact: plugins.smartdata.TExact<ControllerSessionStateModel>;
|
|
1130
|
+
|
|
1131
|
+
@plugins.smartdata.unI()
|
|
1132
|
+
public id!: string;
|
|
1133
|
+
|
|
1134
|
+
@plugins.smartdata.svDb()
|
|
1135
|
+
public controllerId!: string;
|
|
1136
|
+
|
|
1137
|
+
@plugins.smartdata.svDb()
|
|
1138
|
+
public projectId!: string;
|
|
1139
|
+
|
|
1140
|
+
@plugins.smartdata.svDb()
|
|
1141
|
+
public sessionId!: IControllerRuntimeId;
|
|
1142
|
+
|
|
1143
|
+
@plugins.smartdata.svDb()
|
|
1144
|
+
public scratchpad!: IControllerSessionStateDocument['scratchpad'];
|
|
1145
|
+
|
|
1146
|
+
@plugins.smartdata.svDb()
|
|
1147
|
+
public intelligenceExchanges!: IControllerSessionStateDocument['intelligenceExchanges'];
|
|
1148
|
+
|
|
1149
|
+
@plugins.smartdata.svDb()
|
|
1150
|
+
public flexProjectManagement?: IControllerSessionStateDocument['flexProjectManagement'];
|
|
1151
|
+
|
|
1152
|
+
@plugins.smartdata.svDb()
|
|
1153
|
+
public modelChoice?: TControllerModelChoice;
|
|
1154
|
+
|
|
1155
|
+
@plugins.smartdata.svDb()
|
|
1156
|
+
public updateId!: string;
|
|
1157
|
+
|
|
1158
|
+
@plugins.smartdata.index({ expireAfterSeconds: 30 * 24 * 60 * 60 })
|
|
1159
|
+
public deletedAt?: Date;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
export const assertControllerTempPasswordDocument = (
|
|
1163
|
+
valueArg: unknown,
|
|
1164
|
+
): asserts valueArg is IControllerTempPasswordDocument => {
|
|
1165
|
+
if (!isPlainObject(valueArg)) throw new Error('Invalid controller temp password document.');
|
|
1166
|
+
if (
|
|
1167
|
+
!hasExactKeys(valueArg, ['id', 'controllerId', 'secretHash', 'createdAt', 'expiresAt'])
|
|
1168
|
+
|| !isNonEmptyString(valueArg.id)
|
|
1169
|
+
|| valueArg.id.length > 512
|
|
1170
|
+
|| !isNonEmptyString(valueArg.controllerId)
|
|
1171
|
+
|| !/^controller:[1-9][0-9]{0,4}$/.test(valueArg.controllerId)
|
|
1172
|
+
|| !isBase64Url(valueArg.secretHash, 32)
|
|
1173
|
+
|| !isValidDate(valueArg.createdAt)
|
|
1174
|
+
|| !isValidDate(valueArg.expiresAt)
|
|
1175
|
+
|| valueArg.expiresAt.getTime() <= valueArg.createdAt.getTime()
|
|
1176
|
+
) {
|
|
1177
|
+
throw new Error('Invalid controller temp password document.');
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_temp_passwords' })
|
|
1182
|
+
@plugins.smartdata.exactPersistence({
|
|
1183
|
+
assertDocument: assertControllerTempPasswordDocument,
|
|
1184
|
+
})
|
|
1185
|
+
export class ControllerTempPasswordModel
|
|
1186
|
+
extends plugins.smartdata.SmartDataDbDoc<
|
|
1187
|
+
ControllerTempPasswordModel,
|
|
1188
|
+
IControllerTempPasswordDocument
|
|
1189
|
+
> {
|
|
1190
|
+
declare static exact: plugins.smartdata.TExact<ControllerTempPasswordModel>;
|
|
1191
|
+
|
|
1192
|
+
@plugins.smartdata.unI()
|
|
1193
|
+
public id!: string;
|
|
1194
|
+
|
|
1195
|
+
@plugins.smartdata.svDb()
|
|
1196
|
+
public controllerId!: string;
|
|
1197
|
+
|
|
1198
|
+
@plugins.smartdata.svDb()
|
|
1199
|
+
public secretHash!: string;
|
|
1200
|
+
|
|
1201
|
+
@plugins.smartdata.svDb()
|
|
1202
|
+
public createdAt!: Date;
|
|
1203
|
+
|
|
1204
|
+
@plugins.smartdata.index({ expireAfterSeconds: 0 })
|
|
1205
|
+
public expiresAt!: Date;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
@plugins.smartdata.managed({ collectionName: 'opencode_controller_audit' })
|
|
1209
|
+
@plugins.smartdata.exactPersistence({
|
|
1210
|
+
assertDocument: assertControllerAuditEventDocument,
|
|
1211
|
+
})
|
|
1212
|
+
export class ControllerAuditEventModel
|
|
1213
|
+
extends plugins.smartdata.SmartDataDbDoc<ControllerAuditEventModel, IControllerAuditEvent> {
|
|
1214
|
+
declare static exact: plugins.smartdata.TExact<ControllerAuditEventModel>;
|
|
1215
|
+
|
|
1216
|
+
@plugins.smartdata.unI()
|
|
1217
|
+
public id!: string;
|
|
1218
|
+
|
|
1219
|
+
@plugins.smartdata.svDb()
|
|
1220
|
+
public controllerId!: string;
|
|
1221
|
+
|
|
1222
|
+
@plugins.smartdata.index({ expireAfterSeconds: auditRetentionSeconds })
|
|
1223
|
+
public timestamp!: Date;
|
|
1224
|
+
|
|
1225
|
+
@plugins.smartdata.svDb()
|
|
1226
|
+
public type!: IControllerAuditEvent['type'];
|
|
1227
|
+
|
|
1228
|
+
@plugins.smartdata.svDb()
|
|
1229
|
+
public outcome!: IControllerAuditEvent['outcome'];
|
|
1230
|
+
|
|
1231
|
+
@plugins.smartdata.svDb()
|
|
1232
|
+
public operationId?: string;
|
|
1233
|
+
|
|
1234
|
+
@plugins.smartdata.svDb()
|
|
1235
|
+
public peerId?: string;
|
|
1236
|
+
|
|
1237
|
+
@plugins.smartdata.svDb()
|
|
1238
|
+
public credentialId?: string;
|
|
1239
|
+
|
|
1240
|
+
@plugins.smartdata.svDb()
|
|
1241
|
+
public sessionId?: IControllerRuntimeId;
|
|
1242
|
+
|
|
1243
|
+
@plugins.smartdata.svDb()
|
|
1244
|
+
public requestId?: IControllerRuntimeId;
|
|
1245
|
+
}
|