@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.
Files changed (91) hide show
  1. package/dist/addon-runner.js +64 -12
  2. package/dist/addon-runner.mjs +65 -13
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +7 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +7 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/calendars/calendars.addon.js +1 -1
  14. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  15. package/dist/builtins/camera-grid/index.js +9 -1
  16. package/dist/builtins/camera-grid/index.mjs +9 -1
  17. package/dist/builtins/composer/composer.addon.js +2 -2
  18. package/dist/builtins/composer/composer.addon.mjs +2 -2
  19. package/dist/builtins/console-logging/index.js +12 -5
  20. package/dist/builtins/console-logging/index.mjs +12 -5
  21. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  22. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  23. package/dist/builtins/device-manager/device-manager.addon.d.ts +5 -0
  24. package/dist/builtins/device-manager/device-manager.addon.js +702 -28
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +702 -28
  26. package/dist/builtins/device-manager/device-meta-types.d.ts +4 -0
  27. package/dist/builtins/device-manager/device-provider-context.d.ts +9 -0
  28. package/dist/builtins/device-manager/device-removal-hooks.d.ts +103 -0
  29. package/dist/builtins/device-manager/device-row-store.d.ts +1 -0
  30. package/dist/builtins/device-manager/device-tombstone-store.d.ts +58 -0
  31. package/dist/builtins/device-manager/removal-cleanup.d.ts +50 -0
  32. package/dist/builtins/hub-forwarder/index.js +1 -1
  33. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  34. package/dist/builtins/keychain/index.js +37 -4
  35. package/dist/builtins/keychain/index.mjs +37 -4
  36. package/dist/builtins/keychain/keychain-service.d.ts +14 -0
  37. package/dist/builtins/keychain/keychain-store.d.ts +1 -1
  38. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  39. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  40. package/dist/builtins/local-auth/local-auth.addon.js +12 -12
  41. package/dist/builtins/local-auth/local-auth.addon.mjs +12 -12
  42. package/dist/builtins/local-network/local-network.addon.js +10 -1
  43. package/dist/builtins/local-network/local-network.addon.mjs +10 -1
  44. package/dist/builtins/loki-logging/index.js +28 -5
  45. package/dist/builtins/loki-logging/index.mjs +28 -5
  46. package/dist/builtins/native-metrics/native-metrics.addon.js +11 -1
  47. package/dist/builtins/native-metrics/native-metrics.addon.mjs +11 -1
  48. package/dist/builtins/platform-probe/index.js +1 -1
  49. package/dist/builtins/platform-probe/index.mjs +1 -1
  50. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  51. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  52. package/dist/builtins/snapshot/index.js +11 -1
  53. package/dist/builtins/snapshot/index.mjs +11 -1
  54. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +4 -1
  55. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +4 -1
  56. package/dist/builtins/sqlite-storage/index.js +1 -1
  57. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -3
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
  60. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/system-config/system-config.addon.js +20 -2
  63. package/dist/builtins/system-config/system-config.addon.mjs +20 -2
  64. package/dist/builtins/winston-logging/index.js +14 -5
  65. package/dist/builtins/winston-logging/index.mjs +14 -5
  66. package/dist/{composition-sources-BIT8IUKm.js → composition-sources-DI3Z83vG.js} +1 -1
  67. package/dist/{composition-sources-tpYR0f1x.mjs → composition-sources-DpHGCjdL.mjs} +1 -1
  68. package/dist/{constants-Bc6trhoW.mjs → constants-DQK705Sb.mjs} +1 -1
  69. package/dist/{constants-CwRY32Nx.js → constants-wkcn5Ij3.js} +1 -1
  70. package/dist/{device-ca-DImD8IzR.js → device-ca-BSvkXqyI.js} +1 -1
  71. package/dist/{device-ca-BhdBByGD.mjs → device-ca-DvIZjXIl.mjs} +1 -1
  72. package/dist/{dist-m5JkT8UN.mjs → dist-Dc8GKC5H.mjs} +272 -7
  73. package/dist/{dist-CEAyXW3Q.js → dist-GgG-7amw.js} +307 -6
  74. package/dist/file-host-worker.js +1 -1
  75. package/dist/file-host-worker.mjs +1 -1
  76. package/dist/i18n/catalog-registry.d.ts +35 -0
  77. package/dist/i18n/index.d.ts +2 -0
  78. package/dist/index.d.ts +1 -0
  79. package/dist/index.js +663 -20
  80. package/dist/index.mjs +659 -22
  81. package/dist/kernel/gc-census.d.ts +18 -1
  82. package/dist/kernel/settings-view-types.d.ts +2 -0
  83. package/dist/kernel/transport/local-transport.d.ts +3 -0
  84. package/dist/kernel/transport/peer-answered-error.d.ts +5 -1
  85. package/dist/{restore-archive-C75srVbG.js → restore-archive-CTRz8v9o.js} +1 -1
  86. package/dist/{restore-archive-DNxAStrJ.mjs → restore-archive-DPg9duVY.mjs} +1 -1
  87. package/dist/{retired-settings-keys-DNfV28R_.js → retired-settings-keys-Dr_4lfKu.js} +1 -1
  88. package/dist/{retired-settings-keys-C5qP9QIf.mjs → retired-settings-keys-Is0zO8gX.mjs} +1 -1
  89. package/dist/{wal-maintenance-DvnVO5IK.mjs → wal-maintenance-Bx3lcVA7.mjs} +0 -0
  90. package/dist/{wal-maintenance-Ch79gy9S.js → wal-maintenance-mjB2i-VR.js} +0 -0
  91. 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-CEAyXW3Q.js");
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 { Cr as BaseAddon, un as logDestinationCapability } from "../../dist-m5JkT8UN.mjs";
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-CEAyXW3Q.js");
7
- const require_device_ca = require("../../device-ca-DImD8IzR.js");
8
- const require_constants = require("../../constants-CwRY32Nx.js");
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=ring owner=keychain
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 { Cr as BaseAddon, Gr as formatSecretScope, P as KEYCHAIN_RESET_TRIGGER_ACTOR, ei as normalizeSecretOwner, in as keychainCapability, pi as EventCategory, ri as parseSecretRef, rn as keychainAdminCapability } from "../../dist-m5JkT8UN.mjs";
2
- import { d as JWT_SIGNING_KEY_REF, n as DEVICE_CA_KEY_REF, p as SYSTEM_SECRET_OWNER } from "../../device-ca-BhdBByGD.mjs";
3
- import { a as MESH_CA_KEY_REF } from "../../constants-Bc6trhoW.mjs";
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=ring owner=keychain
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=ring owner=keychain
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-CEAyXW3Q.js");
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 { Cr as BaseAddon, H as RecordingStatusSchema, Sr as errMsg, V as RECORDING_SOURCE_CAMSTACK_ADDON, pi as EventCategory, zr as createEvent } from "../../dist-m5JkT8UN.mjs";
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-CEAyXW3Q.js");
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-DImD8IzR.js");
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 new Error("An admin token must expire within 90 days");
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 new Error("An admin token needs a name");
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 new Error("Too many pending device enrollments — try again shortly");
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 new Error(`User with username "${input.username}" already exists`);
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 new Error(`User with id "${id}" not found`);
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 new Error(`User with id "${id}" not found`);
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 new Error("TOTP confirmation failed — code did not match");
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 new Error("Device credentials not available");
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 new Error(`Device credential not found: ${input.id}`);
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 new Error("Device credentials not available");
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 {