@uns-kit/core 3.0.30 → 3.0.31
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 +18 -0
- package/dist/config/app-config.d.ts +1 -0
- package/dist/config/app-config.d.ts.map +1 -1
- package/dist/config/app-config.js.map +1 -1
- package/dist/runtime/mqtt-output-state-contract.d.ts +27 -0
- package/dist/runtime/mqtt-output-state-contract.d.ts.map +1 -0
- package/dist/runtime/mqtt-output-state-contract.js +32 -0
- package/dist/runtime/mqtt-output-state-contract.js.map +1 -0
- package/dist/runtime/mqtt-output-state-contract.json +8 -0
- package/dist/runtime/mqtt-output-state-program.d.mts +28 -0
- package/dist/runtime/mqtt-output-state-program.mjs +511 -0
- package/dist/uns-config/config-schema.d.ts +4 -0
- package/dist/uns-config/config-schema.d.ts.map +1 -1
- package/dist/uns-config/uns-core-schema.d.ts +8 -0
- package/dist/uns-config/uns-core-schema.d.ts.map +1 -1
- package/dist/uns-mqtt/publish-queue-contract.d.ts +4 -0
- package/dist/uns-mqtt/publish-queue-contract.d.ts.map +1 -1
- package/dist/uns-mqtt/publish-queue-contract.js +1 -0
- package/dist/uns-mqtt/publish-queue-contract.js.map +1 -1
- package/dist/uns-mqtt/publish-queue-store.d.ts +1 -0
- package/dist/uns-mqtt/publish-queue-store.d.ts.map +1 -1
- package/dist/uns-mqtt/publish-queue-store.js +38 -39
- package/dist/uns-mqtt/publish-queue-store.js.map +1 -1
- package/package.json +1 -1
package/MIGRATIONS.md
CHANGED
|
@@ -9,6 +9,24 @@ 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.31 - Explicit Runtime-managed MQTT outbox storage
|
|
13
|
+
|
|
14
|
+
For an optional Runtime-managed MQTT outbox set `output.publishQueue.storage` to
|
|
15
|
+
`runtime`, bind its literal two-component directory under the trusted
|
|
16
|
+
`UNS_MQTT_OUTPUT_STATE_ROOT` mount, and provision private per-Controller external
|
|
17
|
+
storage. Matching Controller/tools/helper support is required. Absence remains
|
|
18
|
+
`local` and preserves schema 1. Older strict parsers reject the new setting;
|
|
19
|
+
older stores reject managed schema 2. Never bypass this guard by removing fields.
|
|
20
|
+
Existing queues are preserved; automatic schema 1 adoption is not implemented.
|
|
21
|
+
|
|
22
|
+
Native backup helpers vendor the exact SDK data-only state program and finite
|
|
23
|
+
contract with package/version/SHA receipts. Queue state is a separate signed and
|
|
24
|
+
age-encrypted RTT capsule. Restoration requires an empty offline target and stopped
|
|
25
|
+
source, retains the original identity and blocks application startup until complete.
|
|
26
|
+
The ordinary RTT-only activation path refuses included queues. Buffered RAM is not
|
|
27
|
+
included by live disk capture. See `docs/mqtt-output-runtime-state.md` for the two
|
|
28
|
+
unrelated consumer examples, recovery sequence and delivery limits.
|
|
29
|
+
|
|
12
30
|
## 3.0.30 - Optional MQTT output buffering and durable admission
|
|
13
31
|
|
|
14
32
|
When crossing `<3.0.30` to `>=3.0.30`, output persistence remains optional.
|
|
@@ -115,6 +115,7 @@ export interface ProjectAppConfig {
|
|
|
115
115
|
/** Opt-in MQTT output persistence; does not persist input subscriptions. */
|
|
116
116
|
publishQueue?: {
|
|
117
117
|
mode: "buffered" | "durable";
|
|
118
|
+
storage?: "local" | "runtime";
|
|
118
119
|
directory: string;
|
|
119
120
|
maxRecords?: number;
|
|
120
121
|
maxBytes?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"app-config.d.ts","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"","sourcesContent":["/* Auto-generated. Do not edit by hand. */\nexport interface ProjectAppConfig {\n uns: {\n graphql: string;\n rest: string;\n /** Bearer token used for service-to-service access to the UNS instance. */\n token?: string | undefined;\n /** Email used when authenticating to graphql endpoint of the UNS instance. */\n email?: string | undefined;\n /** Password or secret value paired with the UNS email. */\n password?: string | undefined;\n instanceMode?: \"wait\" | \"force\" | \"handover\";\n /** Process name used in MQTT topics and logs. */\n processName: string;\n handover?: boolean;\n /** Opt-in controller-correlated MQTT handover protocol. */\n handoverProtocol?: \"correlated-v1\" | undefined;\n /** Optional PM2/controller supervisor guard settings for this RTT instance. */\n supervisor?: {\n /** Enable controller/PM2 supervisor handling for this RTT instance. */\n enabled?: boolean;\n /** Let PM2 restart the process when it exits unexpectedly. */\n restartOnExit?: boolean;\n /** Optional PM2 memory restart limit in megabytes. */\n maxMemoryMb?: number | undefined;\n /** Let the controller auto-start this instance when required system-service runtime signals are absent. */\n restartOnUnhealthy?: boolean;\n /** How long runtime signals must stay unhealthy before the controller supervisor can act. */\n unhealthyAfterMs?: number;\n /** Minimum time between controller supervisor restart attempts for this instance. */\n restartCooldownMs?: number;\n } | undefined;\n jwksWellKnownUrl?: string | undefined;\n kidWellKnownUrl?: string | undefined;\n env?: \"dev\" | \"staging\" | \"test\" | \"prod\";\n };\n logging?: {\n adapter?: string;\n host: string;\n port?: number;\n level?: \"error\" | \"warn\" | \"info\" | \"http\" | \"verbose\" | \"debug\" | \"silly\";\n } | undefined;\n input?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n } | undefined;\n output?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n /** Opt-in MQTT output persistence; does not persist input subscriptions. */\n publishQueue?: {\n mode: \"buffered\" | \"durable\";\n directory: string;\n maxRecords?: number;\n maxBytes?: number;\n memoryRecords?: number;\n memoryBytes?: number;\n maxMessageBytes?: number;\n maxDiskBytes?: number;\n minFreeBytes?: number;\n retryIntervalMs?: number;\n publishTimeoutMs?: number;\n } | undefined;\n } | undefined;\n infra: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n };\n devops?: {\n provider?: \"azure-devops\";\n organization: string;\n project?: string | undefined;\n } | undefined;\n}\n\nexport interface AppConfig extends ProjectAppConfig {}\n"]}
|
|
1
|
+
{"version":3,"file":"app-config.js","sourceRoot":"","sources":["../../src/config/app-config.ts"],"names":[],"mappings":"","sourcesContent":["/* Auto-generated. Do not edit by hand. */\nexport interface ProjectAppConfig {\n uns: {\n graphql: string;\n rest: string;\n /** Bearer token used for service-to-service access to the UNS instance. */\n token?: string | undefined;\n /** Email used when authenticating to graphql endpoint of the UNS instance. */\n email?: string | undefined;\n /** Password or secret value paired with the UNS email. */\n password?: string | undefined;\n instanceMode?: \"wait\" | \"force\" | \"handover\";\n /** Process name used in MQTT topics and logs. */\n processName: string;\n handover?: boolean;\n /** Opt-in controller-correlated MQTT handover protocol. */\n handoverProtocol?: \"correlated-v1\" | undefined;\n /** Optional PM2/controller supervisor guard settings for this RTT instance. */\n supervisor?: {\n /** Enable controller/PM2 supervisor handling for this RTT instance. */\n enabled?: boolean;\n /** Let PM2 restart the process when it exits unexpectedly. */\n restartOnExit?: boolean;\n /** Optional PM2 memory restart limit in megabytes. */\n maxMemoryMb?: number | undefined;\n /** Let the controller auto-start this instance when required system-service runtime signals are absent. */\n restartOnUnhealthy?: boolean;\n /** How long runtime signals must stay unhealthy before the controller supervisor can act. */\n unhealthyAfterMs?: number;\n /** Minimum time between controller supervisor restart attempts for this instance. */\n restartCooldownMs?: number;\n } | undefined;\n jwksWellKnownUrl?: string | undefined;\n kidWellKnownUrl?: string | undefined;\n env?: \"dev\" | \"staging\" | \"test\" | \"prod\";\n };\n logging?: {\n adapter?: string;\n host: string;\n port?: number;\n level?: \"error\" | \"warn\" | \"info\" | \"http\" | \"verbose\" | \"debug\" | \"silly\";\n } | undefined;\n input?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n } | undefined;\n output?: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n /** Opt-in MQTT output persistence; does not persist input subscriptions. */\n publishQueue?: {\n mode: \"buffered\" | \"durable\";\n storage?: \"local\" | \"runtime\";\n directory: string;\n maxRecords?: number;\n maxBytes?: number;\n memoryRecords?: number;\n memoryBytes?: number;\n maxMessageBytes?: number;\n maxDiskBytes?: number;\n minFreeBytes?: number;\n retryIntervalMs?: number;\n publishTimeoutMs?: number;\n } | undefined;\n } | undefined;\n infra: {\n host?: string | undefined;\n hosts?: string[] | undefined;\n servers?: {\n host: string;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n }[] | undefined;\n port?: number | undefined;\n protocol?: (\"mqtt\" | \"mqtts\" | \"ws\" | \"wss\" | \"tcp\" | \"ssl\") | undefined;\n username?: string | undefined;\n password?: string | undefined;\n clientId?: string | undefined;\n clean?: boolean | undefined;\n keepalive?: number | undefined;\n connectTimeout?: number | undefined;\n reconnectPeriod?: number | undefined;\n reconnectOnConnackError?: boolean | undefined;\n resubscribe?: boolean | undefined;\n queueQoSZero?: boolean | undefined;\n rejectUnauthorized?: boolean | undefined;\n properties?: {\n sessionExpiryInterval?: number | undefined;\n receiveMaximum?: number | undefined;\n maximumPacketSize?: number | undefined;\n topicAliasMaximum?: number | undefined;\n requestResponseInformation?: boolean | undefined;\n requestProblemInformation?: boolean | undefined;\n userProperties?: {\n [x: string]: string;\n } | undefined;\n } | undefined;\n ca?: string | undefined;\n cert?: string | undefined;\n key?: string | undefined;\n servername?: string | undefined;\n };\n devops?: {\n provider?: \"azure-devops\";\n organization: string;\n project?: string | undefined;\n } | undefined;\n}\n\nexport interface AppConfig extends ProjectAppConfig {}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const mqttOutputStateLimits: Readonly<{
|
|
3
|
+
schemaVersion: 1;
|
|
4
|
+
maximumQueues: 32;
|
|
5
|
+
maximumRecordsPerQueue: 100000;
|
|
6
|
+
maximumManifestBytes: 262144;
|
|
7
|
+
maximumSnapshotBytes: 536870912;
|
|
8
|
+
maximumCapsuleBytes: 1073741824;
|
|
9
|
+
}>;
|
|
10
|
+
export declare const mqttOutputStateBackupSchema: z.ZodObject<{
|
|
11
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
12
|
+
state: z.ZodEnum<{
|
|
13
|
+
included: "included";
|
|
14
|
+
empty: "empty";
|
|
15
|
+
}>;
|
|
16
|
+
rootSha256: z.ZodString;
|
|
17
|
+
queues: z.ZodNumber;
|
|
18
|
+
records: z.ZodNumber;
|
|
19
|
+
bytes: z.ZodNumber;
|
|
20
|
+
sha256: z.ZodString;
|
|
21
|
+
}, z.core.$strict>;
|
|
22
|
+
/** Controller inspection of standard SDK configuration, without application code. */
|
|
23
|
+
export declare function managedMqttOutputBinding(config: unknown, root: string | undefined): {
|
|
24
|
+
directory: string;
|
|
25
|
+
relative: string;
|
|
26
|
+
} | undefined;
|
|
27
|
+
//# sourceMappingURL=mqtt-output-state-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mqtt-output-state-contract.d.ts","sourceRoot":"","sources":["../../src/runtime/mqtt-output-state-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,eAAO,MAAM,qBAAqB;;;;;;;EAA2B,CAAC;AAC9D,eAAO,MAAM,2BAA2B;;;;;;;;;;;kBAcqD,CAAC;AAE9F,qFAAqF;AACrF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAOvI"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { mqttPublishQueueSchema } from "../uns-mqtt/publish-queue-contract.js";
|
|
3
|
+
import fixed from "./mqtt-output-state-contract.json" with { type: "json" };
|
|
4
|
+
import { outputStateLimits } from "./mqtt-output-state-program.mjs";
|
|
5
|
+
import { outputStateRelative } from "./mqtt-output-state-program.mjs";
|
|
6
|
+
export const mqttOutputStateLimits = outputStateLimits(fixed);
|
|
7
|
+
export const mqttOutputStateBackupSchema = z
|
|
8
|
+
.strictObject({
|
|
9
|
+
schemaVersion: z.literal(1),
|
|
10
|
+
state: z.enum(["included", "empty"]),
|
|
11
|
+
rootSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
12
|
+
queues: z.number().int().min(0).max(mqttOutputStateLimits.maximumQueues),
|
|
13
|
+
records: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.min(0)
|
|
17
|
+
.max(mqttOutputStateLimits.maximumQueues * mqttOutputStateLimits.maximumRecordsPerQueue),
|
|
18
|
+
bytes: z.number().int().positive().max(mqttOutputStateLimits.maximumCapsuleBytes),
|
|
19
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
20
|
+
})
|
|
21
|
+
.refine((v) => (v.state === "included" ? v.queues > 0 : v.queues === 0 && v.records === 0));
|
|
22
|
+
/** Controller inspection of standard SDK configuration, without application code. */
|
|
23
|
+
export function managedMqttOutputBinding(config, root) {
|
|
24
|
+
const value = config;
|
|
25
|
+
if (value?.output?.publishQueue === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
const queue = mqttPublishQueueSchema.parse(value.output.publishQueue);
|
|
28
|
+
if (queue.storage !== "runtime" || !root)
|
|
29
|
+
throw Error("Controller-managed MQTT output requires storage=runtime and a reviewed persistent Runtime mount");
|
|
30
|
+
return { directory: queue.directory, relative: outputStateRelative(root, queue.directory) };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=mqtt-output-state-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mqtt-output-state-contract.js","sourceRoot":"","sources":["../../src/runtime/mqtt-output-state-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,KAAK,MAAM,mCAAmC,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAEtE,MAAM,CAAC,MAAM,qBAAqB,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;AAC9D,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC;KACzC,YAAY,CAAC;IACZ,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACpC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC;IAC9C,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,qBAAqB,CAAC,aAAa,CAAC;IACxE,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,qBAAqB,CAAC,aAAa,GAAG,qBAAqB,CAAC,sBAAsB,CAAC;IAC1F,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,qBAAqB,CAAC,mBAAmB,CAAC;IACjF,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC;CAC3C,CAAC;KACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;AAE9F,qFAAqF;AACrF,MAAM,UAAU,wBAAwB,CAAC,MAAe,EAAE,IAAwB;IAChF,MAAM,KAAK,GAAG,MAAiD,CAAC;IAChE,IAAI,KAAK,EAAE,MAAM,EAAE,YAAY,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACtE,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,IAAI;QACtC,MAAM,KAAK,CAAC,iGAAiG,CAAC,CAAC;IACjH,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;AAC9F,CAAC","sourcesContent":["import { z } from \"zod\";\n\nimport { mqttPublishQueueSchema } from \"../uns-mqtt/publish-queue-contract.js\";\nimport fixed from \"./mqtt-output-state-contract.json\" with { type: \"json\" };\nimport { outputStateLimits } from \"./mqtt-output-state-program.mjs\";\nimport { outputStateRelative } from \"./mqtt-output-state-program.mjs\";\n\nexport const mqttOutputStateLimits = outputStateLimits(fixed);\nexport const mqttOutputStateBackupSchema = z\n .strictObject({\n schemaVersion: z.literal(1),\n state: z.enum([\"included\", \"empty\"]),\n rootSha256: z.string().regex(/^[a-f0-9]{64}$/),\n queues: z.number().int().min(0).max(mqttOutputStateLimits.maximumQueues),\n records: z\n .number()\n .int()\n .min(0)\n .max(mqttOutputStateLimits.maximumQueues * mqttOutputStateLimits.maximumRecordsPerQueue),\n bytes: z.number().int().positive().max(mqttOutputStateLimits.maximumCapsuleBytes),\n sha256: z.string().regex(/^[a-f0-9]{64}$/),\n })\n .refine((v) => (v.state === \"included\" ? v.queues > 0 : v.queues === 0 && v.records === 0));\n\n/** Controller inspection of standard SDK configuration, without application code. */\nexport function managedMqttOutputBinding(config: unknown, root: string | undefined): { directory: string; relative: string } | undefined {\n const value = config as { output?: { publishQueue?: unknown } };\n if (value?.output?.publishQueue === undefined) return undefined;\n const queue = mqttPublishQueueSchema.parse(value.output.publishQueue);\n if (queue.storage !== \"runtime\" || !root)\n throw Error(\"Controller-managed MQTT output requires storage=runtime and a reviewed persistent Runtime mount\");\n return { directory: queue.directory, relative: outputStateRelative(root, queue.directory) };\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function openManagedOutputRoot(
|
|
2
|
+
directory: string,
|
|
3
|
+
root: string | undefined,
|
|
4
|
+
): { rootId: string; relative: string; schema: "2"; close(): void } | undefined;
|
|
5
|
+
export function validateOutputRow(row: unknown): {
|
|
6
|
+
seq: number;
|
|
7
|
+
id: string;
|
|
8
|
+
topic: string;
|
|
9
|
+
message: string;
|
|
10
|
+
options: { qos: 1 | 2; retain: boolean };
|
|
11
|
+
bytes: number;
|
|
12
|
+
};
|
|
13
|
+
export function outputStateLimits(
|
|
14
|
+
value: unknown,
|
|
15
|
+
): Readonly<{
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
maximumQueues: 32;
|
|
18
|
+
maximumRecordsPerQueue: 100000;
|
|
19
|
+
maximumManifestBytes: 262144;
|
|
20
|
+
maximumSnapshotBytes: 536870912;
|
|
21
|
+
maximumCapsuleBytes: 1073741824;
|
|
22
|
+
}>;
|
|
23
|
+
export function runOutputStateProgram(args: string[]): Promise<unknown>;
|
|
24
|
+
export function validateOutputSnapshot(directory: string, limits: unknown): { queues: unknown[]; rootId: string };
|
|
25
|
+
|
|
26
|
+
export function outputStateRelative(root: string, directory: string): string;
|
|
27
|
+
|
|
28
|
+
export function outputStateMain(args?: string[]): Promise<void>;
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// SDK-owned data-only runtime/snapshot/restore contract. Never imports an application.
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url),
|
|
9
|
+
sha = (x) => crypto.createHash("sha256").update(x).digest("hex");
|
|
10
|
+
const guardName = ".mqtt-output-root.sqlite",
|
|
11
|
+
markerName = ".mqtt-output-restore.json",
|
|
12
|
+
stageName = ".mqtt-output-restore-stage";
|
|
13
|
+
const safeName = /^[a-z][a-z0-9-]{0,99}$/,
|
|
14
|
+
hex = /^[a-f0-9]{64}$/,
|
|
15
|
+
uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
|
|
16
|
+
const exactKeys = (value, keys) =>
|
|
17
|
+
value &&
|
|
18
|
+
typeof value === "object" &&
|
|
19
|
+
!Array.isArray(value) &&
|
|
20
|
+
Object.keys(value).length === keys.length &&
|
|
21
|
+
keys.every((k) => Object.hasOwn(value, k));
|
|
22
|
+
const fail = () => {
|
|
23
|
+
throw Error("MQTT output state validation failed; preserve storage and review the operation");
|
|
24
|
+
};
|
|
25
|
+
const sqlite = () => require("node:sqlite");
|
|
26
|
+
function hashFile(file) {
|
|
27
|
+
const hash = crypto.createHash("sha256"),
|
|
28
|
+
buffer = Buffer.allocUnsafe(65536),
|
|
29
|
+
fd = fs.openSync(file, "r");
|
|
30
|
+
try {
|
|
31
|
+
let n;
|
|
32
|
+
while ((n = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, n));
|
|
33
|
+
return hash.digest("hex");
|
|
34
|
+
} finally {
|
|
35
|
+
fs.closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function outputStateLimits(value) {
|
|
39
|
+
const expected = {
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
maximumQueues: 32,
|
|
42
|
+
maximumRecordsPerQueue: 100000,
|
|
43
|
+
maximumManifestBytes: 262144,
|
|
44
|
+
maximumSnapshotBytes: 536870912,
|
|
45
|
+
maximumCapsuleBytes: 1073741824,
|
|
46
|
+
};
|
|
47
|
+
if (!value || Object.keys(value).length !== Object.keys(expected).length || Object.entries(expected).some(([k, v]) => value[k] !== v)) fail();
|
|
48
|
+
return Object.freeze({ ...expected });
|
|
49
|
+
}
|
|
50
|
+
function check(p, directory = false, optional = false) {
|
|
51
|
+
let s;
|
|
52
|
+
try {
|
|
53
|
+
s = fs.lstatSync(p);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if (optional && e.code === "ENOENT") return;
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
s.isSymbolicLink() ||
|
|
60
|
+
s.uid !== process.getuid?.() ||
|
|
61
|
+
s.mode & 0o077 ||
|
|
62
|
+
(!directory && (!s.isFile() || s.nlink !== 1)) ||
|
|
63
|
+
(directory && !s.isDirectory())
|
|
64
|
+
)
|
|
65
|
+
fail();
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
68
|
+
function noLinks(p) {
|
|
69
|
+
for (let q = p; q !== "/"; q = path.dirname(q)) {
|
|
70
|
+
try {
|
|
71
|
+
if (fs.lstatSync(q).isSymbolicLink()) fail();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e.code !== "ENOENT") throw e;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function privateJson(p) {
|
|
78
|
+
const s = check(p);
|
|
79
|
+
if (s.size > 262144) fail();
|
|
80
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
81
|
+
}
|
|
82
|
+
function atomicJson(p, value) {
|
|
83
|
+
const temp = p + ".writing-" + crypto.randomUUID();
|
|
84
|
+
let fd;
|
|
85
|
+
try {
|
|
86
|
+
fd = fs.openSync(temp, "wx", 0o600);
|
|
87
|
+
fs.writeFileSync(fd, JSON.stringify(value) + "\n");
|
|
88
|
+
fs.fsyncSync(fd);
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
fd = undefined;
|
|
91
|
+
fs.renameSync(temp, p);
|
|
92
|
+
sync(path.dirname(p));
|
|
93
|
+
} finally {
|
|
94
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
95
|
+
if (fs.existsSync(temp)) fs.unlinkSync(temp);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function sync(directory) {
|
|
99
|
+
const fd = fs.openSync(directory, "r");
|
|
100
|
+
try {
|
|
101
|
+
fs.fsyncSync(fd);
|
|
102
|
+
} finally {
|
|
103
|
+
fs.closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function assertOutputStateRoot(root) {
|
|
107
|
+
if (process.platform !== "linux" || typeof root !== "string" || root !== path.resolve(root) || !root.startsWith("/var/lib/")) fail();
|
|
108
|
+
noLinks(root);
|
|
109
|
+
check(root, true);
|
|
110
|
+
const mount = fs
|
|
111
|
+
.readFileSync("/proc/self/mountinfo", "utf8")
|
|
112
|
+
.split("\n")
|
|
113
|
+
.map((l) => l.split(" "))
|
|
114
|
+
.find((f) => f[4] === root);
|
|
115
|
+
if (!mount || !["ext4", "xfs", "btrfs", "zfs"].includes(mount[mount.indexOf("-") + 1])) fail();
|
|
116
|
+
}
|
|
117
|
+
export function outputStateRelative(root, directory) {
|
|
118
|
+
if (directory !== path.resolve(directory)) fail();
|
|
119
|
+
const rel = path.relative(root, directory).split(path.sep);
|
|
120
|
+
if (rel.length !== 2 || rel.some((x) => !safeName.test(x))) fail();
|
|
121
|
+
return rel.join("/");
|
|
122
|
+
}
|
|
123
|
+
function rootLease(root, exclusive = false) {
|
|
124
|
+
assertOutputStateRoot(root);
|
|
125
|
+
const file = path.join(root, guardName);
|
|
126
|
+
try {
|
|
127
|
+
fs.closeSync(fs.openSync(file, "wx", 0o600));
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (e.code !== "EEXIST") throw e;
|
|
130
|
+
}
|
|
131
|
+
check(file);
|
|
132
|
+
check(file + "-journal", false, true);
|
|
133
|
+
const { DatabaseSync } = sqlite(),
|
|
134
|
+
db = new DatabaseSync(file);
|
|
135
|
+
try {
|
|
136
|
+
db.exec("PRAGMA busy_timeout=3000; PRAGMA synchronous=FULL");
|
|
137
|
+
if (db.prepare("PRAGMA journal_mode").get().journal_mode !== "delete") fail();
|
|
138
|
+
// Concurrent first owners serialize initialization, then retain shared read leases.
|
|
139
|
+
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='metadata'").get()) {
|
|
140
|
+
db.exec("PRAGMA busy_timeout=3000; BEGIN IMMEDIATE");
|
|
141
|
+
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='metadata'").get()) {
|
|
142
|
+
if (db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().length) fail();
|
|
143
|
+
db.exec("CREATE TABLE metadata(key TEXT PRIMARY KEY,value TEXT NOT NULL)");
|
|
144
|
+
db.prepare("INSERT INTO metadata VALUES(?,?)").run("schema", "mqtt-output-root-1");
|
|
145
|
+
db.prepare("INSERT INTO metadata VALUES(?,?)").run("id", crypto.randomUUID());
|
|
146
|
+
db.exec("COMMIT");
|
|
147
|
+
sync(root);
|
|
148
|
+
} else db.exec("ROLLBACK");
|
|
149
|
+
}
|
|
150
|
+
db.exec(exclusive ? "BEGIN EXCLUSIVE" : "BEGIN");
|
|
151
|
+
const meta = Object.fromEntries(
|
|
152
|
+
db
|
|
153
|
+
.prepare("SELECT key,value FROM metadata")
|
|
154
|
+
.all()
|
|
155
|
+
.map((r) => [r.key, r.value]),
|
|
156
|
+
);
|
|
157
|
+
if (!exactKeys(meta, ["schema", "id"]) || meta.schema !== "mqtt-output-root-1" || !uuid.test(meta.id)) fail();
|
|
158
|
+
return { db, rootId: meta.id, close: () => db.close() };
|
|
159
|
+
} catch (e) {
|
|
160
|
+
db.close();
|
|
161
|
+
throw e;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function openManagedOutputRoot(directory, root) {
|
|
165
|
+
if (!root) return undefined;
|
|
166
|
+
assertOutputStateRoot(root);
|
|
167
|
+
const relative = outputStateRelative(root, directory);
|
|
168
|
+
noLinks(directory);
|
|
169
|
+
for (let p = path.dirname(directory); p !== root; p = path.dirname(p)) check(p, true, true);
|
|
170
|
+
const lease = rootLease(root);
|
|
171
|
+
try {
|
|
172
|
+
const marker = path.join(root, markerName);
|
|
173
|
+
if (fs.existsSync(marker)) {
|
|
174
|
+
const m = privateJson(marker);
|
|
175
|
+
if (
|
|
176
|
+
!exactKeys(m, ["schemaVersion", "state", "sourceSha", "rootId"]) ||
|
|
177
|
+
m.schemaVersion !== 1 ||
|
|
178
|
+
m.state !== "complete" ||
|
|
179
|
+
!hex.test(m.sourceSha) ||
|
|
180
|
+
m.rootId !== lease.rootId
|
|
181
|
+
)
|
|
182
|
+
throw Error("MQTT output restoration is incomplete; reconcile the same restore before starting");
|
|
183
|
+
}
|
|
184
|
+
return { rootId: lease.rootId, relative, schema: "2", close: lease.close };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
lease.close();
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export function validateOutputRow(row) {
|
|
191
|
+
if (typeof row.record !== "string" || sha(row.record) !== row.sha256) throw Error("MQTT outbox integrity check failed; pending work preserved");
|
|
192
|
+
const r = JSON.parse(row.record);
|
|
193
|
+
if (
|
|
194
|
+
!exactKeys(r, ["seq", "id", "topic", "message", "options", "bytes"]) ||
|
|
195
|
+
r.seq !== Number(row.seq) ||
|
|
196
|
+
r.id !== row.id ||
|
|
197
|
+
r.bytes !== Number(row.bytes) ||
|
|
198
|
+
!Number.isSafeInteger(r.seq) ||
|
|
199
|
+
r.seq < 1 ||
|
|
200
|
+
typeof r.id !== "string" ||
|
|
201
|
+
!r.id ||
|
|
202
|
+
r.id.length > 200 ||
|
|
203
|
+
typeof r.topic !== "string" ||
|
|
204
|
+
!r.topic ||
|
|
205
|
+
/[\0+#]/.test(r.topic) ||
|
|
206
|
+
Buffer.byteLength(r.topic) > 65535 ||
|
|
207
|
+
typeof r.message !== "string" ||
|
|
208
|
+
!r.options ||
|
|
209
|
+
![1, 2].includes(r.options.qos) ||
|
|
210
|
+
typeof r.options.retain !== "boolean" ||
|
|
211
|
+
Object.keys(r.options).some((k) => !["qos", "retain"].includes(k)) ||
|
|
212
|
+
r.bytes !== Buffer.byteLength(r.topic) + Buffer.byteLength(r.message) + 256
|
|
213
|
+
)
|
|
214
|
+
fail();
|
|
215
|
+
return r;
|
|
216
|
+
}
|
|
217
|
+
function inspectDatabase(file, binding, limits) {
|
|
218
|
+
check(file);
|
|
219
|
+
const { DatabaseSync } = sqlite(),
|
|
220
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
221
|
+
try {
|
|
222
|
+
if (db.prepare("PRAGMA quick_check").get().quick_check !== "ok") fail();
|
|
223
|
+
const meta = Object.fromEntries(
|
|
224
|
+
db
|
|
225
|
+
.prepare("SELECT key,value FROM metadata")
|
|
226
|
+
.all()
|
|
227
|
+
.map((r) => [r.key, r.value]),
|
|
228
|
+
);
|
|
229
|
+
if (
|
|
230
|
+
meta.schema !== "2" ||
|
|
231
|
+
meta.rootId !== binding.rootId ||
|
|
232
|
+
meta.relative !== binding.relative ||
|
|
233
|
+
!hex.test(meta.identity) ||
|
|
234
|
+
Object.keys(meta).some((k) => !["schema", "rootId", "relative", "identity"].includes(k))
|
|
235
|
+
)
|
|
236
|
+
fail();
|
|
237
|
+
let records = 0,
|
|
238
|
+
bytes = 0,
|
|
239
|
+
lastSeq = 0;
|
|
240
|
+
for (const row of db.prepare("SELECT seq,id,record,bytes,sha256 FROM pending ORDER BY seq").iterate()) {
|
|
241
|
+
const r = validateOutputRow(row);
|
|
242
|
+
records++;
|
|
243
|
+
bytes += r.bytes;
|
|
244
|
+
lastSeq = r.seq;
|
|
245
|
+
if (records > limits.maximumRecordsPerQueue || bytes > limits.maximumSnapshotBytes) fail();
|
|
246
|
+
}
|
|
247
|
+
return { identity: meta.identity, records, bytes, lastSeq };
|
|
248
|
+
} finally {
|
|
249
|
+
db.close();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function manifestBytes(directory) {
|
|
253
|
+
const file = path.join(directory, "manifest.json");
|
|
254
|
+
if (check(file).size > 262144) fail();
|
|
255
|
+
return fs.readFileSync(file);
|
|
256
|
+
}
|
|
257
|
+
export function validateOutputSnapshot(directory, suppliedLimits) {
|
|
258
|
+
const limits = outputStateLimits(suppliedLimits);
|
|
259
|
+
noLinks(directory);
|
|
260
|
+
check(directory, true);
|
|
261
|
+
const data = manifestBytes(directory);
|
|
262
|
+
if (data.length > limits.maximumManifestBytes) fail();
|
|
263
|
+
const manifest = JSON.parse(data);
|
|
264
|
+
if (
|
|
265
|
+
!exactKeys(manifest, ["schemaVersion", "kind", "rootId", "capturedAt", "restorePolicy", "queues"]) ||
|
|
266
|
+
manifest.schemaVersion !== 1 ||
|
|
267
|
+
manifest.kind !== "uns-mqtt-output-state-snapshot" ||
|
|
268
|
+
!uuid.test(manifest.rootId) ||
|
|
269
|
+
typeof manifest.capturedAt !== "string" ||
|
|
270
|
+
!Number.isFinite(Date.parse(manifest.capturedAt)) ||
|
|
271
|
+
manifest.restorePolicy !== "offline-source-stop-confirmation-required-no-replay" ||
|
|
272
|
+
!Array.isArray(manifest.queues) ||
|
|
273
|
+
manifest.queues.length > limits.maximumQueues
|
|
274
|
+
)
|
|
275
|
+
fail();
|
|
276
|
+
const expected = new Set(["manifest.json"]),
|
|
277
|
+
directories = new Set();
|
|
278
|
+
let size = 0;
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
for (const q of manifest.queues) {
|
|
281
|
+
if (
|
|
282
|
+
!exactKeys(q, ["path", "file", "size", "sha256", "identity", "records", "bytes", "lastSeq"]) ||
|
|
283
|
+
q.path !== outputStateRelative("/", "/" + q.path) ||
|
|
284
|
+
seen.has(q.path) ||
|
|
285
|
+
q.file !== q.path + "/mqtt-outbox.sqlite" ||
|
|
286
|
+
!hex.test(q.sha256) ||
|
|
287
|
+
!hex.test(q.identity)
|
|
288
|
+
)
|
|
289
|
+
fail();
|
|
290
|
+
seen.add(q.path);
|
|
291
|
+
directories.add(q.path.split("/")[0]);
|
|
292
|
+
directories.add(q.path);
|
|
293
|
+
const file = path.join(directory, q.file);
|
|
294
|
+
noLinks(file);
|
|
295
|
+
const s = check(file);
|
|
296
|
+
size += s.size;
|
|
297
|
+
if (s.size !== q.size || size > limits.maximumSnapshotBytes || hashFile(file) !== q.sha256) fail();
|
|
298
|
+
const actual = inspectDatabase(file, { rootId: manifest.rootId, relative: q.path }, limits);
|
|
299
|
+
if (["identity", "records", "bytes", "lastSeq"].some((k) => actual[k] !== q[k])) fail();
|
|
300
|
+
expected.add(q.file);
|
|
301
|
+
}
|
|
302
|
+
// No undeclared files, symlinks or SQLite sidecars enter a restore.
|
|
303
|
+
function walk(dir, rel = "") {
|
|
304
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
305
|
+
const n = rel ? rel + "/" + e.name : e.name;
|
|
306
|
+
if (e.isSymbolicLink()) fail();
|
|
307
|
+
if (e.isDirectory()) {
|
|
308
|
+
if (!directories.has(n)) fail();
|
|
309
|
+
check(path.join(dir, e.name), true);
|
|
310
|
+
walk(path.join(dir, e.name), n);
|
|
311
|
+
} else if (!expected.has(n)) fail();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
walk(directory);
|
|
315
|
+
return manifest;
|
|
316
|
+
}
|
|
317
|
+
async function capture(root, out, limits) {
|
|
318
|
+
check(out, true);
|
|
319
|
+
if (fs.readdirSync(out).length) fail();
|
|
320
|
+
const lease = rootLease(root);
|
|
321
|
+
const manifest = {
|
|
322
|
+
schemaVersion: 1,
|
|
323
|
+
kind: "uns-mqtt-output-state-snapshot",
|
|
324
|
+
rootId: lease.rootId,
|
|
325
|
+
capturedAt: new Date().toISOString(),
|
|
326
|
+
restorePolicy: "offline-source-stop-confirmation-required-no-replay",
|
|
327
|
+
queues: [],
|
|
328
|
+
};
|
|
329
|
+
let total = 0;
|
|
330
|
+
try {
|
|
331
|
+
const marker = path.join(root, markerName);
|
|
332
|
+
if (fs.existsSync(marker) && privateJson(marker).state !== "complete") fail();
|
|
333
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
334
|
+
if ([guardName, guardName + "-journal", markerName].includes(e.name)) {
|
|
335
|
+
check(path.join(root, e.name));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!e.isDirectory() || !safeName.test(e.name)) fail();
|
|
339
|
+
const ns = path.join(root, e.name);
|
|
340
|
+
check(ns, true);
|
|
341
|
+
for (const id of fs.readdirSync(ns).sort()) {
|
|
342
|
+
if (!safeName.test(id) || manifest.queues.length >= limits.maximumQueues) fail();
|
|
343
|
+
const relative = e.name + "/" + id,
|
|
344
|
+
source = path.join(ns, id);
|
|
345
|
+
noLinks(source);
|
|
346
|
+
check(source, true);
|
|
347
|
+
const allowed = new Set([
|
|
348
|
+
"mqtt-outbox.sqlite",
|
|
349
|
+
"mqtt-outbox.sqlite-wal",
|
|
350
|
+
"mqtt-outbox.sqlite-shm",
|
|
351
|
+
"mqtt-outbox-owner.sqlite",
|
|
352
|
+
"mqtt-outbox-owner.sqlite-journal",
|
|
353
|
+
]);
|
|
354
|
+
for (const f of fs.readdirSync(source)) {
|
|
355
|
+
if (!allowed.has(f)) fail();
|
|
356
|
+
const s = check(path.join(source, f));
|
|
357
|
+
if (s.size > limits.maximumSnapshotBytes) fail();
|
|
358
|
+
}
|
|
359
|
+
const main = path.join(source, "mqtt-outbox.sqlite"),
|
|
360
|
+
before = check(main);
|
|
361
|
+
const targetDir = path.join(out, relative);
|
|
362
|
+
fs.mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
363
|
+
const dest = path.join(targetDir, "mqtt-outbox.sqlite");
|
|
364
|
+
const { DatabaseSync, backup } = sqlite(),
|
|
365
|
+
db = new DatabaseSync(main, { readOnly: true });
|
|
366
|
+
try {
|
|
367
|
+
await backup(db, dest, {
|
|
368
|
+
rate: 128,
|
|
369
|
+
progress: ({ totalPages }) => {
|
|
370
|
+
if (totalPages * 4096 > limits.maximumSnapshotBytes) fail();
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
} finally {
|
|
374
|
+
db.close();
|
|
375
|
+
}
|
|
376
|
+
fs.chmodSync(dest, 0o600);
|
|
377
|
+
const frozen = new (sqlite().DatabaseSync)(dest);
|
|
378
|
+
try {
|
|
379
|
+
frozen.exec("PRAGMA journal_mode=DELETE");
|
|
380
|
+
} finally {
|
|
381
|
+
frozen.close();
|
|
382
|
+
}
|
|
383
|
+
const after = check(main);
|
|
384
|
+
if (before.ino !== after.ino || before.dev !== after.dev) fail();
|
|
385
|
+
const s = check(dest);
|
|
386
|
+
total += s.size;
|
|
387
|
+
if (total > limits.maximumSnapshotBytes) fail();
|
|
388
|
+
const data = inspectDatabase(dest, { rootId: lease.rootId, relative }, limits);
|
|
389
|
+
manifest.queues.push({ path: relative, file: relative + "/mqtt-outbox.sqlite", size: s.size, sha256: hashFile(dest), ...data });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const encoded = JSON.stringify(manifest) + "\n";
|
|
393
|
+
if (Buffer.byteLength(encoded) > limits.maximumManifestBytes) fail();
|
|
394
|
+
fs.writeFileSync(path.join(out, "manifest.json"), encoded, { mode: 0o600 });
|
|
395
|
+
sync(out);
|
|
396
|
+
return manifest;
|
|
397
|
+
} finally {
|
|
398
|
+
lease.close();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function restore(source, root, limits, sourceStopped) {
|
|
402
|
+
if (sourceStopped !== "source-stopped") throw Error("Explicit confirmation of the stopped source is required");
|
|
403
|
+
const manifest = validateOutputSnapshot(source, limits),
|
|
404
|
+
sourceSha = sha(manifestBytes(source));
|
|
405
|
+
assertOutputStateRoot(root);
|
|
406
|
+
// Empty or same-operation target only. An exclusive root lease excludes live SDK writers.
|
|
407
|
+
for (const name of fs.readdirSync(root))
|
|
408
|
+
if (![guardName, guardName + "-journal"].includes(name) && !fs.existsSync(path.join(root, markerName)))
|
|
409
|
+
throw Error("MQTT restore target is not empty; existing state is preserved");
|
|
410
|
+
const lease = rootLease(root, true),
|
|
411
|
+
marker = path.join(root, markerName),
|
|
412
|
+
stage = path.join(root, stageName);
|
|
413
|
+
try {
|
|
414
|
+
let prior;
|
|
415
|
+
if (fs.existsSync(marker)) {
|
|
416
|
+
prior = privateJson(marker);
|
|
417
|
+
if (
|
|
418
|
+
!exactKeys(prior, ["schemaVersion", "state", "sourceSha", "rootId"]) ||
|
|
419
|
+
prior.schemaVersion !== 1 ||
|
|
420
|
+
prior.sourceSha !== sourceSha ||
|
|
421
|
+
!["applying", "complete"].includes(prior.state)
|
|
422
|
+
)
|
|
423
|
+
fail();
|
|
424
|
+
}
|
|
425
|
+
const allowed = new Set([guardName, guardName + "-journal", markerName, stageName, ...manifest.queues.map((q) => q.path.split("/")[0])]);
|
|
426
|
+
for (const e of fs.readdirSync(root)) {
|
|
427
|
+
if (!allowed.has(e) || (!prior && ![guardName, guardName + "-journal"].includes(e)))
|
|
428
|
+
throw Error("MQTT restore target is not empty; existing state is preserved");
|
|
429
|
+
}
|
|
430
|
+
// A resumed restore never adopts undeclared children inside a known namespace.
|
|
431
|
+
for (const ns of new Set(manifest.queues.map((q) => q.path.split("/")[0]))) {
|
|
432
|
+
const dir = path.join(root, ns);
|
|
433
|
+
if (fs.existsSync(dir)) {
|
|
434
|
+
noLinks(dir);
|
|
435
|
+
check(dir, true);
|
|
436
|
+
for (const id of fs.readdirSync(dir)) if (!manifest.queues.some((q) => q.path === ns + "/" + id)) fail();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (!prior) {
|
|
440
|
+
atomicJson(marker, { schemaVersion: 1, state: "applying", sourceSha, rootId: manifest.rootId });
|
|
441
|
+
fs.mkdirSync(stage, { mode: 0o700 });
|
|
442
|
+
} else if (prior.rootId !== manifest.rootId) fail();
|
|
443
|
+
lease.db.prepare("UPDATE metadata SET value=? WHERE key='id'").run(manifest.rootId);
|
|
444
|
+
for (const q of manifest.queues) {
|
|
445
|
+
const target = path.join(root, q.path),
|
|
446
|
+
file = path.join(target, "mqtt-outbox.sqlite");
|
|
447
|
+
noLinks(target);
|
|
448
|
+
if (fs.existsSync(target)) {
|
|
449
|
+
check(target, true);
|
|
450
|
+
if (fs.readdirSync(target).some((x) => x !== "mqtt-outbox.sqlite") || hashFile(file) !== q.sha256) fail();
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (prior?.state === "complete") fail();
|
|
454
|
+
const staged = path.join(stage, q.path);
|
|
455
|
+
fs.mkdirSync(staged, { recursive: true, mode: 0o700 });
|
|
456
|
+
const copy = path.join(staged, "mqtt-outbox.sqlite");
|
|
457
|
+
if (fs.existsSync(copy)) {
|
|
458
|
+
check(copy);
|
|
459
|
+
if (hashFile(copy) !== q.sha256) fail();
|
|
460
|
+
} else {
|
|
461
|
+
fs.copyFileSync(path.join(source, q.file), copy, fs.constants.COPYFILE_EXCL);
|
|
462
|
+
fs.chmodSync(copy, 0o600);
|
|
463
|
+
const fd = fs.openSync(copy, "r");
|
|
464
|
+
fs.fsyncSync(fd);
|
|
465
|
+
fs.closeSync(fd);
|
|
466
|
+
}
|
|
467
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
468
|
+
fs.renameSync(staged, target);
|
|
469
|
+
sync(path.dirname(target));
|
|
470
|
+
}
|
|
471
|
+
// Every restored file is rechecked before the root is declared usable.
|
|
472
|
+
for (const q of manifest.queues) if (hashFile(path.join(root, q.file)) !== q.sha256) fail();
|
|
473
|
+
lease.db.exec("COMMIT");
|
|
474
|
+
if (fs.existsSync(stage)) {
|
|
475
|
+
for (const ns of fs.readdirSync(stage)) {
|
|
476
|
+
const dir = path.join(stage, ns);
|
|
477
|
+
check(dir, true);
|
|
478
|
+
if (fs.readdirSync(dir).length) fail();
|
|
479
|
+
fs.rmdirSync(dir);
|
|
480
|
+
}
|
|
481
|
+
fs.rmdirSync(stage);
|
|
482
|
+
}
|
|
483
|
+
atomicJson(marker, { schemaVersion: 1, state: "complete", sourceSha, rootId: manifest.rootId });
|
|
484
|
+
return {
|
|
485
|
+
schemaVersion: 1,
|
|
486
|
+
state: "complete",
|
|
487
|
+
sourceSha,
|
|
488
|
+
queues: manifest.queues.length,
|
|
489
|
+
records: manifest.queues.reduce((n, q) => n + q.records, 0),
|
|
490
|
+
};
|
|
491
|
+
} finally {
|
|
492
|
+
lease.close();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
export async function runOutputStateProgram(args) {
|
|
496
|
+
const [action, root, out, fixed, confirmation] = args,
|
|
497
|
+
limits = outputStateLimits(JSON.parse(fixed));
|
|
498
|
+
if (action === "capture") return capture(root, out, limits);
|
|
499
|
+
if (action === "validate") return validateOutputSnapshot(root, limits);
|
|
500
|
+
if (action === "restore") return restore(root, out, limits, confirmation);
|
|
501
|
+
fail();
|
|
502
|
+
}
|
|
503
|
+
export function outputStateMain(args = process.argv.slice(2)) {
|
|
504
|
+
return runOutputStateProgram(args)
|
|
505
|
+
.then((r) => process.stdout.write(JSON.stringify(r) + "\n"))
|
|
506
|
+
.catch(() => {
|
|
507
|
+
process.stderr.write("MQTT output state operation refused; preserve source and target for review\n");
|
|
508
|
+
process.exitCode = 1;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await outputStateMain();
|
|
@@ -297,6 +297,10 @@ export declare const baseSchema: z.ZodObject<{
|
|
|
297
297
|
buffered: "buffered";
|
|
298
298
|
durable: "durable";
|
|
299
299
|
}>;
|
|
300
|
+
storage: z.ZodDefault<z.ZodEnum<{
|
|
301
|
+
local: "local";
|
|
302
|
+
runtime: "runtime";
|
|
303
|
+
}>>;
|
|
300
304
|
directory: z.ZodString;
|
|
301
305
|
maxRecords: z.ZodDefault<z.ZodNumber>;
|
|
302
306
|
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["../../src/uns-config/config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,UAAU
|
|
1
|
+
{"version":3,"file":"config-schema.d.ts","sourceRoot":"","sources":["../../src/uns-config/config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;8CAAmE,CAAC;AAG3F,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC"}
|
|
@@ -212,6 +212,10 @@ export declare const mqttOutputChannelSchema: z.ZodObject<{
|
|
|
212
212
|
buffered: "buffered";
|
|
213
213
|
durable: "durable";
|
|
214
214
|
}>;
|
|
215
|
+
storage: z.ZodDefault<z.ZodEnum<{
|
|
216
|
+
local: "local";
|
|
217
|
+
runtime: "runtime";
|
|
218
|
+
}>>;
|
|
215
219
|
directory: z.ZodString;
|
|
216
220
|
maxRecords: z.ZodDefault<z.ZodNumber>;
|
|
217
221
|
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
@@ -522,6 +526,10 @@ export declare const unsCoreSchema: z.ZodObject<{
|
|
|
522
526
|
buffered: "buffered";
|
|
523
527
|
durable: "durable";
|
|
524
528
|
}>;
|
|
529
|
+
storage: z.ZodDefault<z.ZodEnum<{
|
|
530
|
+
local: "local";
|
|
531
|
+
runtime: "runtime";
|
|
532
|
+
}>>;
|
|
525
533
|
directory: z.ZodString;
|
|
526
534
|
maxRecords: z.ZodDefault<z.ZodNumber>;
|
|
527
535
|
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uns-core-schema.d.ts","sourceRoot":"","sources":["../../src/uns-config/uns-core-schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgC3B,CAAC;AAEJ,wFAAwF;AACxF,eAAO,MAAM,uBAAuB
|
|
1
|
+
{"version":3,"file":"uns-core-schema.d.ts","sourceRoot":"","sources":["../../src/uns-config/uns-core-schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgC3B,CAAC;AAEJ,wFAAwF;AACxF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAEzB,CAAC;AAEZ,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA0Cf,CAAC;AAEZ,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
|
@@ -5,6 +5,10 @@ export declare const mqttPublishQueueSchema: z.ZodObject<{
|
|
|
5
5
|
buffered: "buffered";
|
|
6
6
|
durable: "durable";
|
|
7
7
|
}>;
|
|
8
|
+
storage: z.ZodDefault<z.ZodEnum<{
|
|
9
|
+
local: "local";
|
|
10
|
+
runtime: "runtime";
|
|
11
|
+
}>>;
|
|
8
12
|
directory: z.ZodString;
|
|
9
13
|
maxRecords: z.ZodDefault<z.ZodNumber>;
|
|
10
14
|
maxBytes: z.ZodDefault<z.ZodNumber>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-queue-contract.d.ts","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB
|
|
1
|
+
{"version":3,"file":"publish-queue-contract.d.ts","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;kBAexB,CAAC;AACZ,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC7E,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,UAAU,GAAG,SAAS,CAAC;IAC7B,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9D,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC"}
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
export const mqttPublishQueueSchema = z
|
|
4
4
|
.object({
|
|
5
5
|
mode: z.enum(["buffered", "durable"]),
|
|
6
|
+
storage: z.enum(["local", "runtime"]).default("local"),
|
|
6
7
|
directory: z.string().min(1),
|
|
7
8
|
maxRecords: z.number().int().min(1).max(100000).default(10000),
|
|
8
9
|
maxBytes: z.number().int().min(1).max(536870912).default(67108864),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-queue-contract.js","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,4EAA4E;AAC5E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC;KACpC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IACrC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAC9D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IAClE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IAC9D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IACnE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACrE,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;IAC9E,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IACvE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAClE,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;CACrE,CAAC;KACD,MAAM,EAAE,CAAC","sourcesContent":["import { z } from \"zod\";\n\n/** Opt-in output buffering; absence preserves the issued MQTT publisher. */\nexport const mqttPublishQueueSchema = z\n .object({\n mode: z.enum([\"buffered\", \"durable\"]),\n directory: z.string().min(1),\n maxRecords: z.number().int().min(1).max(100000).default(10000),\n maxBytes: z.number().int().min(1).max(536870912).default(67108864),\n memoryRecords: z.number().int().min(0).max(10000).default(256),\n memoryBytes: z.number().int().min(0).max(67108864).default(4194304),\n maxMessageBytes: z.number().int().min(1).max(1048576).default(524288),\n maxDiskBytes: z.number().int().min(1048576).max(1073741824).default(268435456),\n minFreeBytes: z.number().int().min(0).max(1073741824).default(16777216),\n retryIntervalMs: z.number().int().min(10).max(60000).default(1000),\n publishTimeoutMs: z.number().int().min(10).max(60000).default(10000),\n })\n .strict();\nexport type MqttPublishQueueOptions = z.input<typeof mqttPublishQueueSchema>;\nexport type ResolvedMqttPublishQueueOptions = z.output<typeof mqttPublishQueueSchema>;\nexport type MqttPublishQueueStatus = {\n mode: \"buffered\" | \"durable\";\n state: \"idle\" | \"pending\" | \"retrying\" | \"blocked\" | \"closed\";\n pendingRecords: number;\n pendingBytes: number;\n memoryRecords: number;\n diskRecords: number;\n inFlight: boolean;\n delivered: number;\n retries: number;\n lastError: string | null;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"publish-queue-contract.js","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,4EAA4E;AAC5E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC;KACpC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IACrC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IACtD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAC9D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IAClE,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IAC9D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IACnE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACrE,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;IAC9E,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;IACvE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAClE,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;CACrE,CAAC;KACD,MAAM,EAAE,CAAC","sourcesContent":["import { z } from \"zod\";\n\n/** Opt-in output buffering; absence preserves the issued MQTT publisher. */\nexport const mqttPublishQueueSchema = z\n .object({\n mode: z.enum([\"buffered\", \"durable\"]),\n storage: z.enum([\"local\", \"runtime\"]).default(\"local\"),\n directory: z.string().min(1),\n maxRecords: z.number().int().min(1).max(100000).default(10000),\n maxBytes: z.number().int().min(1).max(536870912).default(67108864),\n memoryRecords: z.number().int().min(0).max(10000).default(256),\n memoryBytes: z.number().int().min(0).max(67108864).default(4194304),\n maxMessageBytes: z.number().int().min(1).max(1048576).default(524288),\n maxDiskBytes: z.number().int().min(1048576).max(1073741824).default(268435456),\n minFreeBytes: z.number().int().min(0).max(1073741824).default(16777216),\n retryIntervalMs: z.number().int().min(10).max(60000).default(1000),\n publishTimeoutMs: z.number().int().min(10).max(60000).default(10000),\n })\n .strict();\nexport type MqttPublishQueueOptions = z.input<typeof mqttPublishQueueSchema>;\nexport type ResolvedMqttPublishQueueOptions = z.output<typeof mqttPublishQueueSchema>;\nexport type MqttPublishQueueStatus = {\n mode: \"buffered\" | \"durable\";\n state: \"idle\" | \"pending\" | \"retrying\" | \"blocked\" | \"closed\";\n pendingRecords: number;\n pendingBytes: number;\n memoryRecords: number;\n diskRecords: number;\n inFlight: boolean;\n delivered: number;\n retries: number;\n lastError: string | null;\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-queue-store.d.ts","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"publish-queue-store.d.ts","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-store.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,6BAA6B,CAAC;AAEnF,MAAM,MAAM,aAAa,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE;QAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAGjJ,qFAAqF;AACrF,qBAAa,qBAAqB;IAO9B,OAAO,CAAC,MAAM;IANhB,OAAO,CAAC,EAAE,CAAe;IACzB,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAC,CAA2C;IAC3D,OAAO,CAAC,YAAY,CAAC,CAAsD;gBAEjE,MAAM,EAAE,+BAA+B,EAC/C,QAAQ,EAAE,MAAM;IA4ElB,OAAO,CAAC,WAAW;IAWnB,OAAO,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAK9D,IAAI,IAAI,aAAa,GAAG,SAAS;IAMjC,OAAO,CAAC,aAAa;IAkBrB,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAmBhC,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAIrC,KAAK,IAAI,IAAI;CAUd"}
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, realpathSync, statfsSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { openManagedOutputRoot, validateOutputRow } from "../runtime/mqtt-output-state-program.mjs";
|
|
5
6
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
6
7
|
/** Private, exclusively owned output outbox. It never opens application journals. */
|
|
7
8
|
export class MqttPublishQueueStore {
|
|
@@ -9,33 +10,37 @@ export class MqttPublishQueueStore {
|
|
|
9
10
|
db;
|
|
10
11
|
lease;
|
|
11
12
|
directory;
|
|
13
|
+
managed;
|
|
12
14
|
finalSummary;
|
|
13
15
|
constructor(config, identity) {
|
|
14
16
|
this.config = config;
|
|
15
17
|
if (!path.isAbsolute(config.directory) || path.parse(config.directory).root === config.directory)
|
|
16
18
|
throw Error("MQTT publish queue requires a dedicated absolute directory");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
throw Error("MQTT publish queue directory must be private and owned by this user");
|
|
21
|
-
this.directory = realpathSync(config.directory);
|
|
22
|
-
const file = path.join(this.directory, "mqtt-outbox.sqlite");
|
|
23
|
-
const leaseFile = path.join(this.directory, "mqtt-outbox-owner.sqlite");
|
|
24
|
-
for (const name of [leaseFile, file, file + "-wal", file + "-shm", leaseFile + "-journal"])
|
|
25
|
-
if (existsSync(name)) {
|
|
26
|
-
const stat = lstatSync(name);
|
|
27
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.mode & 0o077 || (process.getuid && stat.uid !== process.getuid()))
|
|
28
|
-
throw Error("Unsafe MQTT publish queue file; preserve and inspect it");
|
|
29
|
-
}
|
|
30
|
-
// node:sqlite is loaded only for the opt-in path; ordinary publishers are unchanged.
|
|
31
|
-
const { DatabaseSync: Sqlite } = createRequire(import.meta.url)("node:sqlite");
|
|
32
|
-
const openPrivate = (name) => {
|
|
33
|
-
if (!existsSync(name))
|
|
34
|
-
closeSync(openSync(name, "wx", 0o600));
|
|
35
|
-
return new Sqlite(name);
|
|
36
|
-
};
|
|
37
|
-
this.lease = openPrivate(leaseFile);
|
|
19
|
+
if (config.storage === "runtime" && !process.env["UNS_MQTT_OUTPUT_STATE_ROOT"])
|
|
20
|
+
throw Error("Runtime MQTT output storage requires a reviewed permanent mount");
|
|
21
|
+
this.managed = config.storage === "runtime" ? openManagedOutputRoot(config.directory, process.env["UNS_MQTT_OUTPUT_STATE_ROOT"]) : undefined;
|
|
38
22
|
try {
|
|
23
|
+
mkdirSync(config.directory, { recursive: true, mode: 0o700 });
|
|
24
|
+
const dir = lstatSync(config.directory);
|
|
25
|
+
if (!dir.isDirectory() || dir.isSymbolicLink() || dir.mode & 0o077 || (process.getuid && dir.uid !== process.getuid()))
|
|
26
|
+
throw Error("MQTT publish queue directory must be private and owned by this user");
|
|
27
|
+
this.directory = realpathSync(config.directory);
|
|
28
|
+
const file = path.join(this.directory, "mqtt-outbox.sqlite");
|
|
29
|
+
const leaseFile = path.join(this.directory, "mqtt-outbox-owner.sqlite");
|
|
30
|
+
for (const name of [leaseFile, file, file + "-wal", file + "-shm", leaseFile + "-journal"])
|
|
31
|
+
if (existsSync(name)) {
|
|
32
|
+
const stat = lstatSync(name);
|
|
33
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.mode & 0o077 || (process.getuid && stat.uid !== process.getuid()))
|
|
34
|
+
throw Error("Unsafe MQTT publish queue file; preserve and inspect it");
|
|
35
|
+
}
|
|
36
|
+
// node:sqlite is loaded only for the opt-in path; ordinary publishers are unchanged.
|
|
37
|
+
const { DatabaseSync: Sqlite } = createRequire(import.meta.url)("node:sqlite");
|
|
38
|
+
const openPrivate = (name) => {
|
|
39
|
+
if (!existsSync(name))
|
|
40
|
+
closeSync(openSync(name, "wx", 0o600));
|
|
41
|
+
return new Sqlite(name);
|
|
42
|
+
};
|
|
43
|
+
this.lease = openPrivate(leaseFile);
|
|
39
44
|
this.lease.exec("PRAGMA busy_timeout=0; PRAGMA journal_mode=DELETE; BEGIN EXCLUSIVE");
|
|
40
45
|
const existed = existsSync(file);
|
|
41
46
|
if (existed) {
|
|
@@ -45,7 +50,9 @@ export class MqttPublishQueueStore {
|
|
|
45
50
|
throw Error("Unsupported MQTT outbox page size; preserve and review storage");
|
|
46
51
|
const meta = reader.prepare("SELECT value FROM metadata WHERE key=?");
|
|
47
52
|
reader.prepare("SELECT seq,id,record,bytes,sha256 FROM pending LIMIT 1").get();
|
|
48
|
-
if (meta.get("schema")?.value !== "
|
|
53
|
+
if (meta.get("schema")?.value !== (this.managed ? "2" : "1") ||
|
|
54
|
+
(this.managed && (meta.get("rootId")?.value !== this.managed.rootId || meta.get("relative")?.value !== this.managed.relative)) ||
|
|
55
|
+
meta.get("identity")?.value !== digest(identity))
|
|
49
56
|
throw Error("MQTT outbox identity or schema differs; preserve pending work and use its original destination");
|
|
50
57
|
}
|
|
51
58
|
finally {
|
|
@@ -62,7 +69,11 @@ export class MqttPublishQueueStore {
|
|
|
62
69
|
CREATE TABLE IF NOT EXISTS pending(seq INTEGER PRIMARY KEY,id TEXT UNIQUE NOT NULL,record TEXT NOT NULL,bytes INTEGER NOT NULL,sha256 TEXT NOT NULL);`);
|
|
63
70
|
if (!existed)
|
|
64
71
|
this.transaction(() => {
|
|
65
|
-
this.db.prepare("INSERT INTO metadata VALUES(?,?)").run("schema", "1");
|
|
72
|
+
this.db.prepare("INSERT INTO metadata VALUES(?,?)").run("schema", this.managed ? "2" : "1");
|
|
73
|
+
if (this.managed) {
|
|
74
|
+
this.db.prepare("INSERT INTO metadata VALUES(?,?)").run("rootId", this.managed.rootId);
|
|
75
|
+
this.db.prepare("INSERT INTO metadata VALUES(?,?)").run("relative", this.managed.relative);
|
|
76
|
+
}
|
|
66
77
|
this.db.prepare("INSERT INTO metadata VALUES(?,?)").run("identity", digest(identity));
|
|
67
78
|
});
|
|
68
79
|
const fd = openSync(this.directory, "r");
|
|
@@ -75,7 +86,8 @@ export class MqttPublishQueueStore {
|
|
|
75
86
|
}
|
|
76
87
|
catch (error) {
|
|
77
88
|
this.db?.close();
|
|
78
|
-
this.lease
|
|
89
|
+
this.lease?.close();
|
|
90
|
+
this.managed?.close();
|
|
79
91
|
throw error;
|
|
80
92
|
}
|
|
81
93
|
}
|
|
@@ -101,21 +113,7 @@ export class MqttPublishQueueStore {
|
|
|
101
113
|
const row = this.db.prepare("SELECT seq,id,record,bytes,sha256 FROM pending ORDER BY seq LIMIT 1").get();
|
|
102
114
|
if (!row)
|
|
103
115
|
return undefined;
|
|
104
|
-
|
|
105
|
-
if (digest(serialized) !== row.sha256)
|
|
106
|
-
throw Error("MQTT outbox integrity check failed; pending work preserved");
|
|
107
|
-
const value = JSON.parse(serialized);
|
|
108
|
-
if (value.seq !== Number(row.seq) ||
|
|
109
|
-
value.id !== row.id ||
|
|
110
|
-
value.bytes !== Number(row.bytes) ||
|
|
111
|
-
!Number.isSafeInteger(value.seq) ||
|
|
112
|
-
typeof value.id !== "string" ||
|
|
113
|
-
typeof value.topic !== "string" ||
|
|
114
|
-
typeof value.message !== "string" ||
|
|
115
|
-
![1, 2].includes(value.options?.qos) ||
|
|
116
|
-
typeof value.options.retain !== "boolean")
|
|
117
|
-
throw Error("Invalid MQTT outbox record; pending work preserved");
|
|
118
|
-
return value;
|
|
116
|
+
return validateOutputRow(row);
|
|
119
117
|
}
|
|
120
118
|
physicalBytes() {
|
|
121
119
|
return [
|
|
@@ -171,6 +169,7 @@ export class MqttPublishQueueStore {
|
|
|
171
169
|
}
|
|
172
170
|
finally {
|
|
173
171
|
this.lease.close();
|
|
172
|
+
this.managed?.close();
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
175
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-queue-store.js","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, realpathSync, statfsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport type { ResolvedMqttPublishQueueOptions } from \"./publish-queue-contract.js\";\n\nexport type PublishRecord = { seq: number; id: string; topic: string; message: string; options: { qos: 1 | 2; retain: boolean }; bytes: number };\nconst digest = (value: string) => createHash(\"sha256\").update(value).digest(\"hex\");\n\n/** Private, exclusively owned output outbox. It never opens application journals. */\nexport class MqttPublishQueueStore {\n private db: DatabaseSync;\n private lease: DatabaseSync;\n private directory: string;\n private finalSummary?: { records: number; bytes: number; lastSeq: number };\n constructor(\n private config: ResolvedMqttPublishQueueOptions,\n identity: string,\n ) {\n if (!path.isAbsolute(config.directory) || path.parse(config.directory).root === config.directory)\n throw Error(\"MQTT publish queue requires a dedicated absolute directory\");\n mkdirSync(config.directory, { recursive: true, mode: 0o700 });\n const dir = lstatSync(config.directory);\n if (!dir.isDirectory() || dir.isSymbolicLink() || dir.mode & 0o077 || (process.getuid && dir.uid !== process.getuid()))\n throw Error(\"MQTT publish queue directory must be private and owned by this user\");\n this.directory = realpathSync(config.directory);\n const file = path.join(this.directory, \"mqtt-outbox.sqlite\");\n const leaseFile = path.join(this.directory, \"mqtt-outbox-owner.sqlite\");\n for (const name of [leaseFile, file, file + \"-wal\", file + \"-shm\", leaseFile + \"-journal\"])\n if (existsSync(name)) {\n const stat = lstatSync(name);\n if (!stat.isFile() || stat.isSymbolicLink() || stat.mode & 0o077 || (process.getuid && stat.uid !== process.getuid()))\n throw Error(\"Unsafe MQTT publish queue file; preserve and inspect it\");\n }\n // node:sqlite is loaded only for the opt-in path; ordinary publishers are unchanged.\n const { DatabaseSync: Sqlite } = createRequire(import.meta.url)(\"node:sqlite\") as typeof import(\"node:sqlite\");\n const openPrivate = (name: string) => {\n if (!existsSync(name)) closeSync(openSync(name, \"wx\", 0o600));\n return new Sqlite(name);\n };\n this.lease = openPrivate(leaseFile);\n try {\n this.lease.exec(\"PRAGMA busy_timeout=0; PRAGMA journal_mode=DELETE; BEGIN EXCLUSIVE\");\n const existed = existsSync(file);\n if (existed) {\n const reader = new Sqlite(file, { readOnly: true });\n try {\n if (reader.prepare(\"PRAGMA page_size\").get().page_size !== 4096)\n throw Error(\"Unsupported MQTT outbox page size; preserve and review storage\");\n const meta = reader.prepare(\"SELECT value FROM metadata WHERE key=?\");\n reader.prepare(\"SELECT seq,id,record,bytes,sha256 FROM pending LIMIT 1\").get();\n if (meta.get(\"schema\")?.value !== \"1\" || meta.get(\"identity\")?.value !== digest(identity))\n throw Error(\"MQTT outbox identity or schema differs; preserve pending work and use its original destination\");\n } finally {\n reader.close();\n }\n }\n this.db = openPrivate(file);\n this.db.exec(\"PRAGMA busy_timeout=1000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA wal_autocheckpoint=64\");\n const pageLimit = Math.floor(config.maxDiskBytes / 4096);\n const selectedPages = Number(this.db.prepare(`PRAGMA max_page_count=${pageLimit}`).get().max_page_count);\n if (selectedPages > pageLimit) throw Error(\"Retained MQTT outbox exceeds maxDiskBytes; preserve and review storage\");\n this.db.exec(`CREATE TABLE IF NOT EXISTS metadata(key TEXT PRIMARY KEY,value TEXT NOT NULL);\n CREATE TABLE IF NOT EXISTS pending(seq INTEGER PRIMARY KEY,id TEXT UNIQUE NOT NULL,record TEXT NOT NULL,bytes INTEGER NOT NULL,sha256 TEXT NOT NULL);`);\n if (!existed)\n this.transaction(() => {\n this.db.prepare(\"INSERT INTO metadata VALUES(?,?)\").run(\"schema\", \"1\");\n this.db.prepare(\"INSERT INTO metadata VALUES(?,?)\").run(\"identity\", digest(identity));\n });\n const fd = openSync(this.directory, \"r\");\n try {\n fsyncSync(fd);\n } finally {\n closeSync(fd);\n }\n } catch (error) {\n this.db?.close();\n this.lease.close();\n throw error;\n }\n }\n private transaction<T>(run: () => T): T {\n this.db.exec(\"BEGIN IMMEDIATE\");\n try {\n const result = run();\n this.db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n }\n summary(): { records: number; bytes: number; lastSeq: number } {\n if (this.finalSummary) return { ...this.finalSummary };\n const row = this.db.prepare(\"SELECT COUNT(*) AS records,COALESCE(SUM(bytes),0) AS bytes,COALESCE(MAX(seq),0) AS lastSeq FROM pending\").get();\n return { records: Number(row.records), bytes: Number(row.bytes), lastSeq: Number(row.lastSeq) };\n }\n head(): PublishRecord | undefined {\n const row = this.db.prepare(\"SELECT seq,id,record,bytes,sha256 FROM pending ORDER BY seq LIMIT 1\").get();\n if (!row) return undefined;\n const serialized = String(row.record);\n if (digest(serialized) !== row.sha256) throw Error(\"MQTT outbox integrity check failed; pending work preserved\");\n const value = JSON.parse(serialized) as PublishRecord;\n if (\n value.seq !== Number(row.seq) ||\n value.id !== row.id ||\n value.bytes !== Number(row.bytes) ||\n !Number.isSafeInteger(value.seq) ||\n typeof value.id !== \"string\" ||\n typeof value.topic !== \"string\" ||\n typeof value.message !== \"string\" ||\n ![1, 2].includes(value.options?.qos) ||\n typeof value.options.retain !== \"boolean\"\n )\n throw Error(\"Invalid MQTT outbox record; pending work preserved\");\n return value;\n }\n private physicalBytes(): number {\n return [\n \"mqtt-outbox.sqlite\",\n \"mqtt-outbox.sqlite-wal\",\n \"mqtt-outbox.sqlite-shm\",\n \"mqtt-outbox-owner.sqlite\",\n \"mqtt-outbox-owner.sqlite-journal\",\n ].reduce((total, name) => {\n const file = path.join(this.directory, name);\n try {\n return total + lstatSync(file).size;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return total;\n throw error;\n }\n }, 0);\n }\n\n put(record: PublishRecord): void {\n const space = statfsSync(this.directory, { bigint: true });\n if (space.bavail * space.bsize < BigInt(this.config.minFreeBytes)) throw Error(\"MQTT outbox free-space reserve exhausted\");\n const serialized = JSON.stringify(record);\n // Account for WAL plus database growth, not just logical pending payloads.\n // Long-lived external readers may prevent truncation: refuse new admission\n // instead of letting their pinned WAL consume the filesystem indefinitely.\n const reserve = 2 * Buffer.byteLength(serialized) + 65536;\n if (this.physicalBytes() + reserve > this.config.maxDiskBytes) {\n this.db.prepare(\"PRAGMA wal_checkpoint(TRUNCATE)\").get();\n if (this.physicalBytes() + reserve > this.config.maxDiskBytes) throw Error(\"MQTT physical disk budget reached; admission rejected\");\n }\n this.transaction(() => {\n const total = this.summary();\n if (total.records >= this.config.maxRecords || total.bytes + record.bytes > this.config.maxBytes)\n throw Error(\"MQTT disk queue capacity reached; admission rejected\");\n this.db.prepare(\"INSERT INTO pending VALUES(?,?,?,?,?)\").run(record.seq, record.id, serialized, record.bytes, digest(serialized));\n });\n }\n complete(record: PublishRecord): void {\n const result = this.db.prepare(\"DELETE FROM pending WHERE seq=? AND id=?\").run(record.seq, record.id);\n if (Number(result.changes) !== 1) throw Error(\"MQTT outbox checkpoint differs; completion not confirmed\");\n }\n close(): void {\n if (this.finalSummary) return;\n this.finalSummary = this.summary();\n try {\n this.db.close();\n } finally {\n this.lease.close();\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"publish-queue-store.js","sourceRoot":"","sources":["../../src/uns-mqtt/publish-queue-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, realpathSync, statfsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\nimport { openManagedOutputRoot, validateOutputRow } from \"../runtime/mqtt-output-state-program.mjs\";\nimport type { ResolvedMqttPublishQueueOptions } from \"./publish-queue-contract.js\";\n\nexport type PublishRecord = { seq: number; id: string; topic: string; message: string; options: { qos: 1 | 2; retain: boolean }; bytes: number };\nconst digest = (value: string) => createHash(\"sha256\").update(value).digest(\"hex\");\n\n/** Private, exclusively owned output outbox. It never opens application journals. */\nexport class MqttPublishQueueStore {\n private db: DatabaseSync;\n private lease: DatabaseSync;\n private directory: string;\n private managed?: ReturnType<typeof openManagedOutputRoot>;\n private finalSummary?: { records: number; bytes: number; lastSeq: number };\n constructor(\n private config: ResolvedMqttPublishQueueOptions,\n identity: string,\n ) {\n if (!path.isAbsolute(config.directory) || path.parse(config.directory).root === config.directory)\n throw Error(\"MQTT publish queue requires a dedicated absolute directory\");\n if (config.storage === \"runtime\" && !process.env[\"UNS_MQTT_OUTPUT_STATE_ROOT\"])\n throw Error(\"Runtime MQTT output storage requires a reviewed permanent mount\");\n this.managed = config.storage === \"runtime\" ? openManagedOutputRoot(config.directory, process.env[\"UNS_MQTT_OUTPUT_STATE_ROOT\"]) : undefined;\n try {\n mkdirSync(config.directory, { recursive: true, mode: 0o700 });\n const dir = lstatSync(config.directory);\n if (!dir.isDirectory() || dir.isSymbolicLink() || dir.mode & 0o077 || (process.getuid && dir.uid !== process.getuid()))\n throw Error(\"MQTT publish queue directory must be private and owned by this user\");\n this.directory = realpathSync(config.directory);\n const file = path.join(this.directory, \"mqtt-outbox.sqlite\");\n const leaseFile = path.join(this.directory, \"mqtt-outbox-owner.sqlite\");\n for (const name of [leaseFile, file, file + \"-wal\", file + \"-shm\", leaseFile + \"-journal\"])\n if (existsSync(name)) {\n const stat = lstatSync(name);\n if (!stat.isFile() || stat.isSymbolicLink() || stat.mode & 0o077 || (process.getuid && stat.uid !== process.getuid()))\n throw Error(\"Unsafe MQTT publish queue file; preserve and inspect it\");\n }\n // node:sqlite is loaded only for the opt-in path; ordinary publishers are unchanged.\n const { DatabaseSync: Sqlite } = createRequire(import.meta.url)(\"node:sqlite\") as typeof import(\"node:sqlite\");\n const openPrivate = (name: string) => {\n if (!existsSync(name)) closeSync(openSync(name, \"wx\", 0o600));\n return new Sqlite(name);\n };\n this.lease = openPrivate(leaseFile);\n this.lease.exec(\"PRAGMA busy_timeout=0; PRAGMA journal_mode=DELETE; BEGIN EXCLUSIVE\");\n const existed = existsSync(file);\n if (existed) {\n const reader = new Sqlite(file, { readOnly: true });\n try {\n if (reader.prepare(\"PRAGMA page_size\").get().page_size !== 4096)\n throw Error(\"Unsupported MQTT outbox page size; preserve and review storage\");\n const meta = reader.prepare(\"SELECT value FROM metadata WHERE key=?\");\n reader.prepare(\"SELECT seq,id,record,bytes,sha256 FROM pending LIMIT 1\").get();\n if (\n meta.get(\"schema\")?.value !== (this.managed ? \"2\" : \"1\") ||\n (this.managed && (meta.get(\"rootId\")?.value !== this.managed.rootId || meta.get(\"relative\")?.value !== this.managed.relative)) ||\n meta.get(\"identity\")?.value !== digest(identity)\n )\n throw Error(\"MQTT outbox identity or schema differs; preserve pending work and use its original destination\");\n } finally {\n reader.close();\n }\n }\n this.db = openPrivate(file);\n this.db.exec(\"PRAGMA busy_timeout=1000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA wal_autocheckpoint=64\");\n const pageLimit = Math.floor(config.maxDiskBytes / 4096);\n const selectedPages = Number(this.db.prepare(`PRAGMA max_page_count=${pageLimit}`).get().max_page_count);\n if (selectedPages > pageLimit) throw Error(\"Retained MQTT outbox exceeds maxDiskBytes; preserve and review storage\");\n this.db.exec(`CREATE TABLE IF NOT EXISTS metadata(key TEXT PRIMARY KEY,value TEXT NOT NULL);\n CREATE TABLE IF NOT EXISTS pending(seq INTEGER PRIMARY KEY,id TEXT UNIQUE NOT NULL,record TEXT NOT NULL,bytes INTEGER NOT NULL,sha256 TEXT NOT NULL);`);\n if (!existed)\n this.transaction(() => {\n this.db.prepare(\"INSERT INTO metadata VALUES(?,?)\").run(\"schema\", this.managed ? \"2\" : \"1\");\n if (this.managed) {\n this.db.prepare(\"INSERT INTO metadata VALUES(?,?)\").run(\"rootId\", this.managed.rootId);\n this.db.prepare(\"INSERT INTO metadata VALUES(?,?)\").run(\"relative\", this.managed.relative);\n }\n this.db.prepare(\"INSERT INTO metadata VALUES(?,?)\").run(\"identity\", digest(identity));\n });\n const fd = openSync(this.directory, \"r\");\n try {\n fsyncSync(fd);\n } finally {\n closeSync(fd);\n }\n } catch (error) {\n this.db?.close();\n this.lease?.close();\n this.managed?.close();\n throw error;\n }\n }\n private transaction<T>(run: () => T): T {\n this.db.exec(\"BEGIN IMMEDIATE\");\n try {\n const result = run();\n this.db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n this.db.exec(\"ROLLBACK\");\n throw error;\n }\n }\n summary(): { records: number; bytes: number; lastSeq: number } {\n if (this.finalSummary) return { ...this.finalSummary };\n const row = this.db.prepare(\"SELECT COUNT(*) AS records,COALESCE(SUM(bytes),0) AS bytes,COALESCE(MAX(seq),0) AS lastSeq FROM pending\").get();\n return { records: Number(row.records), bytes: Number(row.bytes), lastSeq: Number(row.lastSeq) };\n }\n head(): PublishRecord | undefined {\n const row = this.db.prepare(\"SELECT seq,id,record,bytes,sha256 FROM pending ORDER BY seq LIMIT 1\").get();\n if (!row) return undefined;\n return validateOutputRow(row);\n }\n\n private physicalBytes(): number {\n return [\n \"mqtt-outbox.sqlite\",\n \"mqtt-outbox.sqlite-wal\",\n \"mqtt-outbox.sqlite-shm\",\n \"mqtt-outbox-owner.sqlite\",\n \"mqtt-outbox-owner.sqlite-journal\",\n ].reduce((total, name) => {\n const file = path.join(this.directory, name);\n try {\n return total + lstatSync(file).size;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return total;\n throw error;\n }\n }, 0);\n }\n\n put(record: PublishRecord): void {\n const space = statfsSync(this.directory, { bigint: true });\n if (space.bavail * space.bsize < BigInt(this.config.minFreeBytes)) throw Error(\"MQTT outbox free-space reserve exhausted\");\n const serialized = JSON.stringify(record);\n // Account for WAL plus database growth, not just logical pending payloads.\n // Long-lived external readers may prevent truncation: refuse new admission\n // instead of letting their pinned WAL consume the filesystem indefinitely.\n const reserve = 2 * Buffer.byteLength(serialized) + 65536;\n if (this.physicalBytes() + reserve > this.config.maxDiskBytes) {\n this.db.prepare(\"PRAGMA wal_checkpoint(TRUNCATE)\").get();\n if (this.physicalBytes() + reserve > this.config.maxDiskBytes) throw Error(\"MQTT physical disk budget reached; admission rejected\");\n }\n this.transaction(() => {\n const total = this.summary();\n if (total.records >= this.config.maxRecords || total.bytes + record.bytes > this.config.maxBytes)\n throw Error(\"MQTT disk queue capacity reached; admission rejected\");\n this.db.prepare(\"INSERT INTO pending VALUES(?,?,?,?,?)\").run(record.seq, record.id, serialized, record.bytes, digest(serialized));\n });\n }\n complete(record: PublishRecord): void {\n const result = this.db.prepare(\"DELETE FROM pending WHERE seq=? AND id=?\").run(record.seq, record.id);\n if (Number(result.changes) !== 1) throw Error(\"MQTT outbox checkpoint differs; completion not confirmed\");\n }\n close(): void {\n if (this.finalSummary) return;\n this.finalSummary = this.summary();\n try {\n this.db.close();\n } finally {\n this.lease.close();\n this.managed?.close();\n }\n }\n}\n"]}
|