@camstack/system 1.2.32 → 1.2.34

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 (50) hide show
  1. package/dist/auth/scoped-token-manager.d.ts +25 -0
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/alerts/alerts.addon.js +1 -1
  7. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  10. package/dist/builtins/console-logging/index.js +1 -1
  11. package/dist/builtins/console-logging/index.mjs +1 -1
  12. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  14. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  16. package/dist/builtins/hub-forwarder/index.js +1 -1
  17. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  18. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  20. package/dist/builtins/local-auth/local-auth.addon.d.ts +12 -0
  21. package/dist/builtins/local-auth/local-auth.addon.js +100 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +100 -1
  23. package/dist/builtins/local-auth/oauth-session-manager.d.ts +19 -0
  24. package/dist/builtins/local-network/local-network.addon.js +1 -1
  25. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  26. package/dist/builtins/loki-logging/index.js +1 -1
  27. package/dist/builtins/loki-logging/index.mjs +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  29. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  30. package/dist/builtins/platform-probe/index.js +1 -1
  31. package/dist/builtins/platform-probe/index.mjs +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  33. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  34. package/dist/builtins/snapshot/index.js +1 -1
  35. package/dist/builtins/snapshot/index.mjs +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  40. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  41. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  42. package/dist/builtins/system-config/system-config.addon.js +1 -1
  43. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  44. package/dist/builtins/winston-logging/index.js +1 -1
  45. package/dist/builtins/winston-logging/index.mjs +1 -1
  46. package/dist/{dist-BL_FHWPP.mjs → dist-BjUYsgkj.mjs} +189 -5
  47. package/dist/{dist-CSs1A90r.js → dist-Bqyu3d1X.js} +189 -5
  48. package/dist/index.js +1 -1
  49. package/dist/index.mjs +1 -1
  50. package/package.json +1 -1
@@ -21,6 +21,31 @@ export declare class ScopedTokenManager {
21
21
  revoke(tokenId: string): Promise<void>;
22
22
  listForUser(userId: string): Promise<ScopedToken[]>;
23
23
  updateLastUsed(tokenId: string): Promise<void>;
24
+ /**
25
+ * Delete tokens whose expiry has passed.
26
+ *
27
+ * These were never usable — {@link validate} already rejects an expired
28
+ * token — so this is hygiene rather than a hole being closed, and it is
29
+ * worth being precise about what it actually buys:
30
+ *
31
+ * - `listForUser` returns rows with no expiry filter, so the operator's
32
+ * token list showed dead tokens as if they were live. There was no way to
33
+ * tell from the UI which ones still worked.
34
+ * - a database that outlives its tokens keeps their hashes indefinitely.
35
+ * 27 rows on the live hub had been unusable for months.
36
+ *
37
+ * A token with NO expiry (`expiresAt` null/absent) is a deliberate
38
+ * never-expires token and MUST survive: `BETWEEN` never matches NULL in
39
+ * SQL, which is the property this depends on and the spec pins against a
40
+ * real SQLite.
41
+ *
42
+ * The bound is `nowMs - 1` because {@link validate} treats `now ===
43
+ * expiresAt` as still valid; reaping at exactly the expiry instant would
44
+ * delete a token that the same millisecond still accepts.
45
+ *
46
+ * Returns how many rows went.
47
+ */
48
+ reapExpired(nowMs?: number): Promise<number>;
24
49
  /**
25
50
  * One-shot migration: drop tokens whose owner can't be resolved.
26
51
  *
@@ -3,7 +3,7 @@ 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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, at as errMsg, ot as BaseAddon, p as addonPagesCapability } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, at as errMsg, ot as BaseAddon, p as addonPagesCapability } from "../../dist-BjUYsgkj.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ 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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, at as errMsg, h as addonWidgetsCapability, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, at as errMsg, h as addonWidgetsCapability, ot as BaseAddon } from "../../dist-BjUYsgkj.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, at as errMsg, g as alertsCapability, n as AlertSchema, ot as BaseAddon, yt as createEvent } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, at as errMsg, g as alertsCapability, n as AlertSchema, ot as BaseAddon, yt as createEvent } from "../../dist-BjUYsgkj.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -3,7 +3,7 @@ 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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  let node_fs = require("node:fs");
8
8
  let node_fs$1 = require_chunk.__toESM(node_fs, 1);
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, ot as BaseAddon, y as backupCapability } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, ot as BaseAddon, y as backupCapability } from "../../dist-BjUYsgkj.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import Kt from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.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 { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
1
+ import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BjUYsgkj.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 = {
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  let _camstack_types_node = require("@camstack/types/node");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { C as deviceStateCapability, D as enumerateItemArrayFields, K as parseStreamParamsFormPatch, M as getByPath, Mt as EventCategory, O as enumerateSchemaFields, S as deviceManagerCapability, W as normalizeUnit, X as setByPath, _ as applyTransform, a as CAP_NAMES_WITH_STATUS, at as errMsg, b as buildStreamParamsConfigSchema, dt as DeviceRole, ft as DeviceType, ht as WELL_KNOWN_TAB_MAP, it as validateExpressionSource, jt as sleep, k as evaluateLinkExpression, l as STREAM_PROFILE_META, nt as toExpressionValue, o as DeviceStatusSchema, ot as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, ut as DeviceFeature, w as deviceStatusCapability, wt as isDeviceConfigCap } from "../../dist-BL_FHWPP.mjs";
1
+ import { C as deviceStateCapability, D as enumerateItemArrayFields, K as parseStreamParamsFormPatch, M as getByPath, Mt as EventCategory, O as enumerateSchemaFields, S as deviceManagerCapability, W as normalizeUnit, X as setByPath, _ as applyTransform, a as CAP_NAMES_WITH_STATUS, at as errMsg, b as buildStreamParamsConfigSchema, dt as DeviceRole, ft as DeviceType, ht as WELL_KNOWN_TAB_MAP, it as validateExpressionSource, jt as sleep, k as evaluateLinkExpression, l as STREAM_PROFILE_META, nt as toExpressionValue, o as DeviceStatusSchema, ot as BaseAddon, t as ALL_CAPABILITY_DEFINITIONS, ut as DeviceFeature, w as deviceStatusCapability, wt as isDeviceConfigCap } from "../../dist-BjUYsgkj.mjs";
2
2
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  //#region src/builtins/doorbell/trigger-engine.ts
8
8
  /**
9
9
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, T as doorbellCapability, at as errMsg, ft as DeviceType, ot as BaseAddon, yt as createEvent } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, T as doorbellCapability, at as errMsg, ft as DeviceType, ot as BaseAddon, yt as createEvent } from "../../dist-BjUYsgkj.mjs";
2
2
  //#region src/builtins/doorbell/trigger-engine.ts
3
3
  /**
4
4
  * Pure trigger logic for the virtual-doorbell builtin — edge detection over
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.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 { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
1
+ import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BjUYsgkj.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;
@@ -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-CSs1A90r.js");
3
+ const require_dist = require("../../dist-Bqyu3d1X.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 { Mt as EventCategory, at as errMsg, ot as BaseAddon, yt as createEvent } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, at as errMsg, ot as BaseAddon, yt as createEvent } from "../../dist-BjUYsgkj.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";
@@ -10,9 +10,21 @@ export declare class LocalAuthAddon extends BaseAddon<LocalAuthConfig> {
10
10
  private apiKeyManager;
11
11
  private scopedTokenManager;
12
12
  private oauthSessionManager;
13
+ private reapTimer;
13
14
  private totpManager;
14
15
  constructor();
15
16
  protected onInitialize(): Promise<ProviderRegistration[]>;
17
+ /**
18
+ * Drop auth rows that are dead but were never removed: scoped tokens past
19
+ * their expiry, and OAuth sessions revoked longer ago than
20
+ * {@link OAUTH_REVOKED_RETENTION_MS}.
21
+ *
22
+ * Neither row could authorise anything — `validate` rejects an expired
23
+ * token, and every consumer treats a missing session exactly like a revoked
24
+ * one. So this is hygiene, and it is best-effort: a failed sweep is logged
25
+ * and the next tick tries again. It must never be able to fail boot.
26
+ */
27
+ private reapAuthRows;
16
28
  protected onShutdown(): Promise<void>;
17
29
  }
18
30
  export default LocalAuthAddon;
@@ -3,7 +3,7 @@ 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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  node_crypto = require_chunk.__toESM(node_crypto);
9
9
  let crypto$1 = require("crypto");
@@ -6434,6 +6434,37 @@ var ScopedTokenManager = class {
6434
6434
  });
6435
6435
  }
6436
6436
  /**
6437
+ * Delete tokens whose expiry has passed.
6438
+ *
6439
+ * These were never usable — {@link validate} already rejects an expired
6440
+ * token — so this is hygiene rather than a hole being closed, and it is
6441
+ * worth being precise about what it actually buys:
6442
+ *
6443
+ * - `listForUser` returns rows with no expiry filter, so the operator's
6444
+ * token list showed dead tokens as if they were live. There was no way to
6445
+ * tell from the UI which ones still worked.
6446
+ * - a database that outlives its tokens keeps their hashes indefinitely.
6447
+ * 27 rows on the live hub had been unusable for months.
6448
+ *
6449
+ * A token with NO expiry (`expiresAt` null/absent) is a deliberate
6450
+ * never-expires token and MUST survive: `BETWEEN` never matches NULL in
6451
+ * SQL, which is the property this depends on and the spec pins against a
6452
+ * real SQLite.
6453
+ *
6454
+ * The bound is `nowMs - 1` because {@link validate} treats `now ===
6455
+ * expiresAt` as still valid; reaping at exactly the expiry instant would
6456
+ * delete a token that the same millisecond still accepts.
6457
+ *
6458
+ * Returns how many rows went.
6459
+ */
6460
+ async reapExpired(nowMs = Date.now()) {
6461
+ const { deleted } = await this.store.deleteWhere.mutate({
6462
+ collection: TOKENS_COLLECTION,
6463
+ filter: { whereBetween: { expiresAt: [1, nowMs - 1] } }
6464
+ });
6465
+ return deleted;
6466
+ }
6467
+ /**
6437
6468
  * One-shot migration: drop tokens whose owner can't be resolved.
6438
6469
  *
6439
6470
  * Two ways a token can end up orphan:
@@ -6582,6 +6613,31 @@ var OauthSessionManager = class {
6582
6613
  return true;
6583
6614
  }
6584
6615
  /**
6616
+ * Delete sessions revoked longer ago than `horizonMs`.
6617
+ *
6618
+ * Safe because an ABSENT session already fails closed exactly like a revoked
6619
+ * one: every consumer in `oauth-grants.ts` reads
6620
+ * `if (!session || session.revokedAt != null) return null`. Deleting the row
6621
+ * therefore cannot re-enable a link that was cut — it removes a record whose
6622
+ * only remaining job was to say "no", which the absence says too.
6623
+ *
6624
+ * A retention window rather than an immediate delete: a revoked account link
6625
+ * is the kind of thing an operator looks up AFTER the fact ("when did I
6626
+ * unlink that?"), and the row is tiny. Active sessions are never touched —
6627
+ * the filter matches only rows with a `revokedAt` timestamp in range, and
6628
+ * `revokedAt` is NULL on every live session, which SQL's `BETWEEN` never
6629
+ * matches.
6630
+ *
6631
+ * Returns how many rows went.
6632
+ */
6633
+ async reapRevoked(horizonMs) {
6634
+ const { deleted } = await this.store.deleteWhere.mutate({
6635
+ collection: SESSIONS_COLLECTION,
6636
+ filter: { whereBetween: { revokedAt: [1, horizonMs] } }
6637
+ });
6638
+ return deleted;
6639
+ }
6640
+ /**
6585
6641
  * Update `lastUsedAt` to now. No-op (does not throw) when the session
6586
6642
  * id is not found — the caller (token-use hot path) should not fail
6587
6643
  * for a missing session that may have been concurrently revoked.
@@ -9277,6 +9333,17 @@ function createOauthGrants(ssoBridge, sessionManager) {
9277
9333
  }
9278
9334
  //#endregion
9279
9335
  //#region src/builtins/local-auth/local-auth.addon.ts
9336
+ /** How often the auth tables are swept for dead rows. */
9337
+ var AUTH_REAP_INTERVAL_MS = 24 * 36e5;
9338
+ /**
9339
+ * How long a REVOKED OAuth session is kept before deletion.
9340
+ *
9341
+ * A revoked link authorises nothing the moment it is revoked (every consumer
9342
+ * fails closed on a missing session exactly as on a revoked one), so this
9343
+ * window buys only the operator's ability to answer "when did I unlink that?".
9344
+ * 30 days, matching the ops-log retention.
9345
+ */
9346
+ var OAUTH_REVOKED_RETENTION_MS = 720 * 36e5;
9280
9347
  function toAuthResult(user) {
9281
9348
  return {
9282
9349
  userId: user.id,
@@ -9291,6 +9358,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
9291
9358
  apiKeyManager = null;
9292
9359
  scopedTokenManager = null;
9293
9360
  oauthSessionManager = null;
9361
+ reapTimer = null;
9294
9362
  totpManager = null;
9295
9363
  constructor() {
9296
9364
  super({
@@ -9332,6 +9400,11 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
9332
9400
  const liveIds = new Set(liveUsers.map((u) => u.id));
9333
9401
  const removed = await this.scopedTokenManager.cleanupOrphans(liveIds);
9334
9402
  if (removed > 0) this.ctx.logger.warn(`cleaned up ${removed} orphan scoped-token(s) on boot`);
9403
+ await this.reapAuthRows();
9404
+ this.reapTimer = setInterval(() => {
9405
+ this.reapAuthRows();
9406
+ }, AUTH_REAP_INTERVAL_MS);
9407
+ this.reapTimer.unref?.();
9335
9408
  } catch (err) {
9336
9409
  const detail = err instanceof Error ? err.message : String(err);
9337
9410
  throw new Error(`local-auth bootstrap failed: ensureAdminExists threw before \`user-management\` could be registered. Most likely a \`users\` collection schema mismatch in the settings-store. Underlying: ${detail}`, { cause: err });
@@ -9539,7 +9612,33 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
9539
9612
  provider: userMgmt
9540
9613
  }];
9541
9614
  }
9615
+ /**
9616
+ * Drop auth rows that are dead but were never removed: scoped tokens past
9617
+ * their expiry, and OAuth sessions revoked longer ago than
9618
+ * {@link OAUTH_REVOKED_RETENTION_MS}.
9619
+ *
9620
+ * Neither row could authorise anything — `validate` rejects an expired
9621
+ * token, and every consumer treats a missing session exactly like a revoked
9622
+ * one. So this is hygiene, and it is best-effort: a failed sweep is logged
9623
+ * and the next tick tries again. It must never be able to fail boot.
9624
+ */
9625
+ async reapAuthRows() {
9626
+ try {
9627
+ const tokens = await this.scopedTokenManager?.reapExpired() ?? 0;
9628
+ const sessions = await this.oauthSessionManager?.reapRevoked(Date.now() - OAUTH_REVOKED_RETENTION_MS) ?? 0;
9629
+ if (tokens > 0 || sessions > 0) this.ctx.logger.info("auth retention swept", { meta: {
9630
+ expiredTokens: tokens,
9631
+ revokedSessions: sessions
9632
+ } });
9633
+ } catch (err) {
9634
+ this.ctx.logger.warn("auth retention sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
9635
+ }
9636
+ }
9542
9637
  async onShutdown() {
9638
+ if (this.reapTimer !== null) {
9639
+ clearInterval(this.reapTimer);
9640
+ this.reapTimer = null;
9641
+ }
9543
9642
  this.authManager = null;
9544
9643
  this.userManager = null;
9545
9644
  this.apiKeyManager = null;
@@ -1,5 +1,5 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { f as UserRecordSchema, ot as BaseAddon, r as ApiKeyRecordSchema, rt as userManagementCapability, u as ScopedTokenSchema, v as authProviderCapability } from "../../dist-BL_FHWPP.mjs";
2
+ import { f as UserRecordSchema, ot as BaseAddon, r as ApiKeyRecordSchema, rt as userManagementCapability, u as ScopedTokenSchema, v as authProviderCapability } from "../../dist-BjUYsgkj.mjs";
3
3
  import * as crypto$1 from "node:crypto";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
@@ -6426,6 +6426,37 @@ var ScopedTokenManager = class {
6426
6426
  });
6427
6427
  }
6428
6428
  /**
6429
+ * Delete tokens whose expiry has passed.
6430
+ *
6431
+ * These were never usable — {@link validate} already rejects an expired
6432
+ * token — so this is hygiene rather than a hole being closed, and it is
6433
+ * worth being precise about what it actually buys:
6434
+ *
6435
+ * - `listForUser` returns rows with no expiry filter, so the operator's
6436
+ * token list showed dead tokens as if they were live. There was no way to
6437
+ * tell from the UI which ones still worked.
6438
+ * - a database that outlives its tokens keeps their hashes indefinitely.
6439
+ * 27 rows on the live hub had been unusable for months.
6440
+ *
6441
+ * A token with NO expiry (`expiresAt` null/absent) is a deliberate
6442
+ * never-expires token and MUST survive: `BETWEEN` never matches NULL in
6443
+ * SQL, which is the property this depends on and the spec pins against a
6444
+ * real SQLite.
6445
+ *
6446
+ * The bound is `nowMs - 1` because {@link validate} treats `now ===
6447
+ * expiresAt` as still valid; reaping at exactly the expiry instant would
6448
+ * delete a token that the same millisecond still accepts.
6449
+ *
6450
+ * Returns how many rows went.
6451
+ */
6452
+ async reapExpired(nowMs = Date.now()) {
6453
+ const { deleted } = await this.store.deleteWhere.mutate({
6454
+ collection: TOKENS_COLLECTION,
6455
+ filter: { whereBetween: { expiresAt: [1, nowMs - 1] } }
6456
+ });
6457
+ return deleted;
6458
+ }
6459
+ /**
6429
6460
  * One-shot migration: drop tokens whose owner can't be resolved.
6430
6461
  *
6431
6462
  * Two ways a token can end up orphan:
@@ -6574,6 +6605,31 @@ var OauthSessionManager = class {
6574
6605
  return true;
6575
6606
  }
6576
6607
  /**
6608
+ * Delete sessions revoked longer ago than `horizonMs`.
6609
+ *
6610
+ * Safe because an ABSENT session already fails closed exactly like a revoked
6611
+ * one: every consumer in `oauth-grants.ts` reads
6612
+ * `if (!session || session.revokedAt != null) return null`. Deleting the row
6613
+ * therefore cannot re-enable a link that was cut — it removes a record whose
6614
+ * only remaining job was to say "no", which the absence says too.
6615
+ *
6616
+ * A retention window rather than an immediate delete: a revoked account link
6617
+ * is the kind of thing an operator looks up AFTER the fact ("when did I
6618
+ * unlink that?"), and the row is tiny. Active sessions are never touched —
6619
+ * the filter matches only rows with a `revokedAt` timestamp in range, and
6620
+ * `revokedAt` is NULL on every live session, which SQL's `BETWEEN` never
6621
+ * matches.
6622
+ *
6623
+ * Returns how many rows went.
6624
+ */
6625
+ async reapRevoked(horizonMs) {
6626
+ const { deleted } = await this.store.deleteWhere.mutate({
6627
+ collection: SESSIONS_COLLECTION,
6628
+ filter: { whereBetween: { revokedAt: [1, horizonMs] } }
6629
+ });
6630
+ return deleted;
6631
+ }
6632
+ /**
6577
6633
  * Update `lastUsedAt` to now. No-op (does not throw) when the session
6578
6634
  * id is not found — the caller (token-use hot path) should not fail
6579
6635
  * for a missing session that may have been concurrently revoked.
@@ -9269,6 +9325,17 @@ function createOauthGrants(ssoBridge, sessionManager) {
9269
9325
  }
9270
9326
  //#endregion
9271
9327
  //#region src/builtins/local-auth/local-auth.addon.ts
9328
+ /** How often the auth tables are swept for dead rows. */
9329
+ var AUTH_REAP_INTERVAL_MS = 24 * 36e5;
9330
+ /**
9331
+ * How long a REVOKED OAuth session is kept before deletion.
9332
+ *
9333
+ * A revoked link authorises nothing the moment it is revoked (every consumer
9334
+ * fails closed on a missing session exactly as on a revoked one), so this
9335
+ * window buys only the operator's ability to answer "when did I unlink that?".
9336
+ * 30 days, matching the ops-log retention.
9337
+ */
9338
+ var OAUTH_REVOKED_RETENTION_MS = 720 * 36e5;
9272
9339
  function toAuthResult(user) {
9273
9340
  return {
9274
9341
  userId: user.id,
@@ -9283,6 +9350,7 @@ var LocalAuthAddon = class extends BaseAddon {
9283
9350
  apiKeyManager = null;
9284
9351
  scopedTokenManager = null;
9285
9352
  oauthSessionManager = null;
9353
+ reapTimer = null;
9286
9354
  totpManager = null;
9287
9355
  constructor() {
9288
9356
  super({
@@ -9324,6 +9392,11 @@ var LocalAuthAddon = class extends BaseAddon {
9324
9392
  const liveIds = new Set(liveUsers.map((u) => u.id));
9325
9393
  const removed = await this.scopedTokenManager.cleanupOrphans(liveIds);
9326
9394
  if (removed > 0) this.ctx.logger.warn(`cleaned up ${removed} orphan scoped-token(s) on boot`);
9395
+ await this.reapAuthRows();
9396
+ this.reapTimer = setInterval(() => {
9397
+ this.reapAuthRows();
9398
+ }, AUTH_REAP_INTERVAL_MS);
9399
+ this.reapTimer.unref?.();
9327
9400
  } catch (err) {
9328
9401
  const detail = err instanceof Error ? err.message : String(err);
9329
9402
  throw new Error(`local-auth bootstrap failed: ensureAdminExists threw before \`user-management\` could be registered. Most likely a \`users\` collection schema mismatch in the settings-store. Underlying: ${detail}`, { cause: err });
@@ -9531,7 +9604,33 @@ var LocalAuthAddon = class extends BaseAddon {
9531
9604
  provider: userMgmt
9532
9605
  }];
9533
9606
  }
9607
+ /**
9608
+ * Drop auth rows that are dead but were never removed: scoped tokens past
9609
+ * their expiry, and OAuth sessions revoked longer ago than
9610
+ * {@link OAUTH_REVOKED_RETENTION_MS}.
9611
+ *
9612
+ * Neither row could authorise anything — `validate` rejects an expired
9613
+ * token, and every consumer treats a missing session exactly like a revoked
9614
+ * one. So this is hygiene, and it is best-effort: a failed sweep is logged
9615
+ * and the next tick tries again. It must never be able to fail boot.
9616
+ */
9617
+ async reapAuthRows() {
9618
+ try {
9619
+ const tokens = await this.scopedTokenManager?.reapExpired() ?? 0;
9620
+ const sessions = await this.oauthSessionManager?.reapRevoked(Date.now() - OAUTH_REVOKED_RETENTION_MS) ?? 0;
9621
+ if (tokens > 0 || sessions > 0) this.ctx.logger.info("auth retention swept", { meta: {
9622
+ expiredTokens: tokens,
9623
+ revokedSessions: sessions
9624
+ } });
9625
+ } catch (err) {
9626
+ this.ctx.logger.warn("auth retention sweep failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
9627
+ }
9628
+ }
9534
9629
  async onShutdown() {
9630
+ if (this.reapTimer !== null) {
9631
+ clearInterval(this.reapTimer);
9632
+ this.reapTimer = null;
9633
+ }
9535
9634
  this.authManager = null;
9536
9635
  this.userManager = null;
9537
9636
  this.apiKeyManager = null;
@@ -40,6 +40,25 @@ export declare class OauthSessionManager {
40
40
  * - Returns `false` when the session id is not found.
41
41
  */
42
42
  markRevoked(id: string): Promise<boolean>;
43
+ /**
44
+ * Delete sessions revoked longer ago than `horizonMs`.
45
+ *
46
+ * Safe because an ABSENT session already fails closed exactly like a revoked
47
+ * one: every consumer in `oauth-grants.ts` reads
48
+ * `if (!session || session.revokedAt != null) return null`. Deleting the row
49
+ * therefore cannot re-enable a link that was cut — it removes a record whose
50
+ * only remaining job was to say "no", which the absence says too.
51
+ *
52
+ * A retention window rather than an immediate delete: a revoked account link
53
+ * is the kind of thing an operator looks up AFTER the fact ("when did I
54
+ * unlink that?"), and the row is tiny. Active sessions are never touched —
55
+ * the filter matches only rows with a `revokedAt` timestamp in range, and
56
+ * `revokedAt` is NULL on every live session, which SQL's `BETWEEN` never
57
+ * matches.
58
+ *
59
+ * Returns how many rows went.
60
+ */
61
+ reapRevoked(horizonMs: number): Promise<number>;
43
62
  /**
44
63
  * Update `lastUsedAt` to now. No-op (does not throw) when the session
45
64
  * id is not found — the caller (token-use hot path) should not fail
@@ -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-CSs1A90r.js");
3
+ const require_dist = require("../../dist-Bqyu3d1X.js");
4
4
  let node_os = require("node:os");
5
5
  node_os = require_chunk.__toESM(node_os);
6
6
  //#region src/builtins/local-network/local-network.addon.ts
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, ot as BaseAddon, z as localNetworkCapability } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, ot as BaseAddon, z as localNetworkCapability } from "../../dist-BjUYsgkj.mjs";
2
2
  import * as os from "node:os";
3
3
  //#region src/builtins/local-network/local-network.addon.ts
4
4
  /**
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
1
+ import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-BjUYsgkj.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -3,7 +3,7 @@ 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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, U as metricsProviderCapability, ot as BaseAddon, yt as createEvent } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, U as metricsProviderCapability, ot as BaseAddon, yt as createEvent } from "../../dist-BjUYsgkj.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile, execFileSync } from "node:child_process";
4
4
  import { promisify } from "node:util";
@@ -3,7 +3,7 @@ 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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_chunk.__toESM(node_fs);
9
9
  let node_child_process = require("node:child_process");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { E as enumerateInferenceDevices, Mt as EventCategory, Y as scoreRuntimes, at as errMsg, ot as BaseAddon, q as platformProbeCapability, xt as emitReadiness } from "../../dist-BL_FHWPP.mjs";
2
+ import { E as enumerateInferenceDevices, Mt as EventCategory, Y as scoreRuntimes, at as errMsg, ot as BaseAddon, q as platformProbeCapability, xt as emitReadiness } from "../../dist-BjUYsgkj.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { execFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { Mt as EventCategory, ot as BaseAddon } from "../../dist-BL_FHWPP.mjs";
1
+ import { Mt as EventCategory, ot as BaseAddon } from "../../dist-BjUYsgkj.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -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-CSs1A90r.js");
6
+ const require_dist = require("../../dist-Bqyu3d1X.js");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/builtins/snapshot/snapshot-coalescing.ts
9
9
  /**
@@ -1,4 +1,4 @@
1
- import { Q as snapshotCapability, Tt as nodePin, at as errMsg, ft as DeviceType, i as BatteryStatusSchema, ot as BaseAddon, tt as streamQualityLabel, ut as DeviceFeature } from "../../dist-BL_FHWPP.mjs";
1
+ import { Q as snapshotCapability, Tt as nodePin, at as errMsg, ft as DeviceType, i as BatteryStatusSchema, ot as BaseAddon, tt as streamQualityLabel, ut as DeviceFeature } from "../../dist-BjUYsgkj.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  //#region src/builtins/snapshot/snapshot-coalescing.ts
4
4
  /**