@uns-kit/core 3.0.25 → 3.0.27
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/MIGRATIONS.md +51 -0
- package/dist/runtime/compatibility/energenti-authorization-v1.d.ts +95 -0
- package/dist/runtime/compatibility/energenti-authorization-v1.d.ts.map +1 -0
- package/dist/runtime/compatibility/energenti-authorization-v1.js +315 -0
- package/dist/runtime/compatibility/energenti-authorization-v1.js.map +1 -0
- package/dist/runtime/compatibility/energenti-descriptor-v1.d.ts +10 -0
- package/dist/runtime/compatibility/energenti-descriptor-v1.d.ts.map +1 -0
- package/dist/runtime/compatibility/energenti-descriptor-v1.js +18 -0
- package/dist/runtime/compatibility/energenti-descriptor-v1.js.map +1 -0
- package/dist/runtime/process-group-authorization.d.ts +107 -0
- package/dist/runtime/process-group-authorization.d.ts.map +1 -0
- package/dist/runtime/process-group-authorization.js +352 -0
- package/dist/runtime/process-group-authorization.js.map +1 -0
- package/dist/runtime/process-group-contract.d.ts +65 -0
- package/dist/runtime/process-group-contract.d.ts.map +1 -0
- package/dist/runtime/process-group-contract.js +171 -0
- package/dist/runtime/process-group-contract.js.map +1 -0
- package/dist/runtime/process-group-control-contract.d.ts +56 -0
- package/dist/runtime/process-group-control-contract.d.ts.map +1 -0
- package/dist/runtime/process-group-control-contract.js +37 -0
- package/dist/runtime/process-group-control-contract.js.map +1 -0
- package/dist/runtime/process-group-inspection.d.ts +22 -0
- package/dist/runtime/process-group-inspection.d.ts.map +1 -0
- package/dist/runtime/process-group-inspection.js +185 -0
- package/dist/runtime/process-group-inspection.js.map +1 -0
- package/dist/runtime/process-group-launch-environment.d.ts +3 -0
- package/dist/runtime/process-group-launch-environment.d.ts.map +1 -0
- package/dist/runtime/process-group-launch-environment.js +12 -0
- package/dist/runtime/process-group-launch-environment.js.map +1 -0
- package/dist/runtime/process-group-legacy-start.d.ts +2 -0
- package/dist/runtime/process-group-legacy-start.d.ts.map +1 -0
- package/dist/runtime/process-group-legacy-start.js +16 -0
- package/dist/runtime/process-group-legacy-start.js.map +1 -0
- package/dist/runtime/process-group-resources.d.ts +8 -0
- package/dist/runtime/process-group-resources.d.ts.map +1 -0
- package/dist/runtime/process-group-resources.js +96 -0
- package/dist/runtime/process-group-resources.js.map +1 -0
- package/dist/runtime/process-group-snapshot.mjs +199 -0
- package/dist/runtime/process-group-staging.d.ts +5 -0
- package/dist/runtime/process-group-staging.d.ts.map +1 -0
- package/dist/runtime/process-group-staging.js +63 -0
- package/dist/runtime/process-group-staging.js.map +1 -0
- package/dist/runtime/process-group-supervision.d.ts +13 -0
- package/dist/runtime/process-group-supervision.d.ts.map +1 -0
- package/dist/runtime/process-group-supervision.js +34 -0
- package/dist/runtime/process-group-supervision.js.map +1 -0
- package/dist/uns/uns-attributes.d.ts +2 -2
- package/dist/uns-mqtt/mqtt-proxy.d.ts +3 -0
- package/dist/uns-mqtt/mqtt-proxy.d.ts.map +1 -1
- package/dist/uns-mqtt/mqtt-proxy.js +38 -0
- package/dist/uns-mqtt/mqtt-proxy.js.map +1 -1
- package/dist/uns-mqtt/subscription-readiness.d.ts +23 -0
- package/dist/uns-mqtt/subscription-readiness.d.ts.map +1 -0
- package/dist/uns-mqtt/subscription-readiness.js +50 -0
- package/dist/uns-mqtt/subscription-readiness.js.map +1 -0
- package/package.json +1 -1
package/MIGRATIONS.md
CHANGED
|
@@ -9,6 +9,22 @@ Agents must inspect the application's existing ownership and shutdown flow
|
|
|
9
9
|
before editing it. The examples below describe the intended behavior, not a
|
|
10
10
|
mechanical search-and-replace operation.
|
|
11
11
|
|
|
12
|
+
## 3.0.26 - Broker-acknowledged input subscriptions
|
|
13
|
+
|
|
14
|
+
When upgrading from `<3.0.26` to `>=3.0.26`, `mqttProxyStatus` includes the additive
|
|
15
|
+
numeric event `mqtt-subscriptions-ready`: 1 only when the current connection has
|
|
16
|
+
acknowledged every observed subscription and no subscribe/unsubscribe ACK remains
|
|
17
|
+
pending; 0 on pending/rejected subscriptions or connection loss. The normal ten
|
|
18
|
+
second status heartbeat repeats the observation. No topics, credentials or payloads
|
|
19
|
+
are included, and no extra subscriptions or data publications are created.
|
|
20
|
+
|
|
21
|
+
Applications may observe this event on an `UnsMqttProxy` to distinguish process
|
|
22
|
+
Start/connect from prepared broker input. Also verify that application handlers,
|
|
23
|
+
registry and admission policy are prepared. Expire old observations and invalidate
|
|
24
|
+
them during pause/shutdown/restart. This transport observation does not guarantee
|
|
25
|
+
application persistence, a database ACK, replica visibility or exactly-once delivery.
|
|
26
|
+
Existing consumers need no change; old SDKs provide no such confirmation.
|
|
27
|
+
|
|
12
28
|
## 3.0.24 - Generated numeric delta metadata
|
|
13
29
|
|
|
14
30
|
When upgrading from `<3.0.24` to `>=3.0.24`, numeric scalar publications in
|
|
@@ -403,3 +419,38 @@ For every affected application, verify all of the following:
|
|
|
403
419
|
- Confirm producers handle queue-full rejection at their required reliability
|
|
404
420
|
level.
|
|
405
421
|
- Test that shutdown waits for accepted publishes and does not accept new work.
|
|
422
|
+
|
|
423
|
+
## 3.0.27 - Shared process-group runtime protocol 1
|
|
424
|
+
|
|
425
|
+
When crossing `<3.0.27` to `>=3.0.27`, existing applications need no change unless
|
|
426
|
+
using reviewed multi-process consolidation. Such applications and host launchers
|
|
427
|
+
must consume the shared `runtime/process-group-*.js` modules rather than fork
|
|
428
|
+
policy verification, configuration projection, journal identity or supervision.
|
|
429
|
+
Declare a data-only `unsDatahub.processGroupRuntime` in the application package.
|
|
430
|
+
See `docs/process-group-runtime.md` in the SDK repository for its schema and limits.
|
|
431
|
+
A declaration advertises support; it grants no execution or data access rights.
|
|
432
|
+
|
|
433
|
+
Host launchers still own authenticated commands, durable operation state, process
|
|
434
|
+
identity and resource limits. Applications own input parsing, mappings, units,
|
|
435
|
+
publishing and the SQLite writer. Both must implement protocol 1 exactly. The
|
|
436
|
+
SDK supplies the manifest, authorization, journal contract, staging, inspection,
|
|
437
|
+
receiver supervision and standalone snapshot reader. Do not import or execute an
|
|
438
|
+
application module inside the Controller. Do not add package/group-specific branches
|
|
439
|
+
or release-time group enumerations to a host launcher.
|
|
440
|
+
|
|
441
|
+
New authorizations use policy schema 3 and pin the declared descriptor, installed
|
|
442
|
+
artifact, manifest, mappings, three MQTT channel identities, configurations,
|
|
443
|
+
controller, placements, journal and state mount. Policies 1/2 remain frozen readers
|
|
444
|
+
for previously issued artifacts; do not relabel them as schema 3 or widen their
|
|
445
|
+
scope. Adding a group requires a new reviewed manifest/policy, not a Controller
|
|
446
|
+
capability number. Unknown protocols, changed resources, missing retained-state
|
|
447
|
+
policies and uncertain operation results remain blocked pending review.
|
|
448
|
+
|
|
449
|
+
Protocol 1 supports same-package, same-controller MQTT source groups (2–16
|
|
450
|
+
publishers), one target and one separately supervised receiver, with the existing
|
|
451
|
+
bounded SQLite journal format on a local Linux filesystem mount. It is not an
|
|
452
|
+
arbitrary transport, cross-host queue transfer or exactly-once protocol. Durable
|
|
453
|
+
admission starts only after the receiver subscription is acknowledged; a broker
|
|
454
|
+
or pre-admission gap can still lose input. Configuration-defined secrets remain
|
|
455
|
+
in the application; authorization diagnostics and backup outer metadata omit them.
|
|
456
|
+
Existing journal payloads and checkpoints are not converted by this upgrade.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export { configurationAuthorizationHash, groupArtifactHash, groupChannelsHash } from "../process-group-resources.js";
|
|
3
|
+
export declare const hvAuthorizationSchema: z.ZodObject<{
|
|
4
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
5
|
+
kind: z.ZodLiteral<"energenti-hv-runtime-authorization">;
|
|
6
|
+
controllerName: z.ZodString;
|
|
7
|
+
packageRoot: z.ZodString;
|
|
8
|
+
artifactSha256: z.ZodString;
|
|
9
|
+
groupManifestSha256: z.ZodString;
|
|
10
|
+
channelsSha256: z.ZodString;
|
|
11
|
+
stateRoot: z.ZodString;
|
|
12
|
+
directory: z.ZodString;
|
|
13
|
+
journalId: z.ZodNullable<z.ZodString>;
|
|
14
|
+
placements: z.ZodArray<z.ZodObject<{
|
|
15
|
+
instanceId: z.ZodString;
|
|
16
|
+
processName: z.ZodEnum<{
|
|
17
|
+
[x: string]: string;
|
|
18
|
+
}>;
|
|
19
|
+
role: z.ZodEnum<{
|
|
20
|
+
source: "source";
|
|
21
|
+
target: "target";
|
|
22
|
+
receiver: "receiver";
|
|
23
|
+
}>;
|
|
24
|
+
configurationSha256: z.ZodString;
|
|
25
|
+
}, z.core.$strict>>;
|
|
26
|
+
}, z.core.$strict>;
|
|
27
|
+
export declare const productionGroupIds: readonly ["hv", "vv", "jek"];
|
|
28
|
+
declare const groupId: z.ZodEnum<{
|
|
29
|
+
hv: "hv";
|
|
30
|
+
vv: "vv";
|
|
31
|
+
jek: "jek";
|
|
32
|
+
}>;
|
|
33
|
+
/** New groups require an independently reviewed policy; v1 HV remains unchanged. */
|
|
34
|
+
export declare const processGroupAuthorizationSchema: z.ZodObject<{
|
|
35
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
36
|
+
kind: z.ZodLiteral<"energenti-process-group-runtime-authorization">;
|
|
37
|
+
groupId: z.ZodEnum<{
|
|
38
|
+
hv: "hv";
|
|
39
|
+
vv: "vv";
|
|
40
|
+
jek: "jek";
|
|
41
|
+
}>;
|
|
42
|
+
controllerName: z.ZodString;
|
|
43
|
+
packageRoot: z.ZodString;
|
|
44
|
+
artifactSha256: z.ZodString;
|
|
45
|
+
groupManifestSha256: z.ZodString;
|
|
46
|
+
channelsSha256: z.ZodString;
|
|
47
|
+
stateRoot: z.ZodString;
|
|
48
|
+
directory: z.ZodString;
|
|
49
|
+
journalId: z.ZodNullable<z.ZodString>;
|
|
50
|
+
placements: z.ZodArray<z.ZodObject<{
|
|
51
|
+
instanceId: z.ZodString;
|
|
52
|
+
processName: z.ZodString;
|
|
53
|
+
role: z.ZodEnum<{
|
|
54
|
+
source: "source";
|
|
55
|
+
target: "target";
|
|
56
|
+
receiver: "receiver";
|
|
57
|
+
}>;
|
|
58
|
+
configurationSha256: z.ZodString;
|
|
59
|
+
}, z.core.$strict>>;
|
|
60
|
+
}, z.core.$strict>;
|
|
61
|
+
export type HvAuthorizationPolicy = z.infer<typeof hvAuthorizationSchema>;
|
|
62
|
+
export type ProcessGroupAuthorizationPolicy = z.infer<typeof processGroupAuthorizationSchema>;
|
|
63
|
+
export type HvAuthorization = Readonly<{
|
|
64
|
+
sha256: string;
|
|
65
|
+
file: string;
|
|
66
|
+
directory: string;
|
|
67
|
+
journalId: string | null;
|
|
68
|
+
role: "source" | "target" | "receiver";
|
|
69
|
+
groupId: z.infer<typeof groupId>;
|
|
70
|
+
groupProcessName: string;
|
|
71
|
+
}>;
|
|
72
|
+
export declare function hasProcessGroupAuthorization(): boolean;
|
|
73
|
+
/** Blocks retained direct publishers only for groups actually configured by the operator. */
|
|
74
|
+
export declare function configuredGroupProcessNames(): Set<string>;
|
|
75
|
+
export declare function authorizeHvRuntime(context: {
|
|
76
|
+
packageRoot: string;
|
|
77
|
+
instanceDirectory: string;
|
|
78
|
+
rawConfiguration: Record<string, unknown>;
|
|
79
|
+
channels: unknown[];
|
|
80
|
+
controllerName: string;
|
|
81
|
+
file?: string;
|
|
82
|
+
stateRoot?: string;
|
|
83
|
+
initialize?: boolean;
|
|
84
|
+
}): HvAuthorization;
|
|
85
|
+
export declare function assertHvJournalAuthorization(value: HvAuthorization | undefined, directory: string, journalId: string | undefined): void;
|
|
86
|
+
/** Preview must reject a missing/read-only/nonlocal mount before any source drain. */
|
|
87
|
+
export declare function assertHvStateMount(value: HvAuthorization, stateRoot: string): void;
|
|
88
|
+
/** A runtime policy is not permission to bypass the reviewed transition. */
|
|
89
|
+
export declare function assertHvTargetLaunch(value: HvAuthorization, instanceDirectory: string, operationId: string | undefined, launchId: string | undefined): void;
|
|
90
|
+
/** Historical policy identity; never use this bridge for new runtime declarations. */
|
|
91
|
+
export declare function legacyPolicyIdentity(policy: HvAuthorizationPolicy | ProcessGroupAuthorizationPolicy): {
|
|
92
|
+
namespace: string;
|
|
93
|
+
group: "hv" | "vv" | "jek";
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=energenti-authorization-v1.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"energenti-authorization-v1.d.ts","sourceRoot":"","sources":["../../../src/runtime/compatibility/energenti-authorization-v1.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAUxB,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAQrH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;kBAqBhC,CAAC;AAEH,eAAO,MAAM,kBAAkB,8BAA+B,CAAC;AAC/D,QAAA,MAAM,OAAO;;;;EAA6B,CAAC;AAO3C,oFAAoF;AACpF,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;kBAa1C,CAAC;AAwBH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAC1E,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAC9F,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,OAAO,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC,CAAC;AAEH,wBAAgB,4BAA4B,IAAI,OAAO,CAEtD;AAwCD,6FAA6F;AAC7F,wBAAgB,2BAA2B,IAAI,GAAG,CAAC,MAAM,CAAC,CAqCzD;AACD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,eAAe,CAoGlB;AACD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CASvI;AAED,sFAAsF;AACtF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAYlF;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,eAAe,EACtB,iBAAiB,EAAE,MAAM,EACzB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,IAAI,CAkBN;AAED,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,qBAAqB,GAAG,+BAA+B;;;EAEnG"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/** Frozen compatibility reader for previously issued Energenti policies v1/v2.
|
|
2
|
+
* New runtimes use the generic SDK contract; never extend this list for new groups. */
|
|
3
|
+
// Wire-identical to the Controller's runtime verifier. This is trusted node-local
|
|
4
|
+
// policy, never an instance-config flag, network credential or execution permit.
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { configurationAuthorizationHash, groupArtifactHash, groupChannelsHash, readProcessGroupResource as read, sha256 as sha, } from "../process-group-resources.js";
|
|
9
|
+
export { configurationAuthorizationHash, groupArtifactHash, groupChannelsHash } from "../process-group-resources.js";
|
|
10
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
11
|
+
const absolute = z
|
|
12
|
+
.string()
|
|
13
|
+
.max(512)
|
|
14
|
+
.refine((v) => path.isAbsolute(v) && path.resolve(v) === v);
|
|
15
|
+
const names = ["uns-energenti-san", "uns-energenti-crno", "uns-energenti-kepware-hv", "uns-energenti-kepware-hv-admission"];
|
|
16
|
+
export const hvAuthorizationSchema = z.strictObject({
|
|
17
|
+
schemaVersion: z.literal(1),
|
|
18
|
+
kind: z.literal("energenti-hv-runtime-authorization"),
|
|
19
|
+
controllerName: z.string().min(1).max(100),
|
|
20
|
+
packageRoot: absolute,
|
|
21
|
+
artifactSha256: digest,
|
|
22
|
+
groupManifestSha256: digest,
|
|
23
|
+
channelsSha256: digest,
|
|
24
|
+
stateRoot: absolute,
|
|
25
|
+
directory: absolute,
|
|
26
|
+
journalId: z.string().uuid().nullable(),
|
|
27
|
+
placements: z
|
|
28
|
+
.array(z.strictObject({
|
|
29
|
+
instanceId: z.string().regex(/^[a-zA-Z0-9-]{1,100}$/),
|
|
30
|
+
processName: z.enum(names),
|
|
31
|
+
role: z.enum(["source", "target", "receiver"]),
|
|
32
|
+
configurationSha256: digest,
|
|
33
|
+
}))
|
|
34
|
+
.length(4),
|
|
35
|
+
});
|
|
36
|
+
const identity = z.string().regex(/^[a-z][a-z0-9-]{0,99}$/);
|
|
37
|
+
export const productionGroupIds = ["hv", "vv", "jek"];
|
|
38
|
+
const groupId = z.enum(productionGroupIds);
|
|
39
|
+
const placement = z.strictObject({
|
|
40
|
+
instanceId: z.string().regex(/^[a-zA-Z0-9-]{1,100}$/),
|
|
41
|
+
processName: identity,
|
|
42
|
+
role: z.enum(["source", "target", "receiver"]),
|
|
43
|
+
configurationSha256: digest,
|
|
44
|
+
});
|
|
45
|
+
/** New groups require an independently reviewed policy; v1 HV remains unchanged. */
|
|
46
|
+
export const processGroupAuthorizationSchema = z.strictObject({
|
|
47
|
+
schemaVersion: z.literal(2),
|
|
48
|
+
kind: z.literal("energenti-process-group-runtime-authorization"),
|
|
49
|
+
groupId,
|
|
50
|
+
controllerName: z.string().min(1).max(100),
|
|
51
|
+
packageRoot: absolute,
|
|
52
|
+
artifactSha256: digest,
|
|
53
|
+
groupManifestSha256: digest,
|
|
54
|
+
channelsSha256: digest,
|
|
55
|
+
stateRoot: absolute,
|
|
56
|
+
directory: absolute,
|
|
57
|
+
journalId: z.string().uuid().nullable(),
|
|
58
|
+
placements: z.array(placement).min(4).max(18),
|
|
59
|
+
});
|
|
60
|
+
const policySchema = z.union([hvAuthorizationSchema, processGroupAuthorizationSchema]);
|
|
61
|
+
const manifestSchema = z.strictObject({
|
|
62
|
+
schemaVersion: z.literal(1),
|
|
63
|
+
id: groupId,
|
|
64
|
+
inputTopic: z
|
|
65
|
+
.string()
|
|
66
|
+
.min(1)
|
|
67
|
+
.max(512)
|
|
68
|
+
.regex(/^[^#+\0]+$/),
|
|
69
|
+
processName: identity,
|
|
70
|
+
profiles: z
|
|
71
|
+
.array(z.strictObject({
|
|
72
|
+
id: identity,
|
|
73
|
+
processName: identity,
|
|
74
|
+
mappingFile: z.string().min(1).max(512),
|
|
75
|
+
mappingSha256: digest,
|
|
76
|
+
dataGroup: identity,
|
|
77
|
+
}))
|
|
78
|
+
.min(2)
|
|
79
|
+
.max(16),
|
|
80
|
+
});
|
|
81
|
+
const issued = new WeakSet();
|
|
82
|
+
export function hasProcessGroupAuthorization() {
|
|
83
|
+
return !!(process.env["UNS_PROCESS_GROUP_AUTHORIZATION_FILE"] || process.env["UNS_PROCESS_GROUP_AUTHORIZATION_DIRECTORY"]);
|
|
84
|
+
}
|
|
85
|
+
function privateDirectory(directory) {
|
|
86
|
+
// Read checks every ancestor for links even if the leaf is not a policy file.
|
|
87
|
+
for (let p = directory; p !== "/"; p = path.dirname(p))
|
|
88
|
+
if (fs.lstatSync(p).isSymbolicLink())
|
|
89
|
+
throw Error();
|
|
90
|
+
const s = fs.lstatSync(directory);
|
|
91
|
+
if (!s.isDirectory() || s.uid !== process.getuid?.() || (s.mode & 0o077) !== 0)
|
|
92
|
+
throw Error();
|
|
93
|
+
}
|
|
94
|
+
function policyAt(file, stateRoot) {
|
|
95
|
+
const bytes = read(file, true), policy = policySchema.parse(JSON.parse(bytes.toString()));
|
|
96
|
+
const id = policy.schemaVersion === 1 ? "hv" : policy.groupId;
|
|
97
|
+
if (policy.stateRoot !== stateRoot ||
|
|
98
|
+
file !== path.join(stateRoot, "energenti/authorizations", id + ".json") ||
|
|
99
|
+
policy.directory !== path.join(stateRoot, "energenti/admission", id))
|
|
100
|
+
throw Error();
|
|
101
|
+
privateDirectory(stateRoot);
|
|
102
|
+
privateDirectory(path.dirname(file));
|
|
103
|
+
return { bytes, policy, id };
|
|
104
|
+
}
|
|
105
|
+
/** A registry is a private fixed directory, never a scan or an instance-supplied path. */
|
|
106
|
+
function registryDirectory(stateRoot, directory) {
|
|
107
|
+
if (!path.isAbsolute(stateRoot) || path.resolve(stateRoot) !== stateRoot || directory !== path.join(stateRoot, "energenti/authorizations"))
|
|
108
|
+
throw Error();
|
|
109
|
+
privateDirectory(stateRoot);
|
|
110
|
+
privateDirectory(directory);
|
|
111
|
+
return directory;
|
|
112
|
+
}
|
|
113
|
+
function selectedFile(raw, stateRoot) {
|
|
114
|
+
const j = raw["kepwareJournal"];
|
|
115
|
+
const relative = j?.["groupManifest"];
|
|
116
|
+
const id = productionGroupIds.find((id) => relative === `docs/kepware-groups/${id}.json`);
|
|
117
|
+
if (!id)
|
|
118
|
+
throw Error();
|
|
119
|
+
const directory = process.env["UNS_PROCESS_GROUP_AUTHORIZATION_DIRECTORY"];
|
|
120
|
+
if (directory)
|
|
121
|
+
return path.join(registryDirectory(stateRoot, directory), id + ".json");
|
|
122
|
+
const legacy = process.env["UNS_PROCESS_GROUP_AUTHORIZATION_FILE"];
|
|
123
|
+
if (!legacy)
|
|
124
|
+
throw Error();
|
|
125
|
+
return legacy;
|
|
126
|
+
}
|
|
127
|
+
/** Blocks retained direct publishers only for groups actually configured by the operator. */
|
|
128
|
+
export function configuredGroupProcessNames() {
|
|
129
|
+
const result = new Set(), stateRoot = process.env["UNS_PROCESS_GROUP_STATE_ROOT"];
|
|
130
|
+
if (!hasProcessGroupAuthorization())
|
|
131
|
+
return result;
|
|
132
|
+
try {
|
|
133
|
+
if (!stateRoot)
|
|
134
|
+
throw Error();
|
|
135
|
+
const files = new Set();
|
|
136
|
+
const single = process.env["UNS_PROCESS_GROUP_AUTHORIZATION_FILE"];
|
|
137
|
+
if (single)
|
|
138
|
+
files.add(single);
|
|
139
|
+
const directory = process.env["UNS_PROCESS_GROUP_AUTHORIZATION_DIRECTORY"];
|
|
140
|
+
if (directory) {
|
|
141
|
+
registryDirectory(stateRoot, directory);
|
|
142
|
+
for (const id of productionGroupIds) {
|
|
143
|
+
const file = path.join(directory, id + ".json");
|
|
144
|
+
// lstat (not exists) preserves denial and dangling-link failures.
|
|
145
|
+
try {
|
|
146
|
+
fs.lstatSync(file);
|
|
147
|
+
files.add(file);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
if (e.code !== "ENOENT")
|
|
151
|
+
throw e;
|
|
152
|
+
// Removing a policy must never re-enable direct publishers beside retained work.
|
|
153
|
+
let retained = false;
|
|
154
|
+
try {
|
|
155
|
+
fs.lstatSync(path.join(stateRoot, "energenti/admission", id));
|
|
156
|
+
retained = true;
|
|
157
|
+
}
|
|
158
|
+
catch (storageError) {
|
|
159
|
+
if (storageError.code !== "ENOENT")
|
|
160
|
+
throw storageError;
|
|
161
|
+
}
|
|
162
|
+
if (retained)
|
|
163
|
+
throw Error("Retained journal has no reviewed policy");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const file of files)
|
|
168
|
+
for (const p of policyAt(file, stateRoot).policy.placements)
|
|
169
|
+
result.add(p.processName);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw Error("Process-group authorization registry is missing, changed or insecure; review required");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function authorizeHvRuntime(context) {
|
|
177
|
+
try {
|
|
178
|
+
const stateRoot = context.stateRoot ?? process.env["UNS_PROCESS_GROUP_STATE_ROOT"];
|
|
179
|
+
if (!stateRoot || !context.controllerName)
|
|
180
|
+
throw Error();
|
|
181
|
+
const file = context.file ?? selectedFile(context.rawConfiguration, stateRoot);
|
|
182
|
+
const { bytes, policy: p, id } = policyAt(file, stateRoot);
|
|
183
|
+
if (p.controllerName !== context.controllerName || p.packageRoot !== context.packageRoot)
|
|
184
|
+
throw Error();
|
|
185
|
+
const pkg = JSON.parse(read(path.join(p.packageRoot, "package.json")).toString());
|
|
186
|
+
const capability = pkg.unsDatahub?.processGroupProductionAuthorization;
|
|
187
|
+
const supportedCapability = p.schemaVersion === 1 ? [1, 2, 3].includes(capability) : id === "jek" ? capability === 3 : [2, 3].includes(capability);
|
|
188
|
+
if (pkg.name !== "uns-energenti" || !supportedCapability || groupArtifactHash(p.packageRoot) !== p.artifactSha256)
|
|
189
|
+
throw Error();
|
|
190
|
+
const manifestPath = `docs/kepware-groups/${id}.json`;
|
|
191
|
+
const manifestBytes = read(path.join(p.packageRoot, manifestPath));
|
|
192
|
+
if (sha(manifestBytes) !== p.groupManifestSha256 || groupChannelsHash(context.channels) !== p.channelsSha256)
|
|
193
|
+
throw Error();
|
|
194
|
+
const m = manifestSchema.parse(JSON.parse(manifestBytes.toString()));
|
|
195
|
+
if (m.id !== id || m.processName !== `uns-energenti-kepware-${id}`)
|
|
196
|
+
throw Error();
|
|
197
|
+
const receiver = m.processName + "-admission";
|
|
198
|
+
if (new Set(m.profiles.map((x) => x.id)).size !== m.profiles.length ||
|
|
199
|
+
new Set(m.profiles.map((x) => x.processName)).size !== m.profiles.length ||
|
|
200
|
+
m.profiles.some((x) => x.processName === m.processName || x.processName === receiver))
|
|
201
|
+
throw Error();
|
|
202
|
+
// Preserve the exact legacy production boundary; only v2 derives membership from its pinned manifest.
|
|
203
|
+
if (p.schemaVersion === 1) {
|
|
204
|
+
const legacy = [
|
|
205
|
+
{ name: names[0], sha: "d9047de4eef02de32df115cb12e900ad4923e802bb4434afa6560e1eaaf66481" },
|
|
206
|
+
{ name: names[1], sha: "f2a06f15f31e56592ce228c3f12d934a09bd0e24712c50c420062b64f9f73cb5" },
|
|
207
|
+
];
|
|
208
|
+
if (m.profiles.length !== 2 ||
|
|
209
|
+
legacy.some((x) => !m.profiles.some((v) => v.processName === x.name && v.mappingSha256 === x.sha && v.dataGroup === "kepware")))
|
|
210
|
+
throw Error();
|
|
211
|
+
}
|
|
212
|
+
for (const member of m.profiles) {
|
|
213
|
+
const f = path.resolve(p.packageRoot, "docs/kepware-groups", member.mappingFile);
|
|
214
|
+
if (!f.startsWith(p.packageRoot + path.sep) || !f.endsWith(".csv") || sha(read(f)) !== member.mappingSha256)
|
|
215
|
+
throw Error();
|
|
216
|
+
}
|
|
217
|
+
const expected = new Map([
|
|
218
|
+
...m.profiles.map((x) => [x.processName, "source"]),
|
|
219
|
+
[m.processName, "target"],
|
|
220
|
+
[receiver, "receiver"],
|
|
221
|
+
]);
|
|
222
|
+
if (p.placements.length !== expected.size ||
|
|
223
|
+
new Set(p.placements.map((x) => x.instanceId)).size !== expected.size ||
|
|
224
|
+
new Set(p.placements.map((x) => x.processName)).size !== expected.size ||
|
|
225
|
+
p.placements.some((x) => expected.get(x.processName) !== x.role))
|
|
226
|
+
throw Error();
|
|
227
|
+
const entry = p.placements.find((x) => context.instanceDirectory === path.join(p.packageRoot, "instances", x.instanceId));
|
|
228
|
+
const c = context.rawConfiguration, uns = c["uns"], k = c["kepware"], j = c["kepwareJournal"];
|
|
229
|
+
if (!entry ||
|
|
230
|
+
entry.processName !== uns?.["processName"] ||
|
|
231
|
+
entry.configurationSha256 !== configurationAuthorizationHash(c) ||
|
|
232
|
+
c["sourceKind"] !== "kepware" ||
|
|
233
|
+
!j ||
|
|
234
|
+
j["directory"] !== p.directory ||
|
|
235
|
+
(j["journalId"] ?? null) !== p.journalId ||
|
|
236
|
+
j["groupManifest"] !== manifestPath)
|
|
237
|
+
throw Error();
|
|
238
|
+
if (context.initialize ? p.journalId !== null || entry.role !== "receiver" : p.journalId === null)
|
|
239
|
+
throw Error();
|
|
240
|
+
if (entry.role === "receiver"
|
|
241
|
+
? c["kepwareRuntimeRole"] !== "admission"
|
|
242
|
+
: c["kepwareRuntimeRole"] !== undefined && c["kepwareRuntimeRole"] !== "publisher")
|
|
243
|
+
throw Error();
|
|
244
|
+
const mapping = path.resolve(p.packageRoot, String(k?.["mappingFile"]));
|
|
245
|
+
const member = m.profiles.find((x) => x.processName === entry.processName);
|
|
246
|
+
if (entry.role !== "source"
|
|
247
|
+
? k?.["groupManifest"] !== manifestPath
|
|
248
|
+
: k?.["groupManifest"] !== undefined ||
|
|
249
|
+
k?.["dataGroup"] !== member?.dataGroup ||
|
|
250
|
+
!mapping.startsWith(p.packageRoot + path.sep) ||
|
|
251
|
+
sha(read(mapping)) !== member?.mappingSha256)
|
|
252
|
+
throw Error();
|
|
253
|
+
const result = Object.freeze({
|
|
254
|
+
sha256: sha(bytes),
|
|
255
|
+
file,
|
|
256
|
+
directory: p.directory,
|
|
257
|
+
journalId: p.journalId,
|
|
258
|
+
role: entry.role,
|
|
259
|
+
groupId: id,
|
|
260
|
+
groupProcessName: m.processName,
|
|
261
|
+
});
|
|
262
|
+
issued.add(result);
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw Error("Process-group runtime authorization is missing, changed or does not match this installed placement; review required");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
export function assertHvJournalAuthorization(value, directory, journalId) {
|
|
270
|
+
if (!value ||
|
|
271
|
+
!issued.has(value) ||
|
|
272
|
+
value.directory !== directory ||
|
|
273
|
+
value.journalId !== (journalId ?? null) ||
|
|
274
|
+
sha(read(value.file, true)) !== value.sha256)
|
|
275
|
+
throw Error("Exact reviewed process-group runtime authorization is required for this journal");
|
|
276
|
+
}
|
|
277
|
+
/** Preview must reject a missing/read-only/nonlocal mount before any source drain. */
|
|
278
|
+
export function assertHvStateMount(value, stateRoot) {
|
|
279
|
+
assertHvJournalAuthorization(value, value.directory, value.journalId ?? undefined);
|
|
280
|
+
if (process.platform !== "linux" || value.directory !== path.join(stateRoot, "energenti/admission", value.groupId))
|
|
281
|
+
throw Error("Process group requires the reviewed local Linux state mount");
|
|
282
|
+
const mount = fs
|
|
283
|
+
.readFileSync("/proc/self/mountinfo", "utf8")
|
|
284
|
+
.split("\n")
|
|
285
|
+
.map((l) => l.split(" "))
|
|
286
|
+
.filter((f) => f[4] === stateRoot)
|
|
287
|
+
.at(-1);
|
|
288
|
+
if (!mount || !mount[5]?.split(",").includes("rw") || !["xfs", "ext4", "btrfs", "zfs"].includes(mount[mount.indexOf("-") + 1] ?? ""))
|
|
289
|
+
throw Error("Process-group state root must be an explicit writable local Linux filesystem mount");
|
|
290
|
+
}
|
|
291
|
+
/** A runtime policy is not permission to bypass the reviewed transition. */
|
|
292
|
+
export function assertHvTargetLaunch(value, instanceDirectory, operationId, launchId) {
|
|
293
|
+
if (value.role !== "target")
|
|
294
|
+
return;
|
|
295
|
+
try {
|
|
296
|
+
assertHvJournalAuthorization(value, value.directory, value.journalId ?? undefined);
|
|
297
|
+
const r = JSON.parse(read(path.join(instanceDirectory, "process-group-reservation.json"), true).toString());
|
|
298
|
+
if (!z.string().uuid().safeParse(operationId).success ||
|
|
299
|
+
!z.string().uuid().safeParse(launchId).success ||
|
|
300
|
+
r.operationId !== operationId ||
|
|
301
|
+
r.role !== "target" ||
|
|
302
|
+
r.lastLaunch?.launchId !== launchId ||
|
|
303
|
+
!z.string().uuid().safeParse(r.lastLaunch?.requestId).success ||
|
|
304
|
+
!digest.safeParse(r.configurationChecksum).success)
|
|
305
|
+
throw Error();
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
throw Error(`${value.groupId.toUpperCase()} target requires its exact reviewed group reservation and launch identity`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Historical policy identity; never use this bridge for new runtime declarations. */
|
|
312
|
+
export function legacyPolicyIdentity(policy) {
|
|
313
|
+
return { namespace: "energenti", group: policy.schemaVersion === 1 ? "hv" : policy.groupId };
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=energenti-authorization-v1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"energenti-authorization-v1.js","sourceRoot":"","sources":["../../../src/runtime/compatibility/energenti-authorization-v1.ts"],"names":[],"mappings":"AAAA;uFACuF;AACvF,kFAAkF;AAClF,iFAAiF;AACjF,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,8BAA8B,EAC9B,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,IAAI,IAAI,EAChC,MAAM,IAAI,GAAG,GACd,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAErH,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;AAClD,MAAM,QAAQ,GAAG,CAAC;KACf,MAAM,EAAE;KACR,GAAG,CAAC,GAAG,CAAC;KACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC9D,MAAM,KAAK,GAAG,CAAC,mBAAmB,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,oCAAoC,CAAC,CAAC;AAC5H,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,YAAY,CAAC;IAClD,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,oCAAoC,CAAC;IACrD,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC1C,WAAW,EAAE,QAAQ;IACrB,cAAc,EAAE,MAAM;IACtB,mBAAmB,EAAE,MAAM;IAC3B,cAAc,EAAE,MAAM;IACtB,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IACvC,UAAU,EAAE,CAAC;SACV,KAAK,CACJ,CAAC,CAAC,YAAY,CAAC;QACb,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,uBAAuB,CAAC;QACrD,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,KAA8B,CAAC;QACnD,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC9C,mBAAmB,EAAE,MAAM;KAC5B,CAAC,CACH;SACA,MAAM,CAAC,CAAC,CAAC;CACb,CAAC,CAAC;AACH,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAU,CAAC;AAC/D,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;AAC3C,MAAM,SAAS,GAAG,CAAC,CAAC,YAAY,CAAC;IAC/B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,uBAAuB,CAAC;IACrD,WAAW,EAAE,QAAQ;IACrB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC9C,mBAAmB,EAAE,MAAM;CAC5B,CAAC,CAAC;AACH,oFAAoF;AACpF,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,YAAY,CAAC;IAC5D,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,+CAA+C,CAAC;IAChE,OAAO;IACP,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC1C,WAAW,EAAE,QAAQ;IACrB,cAAc,EAAE,MAAM;IACtB,mBAAmB,EAAE,MAAM;IAC3B,cAAc,EAAE,MAAM;IACtB,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,QAAQ;IACnB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;IACvC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;CAC9C,CAAC,CAAC;AACH,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,qBAAqB,EAAE,+BAA+B,CAAC,CAAC,CAAC;AACvF,MAAM,cAAc,GAAG,CAAC,CAAC,YAAY,CAAC;IACpC,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3B,EAAE,EAAE,OAAO;IACX,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,GAAG,CAAC;SACR,KAAK,CAAC,YAAY,CAAC;IACtB,WAAW,EAAE,QAAQ;IACrB,QAAQ,EAAE,CAAC;SACR,KAAK,CACJ,CAAC,CAAC,YAAY,CAAC;QACb,EAAE,EAAE,QAAQ;QACZ,WAAW,EAAE,QAAQ;QACrB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;QACvC,aAAa,EAAE,MAAM;QACrB,SAAS,EAAE,QAAQ;KACpB,CAAC,CACH;SACA,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;CACX,CAAC,CAAC;AAYH,MAAM,MAAM,GAAG,IAAI,OAAO,EAAU,CAAC;AACrC,MAAM,UAAU,4BAA4B;IAC1C,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC,CAAC;AAC7H,CAAC;AACD,SAAS,gBAAgB,CAAC,SAAiB;IACzC,8EAA8E;IAC9E,KAAK,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,cAAc,EAAE;YAAE,MAAM,KAAK,EAAE,CAAC;IAC5G,MAAM,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC;QAAE,MAAM,KAAK,EAAE,CAAC;AAChG,CAAC;AACD,SAAS,QAAQ,CAAC,IAAY,EAAE,SAAiB;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAC5B,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC5D,MAAM,EAAE,GAAG,MAAM,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAC9D,IACE,MAAM,CAAC,SAAS,KAAK,SAAS;QAC9B,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,0BAA0B,EAAE,EAAE,GAAG,OAAO,CAAC;QACvE,MAAM,CAAC,SAAS,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,EAAE,EAAE,CAAC;QAEpE,MAAM,KAAK,EAAE,CAAC;IAChB,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC5B,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;AAC/B,CAAC;AACD,0FAA0F;AAC1F,SAAS,iBAAiB,CAAC,SAAiB,EAAE,SAAiB;IAC7D,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,SAAS,IAAI,SAAS,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,0BAA0B,CAAC;QACxI,MAAM,KAAK,EAAE,CAAC;IAChB,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC5B,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC5B,OAAO,SAAS,CAAC;AACnB,CAAC;AACD,SAAS,YAAY,CAAC,GAA4B,EAAE,SAAiB;IACnE,MAAM,CAAC,GAAG,GAAG,CAAC,gBAAgB,CAAwC,CAAC;IACvE,MAAM,QAAQ,GAAG,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC;IACtC,MAAM,EAAE,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,QAAQ,KAAK,uBAAuB,EAAE,OAAO,CAAC,CAAC;IAC1F,IAAI,CAAC,EAAE;QAAE,MAAM,KAAK,EAAE,CAAC;IACvB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;IAC3E,IAAI,SAAS;QAAE,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,OAAO,CAAC,CAAC;IACvF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;IACnE,IAAI,CAAC,MAAM;QAAE,MAAM,KAAK,EAAE,CAAC;IAC3B,OAAO,MAAM,CAAC;AAChB,CAAC;AACD,6FAA6F;AAC7F,MAAM,UAAU,2BAA2B;IACzC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAU,EAC9B,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAC1D,IAAI,CAAC,4BAA4B,EAAE;QAAE,OAAO,MAAM,CAAC;IACnD,IAAI,CAAC;QACH,IAAI,CAAC,SAAS;YAAE,MAAM,KAAK,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;QAChC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QACnE,IAAI,MAAM;YAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;QAC3E,IAAI,SAAS,EAAE,CAAC;YACd,iBAAiB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACxC,KAAK,MAAM,EAAE,IAAI,kBAAkB,EAAE,CAAC;gBACpC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,OAAO,CAAC,CAAC;gBAChD,kEAAkE;gBAClE,IAAI,CAAC;oBACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;oBACnB,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBAClB,CAAC;gBAAC,OAAO,CAAC,EAAE,CAAC;oBACX,IAAK,CAA2B,CAAC,IAAI,KAAK,QAAQ;wBAAE,MAAM,CAAC,CAAC;oBAC5D,iFAAiF;oBACjF,IAAI,QAAQ,GAAG,KAAK,CAAC;oBACrB,IAAI,CAAC;wBACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,EAAE,EAAE,CAAC,CAAC,CAAC;wBAC9D,QAAQ,GAAG,IAAI,CAAC;oBAClB,CAAC;oBAAC,OAAO,YAAY,EAAE,CAAC;wBACtB,IAAK,YAAsC,CAAC,IAAI,KAAK,QAAQ;4BAAE,MAAM,YAAY,CAAC;oBACpF,CAAC;oBACD,IAAI,QAAQ;wBAAE,MAAM,KAAK,CAAC,yCAAyC,CAAC,CAAC;gBACvE,CAAC;YACH,CAAC;QACH,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,UAAU;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;QACjH,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,KAAK,CAAC,uFAAuF,CAAC,CAAC;IACvG,CAAC;AACH,CAAC;AACD,MAAM,UAAU,kBAAkB,CAAC,OASlC;IACC,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;QACnF,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,cAAc;YAAE,MAAM,KAAK,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC,OAAO,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;QAC/E,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QAC3D,IAAI,CAAC,CAAC,cAAc,KAAK,OAAO,CAAC,cAAc,IAAI,CAAC,CAAC,WAAW,KAAK,OAAO,CAAC,WAAW;YAAE,MAAM,KAAK,EAAE,CAAC;QACxG,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClF,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,EAAE,mCAAmC,CAAC;QACvE,MAAM,mBAAmB,GACvB,CAAC,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QACzH,IAAI,GAAG,CAAC,IAAI,KAAK,eAAe,IAAI,CAAC,mBAAmB,IAAI,iBAAiB,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,cAAc;YAAE,MAAM,KAAK,EAAE,CAAC;QACjI,MAAM,YAAY,GAAG,uBAAuB,EAAE,OAAO,CAAC;QACtD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;QACnE,IAAI,GAAG,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,mBAAmB,IAAI,iBAAiB,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,cAAc;YAAE,MAAM,KAAK,EAAE,CAAC;QAC5H,MAAM,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QACrE,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,WAAW,KAAK,yBAAyB,EAAE,EAAE;YAAE,MAAM,KAAK,EAAE,CAAC;QAClF,MAAM,QAAQ,GAAG,CAAC,CAAC,WAAW,GAAG,YAAY,CAAC;QAC9C,IACE,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM;YAC/D,IAAI,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM;YACxE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,WAAW,KAAK,QAAQ,CAAC;YAErF,MAAM,KAAK,EAAE,CAAC;QAChB,sGAAsG;QACtG,IAAI,CAAC,CAAC,aAAa,KAAK,CAAC,EAAE,CAAC;YAC1B,MAAM,MAAM,GAAG;gBACb,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,kEAAkE,EAAE;gBAC3F,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,kEAAkE,EAAE;aAC5F,CAAC;YACF,IACE,CAAC,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;gBACvB,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,aAAa,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC;gBAE/H,MAAM,KAAK,EAAE,CAAC;QAClB,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAChC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,qBAAqB,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;YACjF,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,aAAa;gBAAE,MAAM,KAAK,EAAE,CAAC;QAC7H,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC;YACvB,GAAG,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,QAAQ,CAAU,CAAC;YAC5D,CAAC,CAAC,CAAC,WAAW,EAAE,QAAQ,CAAU;YAClC,CAAC,QAAQ,EAAE,UAAU,CAAU;SAChC,CAAC,CAAC;QACH,IACE,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC,IAAI;YACrC,IAAI,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI;YACrE,IAAI,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI;YACtE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC;YAEhE,MAAM,KAAK,EAAE,CAAC;QAChB,MAAM,KAAK,GAAG,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,iBAAiB,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;QAC1H,MAAM,CAAC,GAAG,OAAO,CAAC,gBAAgB,EAChC,GAAG,GAAG,CAAC,CAAC,KAAK,CAA4B,EACzC,CAAC,GAAG,CAAC,CAAC,SAAS,CAA4B,EAC3C,CAAC,GAAG,CAAC,CAAC,gBAAgB,CAA4B,CAAC;QACrD,IACE,CAAC,KAAK;YACN,KAAK,CAAC,WAAW,KAAK,GAAG,EAAE,CAAC,aAAa,CAAC;YAC1C,KAAK,CAAC,mBAAmB,KAAK,8BAA8B,CAAC,CAAC,CAAC;YAC/D,CAAC,CAAC,YAAY,CAAC,KAAK,SAAS;YAC7B,CAAC,CAAC;YACF,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,SAAS;YAC9B,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS;YACxC,CAAC,CAAC,eAAe,CAAC,KAAK,YAAY;YAEnC,MAAM,KAAK,EAAE,CAAC;QAChB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI;YAAE,MAAM,KAAK,EAAE,CAAC;QACjH,IACE,KAAK,CAAC,IAAI,KAAK,UAAU;YACvB,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,KAAK,WAAW;YACzC,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,oBAAoB,CAAC,KAAK,WAAW;YAEpF,MAAM,KAAK,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;QACxE,MAAM,MAAM,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,KAAK,CAAC,WAAW,CAAC,CAAC;QAC3E,IACE,KAAK,CAAC,IAAI,KAAK,QAAQ;YACrB,CAAC,CAAC,CAAC,EAAE,CAAC,eAAe,CAAC,KAAK,YAAY;YACvC,CAAC,CAAC,CAAC,EAAE,CAAC,eAAe,CAAC,KAAK,SAAS;gBAClC,CAAC,EAAE,CAAC,WAAW,CAAC,KAAK,MAAM,EAAE,SAAS;gBACtC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC;gBAC7C,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,MAAM,EAAE,aAAa;YAEhD,MAAM,KAAK,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;YAC3B,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC;YAClB,IAAI;YACJ,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,EAAE;YACX,gBAAgB,EAAE,CAAC,CAAC,WAAW;SAChC,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnB,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,KAAK,CAAC,qHAAqH,CAAC,CAAC;IACrI,CAAC;AACH,CAAC;AACD,MAAM,UAAU,4BAA4B,CAAC,KAAkC,EAAE,SAAiB,EAAE,SAA6B;IAC/H,IACE,CAAC,KAAK;QACN,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;QAClB,KAAK,CAAC,SAAS,KAAK,SAAS;QAC7B,KAAK,CAAC,SAAS,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC;QACvC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,MAAM;QAE5C,MAAM,KAAK,CAAC,iFAAiF,CAAC,CAAC;AACnG,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,kBAAkB,CAAC,KAAsB,EAAE,SAAiB;IAC1E,4BAA4B,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC;IACnF,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,EAAE,KAAK,CAAC,OAAO,CAAC;QAChH,MAAM,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,EAAE;SACb,YAAY,CAAC,sBAAsB,EAAE,MAAM,CAAC;SAC5C,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;SACjC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACV,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAClI,MAAM,KAAK,CAAC,oFAAoF,CAAC,CAAC;AACtG,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,oBAAoB,CAClC,KAAsB,EACtB,iBAAyB,EACzB,WAA+B,EAC/B,QAA4B;IAE5B,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO;IACpC,IAAI,CAAC;QACH,4BAA4B,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC;QACnF,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,gCAAgC,CAAC,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5G,IACE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,OAAO;YACjD,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,OAAO;YAC9C,CAAC,CAAC,WAAW,KAAK,WAAW;YAC7B,CAAC,CAAC,IAAI,KAAK,QAAQ;YACnB,CAAC,CAAC,UAAU,EAAE,QAAQ,KAAK,QAAQ;YACnC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,OAAO;YAC7D,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,OAAO;YAElD,MAAM,KAAK,EAAE,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,KAAK,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,2EAA2E,CAAC,CAAC;IACzH,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,oBAAoB,CAAC,MAA+D;IAClG,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,aAAa,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;AAC/F,CAAC","sourcesContent":["/** Frozen compatibility reader for previously issued Energenti policies v1/v2.\n * New runtimes use the generic SDK contract; never extend this list for new groups. */\n// Wire-identical to the Controller's runtime verifier. This is trusted node-local\n// policy, never an instance-config flag, network credential or execution permit.\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { z } from \"zod\";\n\nimport {\n configurationAuthorizationHash,\n groupArtifactHash,\n groupChannelsHash,\n readProcessGroupResource as read,\n sha256 as sha,\n} from \"../process-group-resources.js\";\n\nexport { configurationAuthorizationHash, groupArtifactHash, groupChannelsHash } from \"../process-group-resources.js\";\n\nconst digest = z.string().regex(/^[a-f0-9]{64}$/);\nconst absolute = z\n .string()\n .max(512)\n .refine((v) => path.isAbsolute(v) && path.resolve(v) === v);\nconst names = [\"uns-energenti-san\", \"uns-energenti-crno\", \"uns-energenti-kepware-hv\", \"uns-energenti-kepware-hv-admission\"];\nexport const hvAuthorizationSchema = z.strictObject({\n schemaVersion: z.literal(1),\n kind: z.literal(\"energenti-hv-runtime-authorization\"),\n controllerName: z.string().min(1).max(100),\n packageRoot: absolute,\n artifactSha256: digest,\n groupManifestSha256: digest,\n channelsSha256: digest,\n stateRoot: absolute,\n directory: absolute,\n journalId: z.string().uuid().nullable(),\n placements: z\n .array(\n z.strictObject({\n instanceId: z.string().regex(/^[a-zA-Z0-9-]{1,100}$/),\n processName: z.enum(names as [string, ...string[]]),\n role: z.enum([\"source\", \"target\", \"receiver\"]),\n configurationSha256: digest,\n }),\n )\n .length(4),\n});\nconst identity = z.string().regex(/^[a-z][a-z0-9-]{0,99}$/);\nexport const productionGroupIds = [\"hv\", \"vv\", \"jek\"] as const;\nconst groupId = z.enum(productionGroupIds);\nconst placement = z.strictObject({\n instanceId: z.string().regex(/^[a-zA-Z0-9-]{1,100}$/),\n processName: identity,\n role: z.enum([\"source\", \"target\", \"receiver\"]),\n configurationSha256: digest,\n});\n/** New groups require an independently reviewed policy; v1 HV remains unchanged. */\nexport const processGroupAuthorizationSchema = z.strictObject({\n schemaVersion: z.literal(2),\n kind: z.literal(\"energenti-process-group-runtime-authorization\"),\n groupId,\n controllerName: z.string().min(1).max(100),\n packageRoot: absolute,\n artifactSha256: digest,\n groupManifestSha256: digest,\n channelsSha256: digest,\n stateRoot: absolute,\n directory: absolute,\n journalId: z.string().uuid().nullable(),\n placements: z.array(placement).min(4).max(18),\n});\nconst policySchema = z.union([hvAuthorizationSchema, processGroupAuthorizationSchema]);\nconst manifestSchema = z.strictObject({\n schemaVersion: z.literal(1),\n id: groupId,\n inputTopic: z\n .string()\n .min(1)\n .max(512)\n .regex(/^[^#+\\0]+$/),\n processName: identity,\n profiles: z\n .array(\n z.strictObject({\n id: identity,\n processName: identity,\n mappingFile: z.string().min(1).max(512),\n mappingSha256: digest,\n dataGroup: identity,\n }),\n )\n .min(2)\n .max(16),\n});\nexport type HvAuthorizationPolicy = z.infer<typeof hvAuthorizationSchema>;\nexport type ProcessGroupAuthorizationPolicy = z.infer<typeof processGroupAuthorizationSchema>;\nexport type HvAuthorization = Readonly<{\n sha256: string;\n file: string;\n directory: string;\n journalId: string | null;\n role: \"source\" | \"target\" | \"receiver\";\n groupId: z.infer<typeof groupId>;\n groupProcessName: string;\n}>;\nconst issued = new WeakSet<object>();\nexport function hasProcessGroupAuthorization(): boolean {\n return !!(process.env[\"UNS_PROCESS_GROUP_AUTHORIZATION_FILE\"] || process.env[\"UNS_PROCESS_GROUP_AUTHORIZATION_DIRECTORY\"]);\n}\nfunction privateDirectory(directory: string): void {\n // Read checks every ancestor for links even if the leaf is not a policy file.\n for (let p = directory; p !== \"/\"; p = path.dirname(p)) if (fs.lstatSync(p).isSymbolicLink()) throw Error();\n const s = fs.lstatSync(directory);\n if (!s.isDirectory() || s.uid !== process.getuid?.() || (s.mode & 0o077) !== 0) throw Error();\n}\nfunction policyAt(file: string, stateRoot: string) {\n const bytes = read(file, true),\n policy = policySchema.parse(JSON.parse(bytes.toString()));\n const id = policy.schemaVersion === 1 ? \"hv\" : policy.groupId;\n if (\n policy.stateRoot !== stateRoot ||\n file !== path.join(stateRoot, \"energenti/authorizations\", id + \".json\") ||\n policy.directory !== path.join(stateRoot, \"energenti/admission\", id)\n )\n throw Error();\n privateDirectory(stateRoot);\n privateDirectory(path.dirname(file));\n return { bytes, policy, id };\n}\n/** A registry is a private fixed directory, never a scan or an instance-supplied path. */\nfunction registryDirectory(stateRoot: string, directory: string): string {\n if (!path.isAbsolute(stateRoot) || path.resolve(stateRoot) !== stateRoot || directory !== path.join(stateRoot, \"energenti/authorizations\"))\n throw Error();\n privateDirectory(stateRoot);\n privateDirectory(directory);\n return directory;\n}\nfunction selectedFile(raw: Record<string, unknown>, stateRoot: string): string {\n const j = raw[\"kepwareJournal\"] as Record<string, unknown> | undefined;\n const relative = j?.[\"groupManifest\"];\n const id = productionGroupIds.find((id) => relative === `docs/kepware-groups/${id}.json`);\n if (!id) throw Error();\n const directory = process.env[\"UNS_PROCESS_GROUP_AUTHORIZATION_DIRECTORY\"];\n if (directory) return path.join(registryDirectory(stateRoot, directory), id + \".json\");\n const legacy = process.env[\"UNS_PROCESS_GROUP_AUTHORIZATION_FILE\"];\n if (!legacy) throw Error();\n return legacy;\n}\n/** Blocks retained direct publishers only for groups actually configured by the operator. */\nexport function configuredGroupProcessNames(): Set<string> {\n const result = new Set<string>(),\n stateRoot = process.env[\"UNS_PROCESS_GROUP_STATE_ROOT\"];\n if (!hasProcessGroupAuthorization()) return result;\n try {\n if (!stateRoot) throw Error();\n const files = new Set<string>();\n const single = process.env[\"UNS_PROCESS_GROUP_AUTHORIZATION_FILE\"];\n if (single) files.add(single);\n const directory = process.env[\"UNS_PROCESS_GROUP_AUTHORIZATION_DIRECTORY\"];\n if (directory) {\n registryDirectory(stateRoot, directory);\n for (const id of productionGroupIds) {\n const file = path.join(directory, id + \".json\");\n // lstat (not exists) preserves denial and dangling-link failures.\n try {\n fs.lstatSync(file);\n files.add(file);\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"ENOENT\") throw e;\n // Removing a policy must never re-enable direct publishers beside retained work.\n let retained = false;\n try {\n fs.lstatSync(path.join(stateRoot, \"energenti/admission\", id));\n retained = true;\n } catch (storageError) {\n if ((storageError as NodeJS.ErrnoException).code !== \"ENOENT\") throw storageError;\n }\n if (retained) throw Error(\"Retained journal has no reviewed policy\");\n }\n }\n }\n for (const file of files) for (const p of policyAt(file, stateRoot).policy.placements) result.add(p.processName);\n return result;\n } catch {\n throw Error(\"Process-group authorization registry is missing, changed or insecure; review required\");\n }\n}\nexport function authorizeHvRuntime(context: {\n packageRoot: string;\n instanceDirectory: string;\n rawConfiguration: Record<string, unknown>;\n channels: unknown[];\n controllerName: string;\n file?: string;\n stateRoot?: string;\n initialize?: boolean;\n}): HvAuthorization {\n try {\n const stateRoot = context.stateRoot ?? process.env[\"UNS_PROCESS_GROUP_STATE_ROOT\"];\n if (!stateRoot || !context.controllerName) throw Error();\n const file = context.file ?? selectedFile(context.rawConfiguration, stateRoot);\n const { bytes, policy: p, id } = policyAt(file, stateRoot);\n if (p.controllerName !== context.controllerName || p.packageRoot !== context.packageRoot) throw Error();\n const pkg = JSON.parse(read(path.join(p.packageRoot, \"package.json\")).toString());\n const capability = pkg.unsDatahub?.processGroupProductionAuthorization;\n const supportedCapability =\n p.schemaVersion === 1 ? [1, 2, 3].includes(capability) : id === \"jek\" ? capability === 3 : [2, 3].includes(capability);\n if (pkg.name !== \"uns-energenti\" || !supportedCapability || groupArtifactHash(p.packageRoot) !== p.artifactSha256) throw Error();\n const manifestPath = `docs/kepware-groups/${id}.json`;\n const manifestBytes = read(path.join(p.packageRoot, manifestPath));\n if (sha(manifestBytes) !== p.groupManifestSha256 || groupChannelsHash(context.channels) !== p.channelsSha256) throw Error();\n const m = manifestSchema.parse(JSON.parse(manifestBytes.toString()));\n if (m.id !== id || m.processName !== `uns-energenti-kepware-${id}`) throw Error();\n const receiver = m.processName + \"-admission\";\n if (\n new Set(m.profiles.map((x) => x.id)).size !== m.profiles.length ||\n new Set(m.profiles.map((x) => x.processName)).size !== m.profiles.length ||\n m.profiles.some((x) => x.processName === m.processName || x.processName === receiver)\n )\n throw Error();\n // Preserve the exact legacy production boundary; only v2 derives membership from its pinned manifest.\n if (p.schemaVersion === 1) {\n const legacy = [\n { name: names[0], sha: \"d9047de4eef02de32df115cb12e900ad4923e802bb4434afa6560e1eaaf66481\" },\n { name: names[1], sha: \"f2a06f15f31e56592ce228c3f12d934a09bd0e24712c50c420062b64f9f73cb5\" },\n ];\n if (\n m.profiles.length !== 2 ||\n legacy.some((x) => !m.profiles.some((v) => v.processName === x.name && v.mappingSha256 === x.sha && v.dataGroup === \"kepware\"))\n )\n throw Error();\n }\n for (const member of m.profiles) {\n const f = path.resolve(p.packageRoot, \"docs/kepware-groups\", member.mappingFile);\n if (!f.startsWith(p.packageRoot + path.sep) || !f.endsWith(\".csv\") || sha(read(f)) !== member.mappingSha256) throw Error();\n }\n const expected = new Map([\n ...m.profiles.map((x) => [x.processName, \"source\"] as const),\n [m.processName, \"target\"] as const,\n [receiver, \"receiver\"] as const,\n ]);\n if (\n p.placements.length !== expected.size ||\n new Set(p.placements.map((x) => x.instanceId)).size !== expected.size ||\n new Set(p.placements.map((x) => x.processName)).size !== expected.size ||\n p.placements.some((x) => expected.get(x.processName) !== x.role)\n )\n throw Error();\n const entry = p.placements.find((x) => context.instanceDirectory === path.join(p.packageRoot, \"instances\", x.instanceId));\n const c = context.rawConfiguration,\n uns = c[\"uns\"] as Record<string, unknown>,\n k = c[\"kepware\"] as Record<string, unknown>,\n j = c[\"kepwareJournal\"] as Record<string, unknown>;\n if (\n !entry ||\n entry.processName !== uns?.[\"processName\"] ||\n entry.configurationSha256 !== configurationAuthorizationHash(c) ||\n c[\"sourceKind\"] !== \"kepware\" ||\n !j ||\n j[\"directory\"] !== p.directory ||\n (j[\"journalId\"] ?? null) !== p.journalId ||\n j[\"groupManifest\"] !== manifestPath\n )\n throw Error();\n if (context.initialize ? p.journalId !== null || entry.role !== \"receiver\" : p.journalId === null) throw Error();\n if (\n entry.role === \"receiver\"\n ? c[\"kepwareRuntimeRole\"] !== \"admission\"\n : c[\"kepwareRuntimeRole\"] !== undefined && c[\"kepwareRuntimeRole\"] !== \"publisher\"\n )\n throw Error();\n const mapping = path.resolve(p.packageRoot, String(k?.[\"mappingFile\"]));\n const member = m.profiles.find((x) => x.processName === entry.processName);\n if (\n entry.role !== \"source\"\n ? k?.[\"groupManifest\"] !== manifestPath\n : k?.[\"groupManifest\"] !== undefined ||\n k?.[\"dataGroup\"] !== member?.dataGroup ||\n !mapping.startsWith(p.packageRoot + path.sep) ||\n sha(read(mapping)) !== member?.mappingSha256\n )\n throw Error();\n const result = Object.freeze({\n sha256: sha(bytes),\n file,\n directory: p.directory,\n journalId: p.journalId,\n role: entry.role,\n groupId: id,\n groupProcessName: m.processName,\n });\n issued.add(result);\n return result;\n } catch {\n throw Error(\"Process-group runtime authorization is missing, changed or does not match this installed placement; review required\");\n }\n}\nexport function assertHvJournalAuthorization(value: HvAuthorization | undefined, directory: string, journalId: string | undefined): void {\n if (\n !value ||\n !issued.has(value) ||\n value.directory !== directory ||\n value.journalId !== (journalId ?? null) ||\n sha(read(value.file, true)) !== value.sha256\n )\n throw Error(\"Exact reviewed process-group runtime authorization is required for this journal\");\n}\n\n/** Preview must reject a missing/read-only/nonlocal mount before any source drain. */\nexport function assertHvStateMount(value: HvAuthorization, stateRoot: string): void {\n assertHvJournalAuthorization(value, value.directory, value.journalId ?? undefined);\n if (process.platform !== \"linux\" || value.directory !== path.join(stateRoot, \"energenti/admission\", value.groupId))\n throw Error(\"Process group requires the reviewed local Linux state mount\");\n const mount = fs\n .readFileSync(\"/proc/self/mountinfo\", \"utf8\")\n .split(\"\\n\")\n .map((l) => l.split(\" \"))\n .filter((f) => f[4] === stateRoot)\n .at(-1);\n if (!mount || !mount[5]?.split(\",\").includes(\"rw\") || ![\"xfs\", \"ext4\", \"btrfs\", \"zfs\"].includes(mount[mount.indexOf(\"-\") + 1] ?? \"\"))\n throw Error(\"Process-group state root must be an explicit writable local Linux filesystem mount\");\n}\n\n/** A runtime policy is not permission to bypass the reviewed transition. */\nexport function assertHvTargetLaunch(\n value: HvAuthorization,\n instanceDirectory: string,\n operationId: string | undefined,\n launchId: string | undefined,\n): void {\n if (value.role !== \"target\") return;\n try {\n assertHvJournalAuthorization(value, value.directory, value.journalId ?? undefined);\n const r = JSON.parse(read(path.join(instanceDirectory, \"process-group-reservation.json\"), true).toString());\n if (\n !z.string().uuid().safeParse(operationId).success ||\n !z.string().uuid().safeParse(launchId).success ||\n r.operationId !== operationId ||\n r.role !== \"target\" ||\n r.lastLaunch?.launchId !== launchId ||\n !z.string().uuid().safeParse(r.lastLaunch?.requestId).success ||\n !digest.safeParse(r.configurationChecksum).success\n )\n throw Error();\n } catch {\n throw Error(`${value.groupId.toUpperCase()} target requires its exact reviewed group reservation and launch identity`);\n }\n}\n\n/** Historical policy identity; never use this bridge for new runtime declarations. */\nexport function legacyPolicyIdentity(policy: HvAuthorizationPolicy | ProcessGroupAuthorizationPolicy) {\n return { namespace: \"energenti\", group: policy.schemaVersion === 1 ? \"hv\" : policy.groupId };\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProcessGroupRuntimeDescriptor } from "../process-group-contract.js";
|
|
2
|
+
export declare const legacyEnergentiDescriptor: ProcessGroupRuntimeDescriptor;
|
|
3
|
+
/** Frozen legacy declaration lookup; new packages use processGroupRuntime metadata. */
|
|
4
|
+
export declare const legacyDescriptorForPackage: (name: string) => ProcessGroupRuntimeDescriptor;
|
|
5
|
+
/** Reject the historical receiver spelling when its installed capability is absent. */
|
|
6
|
+
export declare const isUnsupportedLegacyReceiver: (raw: Record<string, unknown>) => boolean;
|
|
7
|
+
export declare const legacyPilotEnvironment: (localPilot: boolean) => {
|
|
8
|
+
UNS_ENERGENTI_GROUP_PILOT: string;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=energenti-descriptor-v1.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"energenti-descriptor-v1.d.ts","sourceRoot":"","sources":["../../../src/runtime/compatibility/energenti-descriptor-v1.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAElF,eAAO,MAAM,yBAAyB,EAAE,6BAWvC,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,0BAA0B,GAAI,MAAM,MAAM,kCAAuE,CAAC;AAE/H,uFAAuF;AACvF,eAAO,MAAM,2BAA2B,GAAI,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YAA8C,CAAC;AACvH,eAAO,MAAM,sBAAsB,GAAI,YAAY,OAAO;;CAA4D,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const legacyEnergentiDescriptor = {
|
|
2
|
+
protocolVersion: 1,
|
|
3
|
+
namespace: "energenti",
|
|
4
|
+
publisherPath: ["kepware"],
|
|
5
|
+
journalPath: ["kepwareJournal"],
|
|
6
|
+
runtimeRolePath: ["kepwareRuntimeRole"],
|
|
7
|
+
receiverRole: "admission",
|
|
8
|
+
defaultDataGroup: "kepware",
|
|
9
|
+
match: { path: ["sourceKind"], value: "kepware" },
|
|
10
|
+
manifests: ["hv", "vv", "jek"].map((id) => ({ id, file: `docs/kepware-groups/${id}.json` })),
|
|
11
|
+
legacy: true,
|
|
12
|
+
};
|
|
13
|
+
/** Frozen legacy declaration lookup; new packages use processGroupRuntime metadata. */
|
|
14
|
+
export const legacyDescriptorForPackage = (name) => (name === "uns-energenti" ? legacyEnergentiDescriptor : undefined);
|
|
15
|
+
/** Reject the historical receiver spelling when its installed capability is absent. */
|
|
16
|
+
export const isUnsupportedLegacyReceiver = (raw) => raw["kepwareRuntimeRole"] === "admission";
|
|
17
|
+
export const legacyPilotEnvironment = (localPilot) => ({ UNS_ENERGENTI_GROUP_PILOT: localPilot ? "1" : "0" });
|
|
18
|
+
//# sourceMappingURL=energenti-descriptor-v1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"energenti-descriptor-v1.js","sourceRoot":"","sources":["../../../src/runtime/compatibility/energenti-descriptor-v1.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,yBAAyB,GAAkC;IACtE,eAAe,EAAE,CAAC;IAClB,SAAS,EAAE,WAAW;IACtB,aAAa,EAAE,CAAC,SAAS,CAAC;IAC1B,WAAW,EAAE,CAAC,gBAAgB,CAAC;IAC/B,eAAe,EAAE,CAAC,oBAAoB,CAAC;IACvC,YAAY,EAAE,WAAW;IACzB,gBAAgB,EAAE,SAAS;IAC3B,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE;IACjD,SAAS,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,uBAAuB,EAAE,OAAO,EAAE,CAAC,CAAC;IAC5F,MAAM,EAAE,IAAI;CACb,CAAC;AAEF,uFAAuF;AACvF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,eAAe,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAE/H,uFAAuF;AACvF,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,GAA4B,EAAE,EAAE,CAAC,GAAG,CAAC,oBAAoB,CAAC,KAAK,WAAW,CAAC;AACvH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,UAAmB,EAAE,EAAE,CAAC,CAAC,EAAE,yBAAyB,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC","sourcesContent":["import type { ProcessGroupRuntimeDescriptor } from \"../process-group-contract.js\";\n\nexport const legacyEnergentiDescriptor: ProcessGroupRuntimeDescriptor = {\n protocolVersion: 1,\n namespace: \"energenti\",\n publisherPath: [\"kepware\"],\n journalPath: [\"kepwareJournal\"],\n runtimeRolePath: [\"kepwareRuntimeRole\"],\n receiverRole: \"admission\",\n defaultDataGroup: \"kepware\",\n match: { path: [\"sourceKind\"], value: \"kepware\" },\n manifests: [\"hv\", \"vv\", \"jek\"].map((id) => ({ id, file: `docs/kepware-groups/${id}.json` })),\n legacy: true,\n};\n\n/** Frozen legacy declaration lookup; new packages use processGroupRuntime metadata. */\nexport const legacyDescriptorForPackage = (name: string) => (name === \"uns-energenti\" ? legacyEnergentiDescriptor : undefined);\n\n/** Reject the historical receiver spelling when its installed capability is absent. */\nexport const isUnsupportedLegacyReceiver = (raw: Record<string, unknown>) => raw[\"kepwareRuntimeRole\"] === \"admission\";\nexport const legacyPilotEnvironment = (localPilot: boolean) => ({ UNS_ENERGENTI_GROUP_PILOT: localPilot ? \"1\" : \"0\" });\n"]}
|