@camstack/system 1.2.62 → 1.2.64

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 (90) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +3 -3
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/block-package.d.ts +9 -27
  16. package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
  17. package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
  18. package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
  19. package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
  20. package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
  21. package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
  22. package/dist/builtins/device-manager/device-manager.addon.js +123 -884
  23. package/dist/builtins/device-manager/device-manager.addon.mjs +123 -884
  24. package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
  25. package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
  26. package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
  27. package/dist/builtins/device-manager/device-projection.d.ts +1 -2
  28. package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
  29. package/dist/builtins/device-manager/device-queries.d.ts +1 -1
  30. package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
  31. package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
  32. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  33. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  34. package/dist/builtins/hub-forwarder/index.js +1 -1
  35. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  37. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  38. package/dist/builtins/local-auth/homeassistant-integration.d.ts +7 -0
  39. package/dist/builtins/local-auth/index.d.ts +1 -0
  40. package/dist/builtins/local-auth/index.js +1 -0
  41. package/dist/builtins/local-auth/index.mjs +2 -2
  42. package/dist/builtins/local-auth/local-auth.addon.js +561 -497
  43. package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
  44. package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
  45. package/dist/builtins/local-auth/oauth-session-manager.d.ts +1 -1
  46. package/dist/builtins/local-network/local-network.addon.js +1 -1
  47. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  48. package/dist/builtins/loki-logging/index.js +1 -1
  49. package/dist/builtins/loki-logging/index.mjs +1 -1
  50. package/dist/builtins/native-metrics/native-metrics.addon.js +2 -2
  51. package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
  52. package/dist/builtins/platform-probe/index.js +2 -2
  53. package/dist/builtins/platform-probe/index.mjs +2 -2
  54. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  55. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  56. package/dist/builtins/snapshot/index.js +319 -20
  57. package/dist/builtins/snapshot/index.mjs +319 -20
  58. package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
  59. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
  60. package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -1
  61. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  62. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  63. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +15 -0
  64. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +73 -7
  65. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +73 -7
  66. package/dist/builtins/sqlite-storage/vector-index-vec.d.ts +19 -0
  67. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  69. package/dist/builtins/system-config/system-config.addon.js +1 -1
  70. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  71. package/dist/builtins/winston-logging/index.js +1 -1
  72. package/dist/builtins/winston-logging/index.mjs +1 -1
  73. package/dist/{dist-DnKpFaP1.mjs → dist-CMb7aKS0.mjs} +1227 -1330
  74. package/dist/{dist-DgPUarcs.js → dist-DP3nHlNM.js} +1251 -1366
  75. package/dist/http/asset-compression.d.ts +93 -0
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.js +64 -9
  78. package/dist/index.mjs +65 -11
  79. package/dist/kernel/heap-watch.d.ts +55 -0
  80. package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
  81. package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-BE6l8Y4W.js} +21 -15
  82. package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-Dz1ryHIN.mjs} +21 -15
  83. package/dist/{model-download-service-D8B-4ktF.mjs → model-download-service-CTBHzORJ.mjs} +220 -2
  84. package/dist/{model-download-service-Cpc6SWzV.js → model-download-service-Caj1kaSi.js} +220 -2
  85. package/package.json +5 -1
  86. package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
  87. package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
  88. package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
  89. package/dist/main-CVLq8IzA.js +0 -2356
  90. package/dist/main-CzH19GkU.mjs +0 -2353
@@ -3,11 +3,139 @@ 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-DgPUarcs.js");
6
+ const require_dist = require("../../dist-DP3nHlNM.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");
10
10
  crypto$1 = require_chunk.__toESM(crypto$1, 1);
11
+ /**
12
+ * Scopes granted to a linked Home Assistant, derived from the tRPC paths the
13
+ * component actually calls. `category:system [create]` was rejected: it would
14
+ * hand `addons.installPackage` to a home-automation bridge.
15
+ */
16
+ var HOME_ASSISTANT_OAUTH_INTEGRATION = {
17
+ integrationId: "homeassistant",
18
+ displayName: "Home Assistant",
19
+ requestedScopes: [
20
+ {
21
+ type: "category",
22
+ target: "device",
23
+ access: ["view", "create"]
24
+ },
25
+ {
26
+ type: "capability",
27
+ target: "device-manager",
28
+ access: ["view"]
29
+ },
30
+ {
31
+ type: "capability",
32
+ target: "device-state",
33
+ access: ["view"]
34
+ },
35
+ {
36
+ type: "capability",
37
+ target: "pipeline-orchestrator",
38
+ access: ["view", "create"]
39
+ }
40
+ ],
41
+ allowedRedirectPrefixes: ["https://my.home-assistant.io/redirect/oauth"],
42
+ allowedPrivateHostPaths: ["/auth/external/callback"],
43
+ requiresPkce: true
44
+ };
45
+ //#endregion
46
+ //#region src/auth/parse-record.ts
47
+ function parseRecord(kind, schema, data) {
48
+ try {
49
+ return schema.parse(data);
50
+ } catch (err) {
51
+ const topKeys = data && typeof data === "object" && !Array.isArray(data) ? Object.keys(data).join(", ") || "(empty)" : `(non-object: ${typeof data})`;
52
+ const detail = err instanceof Error ? err.message : String(err);
53
+ const hint = data && typeof data === "object" && !Array.isArray(data) && Object.keys(data).length === 1 && "data" in data ? " This shape is the legacy double-wrap from a stale settings-store query path. Restart the server with the latest @camstack/system build." : "";
54
+ throw new Error(`Failed to parse ${kind} from settings store. Top-level keys=[${topKeys}].${hint} Underlying: ${detail}`, { cause: err });
55
+ }
56
+ }
57
+ //#endregion
58
+ //#region src/auth/api-key-manager.ts
59
+ var API_KEYS_COLLECTION$1 = "api_keys";
60
+ function parseApiKey(data) {
61
+ return parseRecord("api-key", require_dist.ApiKeyRecordSchema, data);
62
+ }
63
+ var ApiKeyManager = class {
64
+ storageAccess;
65
+ auth;
66
+ constructor(storageAccess, auth) {
67
+ this.storageAccess = storageAccess;
68
+ this.auth = auth;
69
+ }
70
+ get store() {
71
+ return this.storageAccess.getStore();
72
+ }
73
+ async create(input) {
74
+ const { token: rawToken, hash, prefix } = this.auth.generateApiKey();
75
+ const now = Date.now();
76
+ const record = {
77
+ id: node_crypto.randomUUID(),
78
+ label: input.label,
79
+ isAdmin: input.isAdmin ?? false,
80
+ allowedProviders: input.allowedProviders ?? "*",
81
+ allowedDevices: input.allowedDevices ?? {},
82
+ tokenHash: hash,
83
+ tokenPrefix: prefix,
84
+ createdAt: now
85
+ };
86
+ await this.store.insert.mutate({
87
+ collection: API_KEYS_COLLECTION$1,
88
+ record: {
89
+ id: record.id,
90
+ data: { ...record }
91
+ }
92
+ });
93
+ return {
94
+ record,
95
+ token: rawToken
96
+ };
97
+ }
98
+ async validateToken(token) {
99
+ const allKeys = await this.store.query.query({ collection: API_KEYS_COLLECTION$1 });
100
+ for (const entry of allKeys) {
101
+ const record = parseApiKey(entry.data);
102
+ if (this.auth.validateApiKey(token, record.tokenHash)) {
103
+ const updatedData = {
104
+ ...record,
105
+ lastUsedAt: Date.now()
106
+ };
107
+ await this.store.update.mutate({
108
+ collection: API_KEYS_COLLECTION$1,
109
+ id: record.id,
110
+ data: { ...updatedData }
111
+ });
112
+ return updatedData;
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ async listAll() {
118
+ return (await this.store.query.query({ collection: API_KEYS_COLLECTION$1 })).map((r) => {
119
+ const { tokenHash: _th, ...rest } = parseApiKey(r.data);
120
+ return rest;
121
+ });
122
+ }
123
+ async revoke(id) {
124
+ await this.store.delete.mutate({
125
+ collection: API_KEYS_COLLECTION$1,
126
+ key: id
127
+ });
128
+ }
129
+ async findById(id) {
130
+ const results = await this.store.query.query({
131
+ collection: API_KEYS_COLLECTION$1,
132
+ filter: { where: { id } }
133
+ });
134
+ if (results.length === 0) return null;
135
+ return parseApiKey(results[0].data);
136
+ }
137
+ };
138
+ //#endregion
11
139
  //#region ../../node_modules/safe-buffer/index.js
12
140
  var require_safe_buffer = /* @__PURE__ */ require_chunk.__commonJSMin(((exports, module) => {
13
141
  /*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
@@ -6120,316 +6248,103 @@ var AuthManager = class {
6120
6248
  }
6121
6249
  };
6122
6250
  //#endregion
6123
- //#region src/auth/parse-record.ts
6124
- function parseRecord(kind, schema, data) {
6125
- try {
6126
- return schema.parse(data);
6127
- } catch (err) {
6128
- const topKeys = data && typeof data === "object" && !Array.isArray(data) ? Object.keys(data).join(", ") || "(empty)" : `(non-object: ${typeof data})`;
6129
- const detail = err instanceof Error ? err.message : String(err);
6130
- const hint = data && typeof data === "object" && !Array.isArray(data) && Object.keys(data).length === 1 && "data" in data ? " This shape is the legacy double-wrap from a stale settings-store query path. Restart the server with the latest @camstack/system build." : "";
6131
- throw new Error(`Failed to parse ${kind} from settings store. Top-level keys=[${topKeys}].${hint} Underlying: ${detail}`, { cause: err });
6132
- }
6133
- }
6134
- //#endregion
6135
- //#region src/auth/user-manager.ts
6136
- var USERS_COLLECTION$1 = "users";
6137
- function parseUser(data) {
6138
- return parseRecord("user", require_dist.UserRecordSchema, data);
6251
+ //#region src/auth/scoped-token-manager.ts
6252
+ var TOKENS_COLLECTION = "scoped_tokens";
6253
+ var TOKEN_PREFIX = "cst_";
6254
+ function parseToken(data) {
6255
+ return parseRecord("scoped-token", require_dist.ScopedTokenSchema, data);
6139
6256
  }
6140
- var UserManager = class {
6141
- storageAccess;
6142
- auth;
6143
- config;
6144
- constructor(storageAccess, auth, config) {
6145
- this.storageAccess = storageAccess;
6146
- this.auth = auth;
6147
- this.config = config;
6148
- }
6149
- get store() {
6150
- return this.storageAccess.getStore();
6257
+ /**
6258
+ * Manages scoped API tokens with restricted addon/route/capability access.
6259
+ */
6260
+ var ScopedTokenManager = class {
6261
+ store;
6262
+ constructor(store) {
6263
+ this.store = store;
6151
6264
  }
6152
- async create(input) {
6153
- if (await this.findByUsername(input.username)) throw new Error(`User with username "${input.username}" already exists`);
6154
- const passwordHash = await this.auth.hashPassword(input.password);
6155
- const now = Date.now();
6265
+ async create(userId, name, scopes, expiresAt) {
6266
+ const rawToken = `${TOKEN_PREFIX}${node_crypto.randomBytes(32).toString("hex")}`;
6267
+ const tokenHash = node_crypto.createHash("sha256").update(rawToken).digest("hex");
6268
+ const tokenPrefix = rawToken.slice(0, 12);
6156
6269
  const record = {
6157
6270
  id: node_crypto.randomUUID(),
6158
- username: input.username,
6159
- passwordHash,
6160
- isAdmin: input.isAdmin ?? false,
6161
- allowedProviders: input.allowedProviders ?? "*",
6162
- allowedDevices: input.allowedDevices ?? {},
6163
- scopes: input.scopes ?? [],
6164
- createdAt: now,
6165
- updatedAt: now
6271
+ userId,
6272
+ name,
6273
+ tokenHash,
6274
+ tokenPrefix,
6275
+ scopes: scopes.map((s) => ({ ...s })),
6276
+ expiresAt,
6277
+ lastUsedAt: void 0,
6278
+ createdAt: Date.now()
6166
6279
  };
6167
6280
  await this.store.insert.mutate({
6168
- collection: USERS_COLLECTION$1,
6281
+ collection: TOKENS_COLLECTION,
6169
6282
  record: {
6170
6283
  id: record.id,
6171
6284
  data: { ...record }
6172
6285
  }
6173
6286
  });
6174
- return record;
6287
+ return {
6288
+ token: rawToken,
6289
+ record
6290
+ };
6175
6291
  }
6176
- async findByUsername(username) {
6292
+ async validate(rawToken) {
6293
+ if (!rawToken.startsWith(TOKEN_PREFIX)) return null;
6294
+ const tokenHash = node_crypto.createHash("sha256").update(rawToken).digest("hex");
6177
6295
  const results = await this.store.query.query({
6178
- collection: USERS_COLLECTION$1,
6179
- filter: { where: { username } }
6296
+ collection: TOKENS_COLLECTION,
6297
+ filter: { where: { tokenHash } }
6180
6298
  });
6181
6299
  if (results.length === 0) return null;
6182
- return parseUser(results[0].data);
6300
+ const record = parseToken(results[0].data);
6301
+ if (record.expiresAt !== void 0 && record.expiresAt !== null && Date.now() > record.expiresAt) return null;
6302
+ this.updateLastUsed(record.id).catch(() => {});
6303
+ return record;
6183
6304
  }
6184
- async findById(id) {
6185
- const results = await this.store.query.query({
6186
- collection: USERS_COLLECTION$1,
6187
- filter: { where: { id } }
6305
+ /**
6306
+ * Coarse scope match — checks only `addonId` / `capability` (no
6307
+ * `route-prefix` after the caps-only migration). Used by addon-route
6308
+ * HTTP handlers that need a quick allow/deny before the per-method
6309
+ * `access` check kicks in via `METHOD_ACCESS_MAP` in
6310
+ * `protectedProcedure`.
6311
+ */
6312
+ matchesScope(token, addonId, capability) {
6313
+ for (const scope of token.scopes) switch (scope.type) {
6314
+ case "addon":
6315
+ if (addonId && scope.target === addonId) return true;
6316
+ break;
6317
+ case "capability":
6318
+ if (capability && scope.target === capability) return true;
6319
+ break;
6320
+ }
6321
+ return false;
6322
+ }
6323
+ async revoke(tokenId) {
6324
+ await this.store.delete.mutate({
6325
+ collection: TOKENS_COLLECTION,
6326
+ key: tokenId
6188
6327
  });
6189
- if (results.length === 0) return null;
6190
- return parseUser(results[0].data);
6191
6328
  }
6192
- async validateCredentials(username, password) {
6193
- const user = await this.findByUsername(username);
6194
- if (!user) return null;
6195
- return await this.auth.comparePassword(password, user.passwordHash) ? user : null;
6329
+ async listForUser(userId) {
6330
+ return (await this.store.query.query({
6331
+ collection: TOKENS_COLLECTION,
6332
+ filter: { where: { userId } }
6333
+ })).map((r) => parseToken(r.data));
6196
6334
  }
6197
- async listAll() {
6198
- return (await this.store.query.query({ collection: USERS_COLLECTION$1 })).map((r) => {
6199
- const { passwordHash: _ph, ...rest } = parseUser(r.data);
6200
- return rest;
6335
+ async updateLastUsed(tokenId) {
6336
+ const results = await this.store.query.query({
6337
+ collection: TOKENS_COLLECTION,
6338
+ filter: { where: { id: tokenId } }
6201
6339
  });
6202
- }
6203
- async update(id, data) {
6204
- const existing = await this.findById(id);
6205
- if (!existing) throw new Error(`User with id "${id}" not found`);
6340
+ if (results.length === 0) return;
6341
+ const existing = parseToken(results[0].data);
6206
6342
  await this.store.update.mutate({
6207
- collection: USERS_COLLECTION$1,
6208
- id,
6343
+ collection: TOKENS_COLLECTION,
6344
+ id: tokenId,
6209
6345
  data: {
6210
6346
  ...existing,
6211
- ...data,
6212
- updatedAt: Date.now()
6213
- }
6214
- });
6215
- }
6216
- async delete(id) {
6217
- await this.store.delete.mutate({
6218
- collection: USERS_COLLECTION$1,
6219
- key: id
6220
- });
6221
- }
6222
- async resetPassword(id, newPassword) {
6223
- const existing = await this.findById(id);
6224
- if (!existing) throw new Error(`User with id "${id}" not found`);
6225
- const passwordHash = await this.auth.hashPassword(newPassword);
6226
- await this.store.update.mutate({
6227
- collection: USERS_COLLECTION$1,
6228
- id,
6229
- data: {
6230
- ...existing,
6231
- passwordHash,
6232
- updatedAt: Date.now()
6233
- }
6234
- });
6235
- }
6236
- async ensureAdminExists() {
6237
- const adminUsername = this.config.get("auth.adminUsername");
6238
- const adminPassword = this.config.get("auth.adminPassword");
6239
- if (!adminUsername || !adminPassword) return;
6240
- const existing = await this.findByUsername(adminUsername);
6241
- if (existing) {
6242
- if (!await this.auth.comparePassword(adminPassword, existing.passwordHash)) await this.resetPassword(existing.id, adminPassword);
6243
- return;
6244
- }
6245
- await this.create({
6246
- username: adminUsername,
6247
- password: adminPassword,
6248
- isAdmin: true,
6249
- allowedProviders: "*",
6250
- allowedDevices: {}
6251
- });
6252
- }
6253
- };
6254
- //#endregion
6255
- //#region src/auth/api-key-manager.ts
6256
- var API_KEYS_COLLECTION$1 = "api_keys";
6257
- function parseApiKey(data) {
6258
- return parseRecord("api-key", require_dist.ApiKeyRecordSchema, data);
6259
- }
6260
- var ApiKeyManager = class {
6261
- storageAccess;
6262
- auth;
6263
- constructor(storageAccess, auth) {
6264
- this.storageAccess = storageAccess;
6265
- this.auth = auth;
6266
- }
6267
- get store() {
6268
- return this.storageAccess.getStore();
6269
- }
6270
- async create(input) {
6271
- const { token: rawToken, hash, prefix } = this.auth.generateApiKey();
6272
- const now = Date.now();
6273
- const record = {
6274
- id: node_crypto.randomUUID(),
6275
- label: input.label,
6276
- isAdmin: input.isAdmin ?? false,
6277
- allowedProviders: input.allowedProviders ?? "*",
6278
- allowedDevices: input.allowedDevices ?? {},
6279
- tokenHash: hash,
6280
- tokenPrefix: prefix,
6281
- createdAt: now
6282
- };
6283
- await this.store.insert.mutate({
6284
- collection: API_KEYS_COLLECTION$1,
6285
- record: {
6286
- id: record.id,
6287
- data: { ...record }
6288
- }
6289
- });
6290
- return {
6291
- record,
6292
- token: rawToken
6293
- };
6294
- }
6295
- async validateToken(token) {
6296
- const allKeys = await this.store.query.query({ collection: API_KEYS_COLLECTION$1 });
6297
- for (const entry of allKeys) {
6298
- const record = parseApiKey(entry.data);
6299
- if (this.auth.validateApiKey(token, record.tokenHash)) {
6300
- const updatedData = {
6301
- ...record,
6302
- lastUsedAt: Date.now()
6303
- };
6304
- await this.store.update.mutate({
6305
- collection: API_KEYS_COLLECTION$1,
6306
- id: record.id,
6307
- data: { ...updatedData }
6308
- });
6309
- return updatedData;
6310
- }
6311
- }
6312
- return null;
6313
- }
6314
- async listAll() {
6315
- return (await this.store.query.query({ collection: API_KEYS_COLLECTION$1 })).map((r) => {
6316
- const { tokenHash: _th, ...rest } = parseApiKey(r.data);
6317
- return rest;
6318
- });
6319
- }
6320
- async revoke(id) {
6321
- await this.store.delete.mutate({
6322
- collection: API_KEYS_COLLECTION$1,
6323
- key: id
6324
- });
6325
- }
6326
- async findById(id) {
6327
- const results = await this.store.query.query({
6328
- collection: API_KEYS_COLLECTION$1,
6329
- filter: { where: { id } }
6330
- });
6331
- if (results.length === 0) return null;
6332
- return parseApiKey(results[0].data);
6333
- }
6334
- };
6335
- //#endregion
6336
- //#region src/auth/scoped-token-manager.ts
6337
- var TOKENS_COLLECTION = "scoped_tokens";
6338
- var TOKEN_PREFIX = "cst_";
6339
- function parseToken(data) {
6340
- return parseRecord("scoped-token", require_dist.ScopedTokenSchema, data);
6341
- }
6342
- /**
6343
- * Manages scoped API tokens with restricted addon/route/capability access.
6344
- */
6345
- var ScopedTokenManager = class {
6346
- store;
6347
- constructor(store) {
6348
- this.store = store;
6349
- }
6350
- async create(userId, name, scopes, expiresAt) {
6351
- const rawToken = `${TOKEN_PREFIX}${node_crypto.randomBytes(32).toString("hex")}`;
6352
- const tokenHash = node_crypto.createHash("sha256").update(rawToken).digest("hex");
6353
- const tokenPrefix = rawToken.slice(0, 12);
6354
- const record = {
6355
- id: node_crypto.randomUUID(),
6356
- userId,
6357
- name,
6358
- tokenHash,
6359
- tokenPrefix,
6360
- scopes: scopes.map((s) => ({ ...s })),
6361
- expiresAt,
6362
- lastUsedAt: void 0,
6363
- createdAt: Date.now()
6364
- };
6365
- await this.store.insert.mutate({
6366
- collection: TOKENS_COLLECTION,
6367
- record: {
6368
- id: record.id,
6369
- data: { ...record }
6370
- }
6371
- });
6372
- return {
6373
- token: rawToken,
6374
- record
6375
- };
6376
- }
6377
- async validate(rawToken) {
6378
- if (!rawToken.startsWith(TOKEN_PREFIX)) return null;
6379
- const tokenHash = node_crypto.createHash("sha256").update(rawToken).digest("hex");
6380
- const results = await this.store.query.query({
6381
- collection: TOKENS_COLLECTION,
6382
- filter: { where: { tokenHash } }
6383
- });
6384
- if (results.length === 0) return null;
6385
- const record = parseToken(results[0].data);
6386
- if (record.expiresAt !== void 0 && record.expiresAt !== null && Date.now() > record.expiresAt) return null;
6387
- this.updateLastUsed(record.id).catch(() => {});
6388
- return record;
6389
- }
6390
- /**
6391
- * Coarse scope match — checks only `addonId` / `capability` (no
6392
- * `route-prefix` after the caps-only migration). Used by addon-route
6393
- * HTTP handlers that need a quick allow/deny before the per-method
6394
- * `access` check kicks in via `METHOD_ACCESS_MAP` in
6395
- * `protectedProcedure`.
6396
- */
6397
- matchesScope(token, addonId, capability) {
6398
- for (const scope of token.scopes) switch (scope.type) {
6399
- case "addon":
6400
- if (addonId && scope.target === addonId) return true;
6401
- break;
6402
- case "capability":
6403
- if (capability && scope.target === capability) return true;
6404
- break;
6405
- }
6406
- return false;
6407
- }
6408
- async revoke(tokenId) {
6409
- await this.store.delete.mutate({
6410
- collection: TOKENS_COLLECTION,
6411
- key: tokenId
6412
- });
6413
- }
6414
- async listForUser(userId) {
6415
- return (await this.store.query.query({
6416
- collection: TOKENS_COLLECTION,
6417
- filter: { where: { userId } }
6418
- })).map((r) => parseToken(r.data));
6419
- }
6420
- async updateLastUsed(tokenId) {
6421
- const results = await this.store.query.query({
6422
- collection: TOKENS_COLLECTION,
6423
- filter: { where: { id: tokenId } }
6424
- });
6425
- if (results.length === 0) return;
6426
- const existing = parseToken(results[0].data);
6427
- await this.store.update.mutate({
6428
- collection: TOKENS_COLLECTION,
6429
- id: tokenId,
6430
- data: {
6431
- ...existing,
6432
- lastUsedAt: Date.now()
6347
+ lastUsedAt: Date.now()
6433
6348
  }
6434
6349
  });
6435
6350
  }
@@ -6497,164 +6412,6 @@ var ScopedTokenManager = class {
6497
6412
  }
6498
6413
  return removed;
6499
6414
  }
6500
- };
6501
- //#endregion
6502
- //#region src/builtins/local-auth/oauth-session-manager.ts
6503
- /**
6504
- * Manages OAuth session records in the `oauth_sessions` collection.
6505
- *
6506
- * Each record represents one Alexa-style account link. Sessions can be
6507
- * active (`revokedAt = null`) or revoked (`revokedAt = <unix-ms>`).
6508
- * `list()` returns both — callers decide whether to filter.
6509
- *
6510
- * Mirrors the pattern of `ScopedTokenManager`: takes a `SettingsStoreClient`
6511
- * in its constructor and performs all reads/writes via the store's
6512
- * `insert`, `update`, `delete`, and `query` primitives.
6513
- */
6514
- var SESSIONS_COLLECTION = "oauth_sessions";
6515
- function parseSession(data) {
6516
- const id = data["id"];
6517
- const userId = data["userId"];
6518
- const username = data["username"];
6519
- const integrationId = data["integrationId"];
6520
- const scopes = data["scopes"];
6521
- const createdAt = data["createdAt"];
6522
- const lastUsedAt = data["lastUsedAt"];
6523
- const revokedAt = data["revokedAt"];
6524
- if (typeof id !== "string") throw new Error("oauth_sessions row: missing id");
6525
- if (typeof userId !== "string") throw new Error("oauth_sessions row: missing userId");
6526
- if (typeof username !== "string") throw new Error("oauth_sessions row: missing username");
6527
- if (typeof integrationId !== "string") throw new Error("oauth_sessions row: missing integrationId");
6528
- if (!Array.isArray(scopes)) throw new Error("oauth_sessions row: scopes must be an array");
6529
- if (typeof createdAt !== "number") throw new Error("oauth_sessions row: missing createdAt");
6530
- if (typeof lastUsedAt !== "number") throw new Error("oauth_sessions row: missing lastUsedAt");
6531
- return {
6532
- id,
6533
- userId,
6534
- username,
6535
- integrationId,
6536
- scopes,
6537
- createdAt,
6538
- lastUsedAt,
6539
- revokedAt: typeof revokedAt === "number" ? revokedAt : null
6540
- };
6541
- }
6542
- var OauthSessionManager = class {
6543
- store;
6544
- constructor(store) {
6545
- this.store = store;
6546
- }
6547
- /**
6548
- * Create a new OAuth session record. Generates a UUID as the session id,
6549
- * sets `createdAt` and `lastUsedAt` to now, `revokedAt` to null.
6550
- */
6551
- async create(input) {
6552
- const now = Date.now();
6553
- const session = {
6554
- id: node_crypto.randomUUID(),
6555
- userId: input.userId,
6556
- username: input.username,
6557
- integrationId: input.integrationId,
6558
- scopes: input.scopes.map((s) => ({ ...s })),
6559
- createdAt: now,
6560
- lastUsedAt: now,
6561
- revokedAt: null
6562
- };
6563
- await this.store.insert.mutate({
6564
- collection: SESSIONS_COLLECTION,
6565
- record: {
6566
- id: session.id,
6567
- data: { ...session }
6568
- }
6569
- });
6570
- return session;
6571
- }
6572
- /**
6573
- * Return all sessions — both active and revoked. Callers decide
6574
- * whether to filter by `revokedAt`.
6575
- */
6576
- async list() {
6577
- return (await this.store.query.query({
6578
- collection: SESSIONS_COLLECTION,
6579
- filter: {}
6580
- })).map((r) => parseSession(r.data));
6581
- }
6582
- /**
6583
- * Look up a session by its id. Returns null when not found.
6584
- */
6585
- async getById(id) {
6586
- const results = await this.store.query.query({
6587
- collection: SESSIONS_COLLECTION,
6588
- filter: { where: { id } }
6589
- });
6590
- if (results.length === 0) return null;
6591
- return parseSession(results[0].data);
6592
- }
6593
- /**
6594
- * Mark a session as revoked by setting `revokedAt = now`.
6595
- *
6596
- * - Returns `true` on success (including idempotent re-revoke — the
6597
- * existing `revokedAt` timestamp is preserved; it is NOT updated).
6598
- * - Returns `false` when the session id is not found.
6599
- */
6600
- async markRevoked(id) {
6601
- const existing = await this.getById(id);
6602
- if (existing === null) return false;
6603
- if (existing.revokedAt !== null) return true;
6604
- const updated = {
6605
- ...existing,
6606
- revokedAt: Date.now()
6607
- };
6608
- await this.store.update.mutate({
6609
- collection: SESSIONS_COLLECTION,
6610
- id,
6611
- data: { ...updated }
6612
- });
6613
- return true;
6614
- }
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
- /**
6641
- * Update `lastUsedAt` to now. No-op (does not throw) when the session
6642
- * id is not found — the caller (token-use hot path) should not fail
6643
- * for a missing session that may have been concurrently revoked.
6644
- */
6645
- async touch(id) {
6646
- const existing = await this.getById(id);
6647
- if (existing === null) return;
6648
- const updated = {
6649
- ...existing,
6650
- lastUsedAt: Date.now()
6651
- };
6652
- await this.store.update.mutate({
6653
- collection: SESSIONS_COLLECTION,
6654
- id,
6655
- data: { ...updated }
6656
- });
6657
- }
6658
6415
  }, i$2 = class extends Error {
6659
6416
  constructor(e, t) {
6660
6417
  super(e, t), this.name = "OTPError";
@@ -8911,53 +8668,173 @@ var TotpManager = class {
8911
8668
  }
8912
8669
  return true;
8913
8670
  }
8914
- /**
8915
- * Remove enrollment. Idempotent — no error if the user had no row.
8916
- */
8917
- async disable(userId) {
8918
- await this.store.delete.mutate({
8919
- collection: TOTP_COLLECTION,
8920
- key: userId
8671
+ /**
8672
+ * Remove enrollment. Idempotent — no error if the user had no row.
8673
+ */
8674
+ async disable(userId) {
8675
+ await this.store.delete.mutate({
8676
+ collection: TOTP_COLLECTION,
8677
+ key: userId
8678
+ });
8679
+ }
8680
+ /**
8681
+ * Verify a code against the confirmed secret. Returns `false` when:
8682
+ * - The user has no row (not set up).
8683
+ * - The row is pending (`confirmedAt === null`).
8684
+ * - The code doesn't match within the configured window.
8685
+ */
8686
+ async verify(userId, code) {
8687
+ const row = await this.find(userId);
8688
+ if (!row) return false;
8689
+ if (row.confirmedAt === null) return false;
8690
+ return d({
8691
+ token: code,
8692
+ secret: row.secret,
8693
+ epochTolerance: this.epochTolerance
8694
+ }).valid;
8695
+ }
8696
+ /**
8697
+ * Read the user's enrollment status. Pending half-enrollments are
8698
+ * reported as `enabled: false` — only a confirmed row counts.
8699
+ */
8700
+ async getStatus(userId) {
8701
+ const row = await this.find(userId);
8702
+ if (!row) return {
8703
+ enabled: false,
8704
+ confirmedAt: null
8705
+ };
8706
+ return {
8707
+ enabled: row.confirmedAt !== null,
8708
+ confirmedAt: row.confirmedAt
8709
+ };
8710
+ }
8711
+ async find(userId) {
8712
+ const results = await this.store.query.query({
8713
+ collection: TOTP_COLLECTION,
8714
+ filter: { where: { userId } }
8715
+ });
8716
+ if (results.length === 0) return null;
8717
+ return parseRow(results[0].data);
8718
+ }
8719
+ };
8720
+ //#endregion
8721
+ //#region src/auth/user-manager.ts
8722
+ var USERS_COLLECTION$1 = "users";
8723
+ function parseUser(data) {
8724
+ return parseRecord("user", require_dist.UserRecordSchema, data);
8725
+ }
8726
+ var UserManager = class {
8727
+ storageAccess;
8728
+ auth;
8729
+ config;
8730
+ constructor(storageAccess, auth, config) {
8731
+ this.storageAccess = storageAccess;
8732
+ this.auth = auth;
8733
+ this.config = config;
8734
+ }
8735
+ get store() {
8736
+ return this.storageAccess.getStore();
8737
+ }
8738
+ async create(input) {
8739
+ if (await this.findByUsername(input.username)) throw new Error(`User with username "${input.username}" already exists`);
8740
+ const passwordHash = await this.auth.hashPassword(input.password);
8741
+ const now = Date.now();
8742
+ const record = {
8743
+ id: node_crypto.randomUUID(),
8744
+ username: input.username,
8745
+ passwordHash,
8746
+ isAdmin: input.isAdmin ?? false,
8747
+ allowedProviders: input.allowedProviders ?? "*",
8748
+ allowedDevices: input.allowedDevices ?? {},
8749
+ scopes: input.scopes ?? [],
8750
+ createdAt: now,
8751
+ updatedAt: now
8752
+ };
8753
+ await this.store.insert.mutate({
8754
+ collection: USERS_COLLECTION$1,
8755
+ record: {
8756
+ id: record.id,
8757
+ data: { ...record }
8758
+ }
8759
+ });
8760
+ return record;
8761
+ }
8762
+ async findByUsername(username) {
8763
+ const results = await this.store.query.query({
8764
+ collection: USERS_COLLECTION$1,
8765
+ filter: { where: { username } }
8766
+ });
8767
+ if (results.length === 0) return null;
8768
+ return parseUser(results[0].data);
8769
+ }
8770
+ async findById(id) {
8771
+ const results = await this.store.query.query({
8772
+ collection: USERS_COLLECTION$1,
8773
+ filter: { where: { id } }
8774
+ });
8775
+ if (results.length === 0) return null;
8776
+ return parseUser(results[0].data);
8777
+ }
8778
+ async validateCredentials(username, password) {
8779
+ const user = await this.findByUsername(username);
8780
+ if (!user) return null;
8781
+ return await this.auth.comparePassword(password, user.passwordHash) ? user : null;
8782
+ }
8783
+ async listAll() {
8784
+ return (await this.store.query.query({ collection: USERS_COLLECTION$1 })).map((r) => {
8785
+ const { passwordHash: _ph, ...rest } = parseUser(r.data);
8786
+ return rest;
8787
+ });
8788
+ }
8789
+ async update(id, data) {
8790
+ const existing = await this.findById(id);
8791
+ if (!existing) throw new Error(`User with id "${id}" not found`);
8792
+ await this.store.update.mutate({
8793
+ collection: USERS_COLLECTION$1,
8794
+ id,
8795
+ data: {
8796
+ ...existing,
8797
+ ...data,
8798
+ updatedAt: Date.now()
8799
+ }
8921
8800
  });
8922
8801
  }
8923
- /**
8924
- * Verify a code against the confirmed secret. Returns `false` when:
8925
- * - The user has no row (not set up).
8926
- * - The row is pending (`confirmedAt === null`).
8927
- * - The code doesn't match within the configured window.
8928
- */
8929
- async verify(userId, code) {
8930
- const row = await this.find(userId);
8931
- if (!row) return false;
8932
- if (row.confirmedAt === null) return false;
8933
- return d({
8934
- token: code,
8935
- secret: row.secret,
8936
- epochTolerance: this.epochTolerance
8937
- }).valid;
8802
+ async delete(id) {
8803
+ await this.store.delete.mutate({
8804
+ collection: USERS_COLLECTION$1,
8805
+ key: id
8806
+ });
8938
8807
  }
8939
- /**
8940
- * Read the user's enrollment status. Pending half-enrollments are
8941
- * reported as `enabled: false` only a confirmed row counts.
8942
- */
8943
- async getStatus(userId) {
8944
- const row = await this.find(userId);
8945
- if (!row) return {
8946
- enabled: false,
8947
- confirmedAt: null
8948
- };
8949
- return {
8950
- enabled: row.confirmedAt !== null,
8951
- confirmedAt: row.confirmedAt
8952
- };
8808
+ async resetPassword(id, newPassword) {
8809
+ const existing = await this.findById(id);
8810
+ if (!existing) throw new Error(`User with id "${id}" not found`);
8811
+ const passwordHash = await this.auth.hashPassword(newPassword);
8812
+ await this.store.update.mutate({
8813
+ collection: USERS_COLLECTION$1,
8814
+ id,
8815
+ data: {
8816
+ ...existing,
8817
+ passwordHash,
8818
+ updatedAt: Date.now()
8819
+ }
8820
+ });
8953
8821
  }
8954
- async find(userId) {
8955
- const results = await this.store.query.query({
8956
- collection: TOTP_COLLECTION,
8957
- filter: { where: { userId } }
8822
+ async ensureAdminExists() {
8823
+ const adminUsername = this.config.get("auth.adminUsername");
8824
+ const adminPassword = this.config.get("auth.adminPassword");
8825
+ if (!adminUsername || !adminPassword) return;
8826
+ const existing = await this.findByUsername(adminUsername);
8827
+ if (existing) {
8828
+ if (!await this.auth.comparePassword(adminPassword, existing.passwordHash)) await this.resetPassword(existing.id, adminPassword);
8829
+ return;
8830
+ }
8831
+ await this.create({
8832
+ username: adminUsername,
8833
+ password: adminPassword,
8834
+ isAdmin: true,
8835
+ allowedProviders: "*",
8836
+ allowedDevices: {}
8958
8837
  });
8959
- if (results.length === 0) return null;
8960
- return parseRow(results[0].data);
8961
8838
  }
8962
8839
  };
8963
8840
  //#endregion
@@ -9228,6 +9105,21 @@ var TTL = {
9228
9105
  "oauth-access": 3600,
9229
9106
  "oauth-refresh": 2592e3
9230
9107
  };
9108
+ /**
9109
+ * PKCE S256 check (RFC 7636 §4.6): `BASE64URL(SHA256(verifier)) === challenge`.
9110
+ *
9111
+ * Keyed off the CLAIM, never off the descriptor: a code minted with a challenge
9112
+ * stays unusable without the verifier even if the integration later stops
9113
+ * requiring PKCE. Constant-time compare so a wrong verifier cannot be walked
9114
+ * byte by byte inside the code's 60 s window.
9115
+ */
9116
+ function verifyPkceS256(challenge, verifier) {
9117
+ if (!verifier) return false;
9118
+ const expected = Buffer.from((0, node_crypto.createHash)("sha256").update(verifier).digest("base64url"));
9119
+ const actual = Buffer.from(challenge);
9120
+ if (expected.length !== actual.length) return false;
9121
+ return (0, node_crypto.timingSafeEqual)(expected, actual);
9122
+ }
9231
9123
  /** OAuth account-linking grant logic. Pure over an injected sso-bridge
9232
9124
  * signer so it is unit-testable without the capability registry.
9233
9125
  *
@@ -9278,7 +9170,8 @@ function createOauthGrants(ssoBridge, sessionManager) {
9278
9170
  provider: "oauth-code",
9279
9171
  redirectUri: input.redirectUri,
9280
9172
  integrationId: input.integrationId,
9281
- jti
9173
+ jti,
9174
+ ...input.codeChallenge ? { codeChallenge: input.codeChallenge } : {}
9282
9175
  },
9283
9176
  ttlSec: TTL["oauth-code"]
9284
9177
  });
@@ -9288,6 +9181,10 @@ function createOauthGrants(ssoBridge, sessionManager) {
9288
9181
  const claims = await ssoBridge.verifyBridgeToken({ token: input.code });
9289
9182
  if (!claims || claims.provider !== "oauth-code") return null;
9290
9183
  if (claims.redirectUri !== input.redirectUri) return null;
9184
+ const codeChallenge = claims.codeChallenge;
9185
+ if (typeof codeChallenge === "string" && codeChallenge !== "") {
9186
+ if (!verifyPkceS256(codeChallenge, input.codeVerifier)) return null;
9187
+ }
9291
9188
  const jti = claims.jti;
9292
9189
  if (!jti) return null;
9293
9190
  const expiryMs = Date.now() + TTL["oauth-code"] * 1e3;
@@ -9332,6 +9229,164 @@ function createOauthGrants(ssoBridge, sessionManager) {
9332
9229
  };
9333
9230
  }
9334
9231
  //#endregion
9232
+ //#region src/builtins/local-auth/oauth-session-manager.ts
9233
+ /**
9234
+ * Manages OAuth session records in the `oauth_sessions` collection.
9235
+ *
9236
+ * Each record represents one Alexa-style account link. Sessions can be
9237
+ * active (`revokedAt = null`) or revoked (`revokedAt = <unix-ms>`).
9238
+ * `list()` returns both — callers decide whether to filter.
9239
+ *
9240
+ * Mirrors the pattern of `ScopedTokenManager`: takes a `SettingsStoreClient`
9241
+ * in its constructor and performs all reads/writes via the store's
9242
+ * `insert`, `update`, `delete`, and `query` primitives.
9243
+ */
9244
+ var SESSIONS_COLLECTION = "oauth_sessions";
9245
+ function parseSession(data) {
9246
+ const id = data["id"];
9247
+ const userId = data["userId"];
9248
+ const username = data["username"];
9249
+ const integrationId = data["integrationId"];
9250
+ const scopes = data["scopes"];
9251
+ const createdAt = data["createdAt"];
9252
+ const lastUsedAt = data["lastUsedAt"];
9253
+ const revokedAt = data["revokedAt"];
9254
+ if (typeof id !== "string") throw new Error("oauth_sessions row: missing id");
9255
+ if (typeof userId !== "string") throw new Error("oauth_sessions row: missing userId");
9256
+ if (typeof username !== "string") throw new Error("oauth_sessions row: missing username");
9257
+ if (typeof integrationId !== "string") throw new Error("oauth_sessions row: missing integrationId");
9258
+ if (!Array.isArray(scopes)) throw new Error("oauth_sessions row: scopes must be an array");
9259
+ if (typeof createdAt !== "number") throw new Error("oauth_sessions row: missing createdAt");
9260
+ if (typeof lastUsedAt !== "number") throw new Error("oauth_sessions row: missing lastUsedAt");
9261
+ return {
9262
+ id,
9263
+ userId,
9264
+ username,
9265
+ integrationId,
9266
+ scopes,
9267
+ createdAt,
9268
+ lastUsedAt,
9269
+ revokedAt: typeof revokedAt === "number" ? revokedAt : null
9270
+ };
9271
+ }
9272
+ var OauthSessionManager = class {
9273
+ store;
9274
+ constructor(store) {
9275
+ this.store = store;
9276
+ }
9277
+ /**
9278
+ * Create a new OAuth session record. Generates a UUID as the session id,
9279
+ * sets `createdAt` and `lastUsedAt` to now, `revokedAt` to null.
9280
+ */
9281
+ async create(input) {
9282
+ const now = Date.now();
9283
+ const session = {
9284
+ id: node_crypto.randomUUID(),
9285
+ userId: input.userId,
9286
+ username: input.username,
9287
+ integrationId: input.integrationId,
9288
+ scopes: input.scopes.map((s) => ({ ...s })),
9289
+ createdAt: now,
9290
+ lastUsedAt: now,
9291
+ revokedAt: null
9292
+ };
9293
+ await this.store.insert.mutate({
9294
+ collection: SESSIONS_COLLECTION,
9295
+ record: {
9296
+ id: session.id,
9297
+ data: { ...session }
9298
+ }
9299
+ });
9300
+ return session;
9301
+ }
9302
+ /**
9303
+ * Return all sessions — both active and revoked. Callers decide
9304
+ * whether to filter by `revokedAt`.
9305
+ */
9306
+ async list() {
9307
+ return (await this.store.query.query({
9308
+ collection: SESSIONS_COLLECTION,
9309
+ filter: {}
9310
+ })).map((r) => parseSession(r.data));
9311
+ }
9312
+ /**
9313
+ * Look up a session by its id. Returns null when not found.
9314
+ */
9315
+ async getById(id) {
9316
+ const results = await this.store.query.query({
9317
+ collection: SESSIONS_COLLECTION,
9318
+ filter: { where: { id } }
9319
+ });
9320
+ if (results.length === 0) return null;
9321
+ return parseSession(results[0].data);
9322
+ }
9323
+ /**
9324
+ * Mark a session as revoked by setting `revokedAt = now`.
9325
+ *
9326
+ * - Returns `true` on success (including idempotent re-revoke — the
9327
+ * existing `revokedAt` timestamp is preserved; it is NOT updated).
9328
+ * - Returns `false` when the session id is not found.
9329
+ */
9330
+ async markRevoked(id) {
9331
+ const existing = await this.getById(id);
9332
+ if (existing === null) return false;
9333
+ if (existing.revokedAt !== null) return true;
9334
+ const updated = {
9335
+ ...existing,
9336
+ revokedAt: Date.now()
9337
+ };
9338
+ await this.store.update.mutate({
9339
+ collection: SESSIONS_COLLECTION,
9340
+ id,
9341
+ data: { ...updated }
9342
+ });
9343
+ return true;
9344
+ }
9345
+ /**
9346
+ * Delete sessions revoked longer ago than `horizonMs`.
9347
+ *
9348
+ * Safe because an ABSENT session already fails closed exactly like a revoked
9349
+ * one: every consumer in `oauth-grants.ts` reads
9350
+ * `if (!session || session.revokedAt != null) return null`. Deleting the row
9351
+ * therefore cannot re-enable a link that was cut — it removes a record whose
9352
+ * only remaining job was to say "no", which the absence says too.
9353
+ *
9354
+ * A retention window rather than an immediate delete: a revoked account link
9355
+ * is the kind of thing an operator looks up AFTER the fact ("when did I
9356
+ * unlink that?"), and the row is tiny. Active sessions are never touched —
9357
+ * the filter matches only rows with a `revokedAt` timestamp in range, and
9358
+ * `revokedAt` is NULL on every live session, which SQL's `BETWEEN` never
9359
+ * matches.
9360
+ *
9361
+ * Returns how many rows went.
9362
+ */
9363
+ async reapRevoked(horizonMs) {
9364
+ const { deleted } = await this.store.deleteWhere.mutate({
9365
+ collection: SESSIONS_COLLECTION,
9366
+ filter: { whereBetween: { revokedAt: [1, horizonMs] } }
9367
+ });
9368
+ return deleted;
9369
+ }
9370
+ /**
9371
+ * Update `lastUsedAt` to now. No-op (does not throw) when the session
9372
+ * id is not found — the caller (token-use hot path) should not fail
9373
+ * for a missing session that may have been concurrently revoked.
9374
+ */
9375
+ async touch(id) {
9376
+ const existing = await this.getById(id);
9377
+ if (existing === null) return;
9378
+ const updated = {
9379
+ ...existing,
9380
+ lastUsedAt: Date.now()
9381
+ };
9382
+ await this.store.update.mutate({
9383
+ collection: SESSIONS_COLLECTION,
9384
+ id,
9385
+ data: { ...updated }
9386
+ });
9387
+ }
9388
+ };
9389
+ //#endregion
9335
9390
  //#region src/builtins/local-auth/local-auth.addon.ts
9336
9391
  /** How often the auth tables are swept for dead rows. */
9337
9392
  var AUTH_REAP_INTERVAL_MS = 24 * 36e5;
@@ -9603,14 +9658,22 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
9603
9658
  return { valid: await this.totpManager.verify(input.userId, input.code) };
9604
9659
  }
9605
9660
  };
9606
- this.ctx.logger.info("registered auth-provider + user-management capabilities");
9607
- return [{
9608
- capability: require_dist.authProviderCapability,
9609
- provider: authProvider
9610
- }, {
9611
- capability: require_dist.userManagementCapability,
9612
- provider: userMgmt
9613
- }];
9661
+ const homeAssistantOauth = { getDescriptor: async () => HOME_ASSISTANT_OAUTH_INTEGRATION };
9662
+ this.ctx.logger.info("registered auth-provider + user-management + oauth-integration capabilities");
9663
+ return [
9664
+ {
9665
+ capability: require_dist.authProviderCapability,
9666
+ provider: authProvider
9667
+ },
9668
+ {
9669
+ capability: require_dist.userManagementCapability,
9670
+ provider: userMgmt
9671
+ },
9672
+ {
9673
+ capability: require_dist.oauthIntegrationCapability,
9674
+ provider: homeAssistantOauth
9675
+ }
9676
+ ];
9614
9677
  }
9615
9678
  /**
9616
9679
  * Drop auth rows that are dead but were never removed: scoped tokens past
@@ -9649,6 +9712,7 @@ var LocalAuthAddon = class extends require_dist.BaseAddon {
9649
9712
  //#endregion
9650
9713
  exports.ApiKeyManager = ApiKeyManager;
9651
9714
  exports.AuthManager = AuthManager;
9715
+ exports.HOME_ASSISTANT_OAUTH_INTEGRATION = HOME_ASSISTANT_OAUTH_INTEGRATION;
9652
9716
  exports.LocalAuthAddon = LocalAuthAddon;
9653
9717
  exports.default = LocalAuthAddon;
9654
9718
  exports.ScopedTokenManager = ScopedTokenManager;