@camstack/system 1.2.62 → 1.2.63

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 (83) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  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 +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/console-logging/index.js +1 -1
  12. package/dist/builtins/console-logging/index.mjs +1 -1
  13. package/dist/builtins/core-blocks/block-package.d.ts +9 -27
  14. package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
  17. package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
  18. package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
  19. package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
  20. package/dist/builtins/device-manager/device-manager.addon.js +123 -884
  21. package/dist/builtins/device-manager/device-manager.addon.mjs +123 -884
  22. package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
  23. package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
  24. package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
  25. package/dist/builtins/device-manager/device-projection.d.ts +1 -2
  26. package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
  27. package/dist/builtins/device-manager/device-queries.d.ts +1 -1
  28. package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
  29. package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
  30. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  31. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  32. package/dist/builtins/hub-forwarder/index.js +1 -1
  33. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  36. package/dist/builtins/local-auth/homeassistant-integration.d.ts +7 -0
  37. package/dist/builtins/local-auth/index.d.ts +1 -0
  38. package/dist/builtins/local-auth/index.js +1 -0
  39. package/dist/builtins/local-auth/index.mjs +2 -2
  40. package/dist/builtins/local-auth/local-auth.addon.js +561 -497
  41. package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
  42. package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
  43. package/dist/builtins/local-auth/oauth-session-manager.d.ts +1 -1
  44. package/dist/builtins/local-network/local-network.addon.js +1 -1
  45. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  46. package/dist/builtins/loki-logging/index.js +1 -1
  47. package/dist/builtins/loki-logging/index.mjs +1 -1
  48. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  50. package/dist/builtins/platform-probe/index.js +1 -1
  51. package/dist/builtins/platform-probe/index.mjs +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +319 -20
  55. package/dist/builtins/snapshot/index.mjs +319 -20
  56. package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
  57. package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
  58. package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -1
  59. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  60. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  61. package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +15 -0
  62. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +45 -5
  63. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +45 -5
  64. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  65. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  66. package/dist/builtins/system-config/system-config.addon.js +1 -1
  67. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  68. package/dist/builtins/winston-logging/index.js +1 -1
  69. package/dist/builtins/winston-logging/index.mjs +1 -1
  70. package/dist/{dist-DnKpFaP1.mjs → dist-BJ2w3uoN.mjs} +1188 -1329
  71. package/dist/{dist-DgPUarcs.js → dist-LDrOFrd-.js} +1212 -1365
  72. package/dist/index.d.ts +1 -1
  73. package/dist/index.js +3 -2
  74. package/dist/index.mjs +4 -4
  75. package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
  76. package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-CopL1_5C.mjs} +20 -14
  77. package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-DNfmBu20.js} +20 -14
  78. package/package.json +5 -1
  79. package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
  80. package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
  81. package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
  82. package/dist/main-CVLq8IzA.js +0 -2356
  83. package/dist/main-CzH19GkU.mjs +0 -2353
@@ -1,8 +1,136 @@
1
1
  import { i as __require, o as __toESM, t as __commonJSMin } from "../../chunk-CNf5ZN-e.mjs";
2
- import { ct as userManagementCapability, d as ScopedTokenSchema, p as UserRecordSchema, pt as BaseAddon, r as ApiKeyRecordSchema, y as authProviderCapability } from "../../dist-DnKpFaP1.mjs";
3
- import * as crypto$1 from "node:crypto";
4
- import { randomUUID } from "node:crypto";
2
+ import { X as oauthIntegrationCapability, dt as BaseAddon, f as ScopedTokenSchema, m as UserRecordSchema, r as ApiKeyRecordSchema, st as userManagementCapability, y as authProviderCapability } from "../../dist-BJ2w3uoN.mjs";
3
+ import * as crypto$2 from "node:crypto";
4
+ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import nodeCrypto from "crypto";
6
+ /**
7
+ * Scopes granted to a linked Home Assistant, derived from the tRPC paths the
8
+ * component actually calls. `category:system [create]` was rejected: it would
9
+ * hand `addons.installPackage` to a home-automation bridge.
10
+ */
11
+ var HOME_ASSISTANT_OAUTH_INTEGRATION = {
12
+ integrationId: "homeassistant",
13
+ displayName: "Home Assistant",
14
+ requestedScopes: [
15
+ {
16
+ type: "category",
17
+ target: "device",
18
+ access: ["view", "create"]
19
+ },
20
+ {
21
+ type: "capability",
22
+ target: "device-manager",
23
+ access: ["view"]
24
+ },
25
+ {
26
+ type: "capability",
27
+ target: "device-state",
28
+ access: ["view"]
29
+ },
30
+ {
31
+ type: "capability",
32
+ target: "pipeline-orchestrator",
33
+ access: ["view", "create"]
34
+ }
35
+ ],
36
+ allowedRedirectPrefixes: ["https://my.home-assistant.io/redirect/oauth"],
37
+ allowedPrivateHostPaths: ["/auth/external/callback"],
38
+ requiresPkce: true
39
+ };
40
+ //#endregion
41
+ //#region src/auth/parse-record.ts
42
+ function parseRecord(kind, schema, data) {
43
+ try {
44
+ return schema.parse(data);
45
+ } catch (err) {
46
+ const topKeys = data && typeof data === "object" && !Array.isArray(data) ? Object.keys(data).join(", ") || "(empty)" : `(non-object: ${typeof data})`;
47
+ const detail = err instanceof Error ? err.message : String(err);
48
+ 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." : "";
49
+ throw new Error(`Failed to parse ${kind} from settings store. Top-level keys=[${topKeys}].${hint} Underlying: ${detail}`, { cause: err });
50
+ }
51
+ }
52
+ //#endregion
53
+ //#region src/auth/api-key-manager.ts
54
+ var API_KEYS_COLLECTION$1 = "api_keys";
55
+ function parseApiKey(data) {
56
+ return parseRecord("api-key", ApiKeyRecordSchema, data);
57
+ }
58
+ var ApiKeyManager = class {
59
+ storageAccess;
60
+ auth;
61
+ constructor(storageAccess, auth) {
62
+ this.storageAccess = storageAccess;
63
+ this.auth = auth;
64
+ }
65
+ get store() {
66
+ return this.storageAccess.getStore();
67
+ }
68
+ async create(input) {
69
+ const { token: rawToken, hash, prefix } = this.auth.generateApiKey();
70
+ const now = Date.now();
71
+ const record = {
72
+ id: crypto$2.randomUUID(),
73
+ label: input.label,
74
+ isAdmin: input.isAdmin ?? false,
75
+ allowedProviders: input.allowedProviders ?? "*",
76
+ allowedDevices: input.allowedDevices ?? {},
77
+ tokenHash: hash,
78
+ tokenPrefix: prefix,
79
+ createdAt: now
80
+ };
81
+ await this.store.insert.mutate({
82
+ collection: API_KEYS_COLLECTION$1,
83
+ record: {
84
+ id: record.id,
85
+ data: { ...record }
86
+ }
87
+ });
88
+ return {
89
+ record,
90
+ token: rawToken
91
+ };
92
+ }
93
+ async validateToken(token) {
94
+ const allKeys = await this.store.query.query({ collection: API_KEYS_COLLECTION$1 });
95
+ for (const entry of allKeys) {
96
+ const record = parseApiKey(entry.data);
97
+ if (this.auth.validateApiKey(token, record.tokenHash)) {
98
+ const updatedData = {
99
+ ...record,
100
+ lastUsedAt: Date.now()
101
+ };
102
+ await this.store.update.mutate({
103
+ collection: API_KEYS_COLLECTION$1,
104
+ id: record.id,
105
+ data: { ...updatedData }
106
+ });
107
+ return updatedData;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ async listAll() {
113
+ return (await this.store.query.query({ collection: API_KEYS_COLLECTION$1 })).map((r) => {
114
+ const { tokenHash: _th, ...rest } = parseApiKey(r.data);
115
+ return rest;
116
+ });
117
+ }
118
+ async revoke(id) {
119
+ await this.store.delete.mutate({
120
+ collection: API_KEYS_COLLECTION$1,
121
+ key: id
122
+ });
123
+ }
124
+ async findById(id) {
125
+ const results = await this.store.query.query({
126
+ collection: API_KEYS_COLLECTION$1,
127
+ filter: { where: { id } }
128
+ });
129
+ if (results.length === 0) return null;
130
+ return parseApiKey(results[0].data);
131
+ }
132
+ };
133
+ //#endregion
6
134
  //#region ../../node_modules/safe-buffer/index.js
7
135
  var require_safe_buffer = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8
136
  /*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
@@ -226,14 +354,14 @@ var require_buffer_equal_constant_time = /* @__PURE__ */ __commonJSMin(((exports
226
354
  //#region ../../node_modules/jwa/index.js
227
355
  var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
228
356
  var Buffer = require_safe_buffer().Buffer;
229
- var crypto$2 = __require("crypto");
357
+ var crypto$1 = __require("crypto");
230
358
  var formatEcdsa = require_ecdsa_sig_formatter();
231
359
  var util$2 = __require("util");
232
360
  var MSG_INVALID_ALGORITHM = "\"%s\" is not a valid algorithm.\n Supported algorithms are:\n \"HS256\", \"HS384\", \"HS512\", \"RS256\", \"RS384\", \"RS512\", \"PS256\", \"PS384\", \"PS512\", \"ES256\", \"ES384\", \"ES512\" and \"none\".";
233
361
  var MSG_INVALID_SECRET = "secret must be a string or buffer";
234
362
  var MSG_INVALID_VERIFIER_KEY = "key must be a string or a buffer";
235
363
  var MSG_INVALID_SIGNER_KEY = "key must be a string, a buffer or an object";
236
- var supportsKeyObjects = typeof crypto$2.createPublicKey === "function";
364
+ var supportsKeyObjects = typeof crypto$1.createPublicKey === "function";
237
365
  if (supportsKeyObjects) {
238
366
  MSG_INVALID_VERIFIER_KEY += " or a KeyObject";
239
367
  MSG_INVALID_SECRET += "or a KeyObject";
@@ -286,14 +414,14 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
286
414
  return function sign(thing, secret) {
287
415
  checkIsSecretKey(secret);
288
416
  thing = normalizeInput(thing);
289
- var hmac = crypto$2.createHmac("sha" + bits, secret);
417
+ var hmac = crypto$1.createHmac("sha" + bits, secret);
290
418
  return fromBase64((hmac.update(thing), hmac.digest("base64")));
291
419
  };
292
420
  }
293
421
  var bufferEqual;
294
- var timingSafeEqual = "timingSafeEqual" in crypto$2 ? function timingSafeEqual(a, b) {
422
+ var timingSafeEqual = "timingSafeEqual" in crypto$1 ? function timingSafeEqual(a, b) {
295
423
  if (a.byteLength !== b.byteLength) return false;
296
- return crypto$2.timingSafeEqual(a, b);
424
+ return crypto$1.timingSafeEqual(a, b);
297
425
  } : function timingSafeEqual(a, b) {
298
426
  if (!bufferEqual) bufferEqual = require_buffer_equal_constant_time();
299
427
  return bufferEqual(a, b);
@@ -308,7 +436,7 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
308
436
  return function sign(thing, privateKey) {
309
437
  checkIsPrivateKey(privateKey);
310
438
  thing = normalizeInput(thing);
311
- var signer = crypto$2.createSign("RSA-SHA" + bits);
439
+ var signer = crypto$1.createSign("RSA-SHA" + bits);
312
440
  return fromBase64((signer.update(thing), signer.sign(privateKey, "base64")));
313
441
  };
314
442
  }
@@ -317,7 +445,7 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
317
445
  checkIsPublicKey(publicKey);
318
446
  thing = normalizeInput(thing);
319
447
  signature = toBase64(signature);
320
- var verifier = crypto$2.createVerify("RSA-SHA" + bits);
448
+ var verifier = crypto$1.createVerify("RSA-SHA" + bits);
321
449
  verifier.update(thing);
322
450
  return verifier.verify(publicKey, signature, "base64");
323
451
  };
@@ -326,11 +454,11 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
326
454
  return function sign(thing, privateKey) {
327
455
  checkIsPrivateKey(privateKey);
328
456
  thing = normalizeInput(thing);
329
- var signer = crypto$2.createSign("RSA-SHA" + bits);
457
+ var signer = crypto$1.createSign("RSA-SHA" + bits);
330
458
  return fromBase64((signer.update(thing), signer.sign({
331
459
  key: privateKey,
332
- padding: crypto$2.constants.RSA_PKCS1_PSS_PADDING,
333
- saltLength: crypto$2.constants.RSA_PSS_SALTLEN_DIGEST
460
+ padding: crypto$1.constants.RSA_PKCS1_PSS_PADDING,
461
+ saltLength: crypto$1.constants.RSA_PSS_SALTLEN_DIGEST
334
462
  }, "base64")));
335
463
  };
336
464
  }
@@ -339,12 +467,12 @@ var require_jwa = /* @__PURE__ */ __commonJSMin(((exports, module) => {
339
467
  checkIsPublicKey(publicKey);
340
468
  thing = normalizeInput(thing);
341
469
  signature = toBase64(signature);
342
- var verifier = crypto$2.createVerify("RSA-SHA" + bits);
470
+ var verifier = crypto$1.createVerify("RSA-SHA" + bits);
343
471
  verifier.update(thing);
344
472
  return verifier.verify({
345
473
  key: publicKey,
346
- padding: crypto$2.constants.RSA_PKCS1_PSS_PADDING,
347
- saltLength: crypto$2.constants.RSA_PSS_SALTLEN_DIGEST
474
+ padding: crypto$1.constants.RSA_PKCS1_PSS_PADDING,
475
+ saltLength: crypto$1.constants.RSA_PSS_SALTLEN_DIGEST
348
476
  }, signature, "base64");
349
477
  };
350
478
  }
@@ -5941,7 +6069,7 @@ var AuthManager = class {
5941
6069
  const configured = this.config.get("auth.jwtSecret");
5942
6070
  if (configured) this.jwtSecret = configured;
5943
6071
  else {
5944
- const secret = crypto$1.randomBytes(32).toString("hex");
6072
+ const secret = crypto$2.randomBytes(32).toString("hex");
5945
6073
  this.config.update("auth", { jwtSecret: secret });
5946
6074
  this.logger.info("Generated JWT secret and saved to config.yaml (auth.jwtSecret)");
5947
6075
  this.jwtSecret = secret;
@@ -5962,15 +6090,15 @@ var AuthManager = class {
5962
6090
  return compare(password, hash);
5963
6091
  }
5964
6092
  generateApiKey() {
5965
- const token = crypto$1.randomBytes(32).toString("hex");
6093
+ const token = crypto$2.randomBytes(32).toString("hex");
5966
6094
  return {
5967
6095
  token,
5968
- hash: crypto$1.createHash("sha256").update(token).digest("hex"),
6096
+ hash: crypto$2.createHash("sha256").update(token).digest("hex"),
5969
6097
  prefix: token.slice(0, 8)
5970
6098
  };
5971
6099
  }
5972
6100
  validateApiKey(token, storedHash) {
5973
- return crypto$1.createHash("sha256").update(token).digest("hex") === storedHash;
6101
+ return crypto$2.createHash("sha256").update(token).digest("hex") === storedHash;
5974
6102
  }
5975
6103
  /**
5976
6104
  * Create a service token for agent/worker authentication.
@@ -6112,316 +6240,103 @@ var AuthManager = class {
6112
6240
  }
6113
6241
  };
6114
6242
  //#endregion
6115
- //#region src/auth/parse-record.ts
6116
- function parseRecord(kind, schema, data) {
6117
- try {
6118
- return schema.parse(data);
6119
- } catch (err) {
6120
- const topKeys = data && typeof data === "object" && !Array.isArray(data) ? Object.keys(data).join(", ") || "(empty)" : `(non-object: ${typeof data})`;
6121
- const detail = err instanceof Error ? err.message : String(err);
6122
- 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." : "";
6123
- throw new Error(`Failed to parse ${kind} from settings store. Top-level keys=[${topKeys}].${hint} Underlying: ${detail}`, { cause: err });
6124
- }
6125
- }
6126
- //#endregion
6127
- //#region src/auth/user-manager.ts
6128
- var USERS_COLLECTION$1 = "users";
6129
- function parseUser(data) {
6130
- return parseRecord("user", UserRecordSchema, data);
6243
+ //#region src/auth/scoped-token-manager.ts
6244
+ var TOKENS_COLLECTION = "scoped_tokens";
6245
+ var TOKEN_PREFIX = "cst_";
6246
+ function parseToken(data) {
6247
+ return parseRecord("scoped-token", ScopedTokenSchema, data);
6131
6248
  }
6132
- var UserManager = class {
6133
- storageAccess;
6134
- auth;
6135
- config;
6136
- constructor(storageAccess, auth, config) {
6137
- this.storageAccess = storageAccess;
6138
- this.auth = auth;
6139
- this.config = config;
6140
- }
6141
- get store() {
6142
- return this.storageAccess.getStore();
6249
+ /**
6250
+ * Manages scoped API tokens with restricted addon/route/capability access.
6251
+ */
6252
+ var ScopedTokenManager = class {
6253
+ store;
6254
+ constructor(store) {
6255
+ this.store = store;
6143
6256
  }
6144
- async create(input) {
6145
- if (await this.findByUsername(input.username)) throw new Error(`User with username "${input.username}" already exists`);
6146
- const passwordHash = await this.auth.hashPassword(input.password);
6147
- const now = Date.now();
6257
+ async create(userId, name, scopes, expiresAt) {
6258
+ const rawToken = `${TOKEN_PREFIX}${crypto$2.randomBytes(32).toString("hex")}`;
6259
+ const tokenHash = crypto$2.createHash("sha256").update(rawToken).digest("hex");
6260
+ const tokenPrefix = rawToken.slice(0, 12);
6148
6261
  const record = {
6149
- id: crypto$1.randomUUID(),
6150
- username: input.username,
6151
- passwordHash,
6152
- isAdmin: input.isAdmin ?? false,
6153
- allowedProviders: input.allowedProviders ?? "*",
6154
- allowedDevices: input.allowedDevices ?? {},
6155
- scopes: input.scopes ?? [],
6156
- createdAt: now,
6157
- updatedAt: now
6262
+ id: crypto$2.randomUUID(),
6263
+ userId,
6264
+ name,
6265
+ tokenHash,
6266
+ tokenPrefix,
6267
+ scopes: scopes.map((s) => ({ ...s })),
6268
+ expiresAt,
6269
+ lastUsedAt: void 0,
6270
+ createdAt: Date.now()
6158
6271
  };
6159
6272
  await this.store.insert.mutate({
6160
- collection: USERS_COLLECTION$1,
6273
+ collection: TOKENS_COLLECTION,
6161
6274
  record: {
6162
6275
  id: record.id,
6163
6276
  data: { ...record }
6164
6277
  }
6165
6278
  });
6166
- return record;
6167
- }
6168
- async findByUsername(username) {
6169
- const results = await this.store.query.query({
6170
- collection: USERS_COLLECTION$1,
6171
- filter: { where: { username } }
6172
- });
6173
- if (results.length === 0) return null;
6174
- return parseUser(results[0].data);
6279
+ return {
6280
+ token: rawToken,
6281
+ record
6282
+ };
6175
6283
  }
6176
- async findById(id) {
6284
+ async validate(rawToken) {
6285
+ if (!rawToken.startsWith(TOKEN_PREFIX)) return null;
6286
+ const tokenHash = crypto$2.createHash("sha256").update(rawToken).digest("hex");
6177
6287
  const results = await this.store.query.query({
6178
- collection: USERS_COLLECTION$1,
6179
- filter: { where: { id } }
6288
+ collection: TOKENS_COLLECTION,
6289
+ filter: { where: { tokenHash } }
6180
6290
  });
6181
6291
  if (results.length === 0) return null;
6182
- return parseUser(results[0].data);
6292
+ const record = parseToken(results[0].data);
6293
+ if (record.expiresAt !== void 0 && record.expiresAt !== null && Date.now() > record.expiresAt) return null;
6294
+ this.updateLastUsed(record.id).catch(() => {});
6295
+ return record;
6183
6296
  }
6184
- async validateCredentials(username, password) {
6185
- const user = await this.findByUsername(username);
6186
- if (!user) return null;
6187
- return await this.auth.comparePassword(password, user.passwordHash) ? user : null;
6297
+ /**
6298
+ * Coarse scope match — checks only `addonId` / `capability` (no
6299
+ * `route-prefix` after the caps-only migration). Used by addon-route
6300
+ * HTTP handlers that need a quick allow/deny before the per-method
6301
+ * `access` check kicks in via `METHOD_ACCESS_MAP` in
6302
+ * `protectedProcedure`.
6303
+ */
6304
+ matchesScope(token, addonId, capability) {
6305
+ for (const scope of token.scopes) switch (scope.type) {
6306
+ case "addon":
6307
+ if (addonId && scope.target === addonId) return true;
6308
+ break;
6309
+ case "capability":
6310
+ if (capability && scope.target === capability) return true;
6311
+ break;
6312
+ }
6313
+ return false;
6188
6314
  }
6189
- async listAll() {
6190
- return (await this.store.query.query({ collection: USERS_COLLECTION$1 })).map((r) => {
6191
- const { passwordHash: _ph, ...rest } = parseUser(r.data);
6192
- return rest;
6315
+ async revoke(tokenId) {
6316
+ await this.store.delete.mutate({
6317
+ collection: TOKENS_COLLECTION,
6318
+ key: tokenId
6193
6319
  });
6194
6320
  }
6195
- async update(id, data) {
6196
- const existing = await this.findById(id);
6197
- if (!existing) throw new Error(`User with id "${id}" not found`);
6198
- await this.store.update.mutate({
6199
- collection: USERS_COLLECTION$1,
6200
- id,
6201
- data: {
6202
- ...existing,
6203
- ...data,
6204
- updatedAt: Date.now()
6205
- }
6206
- });
6321
+ async listForUser(userId) {
6322
+ return (await this.store.query.query({
6323
+ collection: TOKENS_COLLECTION,
6324
+ filter: { where: { userId } }
6325
+ })).map((r) => parseToken(r.data));
6207
6326
  }
6208
- async delete(id) {
6209
- await this.store.delete.mutate({
6210
- collection: USERS_COLLECTION$1,
6211
- key: id
6327
+ async updateLastUsed(tokenId) {
6328
+ const results = await this.store.query.query({
6329
+ collection: TOKENS_COLLECTION,
6330
+ filter: { where: { id: tokenId } }
6212
6331
  });
6213
- }
6214
- async resetPassword(id, newPassword) {
6215
- const existing = await this.findById(id);
6216
- if (!existing) throw new Error(`User with id "${id}" not found`);
6217
- const passwordHash = await this.auth.hashPassword(newPassword);
6332
+ if (results.length === 0) return;
6333
+ const existing = parseToken(results[0].data);
6218
6334
  await this.store.update.mutate({
6219
- collection: USERS_COLLECTION$1,
6220
- id,
6335
+ collection: TOKENS_COLLECTION,
6336
+ id: tokenId,
6221
6337
  data: {
6222
6338
  ...existing,
6223
- passwordHash,
6224
- updatedAt: Date.now()
6225
- }
6226
- });
6227
- }
6228
- async ensureAdminExists() {
6229
- const adminUsername = this.config.get("auth.adminUsername");
6230
- const adminPassword = this.config.get("auth.adminPassword");
6231
- if (!adminUsername || !adminPassword) return;
6232
- const existing = await this.findByUsername(adminUsername);
6233
- if (existing) {
6234
- if (!await this.auth.comparePassword(adminPassword, existing.passwordHash)) await this.resetPassword(existing.id, adminPassword);
6235
- return;
6236
- }
6237
- await this.create({
6238
- username: adminUsername,
6239
- password: adminPassword,
6240
- isAdmin: true,
6241
- allowedProviders: "*",
6242
- allowedDevices: {}
6243
- });
6244
- }
6245
- };
6246
- //#endregion
6247
- //#region src/auth/api-key-manager.ts
6248
- var API_KEYS_COLLECTION$1 = "api_keys";
6249
- function parseApiKey(data) {
6250
- return parseRecord("api-key", ApiKeyRecordSchema, data);
6251
- }
6252
- var ApiKeyManager = class {
6253
- storageAccess;
6254
- auth;
6255
- constructor(storageAccess, auth) {
6256
- this.storageAccess = storageAccess;
6257
- this.auth = auth;
6258
- }
6259
- get store() {
6260
- return this.storageAccess.getStore();
6261
- }
6262
- async create(input) {
6263
- const { token: rawToken, hash, prefix } = this.auth.generateApiKey();
6264
- const now = Date.now();
6265
- const record = {
6266
- id: crypto$1.randomUUID(),
6267
- label: input.label,
6268
- isAdmin: input.isAdmin ?? false,
6269
- allowedProviders: input.allowedProviders ?? "*",
6270
- allowedDevices: input.allowedDevices ?? {},
6271
- tokenHash: hash,
6272
- tokenPrefix: prefix,
6273
- createdAt: now
6274
- };
6275
- await this.store.insert.mutate({
6276
- collection: API_KEYS_COLLECTION$1,
6277
- record: {
6278
- id: record.id,
6279
- data: { ...record }
6280
- }
6281
- });
6282
- return {
6283
- record,
6284
- token: rawToken
6285
- };
6286
- }
6287
- async validateToken(token) {
6288
- const allKeys = await this.store.query.query({ collection: API_KEYS_COLLECTION$1 });
6289
- for (const entry of allKeys) {
6290
- const record = parseApiKey(entry.data);
6291
- if (this.auth.validateApiKey(token, record.tokenHash)) {
6292
- const updatedData = {
6293
- ...record,
6294
- lastUsedAt: Date.now()
6295
- };
6296
- await this.store.update.mutate({
6297
- collection: API_KEYS_COLLECTION$1,
6298
- id: record.id,
6299
- data: { ...updatedData }
6300
- });
6301
- return updatedData;
6302
- }
6303
- }
6304
- return null;
6305
- }
6306
- async listAll() {
6307
- return (await this.store.query.query({ collection: API_KEYS_COLLECTION$1 })).map((r) => {
6308
- const { tokenHash: _th, ...rest } = parseApiKey(r.data);
6309
- return rest;
6310
- });
6311
- }
6312
- async revoke(id) {
6313
- await this.store.delete.mutate({
6314
- collection: API_KEYS_COLLECTION$1,
6315
- key: id
6316
- });
6317
- }
6318
- async findById(id) {
6319
- const results = await this.store.query.query({
6320
- collection: API_KEYS_COLLECTION$1,
6321
- filter: { where: { id } }
6322
- });
6323
- if (results.length === 0) return null;
6324
- return parseApiKey(results[0].data);
6325
- }
6326
- };
6327
- //#endregion
6328
- //#region src/auth/scoped-token-manager.ts
6329
- var TOKENS_COLLECTION = "scoped_tokens";
6330
- var TOKEN_PREFIX = "cst_";
6331
- function parseToken(data) {
6332
- return parseRecord("scoped-token", ScopedTokenSchema, data);
6333
- }
6334
- /**
6335
- * Manages scoped API tokens with restricted addon/route/capability access.
6336
- */
6337
- var ScopedTokenManager = class {
6338
- store;
6339
- constructor(store) {
6340
- this.store = store;
6341
- }
6342
- async create(userId, name, scopes, expiresAt) {
6343
- const rawToken = `${TOKEN_PREFIX}${crypto$1.randomBytes(32).toString("hex")}`;
6344
- const tokenHash = crypto$1.createHash("sha256").update(rawToken).digest("hex");
6345
- const tokenPrefix = rawToken.slice(0, 12);
6346
- const record = {
6347
- id: crypto$1.randomUUID(),
6348
- userId,
6349
- name,
6350
- tokenHash,
6351
- tokenPrefix,
6352
- scopes: scopes.map((s) => ({ ...s })),
6353
- expiresAt,
6354
- lastUsedAt: void 0,
6355
- createdAt: Date.now()
6356
- };
6357
- await this.store.insert.mutate({
6358
- collection: TOKENS_COLLECTION,
6359
- record: {
6360
- id: record.id,
6361
- data: { ...record }
6362
- }
6363
- });
6364
- return {
6365
- token: rawToken,
6366
- record
6367
- };
6368
- }
6369
- async validate(rawToken) {
6370
- if (!rawToken.startsWith(TOKEN_PREFIX)) return null;
6371
- const tokenHash = crypto$1.createHash("sha256").update(rawToken).digest("hex");
6372
- const results = await this.store.query.query({
6373
- collection: TOKENS_COLLECTION,
6374
- filter: { where: { tokenHash } }
6375
- });
6376
- if (results.length === 0) return null;
6377
- const record = parseToken(results[0].data);
6378
- if (record.expiresAt !== void 0 && record.expiresAt !== null && Date.now() > record.expiresAt) return null;
6379
- this.updateLastUsed(record.id).catch(() => {});
6380
- return record;
6381
- }
6382
- /**
6383
- * Coarse scope match — checks only `addonId` / `capability` (no
6384
- * `route-prefix` after the caps-only migration). Used by addon-route
6385
- * HTTP handlers that need a quick allow/deny before the per-method
6386
- * `access` check kicks in via `METHOD_ACCESS_MAP` in
6387
- * `protectedProcedure`.
6388
- */
6389
- matchesScope(token, addonId, capability) {
6390
- for (const scope of token.scopes) switch (scope.type) {
6391
- case "addon":
6392
- if (addonId && scope.target === addonId) return true;
6393
- break;
6394
- case "capability":
6395
- if (capability && scope.target === capability) return true;
6396
- break;
6397
- }
6398
- return false;
6399
- }
6400
- async revoke(tokenId) {
6401
- await this.store.delete.mutate({
6402
- collection: TOKENS_COLLECTION,
6403
- key: tokenId
6404
- });
6405
- }
6406
- async listForUser(userId) {
6407
- return (await this.store.query.query({
6408
- collection: TOKENS_COLLECTION,
6409
- filter: { where: { userId } }
6410
- })).map((r) => parseToken(r.data));
6411
- }
6412
- async updateLastUsed(tokenId) {
6413
- const results = await this.store.query.query({
6414
- collection: TOKENS_COLLECTION,
6415
- filter: { where: { id: tokenId } }
6416
- });
6417
- if (results.length === 0) return;
6418
- const existing = parseToken(results[0].data);
6419
- await this.store.update.mutate({
6420
- collection: TOKENS_COLLECTION,
6421
- id: tokenId,
6422
- data: {
6423
- ...existing,
6424
- lastUsedAt: Date.now()
6339
+ lastUsedAt: Date.now()
6425
6340
  }
6426
6341
  });
6427
6342
  }
@@ -6489,164 +6404,6 @@ var ScopedTokenManager = class {
6489
6404
  }
6490
6405
  return removed;
6491
6406
  }
6492
- };
6493
- //#endregion
6494
- //#region src/builtins/local-auth/oauth-session-manager.ts
6495
- /**
6496
- * Manages OAuth session records in the `oauth_sessions` collection.
6497
- *
6498
- * Each record represents one Alexa-style account link. Sessions can be
6499
- * active (`revokedAt = null`) or revoked (`revokedAt = <unix-ms>`).
6500
- * `list()` returns both — callers decide whether to filter.
6501
- *
6502
- * Mirrors the pattern of `ScopedTokenManager`: takes a `SettingsStoreClient`
6503
- * in its constructor and performs all reads/writes via the store's
6504
- * `insert`, `update`, `delete`, and `query` primitives.
6505
- */
6506
- var SESSIONS_COLLECTION = "oauth_sessions";
6507
- function parseSession(data) {
6508
- const id = data["id"];
6509
- const userId = data["userId"];
6510
- const username = data["username"];
6511
- const integrationId = data["integrationId"];
6512
- const scopes = data["scopes"];
6513
- const createdAt = data["createdAt"];
6514
- const lastUsedAt = data["lastUsedAt"];
6515
- const revokedAt = data["revokedAt"];
6516
- if (typeof id !== "string") throw new Error("oauth_sessions row: missing id");
6517
- if (typeof userId !== "string") throw new Error("oauth_sessions row: missing userId");
6518
- if (typeof username !== "string") throw new Error("oauth_sessions row: missing username");
6519
- if (typeof integrationId !== "string") throw new Error("oauth_sessions row: missing integrationId");
6520
- if (!Array.isArray(scopes)) throw new Error("oauth_sessions row: scopes must be an array");
6521
- if (typeof createdAt !== "number") throw new Error("oauth_sessions row: missing createdAt");
6522
- if (typeof lastUsedAt !== "number") throw new Error("oauth_sessions row: missing lastUsedAt");
6523
- return {
6524
- id,
6525
- userId,
6526
- username,
6527
- integrationId,
6528
- scopes,
6529
- createdAt,
6530
- lastUsedAt,
6531
- revokedAt: typeof revokedAt === "number" ? revokedAt : null
6532
- };
6533
- }
6534
- var OauthSessionManager = class {
6535
- store;
6536
- constructor(store) {
6537
- this.store = store;
6538
- }
6539
- /**
6540
- * Create a new OAuth session record. Generates a UUID as the session id,
6541
- * sets `createdAt` and `lastUsedAt` to now, `revokedAt` to null.
6542
- */
6543
- async create(input) {
6544
- const now = Date.now();
6545
- const session = {
6546
- id: crypto$1.randomUUID(),
6547
- userId: input.userId,
6548
- username: input.username,
6549
- integrationId: input.integrationId,
6550
- scopes: input.scopes.map((s) => ({ ...s })),
6551
- createdAt: now,
6552
- lastUsedAt: now,
6553
- revokedAt: null
6554
- };
6555
- await this.store.insert.mutate({
6556
- collection: SESSIONS_COLLECTION,
6557
- record: {
6558
- id: session.id,
6559
- data: { ...session }
6560
- }
6561
- });
6562
- return session;
6563
- }
6564
- /**
6565
- * Return all sessions — both active and revoked. Callers decide
6566
- * whether to filter by `revokedAt`.
6567
- */
6568
- async list() {
6569
- return (await this.store.query.query({
6570
- collection: SESSIONS_COLLECTION,
6571
- filter: {}
6572
- })).map((r) => parseSession(r.data));
6573
- }
6574
- /**
6575
- * Look up a session by its id. Returns null when not found.
6576
- */
6577
- async getById(id) {
6578
- const results = await this.store.query.query({
6579
- collection: SESSIONS_COLLECTION,
6580
- filter: { where: { id } }
6581
- });
6582
- if (results.length === 0) return null;
6583
- return parseSession(results[0].data);
6584
- }
6585
- /**
6586
- * Mark a session as revoked by setting `revokedAt = now`.
6587
- *
6588
- * - Returns `true` on success (including idempotent re-revoke — the
6589
- * existing `revokedAt` timestamp is preserved; it is NOT updated).
6590
- * - Returns `false` when the session id is not found.
6591
- */
6592
- async markRevoked(id) {
6593
- const existing = await this.getById(id);
6594
- if (existing === null) return false;
6595
- if (existing.revokedAt !== null) return true;
6596
- const updated = {
6597
- ...existing,
6598
- revokedAt: Date.now()
6599
- };
6600
- await this.store.update.mutate({
6601
- collection: SESSIONS_COLLECTION,
6602
- id,
6603
- data: { ...updated }
6604
- });
6605
- return true;
6606
- }
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
- /**
6633
- * Update `lastUsedAt` to now. No-op (does not throw) when the session
6634
- * id is not found — the caller (token-use hot path) should not fail
6635
- * for a missing session that may have been concurrently revoked.
6636
- */
6637
- async touch(id) {
6638
- const existing = await this.getById(id);
6639
- if (existing === null) return;
6640
- const updated = {
6641
- ...existing,
6642
- lastUsedAt: Date.now()
6643
- };
6644
- await this.store.update.mutate({
6645
- collection: SESSIONS_COLLECTION,
6646
- id,
6647
- data: { ...updated }
6648
- });
6649
- }
6650
6407
  }, i$2 = class extends Error {
6651
6408
  constructor(e, t) {
6652
6409
  super(e, t), this.name = "OTPError";
@@ -8903,53 +8660,173 @@ var TotpManager = class {
8903
8660
  }
8904
8661
  return true;
8905
8662
  }
8906
- /**
8907
- * Remove enrollment. Idempotent — no error if the user had no row.
8908
- */
8909
- async disable(userId) {
8910
- await this.store.delete.mutate({
8911
- collection: TOTP_COLLECTION,
8912
- key: userId
8663
+ /**
8664
+ * Remove enrollment. Idempotent — no error if the user had no row.
8665
+ */
8666
+ async disable(userId) {
8667
+ await this.store.delete.mutate({
8668
+ collection: TOTP_COLLECTION,
8669
+ key: userId
8670
+ });
8671
+ }
8672
+ /**
8673
+ * Verify a code against the confirmed secret. Returns `false` when:
8674
+ * - The user has no row (not set up).
8675
+ * - The row is pending (`confirmedAt === null`).
8676
+ * - The code doesn't match within the configured window.
8677
+ */
8678
+ async verify(userId, code) {
8679
+ const row = await this.find(userId);
8680
+ if (!row) return false;
8681
+ if (row.confirmedAt === null) return false;
8682
+ return d({
8683
+ token: code,
8684
+ secret: row.secret,
8685
+ epochTolerance: this.epochTolerance
8686
+ }).valid;
8687
+ }
8688
+ /**
8689
+ * Read the user's enrollment status. Pending half-enrollments are
8690
+ * reported as `enabled: false` — only a confirmed row counts.
8691
+ */
8692
+ async getStatus(userId) {
8693
+ const row = await this.find(userId);
8694
+ if (!row) return {
8695
+ enabled: false,
8696
+ confirmedAt: null
8697
+ };
8698
+ return {
8699
+ enabled: row.confirmedAt !== null,
8700
+ confirmedAt: row.confirmedAt
8701
+ };
8702
+ }
8703
+ async find(userId) {
8704
+ const results = await this.store.query.query({
8705
+ collection: TOTP_COLLECTION,
8706
+ filter: { where: { userId } }
8707
+ });
8708
+ if (results.length === 0) return null;
8709
+ return parseRow(results[0].data);
8710
+ }
8711
+ };
8712
+ //#endregion
8713
+ //#region src/auth/user-manager.ts
8714
+ var USERS_COLLECTION$1 = "users";
8715
+ function parseUser(data) {
8716
+ return parseRecord("user", UserRecordSchema, data);
8717
+ }
8718
+ var UserManager = class {
8719
+ storageAccess;
8720
+ auth;
8721
+ config;
8722
+ constructor(storageAccess, auth, config) {
8723
+ this.storageAccess = storageAccess;
8724
+ this.auth = auth;
8725
+ this.config = config;
8726
+ }
8727
+ get store() {
8728
+ return this.storageAccess.getStore();
8729
+ }
8730
+ async create(input) {
8731
+ if (await this.findByUsername(input.username)) throw new Error(`User with username "${input.username}" already exists`);
8732
+ const passwordHash = await this.auth.hashPassword(input.password);
8733
+ const now = Date.now();
8734
+ const record = {
8735
+ id: crypto$2.randomUUID(),
8736
+ username: input.username,
8737
+ passwordHash,
8738
+ isAdmin: input.isAdmin ?? false,
8739
+ allowedProviders: input.allowedProviders ?? "*",
8740
+ allowedDevices: input.allowedDevices ?? {},
8741
+ scopes: input.scopes ?? [],
8742
+ createdAt: now,
8743
+ updatedAt: now
8744
+ };
8745
+ await this.store.insert.mutate({
8746
+ collection: USERS_COLLECTION$1,
8747
+ record: {
8748
+ id: record.id,
8749
+ data: { ...record }
8750
+ }
8751
+ });
8752
+ return record;
8753
+ }
8754
+ async findByUsername(username) {
8755
+ const results = await this.store.query.query({
8756
+ collection: USERS_COLLECTION$1,
8757
+ filter: { where: { username } }
8758
+ });
8759
+ if (results.length === 0) return null;
8760
+ return parseUser(results[0].data);
8761
+ }
8762
+ async findById(id) {
8763
+ const results = await this.store.query.query({
8764
+ collection: USERS_COLLECTION$1,
8765
+ filter: { where: { id } }
8766
+ });
8767
+ if (results.length === 0) return null;
8768
+ return parseUser(results[0].data);
8769
+ }
8770
+ async validateCredentials(username, password) {
8771
+ const user = await this.findByUsername(username);
8772
+ if (!user) return null;
8773
+ return await this.auth.comparePassword(password, user.passwordHash) ? user : null;
8774
+ }
8775
+ async listAll() {
8776
+ return (await this.store.query.query({ collection: USERS_COLLECTION$1 })).map((r) => {
8777
+ const { passwordHash: _ph, ...rest } = parseUser(r.data);
8778
+ return rest;
8779
+ });
8780
+ }
8781
+ async update(id, data) {
8782
+ const existing = await this.findById(id);
8783
+ if (!existing) throw new Error(`User with id "${id}" not found`);
8784
+ await this.store.update.mutate({
8785
+ collection: USERS_COLLECTION$1,
8786
+ id,
8787
+ data: {
8788
+ ...existing,
8789
+ ...data,
8790
+ updatedAt: Date.now()
8791
+ }
8913
8792
  });
8914
8793
  }
8915
- /**
8916
- * Verify a code against the confirmed secret. Returns `false` when:
8917
- * - The user has no row (not set up).
8918
- * - The row is pending (`confirmedAt === null`).
8919
- * - The code doesn't match within the configured window.
8920
- */
8921
- async verify(userId, code) {
8922
- const row = await this.find(userId);
8923
- if (!row) return false;
8924
- if (row.confirmedAt === null) return false;
8925
- return d({
8926
- token: code,
8927
- secret: row.secret,
8928
- epochTolerance: this.epochTolerance
8929
- }).valid;
8794
+ async delete(id) {
8795
+ await this.store.delete.mutate({
8796
+ collection: USERS_COLLECTION$1,
8797
+ key: id
8798
+ });
8930
8799
  }
8931
- /**
8932
- * Read the user's enrollment status. Pending half-enrollments are
8933
- * reported as `enabled: false` only a confirmed row counts.
8934
- */
8935
- async getStatus(userId) {
8936
- const row = await this.find(userId);
8937
- if (!row) return {
8938
- enabled: false,
8939
- confirmedAt: null
8940
- };
8941
- return {
8942
- enabled: row.confirmedAt !== null,
8943
- confirmedAt: row.confirmedAt
8944
- };
8800
+ async resetPassword(id, newPassword) {
8801
+ const existing = await this.findById(id);
8802
+ if (!existing) throw new Error(`User with id "${id}" not found`);
8803
+ const passwordHash = await this.auth.hashPassword(newPassword);
8804
+ await this.store.update.mutate({
8805
+ collection: USERS_COLLECTION$1,
8806
+ id,
8807
+ data: {
8808
+ ...existing,
8809
+ passwordHash,
8810
+ updatedAt: Date.now()
8811
+ }
8812
+ });
8945
8813
  }
8946
- async find(userId) {
8947
- const results = await this.store.query.query({
8948
- collection: TOTP_COLLECTION,
8949
- filter: { where: { userId } }
8814
+ async ensureAdminExists() {
8815
+ const adminUsername = this.config.get("auth.adminUsername");
8816
+ const adminPassword = this.config.get("auth.adminPassword");
8817
+ if (!adminUsername || !adminPassword) return;
8818
+ const existing = await this.findByUsername(adminUsername);
8819
+ if (existing) {
8820
+ if (!await this.auth.comparePassword(adminPassword, existing.passwordHash)) await this.resetPassword(existing.id, adminPassword);
8821
+ return;
8822
+ }
8823
+ await this.create({
8824
+ username: adminUsername,
8825
+ password: adminPassword,
8826
+ isAdmin: true,
8827
+ allowedProviders: "*",
8828
+ allowedDevices: {}
8950
8829
  });
8951
- if (results.length === 0) return null;
8952
- return parseRow(results[0].data);
8953
8830
  }
8954
8831
  };
8955
8832
  //#endregion
@@ -9220,6 +9097,21 @@ var TTL = {
9220
9097
  "oauth-access": 3600,
9221
9098
  "oauth-refresh": 2592e3
9222
9099
  };
9100
+ /**
9101
+ * PKCE S256 check (RFC 7636 §4.6): `BASE64URL(SHA256(verifier)) === challenge`.
9102
+ *
9103
+ * Keyed off the CLAIM, never off the descriptor: a code minted with a challenge
9104
+ * stays unusable without the verifier even if the integration later stops
9105
+ * requiring PKCE. Constant-time compare so a wrong verifier cannot be walked
9106
+ * byte by byte inside the code's 60 s window.
9107
+ */
9108
+ function verifyPkceS256(challenge, verifier) {
9109
+ if (!verifier) return false;
9110
+ const expected = Buffer.from(createHash("sha256").update(verifier).digest("base64url"));
9111
+ const actual = Buffer.from(challenge);
9112
+ if (expected.length !== actual.length) return false;
9113
+ return timingSafeEqual(expected, actual);
9114
+ }
9223
9115
  /** OAuth account-linking grant logic. Pure over an injected sso-bridge
9224
9116
  * signer so it is unit-testable without the capability registry.
9225
9117
  *
@@ -9270,7 +9162,8 @@ function createOauthGrants(ssoBridge, sessionManager) {
9270
9162
  provider: "oauth-code",
9271
9163
  redirectUri: input.redirectUri,
9272
9164
  integrationId: input.integrationId,
9273
- jti
9165
+ jti,
9166
+ ...input.codeChallenge ? { codeChallenge: input.codeChallenge } : {}
9274
9167
  },
9275
9168
  ttlSec: TTL["oauth-code"]
9276
9169
  });
@@ -9280,6 +9173,10 @@ function createOauthGrants(ssoBridge, sessionManager) {
9280
9173
  const claims = await ssoBridge.verifyBridgeToken({ token: input.code });
9281
9174
  if (!claims || claims.provider !== "oauth-code") return null;
9282
9175
  if (claims.redirectUri !== input.redirectUri) return null;
9176
+ const codeChallenge = claims.codeChallenge;
9177
+ if (typeof codeChallenge === "string" && codeChallenge !== "") {
9178
+ if (!verifyPkceS256(codeChallenge, input.codeVerifier)) return null;
9179
+ }
9283
9180
  const jti = claims.jti;
9284
9181
  if (!jti) return null;
9285
9182
  const expiryMs = Date.now() + TTL["oauth-code"] * 1e3;
@@ -9324,6 +9221,164 @@ function createOauthGrants(ssoBridge, sessionManager) {
9324
9221
  };
9325
9222
  }
9326
9223
  //#endregion
9224
+ //#region src/builtins/local-auth/oauth-session-manager.ts
9225
+ /**
9226
+ * Manages OAuth session records in the `oauth_sessions` collection.
9227
+ *
9228
+ * Each record represents one Alexa-style account link. Sessions can be
9229
+ * active (`revokedAt = null`) or revoked (`revokedAt = <unix-ms>`).
9230
+ * `list()` returns both — callers decide whether to filter.
9231
+ *
9232
+ * Mirrors the pattern of `ScopedTokenManager`: takes a `SettingsStoreClient`
9233
+ * in its constructor and performs all reads/writes via the store's
9234
+ * `insert`, `update`, `delete`, and `query` primitives.
9235
+ */
9236
+ var SESSIONS_COLLECTION = "oauth_sessions";
9237
+ function parseSession(data) {
9238
+ const id = data["id"];
9239
+ const userId = data["userId"];
9240
+ const username = data["username"];
9241
+ const integrationId = data["integrationId"];
9242
+ const scopes = data["scopes"];
9243
+ const createdAt = data["createdAt"];
9244
+ const lastUsedAt = data["lastUsedAt"];
9245
+ const revokedAt = data["revokedAt"];
9246
+ if (typeof id !== "string") throw new Error("oauth_sessions row: missing id");
9247
+ if (typeof userId !== "string") throw new Error("oauth_sessions row: missing userId");
9248
+ if (typeof username !== "string") throw new Error("oauth_sessions row: missing username");
9249
+ if (typeof integrationId !== "string") throw new Error("oauth_sessions row: missing integrationId");
9250
+ if (!Array.isArray(scopes)) throw new Error("oauth_sessions row: scopes must be an array");
9251
+ if (typeof createdAt !== "number") throw new Error("oauth_sessions row: missing createdAt");
9252
+ if (typeof lastUsedAt !== "number") throw new Error("oauth_sessions row: missing lastUsedAt");
9253
+ return {
9254
+ id,
9255
+ userId,
9256
+ username,
9257
+ integrationId,
9258
+ scopes,
9259
+ createdAt,
9260
+ lastUsedAt,
9261
+ revokedAt: typeof revokedAt === "number" ? revokedAt : null
9262
+ };
9263
+ }
9264
+ var OauthSessionManager = class {
9265
+ store;
9266
+ constructor(store) {
9267
+ this.store = store;
9268
+ }
9269
+ /**
9270
+ * Create a new OAuth session record. Generates a UUID as the session id,
9271
+ * sets `createdAt` and `lastUsedAt` to now, `revokedAt` to null.
9272
+ */
9273
+ async create(input) {
9274
+ const now = Date.now();
9275
+ const session = {
9276
+ id: crypto$2.randomUUID(),
9277
+ userId: input.userId,
9278
+ username: input.username,
9279
+ integrationId: input.integrationId,
9280
+ scopes: input.scopes.map((s) => ({ ...s })),
9281
+ createdAt: now,
9282
+ lastUsedAt: now,
9283
+ revokedAt: null
9284
+ };
9285
+ await this.store.insert.mutate({
9286
+ collection: SESSIONS_COLLECTION,
9287
+ record: {
9288
+ id: session.id,
9289
+ data: { ...session }
9290
+ }
9291
+ });
9292
+ return session;
9293
+ }
9294
+ /**
9295
+ * Return all sessions — both active and revoked. Callers decide
9296
+ * whether to filter by `revokedAt`.
9297
+ */
9298
+ async list() {
9299
+ return (await this.store.query.query({
9300
+ collection: SESSIONS_COLLECTION,
9301
+ filter: {}
9302
+ })).map((r) => parseSession(r.data));
9303
+ }
9304
+ /**
9305
+ * Look up a session by its id. Returns null when not found.
9306
+ */
9307
+ async getById(id) {
9308
+ const results = await this.store.query.query({
9309
+ collection: SESSIONS_COLLECTION,
9310
+ filter: { where: { id } }
9311
+ });
9312
+ if (results.length === 0) return null;
9313
+ return parseSession(results[0].data);
9314
+ }
9315
+ /**
9316
+ * Mark a session as revoked by setting `revokedAt = now`.
9317
+ *
9318
+ * - Returns `true` on success (including idempotent re-revoke — the
9319
+ * existing `revokedAt` timestamp is preserved; it is NOT updated).
9320
+ * - Returns `false` when the session id is not found.
9321
+ */
9322
+ async markRevoked(id) {
9323
+ const existing = await this.getById(id);
9324
+ if (existing === null) return false;
9325
+ if (existing.revokedAt !== null) return true;
9326
+ const updated = {
9327
+ ...existing,
9328
+ revokedAt: Date.now()
9329
+ };
9330
+ await this.store.update.mutate({
9331
+ collection: SESSIONS_COLLECTION,
9332
+ id,
9333
+ data: { ...updated }
9334
+ });
9335
+ return true;
9336
+ }
9337
+ /**
9338
+ * Delete sessions revoked longer ago than `horizonMs`.
9339
+ *
9340
+ * Safe because an ABSENT session already fails closed exactly like a revoked
9341
+ * one: every consumer in `oauth-grants.ts` reads
9342
+ * `if (!session || session.revokedAt != null) return null`. Deleting the row
9343
+ * therefore cannot re-enable a link that was cut — it removes a record whose
9344
+ * only remaining job was to say "no", which the absence says too.
9345
+ *
9346
+ * A retention window rather than an immediate delete: a revoked account link
9347
+ * is the kind of thing an operator looks up AFTER the fact ("when did I
9348
+ * unlink that?"), and the row is tiny. Active sessions are never touched —
9349
+ * the filter matches only rows with a `revokedAt` timestamp in range, and
9350
+ * `revokedAt` is NULL on every live session, which SQL's `BETWEEN` never
9351
+ * matches.
9352
+ *
9353
+ * Returns how many rows went.
9354
+ */
9355
+ async reapRevoked(horizonMs) {
9356
+ const { deleted } = await this.store.deleteWhere.mutate({
9357
+ collection: SESSIONS_COLLECTION,
9358
+ filter: { whereBetween: { revokedAt: [1, horizonMs] } }
9359
+ });
9360
+ return deleted;
9361
+ }
9362
+ /**
9363
+ * Update `lastUsedAt` to now. No-op (does not throw) when the session
9364
+ * id is not found — the caller (token-use hot path) should not fail
9365
+ * for a missing session that may have been concurrently revoked.
9366
+ */
9367
+ async touch(id) {
9368
+ const existing = await this.getById(id);
9369
+ if (existing === null) return;
9370
+ const updated = {
9371
+ ...existing,
9372
+ lastUsedAt: Date.now()
9373
+ };
9374
+ await this.store.update.mutate({
9375
+ collection: SESSIONS_COLLECTION,
9376
+ id,
9377
+ data: { ...updated }
9378
+ });
9379
+ }
9380
+ };
9381
+ //#endregion
9327
9382
  //#region src/builtins/local-auth/local-auth.addon.ts
9328
9383
  /** How often the auth tables are swept for dead rows. */
9329
9384
  var AUTH_REAP_INTERVAL_MS = 24 * 36e5;
@@ -9595,14 +9650,22 @@ var LocalAuthAddon = class extends BaseAddon {
9595
9650
  return { valid: await this.totpManager.verify(input.userId, input.code) };
9596
9651
  }
9597
9652
  };
9598
- this.ctx.logger.info("registered auth-provider + user-management capabilities");
9599
- return [{
9600
- capability: authProviderCapability,
9601
- provider: authProvider
9602
- }, {
9603
- capability: userManagementCapability,
9604
- provider: userMgmt
9605
- }];
9653
+ const homeAssistantOauth = { getDescriptor: async () => HOME_ASSISTANT_OAUTH_INTEGRATION };
9654
+ this.ctx.logger.info("registered auth-provider + user-management + oauth-integration capabilities");
9655
+ return [
9656
+ {
9657
+ capability: authProviderCapability,
9658
+ provider: authProvider
9659
+ },
9660
+ {
9661
+ capability: userManagementCapability,
9662
+ provider: userMgmt
9663
+ },
9664
+ {
9665
+ capability: oauthIntegrationCapability,
9666
+ provider: homeAssistantOauth
9667
+ }
9668
+ ];
9606
9669
  }
9607
9670
  /**
9608
9671
  * Drop auth rows that are dead but were never removed: scoped tokens past
@@ -9639,4 +9702,4 @@ var LocalAuthAddon = class extends BaseAddon {
9639
9702
  }
9640
9703
  };
9641
9704
  //#endregion
9642
- export { LocalAuthAddon, LocalAuthAddon as default, require_ms as a, AuthManager as i, ApiKeyManager as n, require_safe_buffer as o, UserManager as r, ScopedTokenManager as t };
9705
+ export { LocalAuthAddon, LocalAuthAddon as default, require_safe_buffer as a, require_ms as i, ScopedTokenManager as n, ApiKeyManager as o, AuthManager as r, HOME_ASSISTANT_OAUTH_INTEGRATION as s, UserManager as t };