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