@camstack/system 1.2.437 → 1.2.438
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 +64 -12
- package/dist/addon-runner.mjs +65 -13
- 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 +7 -1
- package/dist/builtins/alerts/alerts.addon.mjs +7 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +9 -1
- package/dist/builtins/camera-grid/index.mjs +9 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +12 -5
- package/dist/builtins/console-logging/index.mjs +12 -5
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.d.ts +5 -0
- package/dist/builtins/device-manager/device-manager.addon.js +702 -28
- package/dist/builtins/device-manager/device-manager.addon.mjs +702 -28
- package/dist/builtins/device-manager/device-meta-types.d.ts +4 -0
- package/dist/builtins/device-manager/device-provider-context.d.ts +9 -0
- package/dist/builtins/device-manager/device-removal-hooks.d.ts +103 -0
- package/dist/builtins/device-manager/device-row-store.d.ts +1 -0
- package/dist/builtins/device-manager/device-tombstone-store.d.ts +58 -0
- package/dist/builtins/device-manager/removal-cleanup.d.ts +50 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +37 -4
- package/dist/builtins/keychain/index.mjs +37 -4
- package/dist/builtins/keychain/keychain-service.d.ts +14 -0
- package/dist/builtins/keychain/keychain-store.d.ts +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 +12 -12
- package/dist/builtins/local-auth/local-auth.addon.mjs +12 -12
- package/dist/builtins/local-network/local-network.addon.js +10 -1
- package/dist/builtins/local-network/local-network.addon.mjs +10 -1
- package/dist/builtins/loki-logging/index.js +28 -5
- package/dist/builtins/loki-logging/index.mjs +28 -5
- package/dist/builtins/native-metrics/native-metrics.addon.js +11 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +11 -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 +11 -1
- package/dist/builtins/snapshot/index.mjs +11 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +4 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +4 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
- 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-config/system-config.addon.js +20 -2
- package/dist/builtins/system-config/system-config.addon.mjs +20 -2
- package/dist/builtins/winston-logging/index.js +14 -5
- package/dist/builtins/winston-logging/index.mjs +14 -5
- package/dist/{composition-sources-BIT8IUKm.js → composition-sources-DI3Z83vG.js} +1 -1
- package/dist/{composition-sources-tpYR0f1x.mjs → composition-sources-DpHGCjdL.mjs} +1 -1
- package/dist/{constants-Bc6trhoW.mjs → constants-DQK705Sb.mjs} +1 -1
- package/dist/{constants-CwRY32Nx.js → constants-wkcn5Ij3.js} +1 -1
- package/dist/{device-ca-DImD8IzR.js → device-ca-BSvkXqyI.js} +1 -1
- package/dist/{device-ca-BhdBByGD.mjs → device-ca-DvIZjXIl.mjs} +1 -1
- package/dist/{dist-m5JkT8UN.mjs → dist-Dc8GKC5H.mjs} +272 -7
- package/dist/{dist-CEAyXW3Q.js → dist-GgG-7amw.js} +307 -6
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/i18n/catalog-registry.d.ts +35 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +663 -20
- package/dist/index.mjs +659 -22
- package/dist/kernel/gc-census.d.ts +18 -1
- package/dist/kernel/settings-view-types.d.ts +2 -0
- package/dist/kernel/transport/local-transport.d.ts +3 -0
- package/dist/kernel/transport/peer-answered-error.d.ts +5 -1
- package/dist/{restore-archive-C75srVbG.js → restore-archive-CTRz8v9o.js} +1 -1
- package/dist/{restore-archive-DNxAStrJ.mjs → restore-archive-DPg9duVY.mjs} +1 -1
- package/dist/{retired-settings-keys-DNfV28R_.js → retired-settings-keys-Dr_4lfKu.js} +1 -1
- package/dist/{retired-settings-keys-C5qP9QIf.mjs → retired-settings-keys-Is0zO8gX.mjs} +1 -1
- package/dist/{wal-maintenance-DvnVO5IK.mjs → wal-maintenance-Bx3lcVA7.mjs} +0 -0
- package/dist/{wal-maintenance-Ch79gy9S.js → wal-maintenance-mjB2i-VR.js} +0 -0
- package/package.json +1 -1
|
@@ -25,7 +25,11 @@ export interface ContributionShape {
|
|
|
25
25
|
sections: Array<{
|
|
26
26
|
id: string;
|
|
27
27
|
title: string;
|
|
28
|
+
/** `"<namespace>:<key>"` of the translated `title` (D863). */
|
|
29
|
+
titleKey?: string;
|
|
28
30
|
description?: string;
|
|
31
|
+
/** `"<namespace>:<key>"` of the translated `description` (D863). */
|
|
32
|
+
descriptionKey?: string;
|
|
29
33
|
style?: "card" | "accordion";
|
|
30
34
|
defaultCollapsed?: boolean;
|
|
31
35
|
columns?: 1 | 2 | 3 | 4;
|
|
@@ -20,6 +20,9 @@ import type { DeviceManagerSettings, DeviceMetaStore } from "./device-meta-store
|
|
|
20
20
|
import type { FieldClaimsIndex } from "./field-claims-index.js";
|
|
21
21
|
import type { OwnedCaps } from "./field-ownership.js";
|
|
22
22
|
import type { MigrationGuard } from "./migrate-guard.js";
|
|
23
|
+
import type { DeviceTombstoneStore } from "./device-tombstone-store.js";
|
|
24
|
+
import type { RemovalHookDeps } from "./device-removal-hooks.js";
|
|
25
|
+
import type { RemovalCleanup } from "./removal-cleanup.js";
|
|
23
26
|
import type { RemovalFence } from "./removal-fence.js";
|
|
24
27
|
import type { SecretConfigPathRegistry } from "./secret-config-paths.js";
|
|
25
28
|
import type { SeedMode } from "./device-state-mirror.js";
|
|
@@ -101,6 +104,12 @@ export interface ProviderContext {
|
|
|
101
104
|
* tears down, which is how a deleted camera came back as a config-less
|
|
102
105
|
* shell (D376). */
|
|
103
106
|
readonly removalFence: RemovalFence;
|
|
107
|
+
/** Cleans a removed device's data store by store, bounded retry on failure (D864). */
|
|
108
|
+
readonly removalCleanup: RemovalCleanup;
|
|
109
|
+
/** The tombstone of every deleted device — the reliable record other addons reconcile from (D865). */
|
|
110
|
+
readonly tombstones: DeviceTombstoneStore;
|
|
111
|
+
/** Tells every `device-removal-hook` implementer a device was deleted (D865). */
|
|
112
|
+
readonly removalHooks: RemovalHookDeps;
|
|
104
113
|
/** The secret config paths each device declared at `registerDevice` (D783). */
|
|
105
114
|
readonly secretConfigPaths: SecretConfigPathRegistry;
|
|
106
115
|
/** Resolve the per-device `device-ops` native provider (throws when absent). */
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* device-manager's side of `device-removal-hook` (D865): tell every implementer
|
|
3
|
+
* a device was deleted, and answer the admin question "which stored data belongs
|
|
4
|
+
* to devices that no longer exist?".
|
|
5
|
+
*
|
|
6
|
+
* Two entry points, one rule: **an implementer is told to delete only on a
|
|
7
|
+
* recorded delete (a tombstone), or on an explicit operator confirm.** Nothing
|
|
8
|
+
* here infers a deletion from a device being absent from a single read.
|
|
9
|
+
*/
|
|
10
|
+
import { type CleanupDeletedDevicesResult, type DeviceDataEntry, type DeviceRemovedInput, type DeviceRemovedResult, type IScopedLogger, type OrphanDeviceData, type SettingsStoreClient } from "@camstack/types";
|
|
11
|
+
import type { RemovalCleanup, RemovalStep } from "./removal-cleanup.js";
|
|
12
|
+
/** The capability name implementers register under. */
|
|
13
|
+
export declare const DEVICE_REMOVAL_HOOK_CAP = "device-removal-hook";
|
|
14
|
+
/** The slice of `ctx.api` this module uses — a pin is the `addonId` selector. */
|
|
15
|
+
export interface RemovalHookApi {
|
|
16
|
+
readonly addons: {
|
|
17
|
+
readonly listCapabilityProviders: {
|
|
18
|
+
query(input: {
|
|
19
|
+
capName: string;
|
|
20
|
+
}): Promise<readonly {
|
|
21
|
+
readonly addonId: string;
|
|
22
|
+
}[]>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
readonly deviceRemovalHook: {
|
|
26
|
+
readonly onDeviceRemoved: {
|
|
27
|
+
mutate(input: DeviceRemovedInput & {
|
|
28
|
+
addonId: string;
|
|
29
|
+
}): Promise<DeviceRemovedResult>;
|
|
30
|
+
};
|
|
31
|
+
readonly listDeviceData: {
|
|
32
|
+
query(input: {
|
|
33
|
+
addonId: string;
|
|
34
|
+
}): Promise<readonly DeviceDataEntry[]>;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface RemovalHookDeps {
|
|
39
|
+
readonly api: RemovalHookApi | undefined;
|
|
40
|
+
readonly logger: IScopedLogger;
|
|
41
|
+
readonly cleanup: RemovalCleanup;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The removal step that calls every implementer. Each implementer is its own
|
|
45
|
+
* nested step, so a failing one is retried by the bounded reconcile without
|
|
46
|
+
* re-calling the others. Failing to ENUMERATE the implementers throws, and the
|
|
47
|
+
* outer cleanup retries the whole step.
|
|
48
|
+
*/
|
|
49
|
+
export declare function removalHookStep(deps: RemovalHookDeps, input: DeviceRemovedInput): RemovalStep;
|
|
50
|
+
/** Why a cleanup refuses to list or delete anything. */
|
|
51
|
+
export type AbsenceCheck = {
|
|
52
|
+
readonly ok: true;
|
|
53
|
+
readonly known: ReadonlySet<number>;
|
|
54
|
+
} | {
|
|
55
|
+
readonly ok: false;
|
|
56
|
+
readonly reason: string;
|
|
57
|
+
};
|
|
58
|
+
export interface AbsenceReads {
|
|
59
|
+
/** One read of the device ledger: its ids and the row count the engine reports. */
|
|
60
|
+
readonly readFleet: () => Promise<{
|
|
61
|
+
readonly ids: readonly number[];
|
|
62
|
+
readonly rowCount: number;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The ids device-manager knows, from TWO consistent reads (D49). Refuses when a
|
|
67
|
+
* read throws, answers empty (an empty fleet and a failed read look the same),
|
|
68
|
+
* or returns fewer ids than the ledger has rows (a malformed row is skipped by
|
|
69
|
+
* the decoder, and a device skipped is a device whose data would be deleted).
|
|
70
|
+
* The known set is the UNION of both reads: an id is absent only when BOTH
|
|
71
|
+
* reads lack it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function checkFleetTwice(reads: AbsenceReads): Promise<AbsenceCheck>;
|
|
74
|
+
/** Stores holding data for devices that are not in `known`, grouped per device. */
|
|
75
|
+
export declare function orphanDevices(held: readonly (readonly [string, DeviceDataEntry])[], known: ReadonlySet<number>): {
|
|
76
|
+
readonly devices: readonly OrphanDeviceData[];
|
|
77
|
+
readonly idSpaceMismatch: boolean;
|
|
78
|
+
};
|
|
79
|
+
/** Every addon's per-device settings scopes, read and cleared centrally (D865). */
|
|
80
|
+
export interface SettingsScopeAccess {
|
|
81
|
+
/** One entry per (device, addon) holding settings; throws on a failed or truncated read. */
|
|
82
|
+
list(): Promise<readonly {
|
|
83
|
+
readonly deviceId: number;
|
|
84
|
+
readonly addonId: string;
|
|
85
|
+
}[]>;
|
|
86
|
+
/** Delete every addon's scope of one device; resolves to the rows deleted. */
|
|
87
|
+
clear(deviceId: number): Promise<number>;
|
|
88
|
+
}
|
|
89
|
+
export interface CleanupDeps {
|
|
90
|
+
readonly api: RemovalHookApi | undefined;
|
|
91
|
+
readonly settings: SettingsScopeAccess | undefined;
|
|
92
|
+
readonly logger: IScopedLogger;
|
|
93
|
+
readonly reads: AbsenceReads;
|
|
94
|
+
readonly now?: () => number;
|
|
95
|
+
}
|
|
96
|
+
/** The admin "Clean data of deleted devices" action: dry run unless `confirm`. */
|
|
97
|
+
export declare function cleanupDeletedDevicesData(deps: CleanupDeps, input: {
|
|
98
|
+
readonly confirm: boolean;
|
|
99
|
+
}): Promise<CleanupDeletedDevicesResult>;
|
|
100
|
+
/** The settings-store collection every addon's per-device settings share. */
|
|
101
|
+
export declare const ADDON_DEVICE_SETTINGS_COLLECTION = "addon-device-settings";
|
|
102
|
+
/** Scope access over the settings store; undefined when the store is unavailable. */
|
|
103
|
+
export declare function settingsScopes(store: Pick<SettingsStoreClient, "query" | "deleteWhere"> | undefined): SettingsScopeAccess | undefined;
|
|
@@ -238,6 +238,7 @@ export interface DeviceRow {
|
|
|
238
238
|
/** Has `registerDevice` ever run for this device? (former `deviceIndex` membership) */
|
|
239
239
|
readonly registered: boolean;
|
|
240
240
|
}
|
|
241
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
241
242
|
/**
|
|
242
243
|
* Decode one stored row.
|
|
243
244
|
*
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tombstones of deleted devices (D865).
|
|
3
|
+
*
|
|
4
|
+
* A device DELETE is a fact other addons must be able to learn reliably, long
|
|
5
|
+
* after the `DeviceUnregistered` event (telemetry, droppable — D8/D11) is gone.
|
|
6
|
+
* device-manager is the only authority on whether a device exists, so the record
|
|
7
|
+
* of a deletion lives here, in its own collection, beside the device ledger.
|
|
8
|
+
*
|
|
9
|
+
* ## What a tombstone is NOT
|
|
10
|
+
*
|
|
11
|
+
* It is not "the device is unregistered / offline / its provider is restarting".
|
|
12
|
+
* Those are states of a device that still exists; only {@link record}'s one
|
|
13
|
+
* caller — `removeDevice`, the single door every delete converges on — writes a
|
|
14
|
+
* tombstone. Anything that reconciles from tombstones therefore can never
|
|
15
|
+
* destroy data of a device that merely went quiet.
|
|
16
|
+
*
|
|
17
|
+
* ## Expiry
|
|
18
|
+
*
|
|
19
|
+
* {@link DEVICE_TOMBSTONE_RETENTION_MS} (90 days). A tombstone older than that
|
|
20
|
+
* is deleted the next time one is written or read. Ninety days is generous on
|
|
21
|
+
* purpose: an addon that stayed disabled for a quarter still learns of the
|
|
22
|
+
* deletions on its return. Data of a device deleted longer ago than that is
|
|
23
|
+
* reachable only by the admin "clean data of deleted devices" action.
|
|
24
|
+
*/
|
|
25
|
+
import { type CollectionColumn, type CollectionIndex, type DeviceTombstone, type IScopedLogger } from "@camstack/types";
|
|
26
|
+
import { type DeviceRowBackend } from "./device-row-store.js";
|
|
27
|
+
/**
|
|
28
|
+
* @durable class=ledger owner=device-manager
|
|
29
|
+
* write="`removeDevice` records one row per deleted device (key = device id) after the
|
|
30
|
+
* device row is gone"
|
|
31
|
+
* retention="90 days (`DEVICE_TOMBSTONE_RETENTION_MS`), pruned whenever a tombstone is
|
|
32
|
+
* written or the list is read. Bounded by the deletions an operator makes in a quarter."
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEVICE_TOMBSTONES_COLLECTION = "device-manager:tombstones";
|
|
35
|
+
export declare const DEVICE_TOMBSTONE_COLUMNS: readonly CollectionColumn[];
|
|
36
|
+
export declare const DEVICE_TOMBSTONE_INDEXES: readonly CollectionIndex[];
|
|
37
|
+
export interface DeviceTombstoneStoreOptions {
|
|
38
|
+
readonly backend: DeviceRowBackend;
|
|
39
|
+
readonly logger: IScopedLogger;
|
|
40
|
+
readonly now?: () => number;
|
|
41
|
+
readonly retentionMs?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare class DeviceTombstoneStore {
|
|
44
|
+
private readonly backend;
|
|
45
|
+
private readonly logger;
|
|
46
|
+
private readonly now;
|
|
47
|
+
private readonly retentionMs;
|
|
48
|
+
private declared;
|
|
49
|
+
constructor(opts: DeviceTombstoneStoreOptions);
|
|
50
|
+
/** Lazy, idempotent declaration; a rejection is not latched. */
|
|
51
|
+
private declare;
|
|
52
|
+
/** Record a deletion. Re-recording the same id keeps the EARLIER `deletedAt`. */
|
|
53
|
+
record(tombstone: DeviceTombstone): Promise<void>;
|
|
54
|
+
/** Tombstones with `deletedAt >= sinceMs`, oldest first. Expired ones are pruned first. */
|
|
55
|
+
listSince(sinceMs: number): Promise<readonly DeviceTombstone[]>;
|
|
56
|
+
/** Delete tombstones older than the retention window. Best-effort: a failure is logged. */
|
|
57
|
+
pruneExpired(): Promise<number>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a device removal leaves behind is cleaned AT REMOVAL, by steps that
|
|
3
|
+
* cannot block one another (D864).
|
|
4
|
+
*
|
|
5
|
+
* `removeDevice` used to run its clean-up as one straight line: the first step
|
|
6
|
+
* that threw skipped every later one, and the keychain was not in the line at
|
|
7
|
+
* all — a device-scoped secret (`secret://provider-dreame/device/4483/…`)
|
|
8
|
+
* outlived its device for good. Each store is a {@link RemovalStep} now: run
|
|
9
|
+
* independently, counted, logged ONCE per device with `tags: { deviceId }`,
|
|
10
|
+
* and a step that failed is retried by a BOUNDED in-memory reconcile
|
|
11
|
+
* (a few attempts, growing delays, then it gives up loudly).
|
|
12
|
+
*
|
|
13
|
+
* There is deliberately no boot-time sweep (the operator forbids purges on
|
|
14
|
+
* boot): a retry that dies with the process is picked up by the keychain
|
|
15
|
+
* admin's orphan listing (System -> Secrets), which names a device-scoped
|
|
16
|
+
* secret whose device no longer points at it.
|
|
17
|
+
*/
|
|
18
|
+
import { type IScopedLogger } from "@camstack/types";
|
|
19
|
+
/** One store that holds data of a removed device. `run` returns what it deleted. */
|
|
20
|
+
export interface RemovalStep {
|
|
21
|
+
/** The store, named for the log (`keychain-secrets`, `device-config`, …). */
|
|
22
|
+
readonly store: string;
|
|
23
|
+
/** Delete this device's data from the store; resolves to the number of rows/refs removed. */
|
|
24
|
+
readonly run: () => Promise<number>;
|
|
25
|
+
}
|
|
26
|
+
/** The outcome of one {@link RemovalCleanup.run}. */
|
|
27
|
+
export interface RemovalCleanupReport {
|
|
28
|
+
readonly deviceId: number;
|
|
29
|
+
/** Rows/refs removed per store, for the steps that succeeded. */
|
|
30
|
+
readonly counts: Readonly<Record<string, number>>;
|
|
31
|
+
/** The stores whose step threw — scheduled for a bounded retry. */
|
|
32
|
+
readonly failed: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** Retry delays after a failed step; the list length bounds the attempts. */
|
|
35
|
+
export declare const REMOVAL_RETRY_DELAYS_MS: readonly number[];
|
|
36
|
+
export interface RemovalCleanupOptions {
|
|
37
|
+
readonly logger: IScopedLogger;
|
|
38
|
+
/** The log message stem; the hook fan-out (D865) runs its own instance under another one. */
|
|
39
|
+
readonly label?: string;
|
|
40
|
+
readonly retryDelaysMs?: readonly number[];
|
|
41
|
+
readonly setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
42
|
+
readonly clearTimer?: (handle: ReturnType<typeof setTimeout>) => void;
|
|
43
|
+
}
|
|
44
|
+
export interface RemovalCleanup {
|
|
45
|
+
/** Run every step independently; log once; schedule a bounded retry for the failures. */
|
|
46
|
+
run(deviceId: number, steps: readonly RemovalStep[]): Promise<RemovalCleanupReport>;
|
|
47
|
+
/** Cancel every pending retry (addon shutdown). */
|
|
48
|
+
dispose(): void;
|
|
49
|
+
}
|
|
50
|
+
export declare function createRemovalCleanup(opts: RemovalCleanupOptions): RemovalCleanup;
|
|
@@ -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-GgG-7amw.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
//#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
|
|
9
9
|
var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Or as BaseAddon, mn as logDestinationCapability } from "../../dist-Dc8GKC5H.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
//#region src/builtins/hub-forwarder/hub-forwarder-destination.ts
|
|
4
4
|
var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
|
|
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
7
|
-
const require_device_ca = require("../../device-ca-
|
|
8
|
-
const require_constants = require("../../constants-
|
|
6
|
+
const require_dist = require("../../dist-GgG-7amw.js");
|
|
7
|
+
const require_device_ca = require("../../device-ca-BSvkXqyI.js");
|
|
8
|
+
const require_constants = require("../../constants-wkcn5Ij3.js");
|
|
9
9
|
const require_platform_key_source = require("../../platform-key-source-Di7HrlGR.js");
|
|
10
10
|
let node_crypto = require("node:crypto");
|
|
11
11
|
let _camstack_types_node = require("@camstack/types/node");
|
|
@@ -709,6 +709,38 @@ var KeychainService = class {
|
|
|
709
709
|
}
|
|
710
710
|
return deleted;
|
|
711
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* Delete EVERY value whose scope is `device/<deviceId>`, whatever its owner
|
|
714
|
+
* (D864). A device-scoped secret is owned by the driver that declared it, so
|
|
715
|
+
* the device manager — which knows only that the device is gone — cannot name
|
|
716
|
+
* the owner; this is the one owner-spanning delete, and it is reserved to
|
|
717
|
+
* the device manager (a removal is the only moment a whole device scope
|
|
718
|
+
* stops being anybody's). Works while locked: it removes ciphertext, it
|
|
719
|
+
* never reads it.
|
|
720
|
+
*
|
|
721
|
+
* @returns the refs removed (never values), for the caller's one log line.
|
|
722
|
+
*/
|
|
723
|
+
async deleteDeviceScope(caller, deviceId) {
|
|
724
|
+
if (caller === null || require_dist.normalizeSecretOwner(caller) !== "device-manager") throw new KeychainForbiddenError(`device/${deviceId}`, caller);
|
|
725
|
+
const scope = require_dist.formatSecretScope({
|
|
726
|
+
kind: "device",
|
|
727
|
+
deviceId
|
|
728
|
+
});
|
|
729
|
+
const rows = (await this.deps.repo.listSecrets()).filter((row) => row.scope === scope);
|
|
730
|
+
if (rows.length === 0) return [];
|
|
731
|
+
const refs = rows.map((row) => row.ref);
|
|
732
|
+
await this.deps.repo.deleteSecrets(refs);
|
|
733
|
+
for (const row of rows) {
|
|
734
|
+
await this.audit("delete", actorOfAddon(caller), row.ref, "device-removed");
|
|
735
|
+
this.deps.events.emit({
|
|
736
|
+
ref: row.ref,
|
|
737
|
+
owner: row.owner,
|
|
738
|
+
version: null,
|
|
739
|
+
change: "delete"
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return refs;
|
|
743
|
+
}
|
|
712
744
|
async listMeta(caller) {
|
|
713
745
|
return (await this.deps.repo.listSecrets()).filter((row) => this.owns(caller, row.owner)).map(toMeta);
|
|
714
746
|
}
|
|
@@ -1040,7 +1072,7 @@ var KEYCHAIN_SECRETS_COLLECTION = "keychain:secrets";
|
|
|
1040
1072
|
*/
|
|
1041
1073
|
var KEYCHAIN_KEYS_COLLECTION = "keychain:keys";
|
|
1042
1074
|
/**
|
|
1043
|
-
* @durable class=
|
|
1075
|
+
* @durable class=audit owner=keychain
|
|
1044
1076
|
* write="one row per write / delete / rotation / reveal / recovery download"
|
|
1045
1077
|
* retention="90 days and at most 20 000 rows, pruned at boot and daily"
|
|
1046
1078
|
*/
|
|
@@ -1817,6 +1849,7 @@ var KeychainAddon = class extends require_dist.BaseAddon {
|
|
|
1817
1849
|
put: (input) => service.put(callerOf(input), input.ref, input.value),
|
|
1818
1850
|
resolve: (input) => service.resolve(callerOf(input), input.refs),
|
|
1819
1851
|
delete: async (input) => ({ deleted: await service.delete(callerOf(input), input.refs) }),
|
|
1852
|
+
deleteDeviceScope: async (input) => ({ refs: await service.deleteDeviceScope(callerOf(input), input.deviceId) }),
|
|
1820
1853
|
listMeta: (input) => service.listMeta(callerOf(input)),
|
|
1821
1854
|
getState: async () => service.getState()
|
|
1822
1855
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { d as JWT_SIGNING_KEY_REF, n as DEVICE_CA_KEY_REF, p as SYSTEM_SECRET_OWNER } from "../../device-ca-
|
|
3
|
-
import { a as MESH_CA_KEY_REF } from "../../constants-
|
|
1
|
+
import { L as KEYCHAIN_RESET_TRIGGER_ACTOR, Or as BaseAddon, Xr as formatSecretScope, ai as normalizeSecretOwner, ci as parseSecretRef, cn as keychainCapability, sn as keychainAdminCapability, yi as EventCategory } from "../../dist-Dc8GKC5H.mjs";
|
|
2
|
+
import { d as JWT_SIGNING_KEY_REF, n as DEVICE_CA_KEY_REF, p as SYSTEM_SECRET_OWNER } from "../../device-ca-DvIZjXIl.mjs";
|
|
3
|
+
import { a as MESH_CA_KEY_REF } from "../../constants-DQK705Sb.mjs";
|
|
4
4
|
import { n as getPlatformKeychainKey } from "../../platform-key-source-BWlq0jjn.mjs";
|
|
5
5
|
import { createCipheriv, createDecipheriv, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { restrictDirToOwnerSync, restrictFileToOwnerSync, writeOwnerOnlyFileSync } from "@camstack/types/node";
|
|
@@ -703,6 +703,38 @@ var KeychainService = class {
|
|
|
703
703
|
}
|
|
704
704
|
return deleted;
|
|
705
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Delete EVERY value whose scope is `device/<deviceId>`, whatever its owner
|
|
708
|
+
* (D864). A device-scoped secret is owned by the driver that declared it, so
|
|
709
|
+
* the device manager — which knows only that the device is gone — cannot name
|
|
710
|
+
* the owner; this is the one owner-spanning delete, and it is reserved to
|
|
711
|
+
* the device manager (a removal is the only moment a whole device scope
|
|
712
|
+
* stops being anybody's). Works while locked: it removes ciphertext, it
|
|
713
|
+
* never reads it.
|
|
714
|
+
*
|
|
715
|
+
* @returns the refs removed (never values), for the caller's one log line.
|
|
716
|
+
*/
|
|
717
|
+
async deleteDeviceScope(caller, deviceId) {
|
|
718
|
+
if (caller === null || normalizeSecretOwner(caller) !== "device-manager") throw new KeychainForbiddenError(`device/${deviceId}`, caller);
|
|
719
|
+
const scope = formatSecretScope({
|
|
720
|
+
kind: "device",
|
|
721
|
+
deviceId
|
|
722
|
+
});
|
|
723
|
+
const rows = (await this.deps.repo.listSecrets()).filter((row) => row.scope === scope);
|
|
724
|
+
if (rows.length === 0) return [];
|
|
725
|
+
const refs = rows.map((row) => row.ref);
|
|
726
|
+
await this.deps.repo.deleteSecrets(refs);
|
|
727
|
+
for (const row of rows) {
|
|
728
|
+
await this.audit("delete", actorOfAddon(caller), row.ref, "device-removed");
|
|
729
|
+
this.deps.events.emit({
|
|
730
|
+
ref: row.ref,
|
|
731
|
+
owner: row.owner,
|
|
732
|
+
version: null,
|
|
733
|
+
change: "delete"
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return refs;
|
|
737
|
+
}
|
|
706
738
|
async listMeta(caller) {
|
|
707
739
|
return (await this.deps.repo.listSecrets()).filter((row) => this.owns(caller, row.owner)).map(toMeta);
|
|
708
740
|
}
|
|
@@ -1034,7 +1066,7 @@ var KEYCHAIN_SECRETS_COLLECTION = "keychain:secrets";
|
|
|
1034
1066
|
*/
|
|
1035
1067
|
var KEYCHAIN_KEYS_COLLECTION = "keychain:keys";
|
|
1036
1068
|
/**
|
|
1037
|
-
* @durable class=
|
|
1069
|
+
* @durable class=audit owner=keychain
|
|
1038
1070
|
* write="one row per write / delete / rotation / reveal / recovery download"
|
|
1039
1071
|
* retention="90 days and at most 20 000 rows, pruned at boot and daily"
|
|
1040
1072
|
*/
|
|
@@ -1811,6 +1843,7 @@ var KeychainAddon = class extends BaseAddon {
|
|
|
1811
1843
|
put: (input) => service.put(callerOf(input), input.ref, input.value),
|
|
1812
1844
|
resolve: (input) => service.resolve(callerOf(input), input.refs),
|
|
1813
1845
|
delete: async (input) => ({ deleted: await service.delete(callerOf(input), input.refs) }),
|
|
1846
|
+
deleteDeviceScope: async (input) => ({ refs: await service.deleteDeviceScope(callerOf(input), input.deviceId) }),
|
|
1814
1847
|
listMeta: (input) => service.listMeta(callerOf(input)),
|
|
1815
1848
|
getState: async () => service.getState()
|
|
1816
1849
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type KeychainAuditEntry, type KeychainKeySource, type KeychainResetRecord, type KeychainResolveResult, type KeychainSecretMeta, type KeychainSecretUsage, type KeychainState, type KeychainStatus, type KeychainUnavailableReason, type KnownEventCategory, type SystemHeldSecretName } from "@camstack/types";
|
|
2
2
|
import { type KeySourceCandidate } from "./key-sources.js";
|
|
3
3
|
import type { KeychainRepository, KeychainSecretRow } from "./keychain-store.js";
|
|
4
|
+
/** The one caller that may purge a whole device scope across owners (D864). */
|
|
5
|
+
export declare const DEVICE_SCOPE_PURGER = "device-manager";
|
|
4
6
|
/** `lastReadAt` is written at most this often per ref (hub I/O is the bottleneck). */
|
|
5
7
|
export declare const LAST_READ_GRANULARITY_MS: number;
|
|
6
8
|
export declare class KeychainLockedError extends Error {
|
|
@@ -69,6 +71,18 @@ export declare class KeychainService {
|
|
|
69
71
|
resolve(caller: string | null, refs: readonly string[]): Promise<KeychainResolveResult>;
|
|
70
72
|
private touchLastRead;
|
|
71
73
|
delete(caller: string | null, refs: readonly string[]): Promise<number>;
|
|
74
|
+
/**
|
|
75
|
+
* Delete EVERY value whose scope is `device/<deviceId>`, whatever its owner
|
|
76
|
+
* (D864). A device-scoped secret is owned by the driver that declared it, so
|
|
77
|
+
* the device manager — which knows only that the device is gone — cannot name
|
|
78
|
+
* the owner; this is the one owner-spanning delete, and it is reserved to
|
|
79
|
+
* the device manager (a removal is the only moment a whole device scope
|
|
80
|
+
* stops being anybody's). Works while locked: it removes ciphertext, it
|
|
81
|
+
* never reads it.
|
|
82
|
+
*
|
|
83
|
+
* @returns the refs removed (never values), for the caller's one log line.
|
|
84
|
+
*/
|
|
85
|
+
deleteDeviceScope(caller: string | null, deviceId: number): Promise<readonly string[]>;
|
|
72
86
|
listMeta(caller: string | null): Promise<readonly KeychainSecretMeta[]>;
|
|
73
87
|
status(): Promise<KeychainStatus>;
|
|
74
88
|
listSecrets(): Promise<readonly (KeychainSecretMeta & {
|
|
@@ -25,7 +25,7 @@ export declare const KEYCHAIN_SECRETS_COLLECTION = "keychain:secrets";
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const KEYCHAIN_KEYS_COLLECTION = "keychain:keys";
|
|
27
27
|
/**
|
|
28
|
-
* @durable class=
|
|
28
|
+
* @durable class=audit owner=keychain
|
|
29
29
|
* write="one row per write / delete / rotation / reveal / recovery download"
|
|
30
30
|
* retention="90 days and at most 20 000 rows, pruned at boot and daily"
|
|
31
31
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
require("../../chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("../../dist-
|
|
3
|
+
const require_dist = require("../../dist-GgG-7amw.js");
|
|
4
4
|
//#region src/builtins/liveness-monitor/liveness-checks.ts
|
|
5
5
|
var NO_DEVICES = "liveness:no-devices";
|
|
6
6
|
var ALL_OFFLINE = "liveness:all-devices-offline";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Dr as errMsg, G as RecordingStatusSchema, Or as BaseAddon, W as RECORDING_SOURCE_CAMSTACK_ADDON, Wr as createEvent, yi as EventCategory } from "../../dist-Dc8GKC5H.mjs";
|
|
2
2
|
//#region src/builtins/liveness-monitor/liveness-checks.ts
|
|
3
3
|
var NO_DEVICES = "liveness:no-devices";
|
|
4
4
|
var ALL_OFFLINE = "liveness:all-devices-offline";
|
|
@@ -3,10 +3,10 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-GgG-7amw.js");
|
|
7
7
|
const require_safe_buffer$1 = require("../../safe-buffer-D3aoa-7X.js");
|
|
8
8
|
const require_ms$1 = require("../../ms-LB3giGoU.js");
|
|
9
|
-
const require_device_ca = require("../../device-ca-
|
|
9
|
+
const require_device_ca = require("../../device-ca-BSvkXqyI.js");
|
|
10
10
|
let node_crypto = require("node:crypto");
|
|
11
11
|
node_crypto = require_chunk.__toESM(node_crypto);
|
|
12
12
|
let crypto$1 = require("crypto");
|
|
@@ -7076,9 +7076,9 @@ var AdminTokenManager = class {
|
|
|
7076
7076
|
/** Mint a token for `createdBy`. Refuses an expiry in the past or past 90 days. */
|
|
7077
7077
|
async create(input) {
|
|
7078
7078
|
const at = this.now();
|
|
7079
|
-
if (!(input.expiresAt > at) || input.expiresAt > at + 7776e6) throw
|
|
7079
|
+
if (!(input.expiresAt > at) || input.expiresAt > at + 7776e6) throw require_dist.userError("system.admin-token-expiry-too-long", void 0, "An admin token must expire within 90 days");
|
|
7080
7080
|
const name = input.name.trim().slice(0, 80);
|
|
7081
|
-
if (name.length === 0) throw
|
|
7081
|
+
if (name.length === 0) throw require_dist.userError("system.admin-token-name-required", void 0, "An admin token needs a name");
|
|
7082
7082
|
const token = `${ADMIN_TOKEN_PREFIX}${node_crypto.randomBytes(32).toString("base64url")}`;
|
|
7083
7083
|
const stored = {
|
|
7084
7084
|
id: node_crypto.randomUUID(),
|
|
@@ -7467,7 +7467,7 @@ var DeviceCredentialManager = class {
|
|
|
7467
7467
|
/** A one-shot code for `userId` (admin path). Returned once; only its hash is kept. */
|
|
7468
7468
|
startEnrollment(input) {
|
|
7469
7469
|
this.prune();
|
|
7470
|
-
if (this.pendingEnrollments.size >= MAX_PENDING) throw
|
|
7470
|
+
if (this.pendingEnrollments.size >= MAX_PENDING) throw require_dist.userError("system.device-enrollment-busy", void 0, "Too many pending device enrollments — try again shortly");
|
|
7471
7471
|
const code = node_crypto.randomBytes(24).toString("base64url");
|
|
7472
7472
|
const expiresAt = this.now() + require_dist.DEVICE_ENROLLMENT_TTL_MS;
|
|
7473
7473
|
this.pendingEnrollments.set(hash(code), {
|
|
@@ -8038,7 +8038,7 @@ var UserManager = class {
|
|
|
8038
8038
|
return this.storageAccess.getStore();
|
|
8039
8039
|
}
|
|
8040
8040
|
async create(input) {
|
|
8041
|
-
if (await this.findByUsername(input.username)) throw
|
|
8041
|
+
if (await this.findByUsername(input.username)) throw require_dist.userError("system.user-exists", { username: String(input.username) }, `User with username "${input.username}" already exists`);
|
|
8042
8042
|
const passwordHash = await this.auth.hashPassword(input.password);
|
|
8043
8043
|
const now = Date.now();
|
|
8044
8044
|
const record = {
|
|
@@ -8090,7 +8090,7 @@ var UserManager = class {
|
|
|
8090
8090
|
}
|
|
8091
8091
|
async update(id, data) {
|
|
8092
8092
|
const existing = await this.findById(id);
|
|
8093
|
-
if (!existing) throw
|
|
8093
|
+
if (!existing) throw require_dist.userError("system.user-not-found", { id: String(id) }, `User with id "${id}" not found`);
|
|
8094
8094
|
await this.store.update.mutate({
|
|
8095
8095
|
collection: USERS_COLLECTION$1,
|
|
8096
8096
|
id,
|
|
@@ -8109,7 +8109,7 @@ var UserManager = class {
|
|
|
8109
8109
|
}
|
|
8110
8110
|
async resetPassword(id, newPassword) {
|
|
8111
8111
|
const existing = await this.findById(id);
|
|
8112
|
-
if (!existing) throw
|
|
8112
|
+
if (!existing) throw require_dist.userError("system.user-not-found", { id: String(id) }, `User with id "${id}" not found`);
|
|
8113
8113
|
const passwordHash = await this.auth.hashPassword(newPassword);
|
|
8114
8114
|
await this.store.update.mutate({
|
|
8115
8115
|
collection: USERS_COLLECTION$1,
|
|
@@ -11776,7 +11776,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
11776
11776
|
},
|
|
11777
11777
|
confirmTotp: async (input) => {
|
|
11778
11778
|
if (!this.totpManager) throw new Error("TOTP management not available");
|
|
11779
|
-
if (!await this.totpManager.confirm(input.userId, input.code)) throw
|
|
11779
|
+
if (!await this.totpManager.confirm(input.userId, input.code)) throw require_dist.userError("auth.totp-confirm-failed", void 0, "TOTP confirmation failed — code did not match");
|
|
11780
11780
|
return { success: true };
|
|
11781
11781
|
},
|
|
11782
11782
|
disableTotp: async (input) => {
|
|
@@ -11834,7 +11834,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
11834
11834
|
/** The admin cap (`device-credentials`): list, enroll-by-QR, revoke, rename, settings. */
|
|
11835
11835
|
buildDeviceCredentialsProvider() {
|
|
11836
11836
|
const manager = () => {
|
|
11837
|
-
if (this.deviceCredentials === null) throw
|
|
11837
|
+
if (this.deviceCredentials === null) throw require_dist.userError("device.credentials-unavailable", void 0, "Device credentials not available");
|
|
11838
11838
|
return this.deviceCredentials;
|
|
11839
11839
|
};
|
|
11840
11840
|
const hubOrigins = async () => {
|
|
@@ -11886,7 +11886,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
11886
11886
|
},
|
|
11887
11887
|
revokeDeviceCredential: async (input) => {
|
|
11888
11888
|
const record = manager().get(input.id);
|
|
11889
|
-
if (record === void 0) throw
|
|
11889
|
+
if (record === void 0) throw require_dist.userError("device.credential-not-found", { id: input.id }, `Device credential not found: ${input.id}`);
|
|
11890
11890
|
const revoked = await manager().revoke(input.id, require_dist.formatDeviceRevoker({
|
|
11891
11891
|
kind: "admin",
|
|
11892
11892
|
userId: input.caller.userId
|
|
@@ -11957,7 +11957,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
|
|
|
11957
11957
|
return this.loginSessions;
|
|
11958
11958
|
};
|
|
11959
11959
|
const devices = () => {
|
|
11960
|
-
if (this.deviceCredentials === null) throw
|
|
11960
|
+
if (this.deviceCredentials === null) throw require_dist.userError("device.credentials-unavailable", void 0, "Device credentials not available");
|
|
11961
11961
|
return this.deviceCredentials;
|
|
11962
11962
|
};
|
|
11963
11963
|
return {
|