@camstack/system 1.2.179 → 1.2.181
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon-runner.js +2 -2
- package/dist/addon-runner.mjs +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.d.ts +56 -10
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +657 -62
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +658 -63
- package/dist/builtins/backup-orchestrator/run-queue.d.ts +103 -0
- package/dist/builtins/backup-orchestrator/staging-paths.d.ts +36 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +349 -65
- package/dist/builtins/device-manager/device-manager.addon.mjs +349 -65
- package/dist/builtins/device-manager/device-meta-actions.d.ts +8 -19
- package/dist/builtins/device-manager/device-provider-context.d.ts +4 -0
- package/dist/builtins/device-manager/migrate-device.d.ts +47 -3
- package/dist/builtins/device-manager/migrate-guard.d.ts +39 -0
- package/dist/builtins/device-manager/migrate-hardware-state.d.ts +24 -0
- package/dist/builtins/device-manager/migrate-reload.d.ts +8 -0
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-backup/system-backup.service.d.ts +55 -2
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-L_39vRmH.mjs → dist-DbnNk4Lw.mjs} +211 -2
- package/dist/{dist-C7qWwrK1.js → dist-whe0b4_3.js} +211 -2
- package/dist/index.js +530 -2
- package/dist/index.mjs +533 -5
- package/dist/kernel/addon-installer.d.ts +69 -0
- package/dist/kernel/deps/closure-dedupe.d.ts +105 -0
- package/dist/{manifest-system-deps-CMOGc5nC.mjs → manifest-system-deps-DOcMuUWK.mjs} +1 -1
- package/dist/{manifest-system-deps-PAIfmNun.js → manifest-system-deps-y5vZBO6q.js} +1 -1
- package/dist/{retired-settings-keys-3soU9pfP.js → retired-settings-keys-BdvmiaK7.js} +1 -1
- package/dist/{retired-settings-keys-CnbKAxmX.mjs → retired-settings-keys-CyQG8b-A.mjs} +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export type BackupRunState = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
2
|
+
export type BackupRunPhase = 'queued' | 'building' | 'uploading' | 'done';
|
|
3
|
+
/** What a trigger asks for — the queue keys joining on this. */
|
|
4
|
+
export interface BackupRunRequest {
|
|
5
|
+
readonly destinations?: readonly string[];
|
|
6
|
+
readonly locations?: readonly string[];
|
|
7
|
+
readonly label?: string;
|
|
8
|
+
readonly retentionCount?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Immutable snapshot of one run — the wire shape of `backup.listRuns`. */
|
|
11
|
+
export interface BackupRunSnapshot {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly state: BackupRunState;
|
|
14
|
+
readonly phase: BackupRunPhase;
|
|
15
|
+
readonly destinationIds: readonly string[];
|
|
16
|
+
readonly label?: string;
|
|
17
|
+
readonly requestedAt: number;
|
|
18
|
+
readonly startedAt?: number;
|
|
19
|
+
readonly finishedAt?: number;
|
|
20
|
+
readonly stagedBytes: number;
|
|
21
|
+
readonly archiveSizeBytes?: number;
|
|
22
|
+
readonly uploadedBytes: number;
|
|
23
|
+
readonly completedDestinationIds: readonly string[];
|
|
24
|
+
readonly failedDestinationIds: readonly string[];
|
|
25
|
+
readonly error?: string;
|
|
26
|
+
/**
|
|
27
|
+
* 1-based place in the execution queue — 1 = runs next. Present only
|
|
28
|
+
* while `state === 'queued'`, and derived from the queue's OWN
|
|
29
|
+
* pending order at read time (never from timestamps), so a consumer
|
|
30
|
+
* can only ever show the order the executor will actually honour.
|
|
31
|
+
*/
|
|
32
|
+
readonly queuePosition?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Progress sink handed to the executor. Every update replaces the
|
|
36
|
+
* stored snapshot (no shared mutable state leaks out of the queue).
|
|
37
|
+
*/
|
|
38
|
+
export interface BackupRunProgress {
|
|
39
|
+
setPhase(phase: BackupRunPhase): void;
|
|
40
|
+
/** Resolved destination targets — known only once the run starts. */
|
|
41
|
+
setDestinations(ids: readonly string[]): void;
|
|
42
|
+
setStagedBytes(bytes: number): void;
|
|
43
|
+
setArchiveSize(bytes: number): void;
|
|
44
|
+
setUploadedBytes(bytes: number): void;
|
|
45
|
+
destinationCompleted(id: string): void;
|
|
46
|
+
destinationFailed(id: string): void;
|
|
47
|
+
}
|
|
48
|
+
export interface SubmitResult<T> {
|
|
49
|
+
readonly runId: string;
|
|
50
|
+
/** True when the run had to wait behind an in-flight run. */
|
|
51
|
+
readonly queued: boolean;
|
|
52
|
+
/** True when this submit was coalesced onto an identical queued run. */
|
|
53
|
+
readonly joined: boolean;
|
|
54
|
+
/** Settles when the run terminates. Rejects `BackupCancelledError` on cancel-before-any-result. */
|
|
55
|
+
readonly result: Promise<T>;
|
|
56
|
+
}
|
|
57
|
+
export type BackupRunExecutor<T> = (request: BackupRunRequest, signal: AbortSignal, progress: BackupRunProgress) => Promise<T>;
|
|
58
|
+
export interface BackupRunQueueOptions {
|
|
59
|
+
readonly maxFinished?: number;
|
|
60
|
+
readonly now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* FIFO queue with concurrency 1. `submit` never starts a second
|
|
64
|
+
* executor while one is in flight; an identical request already
|
|
65
|
+
* WAITING (same destinations/locations/label/retention) is joined
|
|
66
|
+
* instead of queued twice — two schedule ticks due on the same minute
|
|
67
|
+
* produce one run, not two.
|
|
68
|
+
*/
|
|
69
|
+
export declare class BackupRunQueue<T> {
|
|
70
|
+
private readonly executor;
|
|
71
|
+
private readonly runs;
|
|
72
|
+
private readonly pending;
|
|
73
|
+
private readonly finished;
|
|
74
|
+
private activeId;
|
|
75
|
+
private readonly maxFinished;
|
|
76
|
+
private readonly now;
|
|
77
|
+
constructor(executor: BackupRunExecutor<T>, opts?: BackupRunQueueOptions);
|
|
78
|
+
submit(request: BackupRunRequest): SubmitResult<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Cancel a run. Queued: removed before it ever starts (its result
|
|
81
|
+
* rejects `BackupCancelledError`). Running: the abort signal fires —
|
|
82
|
+
* the executor's tar/upload streams stop and its cleanup runs.
|
|
83
|
+
* Unknown or already terminal: `false`.
|
|
84
|
+
*/
|
|
85
|
+
cancel(runId: string): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* EXECUTION order, not timestamp order: the running run first, then
|
|
88
|
+
* every queued run in the exact order the pump will start them (each
|
|
89
|
+
* stamped with `queuePosition`, 1 = next), then the bounded finished
|
|
90
|
+
* history newest-first. The queued section is read straight off
|
|
91
|
+
* `this.pending` — a consumer sorting by `requestedAt` descending
|
|
92
|
+
* would put the run that starts NEXT at the BOTTOM, which is
|
|
93
|
+
* backwards exactly when the queue matters (more than one waiter).
|
|
94
|
+
*/
|
|
95
|
+
listRuns(): readonly BackupRunSnapshot[];
|
|
96
|
+
getRun(runId: string): BackupRunSnapshot | undefined;
|
|
97
|
+
private createRecord;
|
|
98
|
+
private pump;
|
|
99
|
+
private execute;
|
|
100
|
+
private progressFor;
|
|
101
|
+
private patch;
|
|
102
|
+
private finish;
|
|
103
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { IScopedLogger } from '@camstack/types';
|
|
2
|
+
/** Basename of the staging dir under the node data root. */
|
|
3
|
+
export declare const STAGING_DIR_NAME = "backup-staging";
|
|
4
|
+
/** Canonical staging dir for a node data root. */
|
|
5
|
+
export declare function resolveStagingDir(nodeDataDir: string): string;
|
|
6
|
+
export interface StagingSweepResult {
|
|
7
|
+
/** Top-level staging entries removed. */
|
|
8
|
+
readonly removedCount: number;
|
|
9
|
+
/** File bytes reclaimed (recursive for removed subdirectories). */
|
|
10
|
+
readonly freedBytes: number;
|
|
11
|
+
/** Entries spared because `keepPaths` named them. */
|
|
12
|
+
readonly keptCount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface DirectoryFootprint {
|
|
15
|
+
readonly present: boolean;
|
|
16
|
+
readonly totalBytes: number;
|
|
17
|
+
readonly fileCount: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Delete every top-level entry of `stagingDir` except those whose
|
|
21
|
+
* resolved path appears in `keepPaths`. Per-entry failures are logged
|
|
22
|
+
* and skipped — a locked file must not abort the sweep. A missing
|
|
23
|
+
* staging dir is a clean no-op.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sweepStagingDir(input: {
|
|
26
|
+
stagingDir: string;
|
|
27
|
+
keepPaths: readonly string[];
|
|
28
|
+
logger: IScopedLogger;
|
|
29
|
+
}): Promise<StagingSweepResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Recursive size + file count of a path (file or directory). Absent
|
|
32
|
+
* path → `present: false` with zeros. Used by the sweep to report
|
|
33
|
+
* reclaimed bytes and by the orchestrator to surface the legacy
|
|
34
|
+
* staging dir's footprint without touching it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function measureDirectoryFootprint(dir: string): Promise<DirectoryFootprint>;
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-whe0b4_3.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
//#region src/builtins/console-logging/console-destination.ts
|
|
9
9
|
var LEVEL_RANK = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, ct as logDestinationCapability } from "../../dist-DbnNk4Lw.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
//#region src/builtins/console-logging/console-destination.ts
|
|
4
4
|
var LEVEL_RANK = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-whe0b4_3.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_fs_promises = require("node:fs/promises");
|
|
6
6
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ft as BaseAddon, I as coreBlockAddonId, L as coreBlockIdFromAddonId, Pt as errMsg, R as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-
|
|
1
|
+
import { Ft as BaseAddon, I as coreBlockAddonId, L as coreBlockIdFromAddonId, Pt as errMsg, R as coreBlocksCapability, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID } from "../../dist-DbnNk4Lw.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
7
|
-
const require_retired_settings_keys = require("../../retired-settings-keys-
|
|
6
|
+
const require_dist = require("../../dist-whe0b4_3.js");
|
|
7
|
+
const require_retired_settings_keys = require("../../retired-settings-keys-BdvmiaK7.js");
|
|
8
8
|
let node_crypto = require("node:crypto");
|
|
9
9
|
let _camstack_types_node = require("@camstack/types/node");
|
|
10
10
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -2114,7 +2114,13 @@ function resolveLinkedDeviceIds(params) {
|
|
|
2114
2114
|
if (config.mode === "auto") {
|
|
2115
2115
|
if (selfLocation !== null && selfLocation.length > 0) for (const c of candidates) {
|
|
2116
2116
|
if (c.type === require_dist.DeviceType.Container) continue;
|
|
2117
|
-
if (c.location
|
|
2117
|
+
if (c.location !== selfLocation) continue;
|
|
2118
|
+
const parent = c.parentDeviceId !== null ? byId.get(c.parentDeviceId) : void 0;
|
|
2119
|
+
if (parent !== void 0 && parent.type === require_dist.DeviceType.Container) {
|
|
2120
|
+
expand(parent.id);
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
ids.add(c.id);
|
|
2118
2124
|
}
|
|
2119
2125
|
} else for (const id of config.manualIds) if (byId.has(id)) expand(id);
|
|
2120
2126
|
ids.delete(selfId);
|
|
@@ -3274,8 +3280,6 @@ var DeviceEventPropagator = class {
|
|
|
3274
3280
|
return chain;
|
|
3275
3281
|
}
|
|
3276
3282
|
};
|
|
3277
|
-
//#endregion
|
|
3278
|
-
//#region src/builtins/device-manager/migrate-device.ts
|
|
3279
3283
|
/** A refusal that names a switch the camera does not offer, rather than one it
|
|
3280
3284
|
* could not be asked about. The cap documents rejecting the former; anything
|
|
3281
3285
|
* else is treated as the camera being out of reach, which is the safe reading:
|
|
@@ -3287,10 +3291,31 @@ function classify(err) {
|
|
|
3287
3291
|
detail
|
|
3288
3292
|
};
|
|
3289
3293
|
}
|
|
3294
|
+
/**
|
|
3295
|
+
* One switch write under the bound. The race, not the write, is what fails on
|
|
3296
|
+
* timeout — the underlying promise is marked handled so a late rejection from
|
|
3297
|
+
* a camera that eventually answers "no" cannot surface as an unhandled
|
|
3298
|
+
* rejection after the migration already reported `unreachable`.
|
|
3299
|
+
*/
|
|
3300
|
+
async function setSwitchBounded(deps, deviceId, switchId, enabled) {
|
|
3301
|
+
const budget = deps.switchTimeoutMs ?? 15e3;
|
|
3302
|
+
const write = deps.setSwitch(deviceId, switchId, enabled);
|
|
3303
|
+
write.catch(() => {});
|
|
3304
|
+
let timer;
|
|
3305
|
+
try {
|
|
3306
|
+
await Promise.race([write, new Promise((_resolve, reject) => {
|
|
3307
|
+
timer = setTimeout(() => {
|
|
3308
|
+
reject(/* @__PURE__ */ new Error(`switch write did not answer within ${budget}ms — reported unreachable; if it lands late, a duplicate write of the same value is a no-op`));
|
|
3309
|
+
}, budget);
|
|
3310
|
+
})]);
|
|
3311
|
+
} finally {
|
|
3312
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3290
3315
|
async function applyAll(deps, deviceId, enabled) {
|
|
3291
3316
|
const out = [];
|
|
3292
3317
|
for (const switchId of require_dist.CAMERA_SWITCH_ORDER) try {
|
|
3293
|
-
await deps
|
|
3318
|
+
await setSwitchBounded(deps, deviceId, switchId, enabled);
|
|
3294
3319
|
out.push({
|
|
3295
3320
|
deviceId,
|
|
3296
3321
|
switchId,
|
|
@@ -3327,8 +3352,17 @@ async function migrateDevice$1(deps, input) {
|
|
|
3327
3352
|
const down = [...await applyAll(deps, sourceId, false), ...await applyAll(deps, targetId, false)];
|
|
3328
3353
|
const sourceStillLive = down.filter((r) => r.deviceId === sourceId && r.outcome === "unreachable").map((r) => r.switchId);
|
|
3329
3354
|
await deps.swapIds(sourceId, targetId);
|
|
3355
|
+
await deps.swapHardwareState(sourceId, targetId);
|
|
3356
|
+
for (const deviceId of [sourceId, targetId]) try {
|
|
3357
|
+
await deps.forgetStreamState(deviceId);
|
|
3358
|
+
} catch (err) {
|
|
3359
|
+
deps.log("migration: broker hardware state NOT forgotten — stale stream state survives", {
|
|
3360
|
+
deviceId,
|
|
3361
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3362
|
+
remedy: "delete the device's derived streams and reassign its stream profiles, or update the stream-broker addon and re-run streamBroker.forgetDeviceHardware"
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3330
3365
|
const up = await applyAll(deps, sourceId, true);
|
|
3331
|
-
await deps.respawnRunners([sourceId, targetId]);
|
|
3332
3366
|
deps.log("camera migrated", {
|
|
3333
3367
|
sourceId,
|
|
3334
3368
|
targetId,
|
|
@@ -3344,6 +3378,148 @@ async function migrateDevice$1(deps, input) {
|
|
|
3344
3378
|
};
|
|
3345
3379
|
}
|
|
3346
3380
|
//#endregion
|
|
3381
|
+
//#region src/builtins/device-manager/migrate-reload.ts
|
|
3382
|
+
async function reloadMigratedDevices(pctx, targets) {
|
|
3383
|
+
const ctx = pctx.host.ctx;
|
|
3384
|
+
for (const target of targets) {
|
|
3385
|
+
pctx.metaStore.idToAddonId.set(target.deviceId, target.addonId);
|
|
3386
|
+
pctx.host.remoteNativeCaps.delete(target.deviceId);
|
|
3387
|
+
pctx.host.capabilityRegistry?.unregisterAllNativeForDevice(target.deviceId);
|
|
3388
|
+
}
|
|
3389
|
+
const fallbackAddons = /* @__PURE__ */ new Map();
|
|
3390
|
+
await Promise.all(targets.map(async (target) => {
|
|
3391
|
+
try {
|
|
3392
|
+
const reloaded = await (await pctx.host.requireDeviceProvider(target.addonId)).reloadDevice({ stableId: target.stableId });
|
|
3393
|
+
ctx.logger.info("migration: device reloaded in place — no other device disturbed", {
|
|
3394
|
+
tags: { deviceId: target.deviceId },
|
|
3395
|
+
meta: {
|
|
3396
|
+
addonId: target.addonId,
|
|
3397
|
+
stableId: target.stableId,
|
|
3398
|
+
reloadedAs: reloaded.deviceId
|
|
3399
|
+
}
|
|
3400
|
+
});
|
|
3401
|
+
} catch (err) {
|
|
3402
|
+
ctx.logger.error("migration: in-place reload FAILED — falling back to a provider restart", {
|
|
3403
|
+
tags: { deviceId: target.deviceId },
|
|
3404
|
+
meta: {
|
|
3405
|
+
addonId: target.addonId,
|
|
3406
|
+
stableId: target.stableId,
|
|
3407
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
fallbackAddons.set(target.addonId, target.deviceId);
|
|
3411
|
+
}
|
|
3412
|
+
}));
|
|
3413
|
+
if (fallbackAddons.size === 0) return;
|
|
3414
|
+
await Promise.all([...fallbackAddons.entries()].map(async ([addonId, migratedDeviceId]) => {
|
|
3415
|
+
const owned = [...pctx.metaStore.idToAddonId.entries()].filter(([, a]) => a === addonId).length;
|
|
3416
|
+
ctx.logger.info("migration: restarting a whole provider to flush the id maps", {
|
|
3417
|
+
tags: { deviceId: migratedDeviceId },
|
|
3418
|
+
meta: {
|
|
3419
|
+
addonId,
|
|
3420
|
+
devicesTakenDown: owned
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
3424
|
+
}));
|
|
3425
|
+
}
|
|
3426
|
+
//#endregion
|
|
3427
|
+
//#region src/builtins/device-manager/migrate-hardware-state.ts
|
|
3428
|
+
/**
|
|
3429
|
+
* The per-device state half of a migration (`deviceManager.migrateDevice`).
|
|
3430
|
+
*
|
|
3431
|
+
* ## The rule
|
|
3432
|
+
*
|
|
3433
|
+
* **Role-scoped state follows the NUMBER. Hardware-scoped state follows the
|
|
3434
|
+
* DEVICE.** `swapIds` moves the device ROWS — `addonId` and `stableId` follow
|
|
3435
|
+
* the hardware — but the per-device CONFIG blob and the runtime-state blob are
|
|
3436
|
+
* keyed by the number in a separate store, and until 2026-09-01 they simply
|
|
3437
|
+
* stayed put. Measured on the live hub after replacing a Hikvision (id 618)
|
|
3438
|
+
* with a Reolink: device 618 — Reolink hardware — was dialled at
|
|
3439
|
+
* `192.168.1.245:80` with `deviceCache.model="DS-2DE2A404IW-DE3/W"`, the
|
|
3440
|
+
* Hikvision's address, credentials and cached model, and never came online.
|
|
3441
|
+
* Device 4362 wore the Reolink's config on Hikvision hardware. The two devices
|
|
3442
|
+
* were wearing each other's identity.
|
|
3443
|
+
*
|
|
3444
|
+
* The proof the config belongs to the hardware, not the number: writing the
|
|
3445
|
+
* Hikvision blob onto the Reolink was REJECTED by the Reolink driver's own
|
|
3446
|
+
* zod schema (`motionSensitivity: 60` vs max 50). A config that fails the
|
|
3447
|
+
* other provider's schema is a config for different hardware.
|
|
3448
|
+
*
|
|
3449
|
+
* ## What swaps, what stays
|
|
3450
|
+
*
|
|
3451
|
+
* - The DRIVER config keys (host, port, credentials, `deviceCache`, every
|
|
3452
|
+
* schema field) SWAP — they describe the box.
|
|
3453
|
+
* - The device-manager-owned section keys ({@link DEVICE_STORE_SECTIONS}:
|
|
3454
|
+
* linked-devices mode/ids) STAY with the number — a sensor is linked to
|
|
3455
|
+
* "the driveway camera", not to a serial number.
|
|
3456
|
+
* - `_profileMap` is DROPPED from both. It is the legacy spelling of the
|
|
3457
|
+
* stream-profile assignment (the live authority is the stream broker's own
|
|
3458
|
+
* map — see `StreamsTab.tsx`, which stopped writing it), and either copy
|
|
3459
|
+
* names stream ids of the wrong hardware after a swap.
|
|
3460
|
+
* - The runtime-state blobs (`__device-state:<id>` — battery snapshot, sleep
|
|
3461
|
+
* flag, feature probe) SWAP wholesale: every slice mirrors the box.
|
|
3462
|
+
*
|
|
3463
|
+
* ## Crash points are loud, same as `swapIds`
|
|
3464
|
+
*
|
|
3465
|
+
* There is no transaction: each blob is cleared and rewritten. A crash between
|
|
3466
|
+
* clear and write leaves a device with NO config — a provider that fails to
|
|
3467
|
+
* construct and says so — never two devices wearing each other's, which is the
|
|
3468
|
+
* state that reads as plausible and costs a day.
|
|
3469
|
+
*/
|
|
3470
|
+
/** Legacy stream-profile assignment key (cf. `device-queries.ts`). */
|
|
3471
|
+
var PROFILE_MAP_CONFIG_KEY = "_profileMap";
|
|
3472
|
+
/** Config keys that belong to the camera's ROLE and stay with the number. */
|
|
3473
|
+
var ROLE_SCOPED_CONFIG_KEYS = new Set(DEVICE_STORE_SECTIONS.flatMap((section) => [...section.keys]));
|
|
3474
|
+
function splitConfig(config) {
|
|
3475
|
+
const hardware = {};
|
|
3476
|
+
const role = {};
|
|
3477
|
+
for (const [key, value] of Object.entries(config)) {
|
|
3478
|
+
if (key === PROFILE_MAP_CONFIG_KEY) continue;
|
|
3479
|
+
if (ROLE_SCOPED_CONFIG_KEYS.has(key)) role[key] = value;
|
|
3480
|
+
else hardware[key] = value;
|
|
3481
|
+
}
|
|
3482
|
+
return {
|
|
3483
|
+
hardware,
|
|
3484
|
+
role
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Swap the hardware-scoped halves of two devices' persisted state. Runs right
|
|
3489
|
+
* after `swapIds`, under the same meta write lock. THROWS on failure: a
|
|
3490
|
+
* migration whose rows swapped but whose configs did not is exactly the
|
|
3491
|
+
* incident this module exists to prevent, and it must never be reported as a
|
|
3492
|
+
* success.
|
|
3493
|
+
*/
|
|
3494
|
+
async function swapHardwareState(settings, sourceId, targetId) {
|
|
3495
|
+
const sourceConfig = await settings.readDeviceStore(sourceId);
|
|
3496
|
+
const targetConfig = await settings.readDeviceStore(targetId);
|
|
3497
|
+
const source = splitConfig(sourceConfig);
|
|
3498
|
+
const target = splitConfig(targetConfig);
|
|
3499
|
+
await settings.clearDeviceStore(sourceId);
|
|
3500
|
+
await settings.writeDeviceStore(sourceId, {
|
|
3501
|
+
...target.hardware,
|
|
3502
|
+
...source.role
|
|
3503
|
+
});
|
|
3504
|
+
await settings.clearDeviceStore(targetId);
|
|
3505
|
+
await settings.writeDeviceStore(targetId, {
|
|
3506
|
+
...source.hardware,
|
|
3507
|
+
...target.role
|
|
3508
|
+
});
|
|
3509
|
+
const sourceRuntimeState = await settings.readDeviceRuntimeState(sourceId);
|
|
3510
|
+
const targetRuntimeState = await settings.readDeviceRuntimeState(targetId);
|
|
3511
|
+
await settings.writeDeviceRuntimeState(sourceId, targetRuntimeState);
|
|
3512
|
+
await settings.writeDeviceRuntimeState(targetId, sourceRuntimeState);
|
|
3513
|
+
return {
|
|
3514
|
+
runtimeStateNowAtSource: targetRuntimeState,
|
|
3515
|
+
runtimeStateNowAtTarget: sourceRuntimeState,
|
|
3516
|
+
hardwareKeysToSource: Object.keys(target.hardware).toSorted(),
|
|
3517
|
+
hardwareKeysToTarget: Object.keys(source.hardware).toSorted(),
|
|
3518
|
+
roleKeysKeptAtSource: Object.keys(source.role).toSorted(),
|
|
3519
|
+
roleKeysKeptAtTarget: Object.keys(target.role).toSorted()
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
//#endregion
|
|
3347
3523
|
//#region src/builtins/device-manager/device-meta-actions.ts
|
|
3348
3524
|
/**
|
|
3349
3525
|
* Device meta-mutation + persistence actions for the device-manager addon —
|
|
@@ -3423,48 +3599,126 @@ async function allocateDeviceId(pctx, input) {
|
|
|
3423
3599
|
* The composition lives in `migrate-device.ts`, deliberately free of this
|
|
3424
3600
|
* file's context so the decisions it carries — above all "never report a switch
|
|
3425
3601
|
* as off when it could not be written" — are testable without a hub. This is
|
|
3426
|
-
* the wiring: the
|
|
3602
|
+
* the wiring: the real dependencies, and the write lock.
|
|
3427
3603
|
*
|
|
3428
3604
|
* The temp id comes from `allocateNextDeviceId`, the monotonic counter, so the
|
|
3429
3605
|
* value a crash could park a row on was never used and will never be reused.
|
|
3430
3606
|
* `swapIds` therefore runs INSIDE the same lock the id was allocated under.
|
|
3607
|
+
*
|
|
3608
|
+
* Retry-safe since 2026-09-04 (`migrate-guard.ts`): a concurrent call touching
|
|
3609
|
+
* either device is REFUSED before it can queue on the lock, and a repeat of a
|
|
3610
|
+
* completed swap — matched against the `addonId`/`stableId` fingerprint the
|
|
3611
|
+
* swap left on the rows — is a no-op returning the original report. A client
|
|
3612
|
+
* retry ran a second full cycle on 2026-09-01 and swapped two cameras back;
|
|
3613
|
+
* neither path can re-run a swap any more.
|
|
3431
3614
|
*/
|
|
3432
3615
|
async function migrateDevice(pctx, input) {
|
|
3433
3616
|
const { sourceId, targetId } = input;
|
|
3434
3617
|
const ctx = pctx.host.ctx;
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
},
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
}
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
for (const deviceId of deviceIds) {
|
|
3452
|
-
const addonId = pctx.metaStore.idToAddonId.get(deviceId);
|
|
3453
|
-
if (addonId !== void 0) owners.add(addonId);
|
|
3454
|
-
}
|
|
3455
|
-
for (const addonId of owners) await ctx.api.addons.restartAddon.mutate({ addonId });
|
|
3456
|
-
},
|
|
3457
|
-
log: (message, meta) => {
|
|
3458
|
-
ctx.logger.info(message, {
|
|
3618
|
+
pctx.migrationGuard.begin(sourceId, targetId);
|
|
3619
|
+
try {
|
|
3620
|
+
const outcome = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3621
|
+
const sourceRow = await pctx.metaStore.rows.get(sourceId);
|
|
3622
|
+
if (sourceRow === null) throw new Error(`[device-manager] migrateDevice: device ${sourceId} does not exist`);
|
|
3623
|
+
const targetRow = await pctx.metaStore.rows.get(targetId);
|
|
3624
|
+
if (targetRow === null) throw new Error(`[device-manager] migrateDevice: device ${targetId} does not exist`);
|
|
3625
|
+
const replay = pctx.migrationGuard.matchReplay(sourceId, targetId, {
|
|
3626
|
+
addonId: sourceRow.meta.addonId,
|
|
3627
|
+
stableId: sourceRow.meta.stableId
|
|
3628
|
+
}, {
|
|
3629
|
+
addonId: targetRow.meta.addonId,
|
|
3630
|
+
stableId: targetRow.meta.stableId
|
|
3631
|
+
});
|
|
3632
|
+
if (replay !== null) {
|
|
3633
|
+
ctx.logger.info("migrateDevice: this swap already completed — replaying the original report, NOTHING re-run", {
|
|
3459
3634
|
tags: { deviceId: sourceId },
|
|
3460
|
-
meta
|
|
3635
|
+
meta: {
|
|
3636
|
+
sourceId,
|
|
3637
|
+
targetId
|
|
3638
|
+
}
|
|
3461
3639
|
});
|
|
3640
|
+
return {
|
|
3641
|
+
result: replay,
|
|
3642
|
+
reloadTargets: null
|
|
3643
|
+
};
|
|
3462
3644
|
}
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3645
|
+
const tempId = await pctx.metaStore.allocateNextDeviceId();
|
|
3646
|
+
const result = await migrateDevice$1({
|
|
3647
|
+
setSwitch: async (deviceId, switchId, enabled) => {
|
|
3648
|
+
if (switchId === "stream-broker") {
|
|
3649
|
+
const moved = await writeDisabledFlagUnderLock(pctx, deviceId, !enabled);
|
|
3650
|
+
emitDisabledEvents(pctx, deviceId, !enabled, moved);
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
await ctx.api.pipelineOrchestrator.setCameraSwitch.mutate({
|
|
3654
|
+
deviceId,
|
|
3655
|
+
switchId,
|
|
3656
|
+
enabled
|
|
3657
|
+
});
|
|
3658
|
+
},
|
|
3659
|
+
swapIds: async (a, b) => {
|
|
3660
|
+
await pctx.metaStore.rows.swapIds(a, b, tempId);
|
|
3661
|
+
},
|
|
3662
|
+
swapHardwareState: async (a, b) => {
|
|
3663
|
+
const swapped = await swapHardwareState(pctx.settings, a, b);
|
|
3664
|
+
pctx.host.seedMirror(a, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtSource));
|
|
3665
|
+
pctx.host.seedMirror(b, pctx.host.withResetSessionProbe(swapped.runtimeStateNowAtTarget));
|
|
3666
|
+
ctx.logger.info("migration: hardware-scoped device state swapped", {
|
|
3667
|
+
tags: { deviceId: a },
|
|
3668
|
+
meta: {
|
|
3669
|
+
sourceId: a,
|
|
3670
|
+
targetId: b,
|
|
3671
|
+
hardwareKeysToSource: swapped.hardwareKeysToSource,
|
|
3672
|
+
hardwareKeysToTarget: swapped.hardwareKeysToTarget,
|
|
3673
|
+
roleKeysKeptAtSource: swapped.roleKeysKeptAtSource,
|
|
3674
|
+
roleKeysKeptAtTarget: swapped.roleKeysKeptAtTarget
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
},
|
|
3678
|
+
forgetStreamState: async (deviceId) => {
|
|
3679
|
+
await ctx.api.streamBroker.forgetDeviceHardware.mutate({ deviceId });
|
|
3680
|
+
},
|
|
3681
|
+
log: (message, meta) => {
|
|
3682
|
+
ctx.logger.info(message, {
|
|
3683
|
+
tags: { deviceId: sourceId },
|
|
3684
|
+
meta
|
|
3685
|
+
});
|
|
3686
|
+
}
|
|
3687
|
+
}, {
|
|
3688
|
+
sourceId,
|
|
3689
|
+
targetId
|
|
3690
|
+
});
|
|
3691
|
+
pctx.migrationGuard.recordCompleted({
|
|
3692
|
+
sourceId,
|
|
3693
|
+
targetId,
|
|
3694
|
+
identityAtSource: {
|
|
3695
|
+
addonId: targetRow.meta.addonId,
|
|
3696
|
+
stableId: targetRow.meta.stableId
|
|
3697
|
+
},
|
|
3698
|
+
identityAtTarget: {
|
|
3699
|
+
addonId: sourceRow.meta.addonId,
|
|
3700
|
+
stableId: sourceRow.meta.stableId
|
|
3701
|
+
},
|
|
3702
|
+
result
|
|
3703
|
+
});
|
|
3704
|
+
return {
|
|
3705
|
+
result,
|
|
3706
|
+
reloadTargets: [{
|
|
3707
|
+
deviceId: sourceId,
|
|
3708
|
+
addonId: targetRow.meta.addonId,
|
|
3709
|
+
stableId: targetRow.meta.stableId
|
|
3710
|
+
}, {
|
|
3711
|
+
deviceId: targetId,
|
|
3712
|
+
addonId: sourceRow.meta.addonId,
|
|
3713
|
+
stableId: sourceRow.meta.stableId
|
|
3714
|
+
}]
|
|
3715
|
+
};
|
|
3466
3716
|
});
|
|
3467
|
-
|
|
3717
|
+
if (outcome.reloadTargets !== null) await reloadMigratedDevices(pctx, outcome.reloadTargets);
|
|
3718
|
+
return outcome.result;
|
|
3719
|
+
} finally {
|
|
3720
|
+
pctx.migrationGuard.end(sourceId, targetId);
|
|
3721
|
+
}
|
|
3468
3722
|
}
|
|
3469
3723
|
async function registerDevice(pctx, input) {
|
|
3470
3724
|
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
@@ -4151,35 +4405,23 @@ async function setMetadata(pctx, input) {
|
|
|
4151
4405
|
});
|
|
4152
4406
|
}
|
|
4153
4407
|
/**
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
4156
|
-
*
|
|
4157
|
-
*
|
|
4158
|
-
*
|
|
4159
|
-
* TWO events leave here, and they are not the same message:
|
|
4160
|
-
*
|
|
4161
|
-
* - `device.meta-changed` fires on EVERY call. It is the meta-surface echo
|
|
4162
|
-
* every UI redraws on, and idempotence is not interesting to it.
|
|
4163
|
-
* - `device.disabled` / `device.enabled` fire ONLY when the flag actually
|
|
4164
|
-
* MOVES. They are the lifecycle announcement — the operator asked to be
|
|
4165
|
-
* told when a camera is switched off, and a write that re-states a flag the
|
|
4166
|
-
* device already had is not that. Telemetry semantics (D8) are fine here: a
|
|
4167
|
-
* dropped announcement costs a notification, never durable state, and the
|
|
4168
|
-
* meta row is the authority either way (D62 — this switch stores nothing of
|
|
4169
|
-
* its own).
|
|
4408
|
+
* Read + patch the `disabled` flag. The caller HOLDS the meta write lock —
|
|
4409
|
+
* this is split out of {@link setDisabled} so `migrateDevice`, which holds the
|
|
4410
|
+
* lock for its whole run, can write the same authority without re-acquiring
|
|
4411
|
+
* it (the self-deadlock documented at its `setSwitch`).
|
|
4170
4412
|
*/
|
|
4171
|
-
async function
|
|
4172
|
-
const
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4413
|
+
async function writeDisabledFlagUnderLock(pctx, deviceId, disabled) {
|
|
4414
|
+
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
4415
|
+
if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
|
|
4416
|
+
const { meta: m } = persisted;
|
|
4417
|
+
await pctx.metaStore.rows.patch(deviceId, { disabled });
|
|
4418
|
+
return {
|
|
4419
|
+
changed: (m.disabled ?? false) !== disabled,
|
|
4420
|
+
integrationId: m.integrationId ?? ""
|
|
4421
|
+
};
|
|
4422
|
+
}
|
|
4423
|
+
/** The two announcements — meta echo always, lifecycle only on a transition. */
|
|
4424
|
+
function emitDisabledEvents(pctx, deviceId, disabled, moved) {
|
|
4183
4425
|
pctx.host.ctx.eventBus.emit({
|
|
4184
4426
|
id: (0, node_crypto.randomUUID)(),
|
|
4185
4427
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -4209,6 +4451,10 @@ async function setDisabled(pctx, input) {
|
|
|
4209
4451
|
}
|
|
4210
4452
|
});
|
|
4211
4453
|
}
|
|
4454
|
+
async function setDisabled(pctx, input) {
|
|
4455
|
+
const { deviceId, disabled } = input;
|
|
4456
|
+
emitDisabledEvents(pctx, deviceId, disabled, await pctx.metaStore.withMetaWriteLock(() => writeDisabledFlagUnderLock(pctx, deviceId, disabled)));
|
|
4457
|
+
}
|
|
4212
4458
|
async function loadRuntimeState(pctx, input) {
|
|
4213
4459
|
const { deviceId } = input;
|
|
4214
4460
|
if (!await pctx.metaStore.resolvePersistedById(deviceId)) return {};
|
|
@@ -5823,6 +6069,43 @@ function resolveNativeCapOwnerSync(pctx, capName, deviceId) {
|
|
|
5823
6069
|
};
|
|
5824
6070
|
return resolveRemoteNativeCapFromRegistry(pctx.bindingsDeps, capName, deviceId);
|
|
5825
6071
|
}
|
|
6072
|
+
function sameIdentity(a, b) {
|
|
6073
|
+
return a.addonId === b.addonId && a.stableId === b.stableId;
|
|
6074
|
+
}
|
|
6075
|
+
/** One guard per device-manager instance. `now` is injectable for the specs. */
|
|
6076
|
+
function createMigrationGuard(now = Date.now) {
|
|
6077
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
6078
|
+
const completed = /* @__PURE__ */ new Map();
|
|
6079
|
+
const pairKey = (sourceId, targetId) => `${sourceId}->${targetId}`;
|
|
6080
|
+
return {
|
|
6081
|
+
begin(sourceId, targetId) {
|
|
6082
|
+
for (const id of [sourceId, targetId]) if (inFlight.has(id)) throw new Error(`[device-manager] migrateDevice: a migration involving device ${id} is already in flight — this call was refused, not queued. The running migration's report stands; re-read the device rows before retrying.`);
|
|
6083
|
+
inFlight.add(sourceId);
|
|
6084
|
+
inFlight.add(targetId);
|
|
6085
|
+
},
|
|
6086
|
+
end(sourceId, targetId) {
|
|
6087
|
+
inFlight.delete(sourceId);
|
|
6088
|
+
inFlight.delete(targetId);
|
|
6089
|
+
},
|
|
6090
|
+
recordCompleted(record) {
|
|
6091
|
+
completed.set(pairKey(record.sourceId, record.targetId), {
|
|
6092
|
+
...record,
|
|
6093
|
+
completedAt: now()
|
|
6094
|
+
});
|
|
6095
|
+
},
|
|
6096
|
+
matchReplay(sourceId, targetId, atSource, atTarget) {
|
|
6097
|
+
const key = pairKey(sourceId, targetId);
|
|
6098
|
+
const record = completed.get(key);
|
|
6099
|
+
if (record === void 0) return null;
|
|
6100
|
+
if (now() - record.completedAt > 9e5) {
|
|
6101
|
+
completed.delete(key);
|
|
6102
|
+
return null;
|
|
6103
|
+
}
|
|
6104
|
+
if (!sameIdentity(record.identityAtSource, atSource) || !sameIdentity(record.identityAtTarget, atTarget)) return null;
|
|
6105
|
+
return record.result;
|
|
6106
|
+
}
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
5826
6109
|
//#endregion
|
|
5827
6110
|
//#region src/builtins/device-manager/device-manager.addon.ts
|
|
5828
6111
|
var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
@@ -6063,6 +6346,7 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
|
|
|
6063
6346
|
settings,
|
|
6064
6347
|
registry,
|
|
6065
6348
|
bindingsDeps: this.bindingsDeps,
|
|
6349
|
+
migrationGuard: createMigrationGuard(),
|
|
6066
6350
|
requireDeviceOps,
|
|
6067
6351
|
stampIntegrationId: stampIntegrationId$1,
|
|
6068
6352
|
get provider() {
|