@camstack/system 1.2.61 → 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.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-package.d.ts +9 -27
- package/dist/builtins/core-blocks/block-supervisor.d.ts +15 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +91 -24
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +91 -25
- package/dist/builtins/device-manager/device-aggregation.d.ts +49 -2
- package/dist/builtins/device-manager/device-bindings-store.d.ts +28 -5
- package/dist/builtins/device-manager/device-linked-devices.d.ts +2 -2
- package/dist/builtins/device-manager/device-manager.addon.d.ts +3 -45
- package/dist/builtins/device-manager/device-manager.addon.js +182 -888
- package/dist/builtins/device-manager/device-manager.addon.mjs +182 -888
- package/dist/builtins/device-manager/device-meta-actions.d.ts +0 -8
- package/dist/builtins/device-manager/device-meta-store.d.ts +1 -28
- package/dist/builtins/device-manager/device-meta-types.d.ts +2 -6
- package/dist/builtins/device-manager/device-projection.d.ts +1 -2
- package/dist/builtins/device-manager/device-provider-context.d.ts +2 -13
- package/dist/builtins/device-manager/device-queries.d.ts +1 -1
- package/dist/builtins/device-manager/device-state-mirror.d.ts +8 -49
- package/dist/builtins/device-manager/device-sync-resolvers.d.ts +0 -11
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/homeassistant-integration.d.ts +7 -0
- package/dist/builtins/local-auth/index.d.ts +1 -0
- package/dist/builtins/local-auth/index.js +1 -0
- package/dist/builtins/local-auth/index.mjs +2 -2
- package/dist/builtins/local-auth/local-auth.addon.js +561 -497
- package/dist/builtins/local-auth/local-auth.addon.mjs +580 -517
- package/dist/builtins/local-auth/oauth-grants.d.ts +11 -0
- package/dist/builtins/local-auth/oauth-session-manager.d.ts +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +319 -20
- package/dist/builtins/snapshot/index.mjs +319 -20
- package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +86 -0
- package/dist/builtins/snapshot/snapshot-media-handler.d.ts +48 -1
- package/dist/builtins/snapshot/snapshot.addon.d.ts +65 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +15 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +45 -5
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +45 -5
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-L6QPJWsr.mjs → dist-BJ2w3uoN.mjs} +1307 -1313
- package/dist/{dist-BN681PgO.js → dist-LDrOFrd-.js} +1255 -1273
- package/dist/index.d.ts +1 -1
- package/dist/index.js +209 -5
- package/dist/index.mjs +206 -7
- package/dist/kernel/addon-residue.d.ts +49 -0
- package/dist/kernel/index.d.ts +2 -0
- package/dist/kernel/moleculer/device-cap-proxy.d.ts +2 -2
- package/dist/{manifest-python-deps-CX5tGRqf.mjs → manifest-python-deps-CopL1_5C.mjs} +20 -14
- package/dist/{manifest-python-deps-d4acYXVV.js → manifest-python-deps-DNfmBu20.js} +20 -14
- package/package.json +5 -1
- package/dist/builtins/device-manager/device-link-cycle.d.ts +0 -18
- package/dist/builtins/device-manager/device-link-overlay.d.ts +0 -33
- package/dist/builtins/device-manager/device-link-resolver.d.ts +0 -44
- package/dist/main-CVLq8IzA.js +0 -2356
- 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 {
|
|
3
|
-
import * as 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$
|
|
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$
|
|
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$
|
|
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$
|
|
422
|
+
var timingSafeEqual = "timingSafeEqual" in crypto$1 ? function timingSafeEqual(a, b) {
|
|
295
423
|
if (a.byteLength !== b.byteLength) return false;
|
|
296
|
-
return crypto$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
333
|
-
saltLength: crypto$
|
|
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$
|
|
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$
|
|
347
|
-
saltLength: crypto$
|
|
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$
|
|
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$
|
|
6093
|
+
const token = crypto$2.randomBytes(32).toString("hex");
|
|
5966
6094
|
return {
|
|
5967
6095
|
token,
|
|
5968
|
-
hash: crypto$
|
|
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$
|
|
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/
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
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
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
this.
|
|
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(
|
|
6145
|
-
|
|
6146
|
-
const
|
|
6147
|
-
const
|
|
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$
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
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:
|
|
6273
|
+
collection: TOKENS_COLLECTION,
|
|
6161
6274
|
record: {
|
|
6162
6275
|
id: record.id,
|
|
6163
6276
|
data: { ...record }
|
|
6164
6277
|
}
|
|
6165
6278
|
});
|
|
6166
|
-
return
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
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
|
|
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:
|
|
6179
|
-
filter: { where: {
|
|
6288
|
+
collection: TOKENS_COLLECTION,
|
|
6289
|
+
filter: { where: { tokenHash } }
|
|
6180
6290
|
});
|
|
6181
6291
|
if (results.length === 0) return null;
|
|
6182
|
-
|
|
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
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
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
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6315
|
+
async revoke(tokenId) {
|
|
6316
|
+
await this.store.delete.mutate({
|
|
6317
|
+
collection: TOKENS_COLLECTION,
|
|
6318
|
+
key: tokenId
|
|
6193
6319
|
});
|
|
6194
6320
|
}
|
|
6195
|
-
async
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
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
|
|
6209
|
-
await this.store.
|
|
6210
|
-
collection:
|
|
6211
|
-
|
|
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
|
-
|
|
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:
|
|
6220
|
-
id,
|
|
6335
|
+
collection: TOKENS_COLLECTION,
|
|
6336
|
+
id: tokenId,
|
|
6221
6337
|
data: {
|
|
6222
6338
|
...existing,
|
|
6223
|
-
|
|
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
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
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
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
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
|
|
8947
|
-
const
|
|
8948
|
-
|
|
8949
|
-
|
|
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
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
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,
|
|
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 };
|