@camstack/addon-waste-collections 0.1.36 → 0.1.38
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/CHANGELOG.md +14 -0
- package/dist/waste-collections.addon.js +581 -845
- package/dist/waste-collections.addon.mjs +581 -845
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
//#region ../types/dist/event-category-
|
|
5
|
+
//#region ../types/dist/event-category-78tcBemo.mjs
|
|
6
6
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
7
7
|
EventCategory["SystemBoot"] = "system.boot";
|
|
8
8
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -123,6 +123,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
123
123
|
*/
|
|
124
124
|
EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
|
|
125
125
|
/**
|
|
126
|
+
* An addon is RUNNING although its installed package is below what the
|
|
127
|
+
* running server requires (`camstack.requires.addons` of the server, or the
|
|
128
|
+
* addon's own `requires.server`). Raised once per streak by the unsupported-
|
|
129
|
+
* addon tracker, on the hub and on agents; AlertCenter turns it into one
|
|
130
|
+
* operator alert per node and package. The addon is never held back for it.
|
|
131
|
+
*
|
|
132
|
+
* Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
|
|
133
|
+
*/
|
|
134
|
+
EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
|
|
135
|
+
/**
|
|
136
|
+
* The package of an {@link AddonUnsupportedVersion} alert reached a supported
|
|
137
|
+
* version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
|
|
138
|
+
*
|
|
139
|
+
* Payload: `{ packageName, addonId, nodeId }`.
|
|
140
|
+
*/
|
|
141
|
+
EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
|
|
142
|
+
/**
|
|
126
143
|
* Monitor scheduled the next retry for a failed addon. Transient —
|
|
127
144
|
* surfaced to the UI for live-updating the "next retry in Ns"
|
|
128
145
|
* countdown on the Addons page row, NOT persisted as an alert.
|
|
@@ -233,7 +250,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
233
250
|
EventCategory["ProcessRestarted"] = "process.restarted";
|
|
234
251
|
/**
|
|
235
252
|
* The SET of storage locations changed — one was created, edited, enabled,
|
|
236
|
-
* disabled or deleted through
|
|
253
|
+
* disabled or deleted through the location instances or the forced-delete action.
|
|
237
254
|
*
|
|
238
255
|
* Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
|
|
239
256
|
* it must also converge on its own periodic path, because a dropped event
|
|
@@ -5984,7 +6001,7 @@ var ZodIssueCode = {
|
|
|
5984
6001
|
var ZodFirstPartyTypeKind;
|
|
5985
6002
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5986
6003
|
//#endregion
|
|
5987
|
-
//#region ../types/dist/sleep-
|
|
6004
|
+
//#region ../types/dist/sleep-CQP-REJp.mjs
|
|
5988
6005
|
/**
|
|
5989
6006
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5990
6007
|
* window to float samples (D455).
|
|
@@ -6079,16 +6096,6 @@ var AddonMigrationsBlockedError = class extends Error {
|
|
|
6079
6096
|
this.migrationIds = migrationIds;
|
|
6080
6097
|
}
|
|
6081
6098
|
};
|
|
6082
|
-
/**
|
|
6083
|
-
* The value a redacted secret is replaced with on every read surface.
|
|
6084
|
-
*
|
|
6085
|
-
* It is also the WRITE-BACK token: an `upsertLocation` that sends this value
|
|
6086
|
-
* back for a key means "keep what is stored", which is what lets an operator
|
|
6087
|
-
* rename a location without retyping its password. A literal an operator could
|
|
6088
|
-
* plausibly choose as a real password would turn that convenience into a way
|
|
6089
|
-
* to lock yourself out, hence the sentinel shape.
|
|
6090
|
-
*/
|
|
6091
|
-
var REDACTED_SECRET = "__camstack_redacted__";
|
|
6092
6099
|
/** Is this field's stored value a credential? */
|
|
6093
6100
|
function isSecretConfigField(field) {
|
|
6094
6101
|
if (field.type === "password") return true;
|
|
@@ -6358,6 +6365,59 @@ Object.fromEntries([
|
|
|
6358
6365
|
function isValuelessField(field) {
|
|
6359
6366
|
return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button" || field.type === "account-sign-in" || field.type === "secret-presence";
|
|
6360
6367
|
}
|
|
6368
|
+
/**
|
|
6369
|
+
* Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
|
|
6370
|
+
* `key → { value, default }`, recursing through `group` and `sub-tabs`
|
|
6371
|
+
* layout fields. Keeping `value` and `default` SEPARATE (rather than
|
|
6372
|
+
* pre-resolving) lets callers both read the effective value AND detect
|
|
6373
|
+
* overrides (`value` differs from `default`). Structural over `unknown`;
|
|
6374
|
+
* malformed input yields an empty map, never throws.
|
|
6375
|
+
*/
|
|
6376
|
+
function collectHydratedFieldEntries(schema) {
|
|
6377
|
+
const out = /* @__PURE__ */ new Map();
|
|
6378
|
+
if (typeof schema !== "object" || schema === null) return out;
|
|
6379
|
+
const sections = schema.sections;
|
|
6380
|
+
if (!Array.isArray(sections)) return out;
|
|
6381
|
+
for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
|
|
6382
|
+
return out;
|
|
6383
|
+
}
|
|
6384
|
+
/** Recursive field walker for {@link collectHydratedFieldEntries}. */
|
|
6385
|
+
function collectFieldEntriesInto(fields, into) {
|
|
6386
|
+
if (!Array.isArray(fields)) return;
|
|
6387
|
+
for (const field of fields) {
|
|
6388
|
+
if (typeof field !== "object" || field === null) continue;
|
|
6389
|
+
const type = field.type;
|
|
6390
|
+
if (type === "group") {
|
|
6391
|
+
collectFieldEntriesInto(field.fields, into);
|
|
6392
|
+
continue;
|
|
6393
|
+
}
|
|
6394
|
+
if (type === "sub-tabs") {
|
|
6395
|
+
const tabs = field.tabs;
|
|
6396
|
+
if (Array.isArray(tabs)) {
|
|
6397
|
+
for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
|
|
6398
|
+
}
|
|
6399
|
+
continue;
|
|
6400
|
+
}
|
|
6401
|
+
const key = field.key;
|
|
6402
|
+
if (typeof key !== "string" || key.length === 0) continue;
|
|
6403
|
+
if (!("value" in field)) continue;
|
|
6404
|
+
into.set(key, {
|
|
6405
|
+
value: field.value,
|
|
6406
|
+
default: field.default
|
|
6407
|
+
});
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
/**
|
|
6411
|
+
* Flatten a hydrated settings payload into `key → EFFECTIVE value`
|
|
6412
|
+
* (`value ?? default`). The read-side inverse of `hydrateSchema` for the
|
|
6413
|
+
* common "just give me the resolved values" case. Built on
|
|
6414
|
+
* {@link collectHydratedFieldEntries}.
|
|
6415
|
+
*/
|
|
6416
|
+
function collectHydratedFieldValues(schema) {
|
|
6417
|
+
const out = /* @__PURE__ */ new Map();
|
|
6418
|
+
for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
|
|
6419
|
+
return out;
|
|
6420
|
+
}
|
|
6361
6421
|
function hydrateSchema(schema, values, options = {}) {
|
|
6362
6422
|
return {
|
|
6363
6423
|
...schema.tabs ? { tabs: [...schema.tabs] } : {},
|
|
@@ -7399,17 +7459,35 @@ var RecordIntegrationInstances = class {
|
|
|
7399
7459
|
...kind !== void 0 ? { kind } : {}
|
|
7400
7460
|
});
|
|
7401
7461
|
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
7462
|
+
const initial = this.hooks.initialBody(name, kind);
|
|
7463
|
+
const patch = this.patchOf(values ?? {});
|
|
7464
|
+
if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
|
|
7465
|
+
kind,
|
|
7466
|
+
body: {
|
|
7467
|
+
...initial,
|
|
7468
|
+
...patch
|
|
7469
|
+
}
|
|
7470
|
+
}));
|
|
7402
7471
|
await this.handle.put({
|
|
7403
7472
|
id: minted,
|
|
7404
|
-
data:
|
|
7473
|
+
data: initial
|
|
7405
7474
|
}, { ifRev: 0 });
|
|
7406
|
-
const patch = this.patchOf(values ?? {});
|
|
7407
7475
|
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
7408
7476
|
await this.hooks.onChanged?.(minted, "created");
|
|
7409
7477
|
return { id: minted };
|
|
7410
7478
|
};
|
|
7411
7479
|
updateInstance = async ({ id, name, values }) => {
|
|
7412
7480
|
const patch = this.patchOf(values);
|
|
7481
|
+
if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
|
|
7482
|
+
const stored = await this.handle.get(id);
|
|
7483
|
+
if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
|
|
7484
|
+
kind: void 0,
|
|
7485
|
+
body: {
|
|
7486
|
+
...stored.data,
|
|
7487
|
+
...patch
|
|
7488
|
+
}
|
|
7489
|
+
}));
|
|
7490
|
+
}
|
|
7413
7491
|
if (name !== void 0) patch[this.nameField] = name;
|
|
7414
7492
|
await this.handle.patch(id, patch);
|
|
7415
7493
|
await this.hooks.onChanged?.(id, "updated");
|
|
@@ -7720,6 +7798,21 @@ var SCOPED_SETTINGS_SCOPES = [
|
|
|
7720
7798
|
"device-defaults",
|
|
7721
7799
|
"node"
|
|
7722
7800
|
];
|
|
7801
|
+
/** The one refusal of a key a scope did not declare. */
|
|
7802
|
+
function assertDeclaredKeys(keys, declared) {
|
|
7803
|
+
const undeclared = keys.filter((key) => !declared.has(key));
|
|
7804
|
+
if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
|
|
7805
|
+
}
|
|
7806
|
+
/** Build the handle over an addon's scoped read, write and unset. */
|
|
7807
|
+
function createScopedSettings(deps) {
|
|
7808
|
+
return {
|
|
7809
|
+
async get() {
|
|
7810
|
+
return deps.schema.parse(await deps.read());
|
|
7811
|
+
},
|
|
7812
|
+
patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
|
|
7813
|
+
unset: (keys) => deps.unset(keys)
|
|
7814
|
+
};
|
|
7815
|
+
}
|
|
7723
7816
|
/** A write to a secret path could not be stored in the keychain, so it was refused. */
|
|
7724
7817
|
var KeychainWriteRefusedError = class extends Error {
|
|
7725
7818
|
fieldPath;
|
|
@@ -8295,7 +8388,16 @@ function createHandle(raw, opts) {
|
|
|
8295
8388
|
return blob;
|
|
8296
8389
|
}
|
|
8297
8390
|
},
|
|
8298
|
-
writeAddonStore: writeAddon
|
|
8391
|
+
writeAddonStore: writeAddon,
|
|
8392
|
+
removeClearedKeys: (keys) => serialize("addon", async () => {
|
|
8393
|
+
const stored = await readStoredAddonBlob(raw);
|
|
8394
|
+
index.seen("addon", stored);
|
|
8395
|
+
const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
|
|
8396
|
+
if (removable.length === 0) return;
|
|
8397
|
+
const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
|
|
8398
|
+
await raw.writeAddonStore(patch);
|
|
8399
|
+
index.wrote("addon", patch);
|
|
8400
|
+
})
|
|
8299
8401
|
};
|
|
8300
8402
|
}
|
|
8301
8403
|
/** The context the addon sees, plus the private raw view and write outcome. */
|
|
@@ -8840,6 +8942,151 @@ var BaseAddon = class {
|
|
|
8840
8942
|
}
|
|
8841
8943
|
}
|
|
8842
8944
|
/**
|
|
8945
|
+
* Hook: values layered over the stored ones when the FORM of one scope is served
|
|
8946
|
+
* ({@link getScopedSettings}) — a preview that derives dependent options without writing.
|
|
8947
|
+
* Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
|
|
8948
|
+
*/
|
|
8949
|
+
async scopedSettingsOverlay(_scope, _cap, _nodeId) {
|
|
8950
|
+
return null;
|
|
8951
|
+
}
|
|
8952
|
+
/**
|
|
8953
|
+
* Hook: a write of one scope changed stored values (not called when the store reports no change,
|
|
8954
|
+
* e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
|
|
8955
|
+
*/
|
|
8956
|
+
async onScopedSettingsChanged(_scope, _cap, _keys) {}
|
|
8957
|
+
/** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
|
|
8958
|
+
clusterSettings(cap, schema) {
|
|
8959
|
+
return this.scopedHandle("cluster", cap, schema);
|
|
8960
|
+
}
|
|
8961
|
+
/** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
|
|
8962
|
+
deviceDefaults(cap, schema) {
|
|
8963
|
+
return this.scopedHandle("device-defaults", cap, schema);
|
|
8964
|
+
}
|
|
8965
|
+
/**
|
|
8966
|
+
* Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
|
|
8967
|
+
* with no row yields the schema defaults — never a bare-key fallback.
|
|
8968
|
+
*/
|
|
8969
|
+
nodeSettings(cap, schema, nodeId) {
|
|
8970
|
+
return this.scopedHandle("node", cap, schema, nodeId);
|
|
8971
|
+
}
|
|
8972
|
+
/**
|
|
8973
|
+
* {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
|
|
8974
|
+
* settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
|
|
8975
|
+
* central delete. `nodeId` omitted ⇒ the local node.
|
|
8976
|
+
*/
|
|
8977
|
+
nodeState(key, schema, fallback, nodeId) {
|
|
8978
|
+
this._declaredStateKeys.add(key);
|
|
8979
|
+
return createDurableState({
|
|
8980
|
+
key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
|
|
8981
|
+
schema,
|
|
8982
|
+
fallback,
|
|
8983
|
+
read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
|
|
8984
|
+
write: async (patch) => {
|
|
8985
|
+
await this._ctx?.settings?.writeAddonStore(patch);
|
|
8986
|
+
},
|
|
8987
|
+
onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
|
|
8988
|
+
addonId: this._ctx?.id,
|
|
8989
|
+
key: k,
|
|
8990
|
+
error: String(e)
|
|
8991
|
+
} })
|
|
8992
|
+
});
|
|
8993
|
+
}
|
|
8994
|
+
/** The loaded schema `cap` declares for `scope`, or `null`. */
|
|
8995
|
+
async declaredScopedSchema(scope, cap) {
|
|
8996
|
+
await this.loadScopedSchemas(cap);
|
|
8997
|
+
return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
|
|
8998
|
+
}
|
|
8999
|
+
async requireScopedSchema(scope, cap) {
|
|
9000
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9001
|
+
if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
|
|
9002
|
+
return schema;
|
|
9003
|
+
}
|
|
9004
|
+
scopedKeys(schema) {
|
|
9005
|
+
return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
|
|
9006
|
+
}
|
|
9007
|
+
/** `store` narrowed to the keys `schema` declares. */
|
|
9008
|
+
narrowToScope(store, schema) {
|
|
9009
|
+
const keys = this.scopedKeys(schema);
|
|
9010
|
+
return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
|
|
9011
|
+
}
|
|
9012
|
+
scopedHandle(scope, cap, schema, nodeId) {
|
|
9013
|
+
return createScopedSettings({
|
|
9014
|
+
schema,
|
|
9015
|
+
read: async () => {
|
|
9016
|
+
const declared = await this.requireScopedSchema(scope, cap);
|
|
9017
|
+
const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
|
|
9018
|
+
const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
|
|
9019
|
+
return {
|
|
9020
|
+
...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
|
|
9021
|
+
...stored
|
|
9022
|
+
};
|
|
9023
|
+
},
|
|
9024
|
+
write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
|
|
9025
|
+
unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
|
|
9026
|
+
});
|
|
9027
|
+
}
|
|
9028
|
+
/** The one write path of the scoped API: declared keys only, then the global patch. */
|
|
9029
|
+
async writeScoped(scope, cap, patch, nodeId) {
|
|
9030
|
+
assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
9031
|
+
if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
|
|
9032
|
+
}
|
|
9033
|
+
/**
|
|
9034
|
+
* Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
|
|
9035
|
+
* value is released exactly as there — and the emptied row is then removed; every other key is
|
|
9036
|
+
* removed by an `undefined` patch (which a secret transform would read as "keep").
|
|
9037
|
+
*/
|
|
9038
|
+
async unsetScoped(scope, cap, keys, nodeId) {
|
|
9039
|
+
assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
9040
|
+
const target = scope === "node" ? nodeId : void 0;
|
|
9041
|
+
const tree = this.addonSecretTree();
|
|
9042
|
+
const secretKeys = keys.filter((key) => tree.has(key));
|
|
9043
|
+
const plainKeys = keys.filter((key) => !tree.has(key));
|
|
9044
|
+
let changed = false;
|
|
9045
|
+
if (plainKeys.length > 0) {
|
|
9046
|
+
const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
|
|
9047
|
+
changed = await this.applyGlobalPatch(patch, target, cap) || changed;
|
|
9048
|
+
}
|
|
9049
|
+
if (secretKeys.length > 0) {
|
|
9050
|
+
await this.clearSecretKeys(secretKeys, target, cap);
|
|
9051
|
+
const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
|
|
9052
|
+
const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
|
|
9053
|
+
if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
|
|
9054
|
+
else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
|
|
9055
|
+
changed = true;
|
|
9056
|
+
}
|
|
9057
|
+
if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
|
|
9058
|
+
}
|
|
9059
|
+
/**
|
|
9060
|
+
* The hydrated form of one scope of one capability's settings (any
|
|
9061
|
+
* {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
|
|
9062
|
+
*/
|
|
9063
|
+
async getScopedSettings(scope, cap, nodeId) {
|
|
9064
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9065
|
+
if (schema === null) return null;
|
|
9066
|
+
const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
|
|
9067
|
+
const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
|
|
9068
|
+
return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
|
|
9069
|
+
}
|
|
9070
|
+
/** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
|
|
9071
|
+
async updateScopedSettings(scope, cap, patch, nodeId) {
|
|
9072
|
+
this._operatorWriteDepth++;
|
|
9073
|
+
try {
|
|
9074
|
+
await this.writeScoped(scope, cap, patch, nodeId);
|
|
9075
|
+
} finally {
|
|
9076
|
+
this._operatorWriteDepth--;
|
|
9077
|
+
}
|
|
9078
|
+
}
|
|
9079
|
+
/**
|
|
9080
|
+
* The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
|
|
9081
|
+
* its value. `null` when the capability declares nothing for the scope.
|
|
9082
|
+
*/
|
|
9083
|
+
async readScopedValues(scope, cap, nodeId) {
|
|
9084
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9085
|
+
if (schema === null) return null;
|
|
9086
|
+
const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
|
|
9087
|
+
return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
|
|
9088
|
+
}
|
|
9089
|
+
/**
|
|
8843
9090
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
8844
9091
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
8845
9092
|
*/
|
|
@@ -8991,6 +9238,9 @@ var BaseAddon = class {
|
|
|
8991
9238
|
* a keychain (an older kernel) it writes `''`, as such code always did.
|
|
8992
9239
|
*/
|
|
8993
9240
|
async clearSecrets(keys, nodeId) {
|
|
9241
|
+
await this.clearSecretKeys(keys, nodeId);
|
|
9242
|
+
}
|
|
9243
|
+
async clearSecretKeys(keys, nodeId, cap) {
|
|
8994
9244
|
const tree = this.addonSecretTree();
|
|
8995
9245
|
const keychain = this._ctx?.secrets !== void 0;
|
|
8996
9246
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
@@ -9003,25 +9253,27 @@ var BaseAddon = class {
|
|
|
9003
9253
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
9004
9254
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
9005
9255
|
}
|
|
9006
|
-
await this.applyGlobalPatch(patch, nodeId);
|
|
9256
|
+
await this.applyGlobalPatch(patch, nodeId, cap);
|
|
9007
9257
|
}
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9258
|
+
/** Resolves `true` when the write changed stored values. */
|
|
9259
|
+
async applyGlobalPatch(barePatch, nodeId, cap) {
|
|
9260
|
+
await this.loadScopedSchemas(cap);
|
|
9261
|
+
const keys = this.perNodeKeys(cap);
|
|
9011
9262
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
9012
9263
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
9013
9264
|
const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
|
|
9014
9265
|
const handle = this._secretSettings;
|
|
9015
9266
|
if (handle !== null) {
|
|
9016
9267
|
const { changed } = await handle.writeAddonStore(scoped);
|
|
9017
|
-
if (!changed) return;
|
|
9268
|
+
if (!changed) return false;
|
|
9018
9269
|
} else await this._ctx?.settings?.writeAddonStore(scoped);
|
|
9019
|
-
if (target !== localNode) return;
|
|
9270
|
+
if (target !== localNode) return true;
|
|
9020
9271
|
await this.resolveConfig();
|
|
9021
9272
|
await this.onConfigChanged();
|
|
9022
9273
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
9023
9274
|
const legacy = this.globalSettingsSchema();
|
|
9024
9275
|
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
9276
|
+
return true;
|
|
9025
9277
|
}
|
|
9026
9278
|
/**
|
|
9027
9279
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -10773,7 +11025,6 @@ var CAP_NAMES = Object.freeze([
|
|
|
10773
11025
|
"notifier",
|
|
10774
11026
|
"numeric-sensor",
|
|
10775
11027
|
"oauth-integration",
|
|
10776
|
-
"oidc-connections",
|
|
10777
11028
|
"osd",
|
|
10778
11029
|
"osd-manager",
|
|
10779
11030
|
"pet-feeder",
|
|
@@ -12665,8 +12916,7 @@ Object.freeze({
|
|
|
12665
12916
|
"device-provider": { "reloadDevice": 18e4 },
|
|
12666
12917
|
"llm": {
|
|
12667
12918
|
"generate": 6e5,
|
|
12668
|
-
"generateVision": 6e5
|
|
12669
|
-
"testProfile": 6e5
|
|
12919
|
+
"generateVision": 6e5
|
|
12670
12920
|
},
|
|
12671
12921
|
"llm-runtime": {
|
|
12672
12922
|
"complete": 6e5,
|
|
@@ -14069,17 +14319,18 @@ var IntegrationInstanceRefSchema = object({
|
|
|
14069
14319
|
* message to what the kind supports — callers never special-case a service.
|
|
14070
14320
|
*
|
|
14071
14321
|
* DESIGN DECISIONS (locked):
|
|
14072
|
-
* -
|
|
14073
|
-
*
|
|
14074
|
-
*
|
|
14075
|
-
*
|
|
14076
|
-
*
|
|
14077
|
-
*
|
|
14078
|
-
*
|
|
14079
|
-
*
|
|
14080
|
-
*
|
|
14081
|
-
* routable. `send` / `testTarget` /
|
|
14082
|
-
* `
|
|
14322
|
+
* - A target is an INSTANCE of this function cap (epic #440, step 6): the
|
|
14323
|
+
* providers serve their targets as `integration-instances` records, one
|
|
14324
|
+
* record kind per target kind, and the admin lists, adds, edits, switches
|
|
14325
|
+
* and deletes them through that cap. This cap keeps what an instance cannot
|
|
14326
|
+
* say: the kind catalogue (`listTargetKinds`: capabilities, delivery
|
|
14327
|
+
* parameters, brand icon), dispatch (`send`, `testTarget`) and the form
|
|
14328
|
+
* helpers (`discoverTargets`, `probeTargetConfig`).
|
|
14329
|
+
* - `listTargetKinds` / `discoverTargets` return arrays → the generated
|
|
14330
|
+
* cap-mount auto-`concatCollection`-fans them across every registered
|
|
14331
|
+
* provider so one catalog is routable. `send` / `testTarget` /
|
|
14332
|
+
* `probeTargetConfig` route to ONE provider by the `addonId` the generated
|
|
14333
|
+
* collection router extracts from the call input.
|
|
14083
14334
|
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
14084
14335
|
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
14085
14336
|
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
@@ -14324,7 +14575,7 @@ var TargetKindCapsSchema = object({
|
|
|
14324
14575
|
* the union is large and not meant for runtime validation here; the exported
|
|
14325
14576
|
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
14326
14577
|
*/
|
|
14327
|
-
var ConfigSchemaPassthrough$
|
|
14578
|
+
var ConfigSchemaPassthrough$3 = unknown();
|
|
14328
14579
|
var TargetKindSchema = object({
|
|
14329
14580
|
kind: string(),
|
|
14330
14581
|
label: string(),
|
|
@@ -14361,7 +14612,7 @@ var TargetKindSchema = object({
|
|
|
14361
14612
|
* its raster path, which is the safe default for an unknown image.
|
|
14362
14613
|
*/
|
|
14363
14614
|
iconMediaType: string().optional(),
|
|
14364
|
-
configSchema: ConfigSchemaPassthrough$
|
|
14615
|
+
configSchema: ConfigSchemaPassthrough$3,
|
|
14365
14616
|
/**
|
|
14366
14617
|
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
14367
14618
|
* base URL, instance, chat, recipient). A save that changes any of them
|
|
@@ -14383,9 +14634,11 @@ var TargetKindSchema = object({
|
|
|
14383
14634
|
caps: TargetKindCapsSchema
|
|
14384
14635
|
});
|
|
14385
14636
|
/**
|
|
14386
|
-
* A
|
|
14387
|
-
*
|
|
14388
|
-
*
|
|
14637
|
+
* A target as a provider's own code sees it (the notification-center bridge, the
|
|
14638
|
+
* send path): the flat record body split into the name, the switch and the
|
|
14639
|
+
* kind's fields. `config` holds secrets; a reader that serves it to a client
|
|
14640
|
+
* REDACTS secret fields (a presence marker only) — never round-trip a stored
|
|
14641
|
+
* secret to the UI.
|
|
14389
14642
|
*/
|
|
14390
14643
|
var TargetSchema = object({
|
|
14391
14644
|
id: string(),
|
|
@@ -14428,13 +14681,13 @@ var ProbeTargetConfigResultSchema = object({
|
|
|
14428
14681
|
stateInstance: string().optional(),
|
|
14429
14682
|
error: string().optional()
|
|
14430
14683
|
});
|
|
14431
|
-
method(object({}), array(TargetKindSchema)), method(object({
|
|
14684
|
+
method(object({}), array(TargetKindSchema)), method(object({
|
|
14432
14685
|
kind: string(),
|
|
14433
14686
|
config: record(string(), unknown()).optional(),
|
|
14434
14687
|
/**
|
|
14435
14688
|
* When editing / duplicating, the blank password field never round-trips
|
|
14436
14689
|
* the stored token. Pass the source target id so the provider can restore
|
|
14437
|
-
* secrets server-side before dialling
|
|
14690
|
+
* secrets server-side before dialling.
|
|
14438
14691
|
*/
|
|
14439
14692
|
targetId: string().optional()
|
|
14440
14693
|
}), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
|
|
@@ -14457,27 +14710,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
|
|
|
14457
14710
|
}), TestResultSchema, {
|
|
14458
14711
|
kind: "mutation",
|
|
14459
14712
|
auth: "admin"
|
|
14460
|
-
}), method(object({
|
|
14461
|
-
target: TargetSchema,
|
|
14462
|
-
/**
|
|
14463
|
-
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
14464
|
-
* existing row. The UI never holds the stored token; blank / redacted
|
|
14465
|
-
* password fields are restored from this source (D634 still applies if
|
|
14466
|
-
* destinations diverge without a re-typed secret).
|
|
14467
|
-
*/
|
|
14468
|
-
cloneSecretsFrom: string().optional()
|
|
14469
|
-
}), TargetSchema, {
|
|
14470
|
-
kind: "mutation",
|
|
14471
|
-
auth: "admin"
|
|
14472
|
-
}), method(object({ targetId: string() }), _void(), {
|
|
14473
|
-
kind: "mutation",
|
|
14474
|
-
auth: "admin"
|
|
14475
|
-
}), method(object({
|
|
14476
|
-
targetId: string(),
|
|
14477
|
-
enabled: boolean()
|
|
14478
|
-
}), _void(), {
|
|
14479
|
-
kind: "mutation",
|
|
14480
|
-
auth: "admin"
|
|
14481
14713
|
});
|
|
14482
14714
|
looseObject({
|
|
14483
14715
|
name: string().min(1),
|
|
@@ -22735,7 +22967,6 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
22735
22967
|
"notification-output": system("notification delivery — reached through notifier devices and rules"),
|
|
22736
22968
|
"notification-rules": system("notification rules — authored on their own page"),
|
|
22737
22969
|
"oauth-integration": system(IDENTITY),
|
|
22738
|
-
"oidc-connections": system(IDENTITY),
|
|
22739
22970
|
"osd-manager": system(PIPELINE),
|
|
22740
22971
|
"pipeline-executor": system(PIPELINE),
|
|
22741
22972
|
"pipeline-orchestrator": system(PIPELINE),
|
|
@@ -44877,6 +45108,8 @@ var SettingsSchemaWithValuesSchema = object({
|
|
|
44877
45108
|
});
|
|
44878
45109
|
/** Patch object — keys are field names, values are the new field values. */
|
|
44879
45110
|
var SettingsPatchSchema = record(string(), unknown());
|
|
45111
|
+
/** The scope of a scoped-settings call (D986): where the capability declared the values. */
|
|
45112
|
+
var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
|
|
44880
45113
|
/** Standard success response for update operations. */
|
|
44881
45114
|
var SettingsUpdateResultSchema = object({ success: literal(true) });
|
|
44882
45115
|
method(object({
|
|
@@ -44892,6 +45125,25 @@ method(object({
|
|
|
44892
45125
|
kind: "mutation",
|
|
44893
45126
|
auth: "admin"
|
|
44894
45127
|
}), method(object({
|
|
45128
|
+
addonId: string(),
|
|
45129
|
+
cap: string(),
|
|
45130
|
+
scope: ScopedSettingsScopeSchema,
|
|
45131
|
+
nodeId: string().optional()
|
|
45132
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
45133
|
+
addonId: string(),
|
|
45134
|
+
cap: string(),
|
|
45135
|
+
scope: ScopedSettingsScopeSchema,
|
|
45136
|
+
nodeId: string().optional(),
|
|
45137
|
+
patch: SettingsPatchSchema
|
|
45138
|
+
}), SettingsUpdateResultSchema, {
|
|
45139
|
+
kind: "mutation",
|
|
45140
|
+
auth: "admin"
|
|
45141
|
+
}), method(object({
|
|
45142
|
+
addonId: string(),
|
|
45143
|
+
cap: string(),
|
|
45144
|
+
scope: ScopedSettingsScopeSchema,
|
|
45145
|
+
nodeId: string().optional()
|
|
45146
|
+
}), record(string(), unknown()).nullable()), method(object({
|
|
44895
45147
|
addonId: string(),
|
|
44896
45148
|
nodeId: string().optional()
|
|
44897
45149
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
@@ -54423,7 +54675,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54423
54675
|
* or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
|
|
54424
54676
|
* clears it.
|
|
54425
54677
|
*/
|
|
54426
|
-
var ConfigSchemaPassthrough$
|
|
54678
|
+
var ConfigSchemaPassthrough$2 = custom();
|
|
54427
54679
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54428
54680
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54429
54681
|
/**
|
|
@@ -54469,7 +54721,16 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54469
54721
|
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
54470
54722
|
connectPath: string().min(1).optional(),
|
|
54471
54723
|
/** Relative hub path to disconnect that account. */
|
|
54472
|
-
disconnectPath: string().min(1).optional()
|
|
54724
|
+
disconnectPath: string().min(1).optional(),
|
|
54725
|
+
/**
|
|
54726
|
+
* Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
|
|
54727
|
+
* GENERIC widget id the client maps to a component; a client that knows none of them shows none.
|
|
54728
|
+
* `connection` widgets write connection values, `model` widgets own the model choice.
|
|
54729
|
+
*/
|
|
54730
|
+
widgets: array(object({
|
|
54731
|
+
id: string().min(1),
|
|
54732
|
+
placement: _enum(["connection", "model"])
|
|
54733
|
+
})).optional()
|
|
54473
54734
|
});
|
|
54474
54735
|
/**
|
|
54475
54736
|
* How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
|
|
@@ -54574,7 +54835,7 @@ var integrationInstancesCapability = {
|
|
|
54574
54835
|
id: IntegrationInstanceIdSchema.nullable(),
|
|
54575
54836
|
/** The kind of the create form (`id: null`) of a provider with several. */
|
|
54576
54837
|
kind: string().min(1).optional()
|
|
54577
|
-
}), ConfigSchemaPassthrough$
|
|
54838
|
+
}), ConfigSchemaPassthrough$2, { auth: "admin" }),
|
|
54578
54839
|
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54579
54840
|
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54580
54841
|
/**
|
|
@@ -55525,12 +55786,12 @@ var LlmModelInfoSchema = object({
|
|
|
55525
55786
|
});
|
|
55526
55787
|
/**
|
|
55527
55788
|
* The provider-specific part of a profile, validated by the provider that owns
|
|
55528
|
-
* the kind (
|
|
55789
|
+
* the kind (saving an instance refuses what it does not accept). A field its
|
|
55529
55790
|
* `connectionSchema` declares `type: 'password'` is a secret: it is held in the
|
|
55530
55791
|
* keychain, never in the row, and reads back as the redaction marker.
|
|
55531
55792
|
*/
|
|
55532
55793
|
var LlmProviderConfigSchema = record(string(), unknown());
|
|
55533
|
-
|
|
55794
|
+
object({
|
|
55534
55795
|
id: string(),
|
|
55535
55796
|
name: string(),
|
|
55536
55797
|
/** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
|
|
@@ -55624,8 +55885,8 @@ var LlmProviderWidgetSchema = object({
|
|
|
55624
55885
|
/** ConfigUISchema tree passed through untyped on the wire (the
|
|
55625
55886
|
* notification-output `ConfigSchemaPassthrough` precedent at
|
|
55626
55887
|
* notification-output.cap.ts:151); the exported TS type re-tightens it. */
|
|
55627
|
-
var ConfigSchemaPassthrough$
|
|
55628
|
-
|
|
55888
|
+
var ConfigSchemaPassthrough$1 = unknown();
|
|
55889
|
+
object({
|
|
55629
55890
|
kind: LlmProfileKindSchema,
|
|
55630
55891
|
label: string(),
|
|
55631
55892
|
/** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
|
|
@@ -55633,9 +55894,9 @@ var LlmProfileKindDescriptorSchema = object({
|
|
|
55633
55894
|
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
55634
55895
|
addonId: string(),
|
|
55635
55896
|
/** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
|
|
55636
|
-
connectionSchema: ConfigSchemaPassthrough$
|
|
55897
|
+
connectionSchema: ConfigSchemaPassthrough$1,
|
|
55637
55898
|
/** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
|
|
55638
|
-
advancedSchema: ConfigSchemaPassthrough$
|
|
55899
|
+
advancedSchema: ConfigSchemaPassthrough$1.optional(),
|
|
55639
55900
|
/** The kind's defaults; a model from `listModels` may override any of them. */
|
|
55640
55901
|
features: LlmModelFeaturesSchema,
|
|
55641
55902
|
/** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
|
|
@@ -55764,20 +56025,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
|
|
|
55764
56025
|
}), method(object({
|
|
55765
56026
|
addonId: string().optional(),
|
|
55766
56027
|
requestId: string()
|
|
55767
|
-
}), _void(), { kind: "mutation" }), method(
|
|
55768
|
-
kind: "mutation",
|
|
55769
|
-
auth: "admin"
|
|
55770
|
-
}), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
55771
|
-
kind: "mutation",
|
|
55772
|
-
auth: "admin"
|
|
55773
|
-
}), method(ProfileRefInputSchema, _void(), {
|
|
55774
|
-
kind: "mutation",
|
|
55775
|
-
auth: "admin"
|
|
55776
|
-
}), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
|
|
55777
|
-
kind: "mutation",
|
|
55778
|
-
auth: "admin",
|
|
55779
|
-
timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
|
|
55780
|
-
}), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
|
|
56028
|
+
}), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
|
|
55781
56029
|
kind: "mutation",
|
|
55782
56030
|
auth: "admin"
|
|
55783
56031
|
}), method(object({}), array(LlmDefaultSchema)), method(object({
|
|
@@ -56772,20 +57020,7 @@ method(_void(), MeshStatusSchema), method(object({
|
|
|
56772
57020
|
/** Optional hostname override the host should advertise once joined. */
|
|
56773
57021
|
hostname: string().optional() }), object({
|
|
56774
57022
|
/** Authentication URL the operator should open in a browser. */
|
|
56775
|
-
loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() }))
|
|
56776
|
-
/** Optional auth key — when provided, probes the key validity
|
|
56777
|
-
* against the provider's API. Omit when already joined to
|
|
56778
|
-
* just ping the daemon. */
|
|
56779
|
-
authKey: string().optional() }), object({
|
|
56780
|
-
ok: boolean(),
|
|
56781
|
-
/** Provider-side identifier resolved by the probe (tailnet
|
|
56782
|
-
* name for Tailscale, network id for ZeroTier, etc.). */
|
|
56783
|
-
tenant: string().optional(),
|
|
56784
|
-
/** Daemon binary version, when reachable. */
|
|
56785
|
-
daemonVersion: string().optional(),
|
|
56786
|
-
/** Human-readable error when `ok: false`. */
|
|
56787
|
-
error: string().optional()
|
|
56788
|
-
}), { kind: "mutation" });
|
|
57023
|
+
loginUrl: string() }), { kind: "mutation" }), method(_void(), object({ left: literal(true) }), { kind: "mutation" }), method(_void(), object({ loggedOut: literal(true) }), { kind: "mutation" }), method(_void(), object({ peers: array(MeshPeerSchema).readonly() }));
|
|
56789
57024
|
object({
|
|
56790
57025
|
value: number().nullable().meta(readingMeta("device", { label: "Total" })),
|
|
56791
57026
|
lastFetchedAt: number()
|
|
@@ -66443,61 +66678,6 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
66443
66678
|
});
|
|
66444
66679
|
method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
|
|
66445
66680
|
/**
|
|
66446
|
-
* `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
|
|
66447
|
-
*
|
|
66448
|
-
* One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
|
|
66449
|
-
* ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
|
|
66450
|
-
* Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
|
|
66451
|
-
* immutable after create.
|
|
66452
|
-
*/
|
|
66453
|
-
var ConfigSchemaPassthrough$1 = unknown();
|
|
66454
|
-
var OidcConnectionSchema = object({
|
|
66455
|
-
id: string().min(1),
|
|
66456
|
-
displayName: string(),
|
|
66457
|
-
icon: string(),
|
|
66458
|
-
issuerUrl: string(),
|
|
66459
|
-
clientId: string(),
|
|
66460
|
-
/** Client id + secret present — never the secret value. */
|
|
66461
|
-
configured: boolean(),
|
|
66462
|
-
/**
|
|
66463
|
-
* Values for the connection form schema. `clientSecret` is a keychain
|
|
66464
|
-
* {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
|
|
66465
|
-
*/
|
|
66466
|
-
formValues: record(string(), unknown()).optional()
|
|
66467
|
-
});
|
|
66468
|
-
/**
|
|
66469
|
-
* Patch one connection. Omitted or empty `clientSecret` keeps the stored
|
|
66470
|
-
* secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
|
|
66471
|
-
*/
|
|
66472
|
-
var OidcConnectionUpdateInputSchema = object({
|
|
66473
|
-
id: string().min(1),
|
|
66474
|
-
displayName: string().min(1),
|
|
66475
|
-
icon: string().optional(),
|
|
66476
|
-
issuerUrl: string().optional(),
|
|
66477
|
-
clientId: string().optional(),
|
|
66478
|
-
clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
|
|
66479
|
-
redirectUri: string().optional(),
|
|
66480
|
-
scopes: string().optional(),
|
|
66481
|
-
usernameClaim: _enum([
|
|
66482
|
-
"preferred_username",
|
|
66483
|
-
"email",
|
|
66484
|
-
"sub"
|
|
66485
|
-
]).optional()
|
|
66486
|
-
});
|
|
66487
|
-
method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
|
|
66488
|
-
id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
|
|
66489
|
-
displayName: string().min(1)
|
|
66490
|
-
}), object({ id: string() }), {
|
|
66491
|
-
kind: "mutation",
|
|
66492
|
-
auth: "admin"
|
|
66493
|
-
}), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
|
|
66494
|
-
kind: "mutation",
|
|
66495
|
-
auth: "admin"
|
|
66496
|
-
}), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
|
|
66497
|
-
kind: "mutation",
|
|
66498
|
-
auth: "admin"
|
|
66499
|
-
});
|
|
66500
|
-
/**
|
|
66501
66681
|
* Generic on-screen-display (video overlay) cap. Each camera exposes
|
|
66502
66682
|
* a firmware-defined list of overlays (channel name, timestamp,
|
|
66503
66683
|
* watermark, logo, …); the cap models them as a homogeneous array of
|
|
@@ -72670,21 +72850,6 @@ method(object({
|
|
|
72670
72850
|
*/
|
|
72671
72851
|
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
72672
72852
|
}), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
|
|
72673
|
-
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
72674
|
-
providerId: string().min(1),
|
|
72675
|
-
displayName: string().min(1),
|
|
72676
|
-
configSchema: unknown(),
|
|
72677
|
-
nodeLocal: boolean(),
|
|
72678
|
-
shouldSaveDiskSpace: literal(true),
|
|
72679
|
-
minFreePercent: number().min(0).max(100)
|
|
72680
|
-
}), object({
|
|
72681
|
-
providerId: string().min(1),
|
|
72682
|
-
displayName: string().min(1),
|
|
72683
|
-
configSchema: unknown(),
|
|
72684
|
-
nodeLocal: boolean(),
|
|
72685
|
-
shouldSaveDiskSpace: literal(false),
|
|
72686
|
-
minFreePercent: literal(null)
|
|
72687
|
-
})]);
|
|
72688
72853
|
method(object({
|
|
72689
72854
|
location: StorageLocationRefSchema,
|
|
72690
72855
|
relativePath: string()
|
|
@@ -72746,42 +72911,7 @@ method(object({
|
|
|
72746
72911
|
}), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
|
|
72747
72912
|
kind: "mutation",
|
|
72748
72913
|
auth: "admin"
|
|
72749
|
-
}), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(
|
|
72750
|
-
createdAt: true,
|
|
72751
|
-
updatedAt: true
|
|
72752
|
-
}), StorageLocationSchema, {
|
|
72753
|
-
kind: "mutation",
|
|
72754
|
-
auth: "admin"
|
|
72755
|
-
}), method(object({
|
|
72756
|
-
id: string(),
|
|
72757
|
-
force: boolean().optional()
|
|
72758
|
-
}), _void(), {
|
|
72759
|
-
kind: "mutation",
|
|
72760
|
-
auth: "admin"
|
|
72761
|
-
}), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
|
|
72762
|
-
ok: boolean(),
|
|
72763
|
-
error: string().optional()
|
|
72764
|
-
}), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
|
|
72765
|
-
providerId: string(),
|
|
72766
|
-
/**
|
|
72767
|
-
* The location this config is an UNSAVED edit of, when there is one.
|
|
72768
|
-
*
|
|
72769
|
-
* `listLocations` replaces every declared secret with the redaction
|
|
72770
|
-
* sentinel, so the edit modal's form state holds the sentinel for any
|
|
72771
|
-
* credential the operator did not retype — and posting that here
|
|
72772
|
-
* without a way to resolve it makes the provider try to authenticate
|
|
72773
|
-
* as `__camstack_redacted__` and report the operator's own working
|
|
72774
|
-
* password as wrong. Given this id, the orchestrator restores each
|
|
72775
|
-
* sentinel from the stored config (same rule as `upsertLocation`)
|
|
72776
|
-
* before dispatching. Omitted by the "Add location" wizard, where
|
|
72777
|
-
* every value was typed just now and nothing is stored yet.
|
|
72778
|
-
*/
|
|
72779
|
-
locationId: string().optional(),
|
|
72780
|
-
config: record(string(), unknown())
|
|
72781
|
-
}), object({
|
|
72782
|
-
ok: boolean(),
|
|
72783
|
-
error: string().optional()
|
|
72784
|
-
}), { auth: "admin" });
|
|
72914
|
+
}), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
|
|
72785
72915
|
/** One table of the settings database. */
|
|
72786
72916
|
var StorageTableSchema = object({
|
|
72787
72917
|
/** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
|
|
@@ -76902,6 +77032,20 @@ Object.freeze({
|
|
|
76902
77032
|
access: "view",
|
|
76903
77033
|
role: "admin"
|
|
76904
77034
|
},
|
|
77035
|
+
"addonSettings.getScopedSettings": {
|
|
77036
|
+
capName: "addon-settings",
|
|
77037
|
+
capScope: "system",
|
|
77038
|
+
addonId: null,
|
|
77039
|
+
access: "view",
|
|
77040
|
+
role: "admin"
|
|
77041
|
+
},
|
|
77042
|
+
"addonSettings.readScopedValues": {
|
|
77043
|
+
capName: "addon-settings",
|
|
77044
|
+
capScope: "system",
|
|
77045
|
+
addonId: null,
|
|
77046
|
+
access: "view",
|
|
77047
|
+
role: "admin"
|
|
77048
|
+
},
|
|
76905
77049
|
"addonSettings.unsetStoredSettingsKeys": {
|
|
76906
77050
|
capName: "addon-settings",
|
|
76907
77051
|
capScope: "system",
|
|
@@ -76923,6 +77067,13 @@ Object.freeze({
|
|
|
76923
77067
|
access: "create",
|
|
76924
77068
|
role: "admin"
|
|
76925
77069
|
},
|
|
77070
|
+
"addonSettings.updateScopedSettings": {
|
|
77071
|
+
capName: "addon-settings",
|
|
77072
|
+
capScope: "system",
|
|
77073
|
+
addonId: null,
|
|
77074
|
+
access: "create",
|
|
77075
|
+
role: "admin"
|
|
77076
|
+
},
|
|
76926
77077
|
"addonWidgets.listWidgets": {
|
|
76927
77078
|
capName: "addon-widgets",
|
|
76928
77079
|
capScope: "system",
|
|
@@ -80374,13 +80525,6 @@ Object.freeze({
|
|
|
80374
80525
|
access: "delete",
|
|
80375
80526
|
role: "admin"
|
|
80376
80527
|
},
|
|
80377
|
-
"llm.deleteProfile": {
|
|
80378
|
-
capName: "llm",
|
|
80379
|
-
capScope: "system",
|
|
80380
|
-
addonId: null,
|
|
80381
|
-
access: "delete",
|
|
80382
|
-
role: "admin"
|
|
80383
|
-
},
|
|
80384
80528
|
"llm.generate": {
|
|
80385
80529
|
capName: "llm",
|
|
80386
80530
|
capScope: "system",
|
|
@@ -80444,20 +80588,6 @@ Object.freeze({
|
|
|
80444
80588
|
access: "view",
|
|
80445
80589
|
role: "admin"
|
|
80446
80590
|
},
|
|
80447
|
-
"llm.listProfileKinds": {
|
|
80448
|
-
capName: "llm",
|
|
80449
|
-
capScope: "system",
|
|
80450
|
-
addonId: null,
|
|
80451
|
-
access: "view",
|
|
80452
|
-
role: "admin"
|
|
80453
|
-
},
|
|
80454
|
-
"llm.listProfiles": {
|
|
80455
|
-
capName: "llm",
|
|
80456
|
-
capScope: "system",
|
|
80457
|
-
addonId: null,
|
|
80458
|
-
access: "view",
|
|
80459
|
-
role: "admin"
|
|
80460
|
-
},
|
|
80461
80591
|
"llm.listRuntimeNodes": {
|
|
80462
80592
|
capName: "llm",
|
|
80463
80593
|
capScope: "system",
|
|
@@ -80465,13 +80595,6 @@ Object.freeze({
|
|
|
80465
80595
|
access: "view",
|
|
80466
80596
|
role: "admin"
|
|
80467
80597
|
},
|
|
80468
|
-
"llm.migrateRawSecrets": {
|
|
80469
|
-
capName: "llm",
|
|
80470
|
-
capScope: "system",
|
|
80471
|
-
addonId: null,
|
|
80472
|
-
access: "create",
|
|
80473
|
-
role: "admin"
|
|
80474
|
-
},
|
|
80475
80598
|
"llm.resolveModelRef": {
|
|
80476
80599
|
capName: "llm",
|
|
80477
80600
|
capScope: "system",
|
|
@@ -80500,20 +80623,6 @@ Object.freeze({
|
|
|
80500
80623
|
access: "create",
|
|
80501
80624
|
role: "admin"
|
|
80502
80625
|
},
|
|
80503
|
-
"llm.testProfile": {
|
|
80504
|
-
capName: "llm",
|
|
80505
|
-
capScope: "system",
|
|
80506
|
-
addonId: null,
|
|
80507
|
-
access: "create",
|
|
80508
|
-
role: "admin"
|
|
80509
|
-
},
|
|
80510
|
-
"llm.upsertProfile": {
|
|
80511
|
-
capName: "llm",
|
|
80512
|
-
capScope: "system",
|
|
80513
|
-
addonId: null,
|
|
80514
|
-
access: "create",
|
|
80515
|
-
role: "admin"
|
|
80516
|
-
},
|
|
80517
80626
|
"llmRuntime.appleAvailability": {
|
|
80518
80627
|
capName: "llm-runtime",
|
|
80519
80628
|
capScope: "system",
|
|
@@ -81018,13 +81127,6 @@ Object.freeze({
|
|
|
81018
81127
|
access: "create",
|
|
81019
81128
|
role: "admin"
|
|
81020
81129
|
},
|
|
81021
|
-
"meshNetwork.testConnection": {
|
|
81022
|
-
capName: "mesh-network",
|
|
81023
|
-
capScope: "system",
|
|
81024
|
-
addonId: null,
|
|
81025
|
-
access: "create",
|
|
81026
|
-
role: "admin"
|
|
81027
|
-
},
|
|
81028
81130
|
"meter.getStatus": {
|
|
81029
81131
|
capName: "meter",
|
|
81030
81132
|
capScope: "device",
|
|
@@ -81683,13 +81785,6 @@ Object.freeze({
|
|
|
81683
81785
|
access: "create",
|
|
81684
81786
|
role: "admin"
|
|
81685
81787
|
},
|
|
81686
|
-
"notificationOutput.deleteTarget": {
|
|
81687
|
-
capName: "notification-output",
|
|
81688
|
-
capScope: "system",
|
|
81689
|
-
addonId: null,
|
|
81690
|
-
access: "delete",
|
|
81691
|
-
role: "admin"
|
|
81692
|
-
},
|
|
81693
81788
|
"notificationOutput.discoverTargets": {
|
|
81694
81789
|
capName: "notification-output",
|
|
81695
81790
|
capScope: "system",
|
|
@@ -81697,13 +81792,6 @@ Object.freeze({
|
|
|
81697
81792
|
access: "view",
|
|
81698
81793
|
role: "admin"
|
|
81699
81794
|
},
|
|
81700
|
-
"notificationOutput.getTarget": {
|
|
81701
|
-
capName: "notification-output",
|
|
81702
|
-
capScope: "system",
|
|
81703
|
-
addonId: null,
|
|
81704
|
-
access: "view",
|
|
81705
|
-
role: "admin"
|
|
81706
|
-
},
|
|
81707
81795
|
"notificationOutput.listTargetKinds": {
|
|
81708
81796
|
capName: "notification-output",
|
|
81709
81797
|
capScope: "system",
|
|
@@ -81711,13 +81799,6 @@ Object.freeze({
|
|
|
81711
81799
|
access: "view",
|
|
81712
81800
|
role: "admin"
|
|
81713
81801
|
},
|
|
81714
|
-
"notificationOutput.listTargets": {
|
|
81715
|
-
capName: "notification-output",
|
|
81716
|
-
capScope: "system",
|
|
81717
|
-
addonId: null,
|
|
81718
|
-
access: "view",
|
|
81719
|
-
role: "admin"
|
|
81720
|
-
},
|
|
81721
81802
|
"notificationOutput.probeTargetConfig": {
|
|
81722
81803
|
capName: "notification-output",
|
|
81723
81804
|
capScope: "system",
|
|
@@ -81732,13 +81813,6 @@ Object.freeze({
|
|
|
81732
81813
|
access: "create",
|
|
81733
81814
|
role: "admin"
|
|
81734
81815
|
},
|
|
81735
|
-
"notificationOutput.setTargetEnabled": {
|
|
81736
|
-
capName: "notification-output",
|
|
81737
|
-
capScope: "system",
|
|
81738
|
-
addonId: null,
|
|
81739
|
-
access: "create",
|
|
81740
|
-
role: "admin"
|
|
81741
|
-
},
|
|
81742
81816
|
"notificationOutput.testTarget": {
|
|
81743
81817
|
capName: "notification-output",
|
|
81744
81818
|
capScope: "system",
|
|
@@ -81746,13 +81820,6 @@ Object.freeze({
|
|
|
81746
81820
|
access: "create",
|
|
81747
81821
|
role: "admin"
|
|
81748
81822
|
},
|
|
81749
|
-
"notificationOutput.upsertTarget": {
|
|
81750
|
-
capName: "notification-output",
|
|
81751
|
-
capScope: "system",
|
|
81752
|
-
addonId: null,
|
|
81753
|
-
access: "create",
|
|
81754
|
-
role: "admin"
|
|
81755
|
-
},
|
|
81756
81823
|
"notificationRules.activateAlarmProfile": {
|
|
81757
81824
|
capName: "notification-rules",
|
|
81758
81825
|
capScope: "system",
|
|
@@ -82012,41 +82079,6 @@ Object.freeze({
|
|
|
82012
82079
|
access: "view",
|
|
82013
82080
|
role: "admin"
|
|
82014
82081
|
},
|
|
82015
|
-
"oidcConnections.connectionFormSchema": {
|
|
82016
|
-
capName: "oidc-connections",
|
|
82017
|
-
capScope: "system",
|
|
82018
|
-
addonId: null,
|
|
82019
|
-
access: "view",
|
|
82020
|
-
role: "admin"
|
|
82021
|
-
},
|
|
82022
|
-
"oidcConnections.createConnection": {
|
|
82023
|
-
capName: "oidc-connections",
|
|
82024
|
-
capScope: "system",
|
|
82025
|
-
addonId: null,
|
|
82026
|
-
access: "create",
|
|
82027
|
-
role: "admin"
|
|
82028
|
-
},
|
|
82029
|
-
"oidcConnections.deleteConnection": {
|
|
82030
|
-
capName: "oidc-connections",
|
|
82031
|
-
capScope: "system",
|
|
82032
|
-
addonId: null,
|
|
82033
|
-
access: "delete",
|
|
82034
|
-
role: "admin"
|
|
82035
|
-
},
|
|
82036
|
-
"oidcConnections.listConnections": {
|
|
82037
|
-
capName: "oidc-connections",
|
|
82038
|
-
capScope: "system",
|
|
82039
|
-
addonId: null,
|
|
82040
|
-
access: "view",
|
|
82041
|
-
role: "admin"
|
|
82042
|
-
},
|
|
82043
|
-
"oidcConnections.updateConnection": {
|
|
82044
|
-
capName: "oidc-connections",
|
|
82045
|
-
capScope: "system",
|
|
82046
|
-
addonId: null,
|
|
82047
|
-
access: "create",
|
|
82048
|
-
role: "admin"
|
|
82049
|
-
},
|
|
82050
82082
|
"osd.getStatus": {
|
|
82051
82083
|
capName: "osd",
|
|
82052
82084
|
capScope: "device",
|
|
@@ -85078,13 +85110,6 @@ Object.freeze({
|
|
|
85078
85110
|
access: "delete",
|
|
85079
85111
|
role: "admin"
|
|
85080
85112
|
},
|
|
85081
|
-
"storage.deleteLocation": {
|
|
85082
|
-
capName: "storage",
|
|
85083
|
-
capScope: "system",
|
|
85084
|
-
addonId: null,
|
|
85085
|
-
access: "delete",
|
|
85086
|
-
role: "admin"
|
|
85087
|
-
},
|
|
85088
85113
|
"storage.endDownload": {
|
|
85089
85114
|
capName: "storage",
|
|
85090
85115
|
capScope: "system",
|
|
@@ -85141,13 +85166,6 @@ Object.freeze({
|
|
|
85141
85166
|
access: "view",
|
|
85142
85167
|
role: "admin"
|
|
85143
85168
|
},
|
|
85144
|
-
"storage.listProviders": {
|
|
85145
|
-
capName: "storage",
|
|
85146
|
-
capScope: "system",
|
|
85147
|
-
addonId: null,
|
|
85148
|
-
access: "view",
|
|
85149
|
-
role: "admin"
|
|
85150
|
-
},
|
|
85151
85169
|
"storage.read": {
|
|
85152
85170
|
capName: "storage",
|
|
85153
85171
|
capScope: "system",
|
|
@@ -85169,27 +85187,6 @@ Object.freeze({
|
|
|
85169
85187
|
access: "view",
|
|
85170
85188
|
role: "admin"
|
|
85171
85189
|
},
|
|
85172
|
-
"storage.testConfig": {
|
|
85173
|
-
capName: "storage",
|
|
85174
|
-
capScope: "system",
|
|
85175
|
-
addonId: null,
|
|
85176
|
-
access: "view",
|
|
85177
|
-
role: "admin"
|
|
85178
|
-
},
|
|
85179
|
-
"storage.testLocation": {
|
|
85180
|
-
capName: "storage",
|
|
85181
|
-
capScope: "system",
|
|
85182
|
-
addonId: null,
|
|
85183
|
-
access: "view",
|
|
85184
|
-
role: "admin"
|
|
85185
|
-
},
|
|
85186
|
-
"storage.upsertLocation": {
|
|
85187
|
-
capName: "storage",
|
|
85188
|
-
capScope: "system",
|
|
85189
|
-
addonId: null,
|
|
85190
|
-
access: "create",
|
|
85191
|
-
role: "admin"
|
|
85192
|
-
},
|
|
85193
85190
|
"storage.write": {
|
|
85194
85191
|
capName: "storage",
|
|
85195
85192
|
capScope: "system",
|
|
@@ -91342,20 +91339,6 @@ object({
|
|
|
91342
91339
|
});
|
|
91343
91340
|
//#endregion
|
|
91344
91341
|
//#region src/custom-actions.ts
|
|
91345
|
-
var SourceIdSchema = string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug");
|
|
91346
|
-
/**
|
|
91347
|
-
* A source as the admin list shows it. No `args`: a credential never leaves the
|
|
91348
|
-
* addon - `formValues` carries the redaction token where one is stored (D880).
|
|
91349
|
-
*/
|
|
91350
|
-
var WasteSourceRowSchema = object({
|
|
91351
|
-
id: string(),
|
|
91352
|
-
label: string(),
|
|
91353
|
-
module: string(),
|
|
91354
|
-
enabled: boolean(),
|
|
91355
|
-
formValues: record(string(), unknown()).optional(),
|
|
91356
|
-
serviceId: string().optional(),
|
|
91357
|
-
countryId: string().optional()
|
|
91358
|
-
});
|
|
91359
91342
|
var wasteCollectionsActions = defineCustomActions({
|
|
91360
91343
|
fetchNow: customAction(object({
|
|
91361
91344
|
/** When set, refresh only this configured source id; otherwise all enabled. */
|
|
@@ -91370,10 +91353,6 @@ sourceId: string().min(1).optional() }).optional(), object({
|
|
|
91370
91353
|
kind: "mutation",
|
|
91371
91354
|
auth: "admin"
|
|
91372
91355
|
}),
|
|
91373
|
-
listSources: customAction(object({}), array(WasteSourceRowSchema).readonly(), {
|
|
91374
|
-
kind: "mutation",
|
|
91375
|
-
auth: "admin"
|
|
91376
|
-
}),
|
|
91377
91356
|
listCountries: customAction(object({}), array(object({
|
|
91378
91357
|
id: string(),
|
|
91379
91358
|
label: string()
|
|
@@ -91391,43 +91370,6 @@ sourceId: string().min(1).optional() }).optional(), object({
|
|
|
91391
91370
|
})).readonly(), {
|
|
91392
91371
|
kind: "mutation",
|
|
91393
91372
|
auth: "admin"
|
|
91394
|
-
}),
|
|
91395
|
-
serviceFormSchema: customAction(object({ serviceId: string().min(1) }), unknown(), {
|
|
91396
|
-
kind: "mutation",
|
|
91397
|
-
auth: "admin"
|
|
91398
|
-
}),
|
|
91399
|
-
createSource: customAction(object({
|
|
91400
|
-
/** Optional override; when omitted the id is derived from `label`. */
|
|
91401
|
-
id: SourceIdSchema.optional(),
|
|
91402
|
-
label: string().min(1),
|
|
91403
|
-
serviceId: string().min(1),
|
|
91404
|
-
args: record(string(), unknown()).default({}),
|
|
91405
|
-
enabled: boolean().optional()
|
|
91406
|
-
}), object({ id: string() }), {
|
|
91407
|
-
kind: "mutation",
|
|
91408
|
-
auth: "admin"
|
|
91409
|
-
}),
|
|
91410
|
-
updateSource: customAction(object({
|
|
91411
|
-
id: string().min(1),
|
|
91412
|
-
label: string().min(1),
|
|
91413
|
-
serviceId: string().min(1).optional(),
|
|
91414
|
-
args: record(string(), unknown()).optional(),
|
|
91415
|
-
enabled: boolean().optional()
|
|
91416
|
-
}), object({ ok: literal(true) }), {
|
|
91417
|
-
kind: "mutation",
|
|
91418
|
-
auth: "admin"
|
|
91419
|
-
}),
|
|
91420
|
-
/**
|
|
91421
|
-
* D880: rewrite every legacy source (free `argsJson`) as typed `args`, moving its
|
|
91422
|
-
* credentials into the keychain. Dry run unless `apply`; idempotent; counts only.
|
|
91423
|
-
*/
|
|
91424
|
-
migrateSources: customAction(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
91425
|
-
kind: "mutation",
|
|
91426
|
-
auth: "admin"
|
|
91427
|
-
}),
|
|
91428
|
-
deleteSource: customAction(object({ id: string().min(1) }), object({ ok: literal(true) }), {
|
|
91429
|
-
kind: "mutation",
|
|
91430
|
-
auth: "admin"
|
|
91431
91373
|
})
|
|
91432
91374
|
});
|
|
91433
91375
|
//#endregion
|
|
@@ -91461,33 +91403,6 @@ var WASTE_SECRET_STATE_PATHS = SECRET_ARG_KEYS.map((key) => `sources[id].args.${
|
|
|
91461
91403
|
function isSecretArg(key) {
|
|
91462
91404
|
return SECRET_ARG_KEYS.includes(key.toLowerCase());
|
|
91463
91405
|
}
|
|
91464
|
-
/** Is a credential stored for this argument (a value, or a marker the keychain could not serve)? */
|
|
91465
|
-
function isSet(value) {
|
|
91466
|
-
return isSecretMarker(value) || typeof value === "string" && value.length > 0;
|
|
91467
|
-
}
|
|
91468
|
-
/** The arguments as a read surface may show them: a stored credential is the redaction token. */
|
|
91469
|
-
function redactArgs(args) {
|
|
91470
|
-
const out = {};
|
|
91471
|
-
for (const [key, value] of Object.entries(args)) out[key] = isSecretArg(key) && isSet(value) ? REDACTED_SECRET : value;
|
|
91472
|
-
return out;
|
|
91473
|
-
}
|
|
91474
|
-
/**
|
|
91475
|
-
* The write-back of {@link redactArgs}: a credential sent back as the redaction
|
|
91476
|
-
* token means "keep what is stored", so it takes the stored (resolved) value; one
|
|
91477
|
-
* with nothing stored is dropped, never saved as the token.
|
|
91478
|
-
*/
|
|
91479
|
-
function restoreRedactedArgs(incoming, stored) {
|
|
91480
|
-
const out = {};
|
|
91481
|
-
for (const [key, value] of Object.entries(incoming)) {
|
|
91482
|
-
if (isSecretArg(key) && value === "__camstack_redacted__") {
|
|
91483
|
-
const previous = stored[key];
|
|
91484
|
-
if (previous !== void 0) out[key] = previous;
|
|
91485
|
-
continue;
|
|
91486
|
-
}
|
|
91487
|
-
out[key] = value;
|
|
91488
|
-
}
|
|
91489
|
-
return out;
|
|
91490
|
-
}
|
|
91491
91406
|
/**
|
|
91492
91407
|
* Does this argument bag hold a credential that is NOT a usable value: a MARKER the keychain did
|
|
91493
91408
|
* not resolve, or the redaction token (a row read without its credential)? Either would reach a
|
|
@@ -91496,10 +91411,6 @@ function restoreRedactedArgs(incoming, stored) {
|
|
|
91496
91411
|
function hasUnresolvedCredential(args) {
|
|
91497
91412
|
return Object.entries(args).some(([key, value]) => isSecretArg(key) && (isSecretMarker(value) || value === "__camstack_redacted__"));
|
|
91498
91413
|
}
|
|
91499
|
-
/** Does it hold a non-empty credential (any form)? */
|
|
91500
|
-
function hasCredential(args) {
|
|
91501
|
-
return Object.entries(args).some(([key, value]) => isSecretArg(key) && isSet(value));
|
|
91502
|
-
}
|
|
91503
91414
|
//#endregion
|
|
91504
91415
|
//#region src/waste-types.ts
|
|
91505
91416
|
/**
|
|
@@ -91781,42 +91692,6 @@ async function copySourcesToRecords(ports) {
|
|
|
91781
91692
|
}
|
|
91782
91693
|
};
|
|
91783
91694
|
}
|
|
91784
|
-
/** The `waste-collections` catalogue (D987): offered to the engine when the addon initialises. */
|
|
91785
|
-
var ADDON_MIGRATIONS = [{
|
|
91786
|
-
meta: {
|
|
91787
|
-
id: "waste-collections/2026-11-06-sources-from-settings-and-argsjson",
|
|
91788
|
-
owner: "waste-collections",
|
|
91789
|
-
level: "addon-state",
|
|
91790
|
-
seq: 1,
|
|
91791
|
-
dependsOn: [],
|
|
91792
|
-
store: "settings",
|
|
91793
|
-
scope: "cluster",
|
|
91794
|
-
onFailure: "tolerate",
|
|
91795
|
-
bodyKind: "procedure",
|
|
91796
|
-
description: "Copy the addon-level sources setting (typed args, or a legacy argsJson string) into waste-source records, credentials in the keychain, each read back; an existing record is never overwritten",
|
|
91797
|
-
fixtureMark: "the credential of a legacy argsJson row, read back from its record"
|
|
91798
|
-
},
|
|
91799
|
-
procedure: copySourcesToRecords
|
|
91800
|
-
}];
|
|
91801
|
-
//#endregion
|
|
91802
|
-
//#region src/python-paths.ts
|
|
91803
|
-
/**
|
|
91804
|
-
* Locate the addon's bundled `python/` dir at runtime (deployed package
|
|
91805
|
-
* root or workspace package root). Same candidate walk as detection-pipeline.
|
|
91806
|
-
*/
|
|
91807
|
-
function resolvePythonDir(fromModuleUrl = import.meta.url) {
|
|
91808
|
-
const here = dirname(fileURLToPath(fromModuleUrl));
|
|
91809
|
-
const candidates = [
|
|
91810
|
-
join(here, "../python"),
|
|
91811
|
-
join(here, "../../python"),
|
|
91812
|
-
join(here, "python")
|
|
91813
|
-
];
|
|
91814
|
-
for (const c of candidates) if (existsSync(join(c, "fetch_source.py"))) return c;
|
|
91815
|
-
throw new Error(`addon-waste-collections: python/ dir not found. Searched:\n${candidates.join("\n")}`);
|
|
91816
|
-
}
|
|
91817
|
-
function resolveFetchScript(fromModuleUrl = import.meta.url) {
|
|
91818
|
-
return join(resolvePythonDir(fromModuleUrl), "fetch_source.py");
|
|
91819
|
-
}
|
|
91820
91695
|
//#endregion
|
|
91821
91696
|
//#region src/waste-source-record-kinds.ts
|
|
91822
91697
|
var WASTE_COLLECTIONS_OWNER = "waste-collections";
|
|
@@ -91862,6 +91737,202 @@ var plainWasteSourceKind = defineEntryKind("waste-source", {
|
|
|
91862
91737
|
writeTrigger: WRITE_TRIGGER
|
|
91863
91738
|
});
|
|
91864
91739
|
//#endregion
|
|
91740
|
+
//#region src/waste-source-store.ts
|
|
91741
|
+
/**
|
|
91742
|
+
* A stored body as a source. A row read while the keychain is locked still carries its markers
|
|
91743
|
+
* in `args`; the config schema keeps them, so the source lists and its fetch refuses by name.
|
|
91744
|
+
*/
|
|
91745
|
+
function rowToSource(id, data) {
|
|
91746
|
+
const parsed = WasteSourceConfigSchema.safeParse({
|
|
91747
|
+
id,
|
|
91748
|
+
label: data.name,
|
|
91749
|
+
module: data.module,
|
|
91750
|
+
args: data.args,
|
|
91751
|
+
enabled: data.enabled
|
|
91752
|
+
});
|
|
91753
|
+
return parsed.success ? parsed.data : null;
|
|
91754
|
+
}
|
|
91755
|
+
function bodyOf(source) {
|
|
91756
|
+
const { id, label, module, args, enabled } = source;
|
|
91757
|
+
const body = {
|
|
91758
|
+
name: label,
|
|
91759
|
+
module,
|
|
91760
|
+
args,
|
|
91761
|
+
enabled
|
|
91762
|
+
};
|
|
91763
|
+
const parsed = wasteSourceEntryKind.schema.safeParse(body);
|
|
91764
|
+
if (!parsed.success) throw new Error(`invalid waste source '${id}': ${parsed.error.message}`);
|
|
91765
|
+
return parsed.data;
|
|
91766
|
+
}
|
|
91767
|
+
/**
|
|
91768
|
+
* While the sources migration has not landed: every record, plus the legacy rows that have no
|
|
91769
|
+
* record yet (a read-only view of those). A legacy row leaves the view as soon as a record has
|
|
91770
|
+
* its id, so the two never diverge; writes go to the records.
|
|
91771
|
+
*/
|
|
91772
|
+
function withUnmigratedLegacyRows(records, legacy) {
|
|
91773
|
+
const have = new Set(records.map((r) => r.id));
|
|
91774
|
+
return [...records, ...legacy.filter((r) => !have.has(r.id))];
|
|
91775
|
+
}
|
|
91776
|
+
var WasteSourceStore = class WasteSourceStore {
|
|
91777
|
+
kind;
|
|
91778
|
+
constructor(kind) {
|
|
91779
|
+
this.kind = kind;
|
|
91780
|
+
}
|
|
91781
|
+
static of(records, kind = wasteSourceEntryKind) {
|
|
91782
|
+
return new WasteSourceStore(records.kind(kind));
|
|
91783
|
+
}
|
|
91784
|
+
/** The record ids, whatever their content. */
|
|
91785
|
+
async listIds() {
|
|
91786
|
+
return (await this.kind.query({ limit: 1e3 })).records.map((record) => record.id);
|
|
91787
|
+
}
|
|
91788
|
+
/** One source with its credentials resolved; markers stay where the keychain cannot serve. */
|
|
91789
|
+
async readResolved(id) {
|
|
91790
|
+
const stored = await this.kind.get(id);
|
|
91791
|
+
if (stored === null) return null;
|
|
91792
|
+
let data = stored.data;
|
|
91793
|
+
try {
|
|
91794
|
+
const resolved = await this.kind.getResolved(id);
|
|
91795
|
+
if (resolved !== null) data = resolved.data;
|
|
91796
|
+
} catch (err) {
|
|
91797
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
91798
|
+
}
|
|
91799
|
+
return rowToSource(id, data);
|
|
91800
|
+
}
|
|
91801
|
+
async listResolved() {
|
|
91802
|
+
const out = [];
|
|
91803
|
+
for (const id of await this.listIds()) {
|
|
91804
|
+
const row = await this.readResolved(id);
|
|
91805
|
+
if (row !== null) out.push(row);
|
|
91806
|
+
}
|
|
91807
|
+
return out;
|
|
91808
|
+
}
|
|
91809
|
+
/** Create the record when the id is free; an existing record is never touched. */
|
|
91810
|
+
async putIfAbsent(source) {
|
|
91811
|
+
if (await this.kind.get(source.id) !== null) return false;
|
|
91812
|
+
await this.kind.put({
|
|
91813
|
+
id: source.id,
|
|
91814
|
+
data: bodyOf(source)
|
|
91815
|
+
}, { ifRev: 0 });
|
|
91816
|
+
return true;
|
|
91817
|
+
}
|
|
91818
|
+
/**
|
|
91819
|
+
* Make the records equal `sources`. A source whose credential the keychain did not serve is
|
|
91820
|
+
* left exactly as stored: rewriting it would save a marker as the credential.
|
|
91821
|
+
*/
|
|
91822
|
+
async replaceAll(sources) {
|
|
91823
|
+
const nextIds = new Set(sources.map((s) => s.id));
|
|
91824
|
+
for (const id of await this.listIds()) if (!nextIds.has(id)) await this.kind.delete(id);
|
|
91825
|
+
for (const source of sources) {
|
|
91826
|
+
if (hasUnresolvedCredential(source.args)) continue;
|
|
91827
|
+
await this.kind.put({
|
|
91828
|
+
id: source.id,
|
|
91829
|
+
data: bodyOf(source)
|
|
91830
|
+
});
|
|
91831
|
+
}
|
|
91832
|
+
}
|
|
91833
|
+
};
|
|
91834
|
+
//#endregion
|
|
91835
|
+
//#region src/migrations/ports.ts
|
|
91836
|
+
var PAGE = 1e3;
|
|
91837
|
+
/**
|
|
91838
|
+
* The sources migration's ports over {@link MigrationApiV1}, and nothing else (planning#447): the
|
|
91839
|
+
* addon's own settings store, its keychain handle and its `waste-source` record kind.
|
|
91840
|
+
*/
|
|
91841
|
+
function wasteSourcePortsOver(api, kind) {
|
|
91842
|
+
const handle = api.records.kind(kind);
|
|
91843
|
+
const secrets = { resolve: async (refs) => {
|
|
91844
|
+
const { values } = await api.secrets.resolve({ refs: [...refs] });
|
|
91845
|
+
return Object.fromEntries(refs.map((ref) => {
|
|
91846
|
+
const value = values[ref];
|
|
91847
|
+
return [ref, value === null || value === void 0 ? { kind: "missing" } : {
|
|
91848
|
+
kind: "value",
|
|
91849
|
+
value,
|
|
91850
|
+
version: 0
|
|
91851
|
+
}];
|
|
91852
|
+
}));
|
|
91853
|
+
} };
|
|
91854
|
+
const readStored = async (key) => {
|
|
91855
|
+
let store;
|
|
91856
|
+
try {
|
|
91857
|
+
store = (await api.settings.read({})).store;
|
|
91858
|
+
} catch (err) {
|
|
91859
|
+
return {
|
|
91860
|
+
kind: "unreadable",
|
|
91861
|
+
reason: "store-read-failed",
|
|
91862
|
+
error: err instanceof Error ? err.message : String(err)
|
|
91863
|
+
};
|
|
91864
|
+
}
|
|
91865
|
+
if (!Object.hasOwn(store, key)) return { kind: "absent" };
|
|
91866
|
+
const { value } = await resolveSecretMarkers(store[key], secrets, { unresolved: "keep-marker" });
|
|
91867
|
+
return {
|
|
91868
|
+
kind: "present",
|
|
91869
|
+
value
|
|
91870
|
+
};
|
|
91871
|
+
};
|
|
91872
|
+
const asRow = (row) => rowToSource(row.id, row.data);
|
|
91873
|
+
return {
|
|
91874
|
+
readStored,
|
|
91875
|
+
listSourceIds: async () => (await handle.query({ limit: PAGE })).records.map((row) => row.id),
|
|
91876
|
+
putSource: async (source) => {
|
|
91877
|
+
if (await handle.get({ id: source.id }) !== null) return;
|
|
91878
|
+
await handle.put({
|
|
91879
|
+
id: source.id,
|
|
91880
|
+
data: bodyOf(source),
|
|
91881
|
+
ifRev: 0
|
|
91882
|
+
});
|
|
91883
|
+
},
|
|
91884
|
+
readSource: async (id) => {
|
|
91885
|
+
const stored = await handle.get({ id });
|
|
91886
|
+
if (stored === null) return null;
|
|
91887
|
+
try {
|
|
91888
|
+
return asRow(await handle.getResolved({ id }) ?? stored);
|
|
91889
|
+
} catch (err) {
|
|
91890
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
91891
|
+
return asRow(stored);
|
|
91892
|
+
}
|
|
91893
|
+
},
|
|
91894
|
+
warn: (message) => {
|
|
91895
|
+
api.log.warn({ message });
|
|
91896
|
+
}
|
|
91897
|
+
};
|
|
91898
|
+
}
|
|
91899
|
+
/** The `waste-collections` catalogue (D987): offered to the engine when the addon initialises. */
|
|
91900
|
+
var ADDON_MIGRATIONS = [{
|
|
91901
|
+
meta: {
|
|
91902
|
+
id: "waste-collections/2026-11-06-sources-from-settings-and-argsjson",
|
|
91903
|
+
owner: "waste-collections",
|
|
91904
|
+
level: "addon-state",
|
|
91905
|
+
seq: 1,
|
|
91906
|
+
dependsOn: [],
|
|
91907
|
+
store: "settings",
|
|
91908
|
+
scope: "cluster",
|
|
91909
|
+
onFailure: "tolerate",
|
|
91910
|
+
bodyKind: "procedure",
|
|
91911
|
+
description: "Copy the addon-level sources setting (typed args, or a legacy argsJson string) into waste-source records, credentials in the keychain, each read back; an existing record is never overwritten",
|
|
91912
|
+
fixtureMark: "the credential of a legacy argsJson row, read back from its record"
|
|
91913
|
+
},
|
|
91914
|
+
procedure: copySourcesToRecords
|
|
91915
|
+
}];
|
|
91916
|
+
//#endregion
|
|
91917
|
+
//#region src/python-paths.ts
|
|
91918
|
+
/**
|
|
91919
|
+
* Locate the addon's bundled `python/` dir at runtime (deployed package
|
|
91920
|
+
* root or workspace package root). Same candidate walk as detection-pipeline.
|
|
91921
|
+
*/
|
|
91922
|
+
function resolvePythonDir(fromModuleUrl = import.meta.url) {
|
|
91923
|
+
const here = dirname(fileURLToPath(fromModuleUrl));
|
|
91924
|
+
const candidates = [
|
|
91925
|
+
join(here, "../python"),
|
|
91926
|
+
join(here, "../../python"),
|
|
91927
|
+
join(here, "python")
|
|
91928
|
+
];
|
|
91929
|
+
for (const c of candidates) if (existsSync(join(c, "fetch_source.py"))) return c;
|
|
91930
|
+
throw new Error(`addon-waste-collections: python/ dir not found. Searched:\n${candidates.join("\n")}`);
|
|
91931
|
+
}
|
|
91932
|
+
function resolveFetchScript(fromModuleUrl = import.meta.url) {
|
|
91933
|
+
return join(resolvePythonDir(fromModuleUrl), "fetch_source.py");
|
|
91934
|
+
}
|
|
91935
|
+
//#endregion
|
|
91865
91936
|
//#region src/seal-plaintext.ts
|
|
91866
91937
|
/**
|
|
91867
91938
|
* A credential an older version stored as plain text in a `waste-source` record is moved into the
|
|
@@ -91964,35 +92035,6 @@ function reportFailed(log, failures) {
|
|
|
91964
92035
|
};
|
|
91965
92036
|
}
|
|
91966
92037
|
//#endregion
|
|
91967
|
-
//#region src/source-id.ts
|
|
91968
|
-
/**
|
|
91969
|
-
* Stable calendar source ids derived from the operator label.
|
|
91970
|
-
* Pattern: `^[a-z0-9][a-z0-9-]{0,63}$` (max 64 chars).
|
|
91971
|
-
*/
|
|
91972
|
-
var SOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
91973
|
-
/** Slugify a display label into a candidate source id (may be empty). */
|
|
91974
|
-
function slugifySourceLabel(label) {
|
|
91975
|
-
const s = label.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
91976
|
-
if (s.length === 0) return "";
|
|
91977
|
-
return s.slice(0, 64);
|
|
91978
|
-
}
|
|
91979
|
-
/**
|
|
91980
|
-
* Pick an unused id from `label`. Collisions append `-2`, `-3`, …
|
|
91981
|
-
* (truncated so the result stays within the 64-char pattern).
|
|
91982
|
-
*/
|
|
91983
|
-
function allocateSourceId(label, existingIds) {
|
|
91984
|
-
const taken = existingIds instanceof Set ? existingIds : new Set(existingIds);
|
|
91985
|
-
const baseRaw = slugifySourceLabel(label);
|
|
91986
|
-
const base = baseRaw.length > 0 ? baseRaw : "source";
|
|
91987
|
-
if (!taken.has(base) && SOURCE_ID_PATTERN.test(base)) return base;
|
|
91988
|
-
for (let n = 2; n < 1e4; n++) {
|
|
91989
|
-
const suffix = `-${n}`;
|
|
91990
|
-
const candidate = `${base.slice(0, Math.max(1, 64 - suffix.length)).replace(/-+$/g, "")}${suffix}`;
|
|
91991
|
-
if (SOURCE_ID_PATTERN.test(candidate) && !taken.has(candidate)) return candidate;
|
|
91992
|
-
}
|
|
91993
|
-
throw new Error("could not allocate a unique source id");
|
|
91994
|
-
}
|
|
91995
|
-
//#endregion
|
|
91996
92038
|
//#region src/waste-calendar-source.ts
|
|
91997
92039
|
function startOfUtcDayMs(isoDate) {
|
|
91998
92040
|
const [y, m, d] = isoDate.split("-").map((p) => Number(p));
|
|
@@ -178037,100 +178079,34 @@ function mergeServiceArgs(service, formValues) {
|
|
|
178037
178079
|
return out;
|
|
178038
178080
|
}
|
|
178039
178081
|
//#endregion
|
|
178040
|
-
//#region src/
|
|
178082
|
+
//#region src/source-id.ts
|
|
178041
178083
|
/**
|
|
178042
|
-
*
|
|
178043
|
-
*
|
|
178084
|
+
* Stable calendar source ids derived from the operator label.
|
|
178085
|
+
* Pattern: `^[a-z0-9][a-z0-9-]{0,63}$` (max 64 chars).
|
|
178044
178086
|
*/
|
|
178045
|
-
|
|
178046
|
-
|
|
178047
|
-
|
|
178048
|
-
|
|
178049
|
-
|
|
178050
|
-
|
|
178051
|
-
enabled: data.enabled
|
|
178052
|
-
});
|
|
178053
|
-
return parsed.success ? parsed.data : null;
|
|
178054
|
-
}
|
|
178055
|
-
function bodyOf(source) {
|
|
178056
|
-
const { id, label, module, args, enabled } = source;
|
|
178057
|
-
const body = {
|
|
178058
|
-
name: label,
|
|
178059
|
-
module,
|
|
178060
|
-
args,
|
|
178061
|
-
enabled
|
|
178062
|
-
};
|
|
178063
|
-
const parsed = wasteSourceEntryKind.schema.safeParse(body);
|
|
178064
|
-
if (!parsed.success) throw new Error(`invalid waste source '${id}': ${parsed.error.message}`);
|
|
178065
|
-
return parsed.data;
|
|
178087
|
+
var SOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
178088
|
+
/** Slugify a display label into a candidate source id (may be empty). */
|
|
178089
|
+
function slugifySourceLabel(label) {
|
|
178090
|
+
const s = label.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
178091
|
+
if (s.length === 0) return "";
|
|
178092
|
+
return s.slice(0, 64);
|
|
178066
178093
|
}
|
|
178067
178094
|
/**
|
|
178068
|
-
*
|
|
178069
|
-
*
|
|
178070
|
-
* its id, so the two never diverge; writes go to the records.
|
|
178095
|
+
* Pick an unused id from `label`. Collisions append `-2`, `-3`, …
|
|
178096
|
+
* (truncated so the result stays within the 64-char pattern).
|
|
178071
178097
|
*/
|
|
178072
|
-
function
|
|
178073
|
-
const
|
|
178074
|
-
|
|
178075
|
-
|
|
178076
|
-
|
|
178077
|
-
|
|
178078
|
-
|
|
178079
|
-
|
|
178080
|
-
|
|
178081
|
-
static of(records, kind = wasteSourceEntryKind) {
|
|
178082
|
-
return new WasteSourceStore(records.kind(kind));
|
|
178083
|
-
}
|
|
178084
|
-
/** The record ids, whatever their content. */
|
|
178085
|
-
async listIds() {
|
|
178086
|
-
return (await this.kind.query({ limit: 1e3 })).records.map((record) => record.id);
|
|
178087
|
-
}
|
|
178088
|
-
/** One source with its credentials resolved; markers stay where the keychain cannot serve. */
|
|
178089
|
-
async readResolved(id) {
|
|
178090
|
-
const stored = await this.kind.get(id);
|
|
178091
|
-
if (stored === null) return null;
|
|
178092
|
-
let data = stored.data;
|
|
178093
|
-
try {
|
|
178094
|
-
const resolved = await this.kind.getResolved(id);
|
|
178095
|
-
if (resolved !== null) data = resolved.data;
|
|
178096
|
-
} catch (err) {
|
|
178097
|
-
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
178098
|
-
}
|
|
178099
|
-
return rowToSource(id, data);
|
|
178100
|
-
}
|
|
178101
|
-
async listResolved() {
|
|
178102
|
-
const out = [];
|
|
178103
|
-
for (const id of await this.listIds()) {
|
|
178104
|
-
const row = await this.readResolved(id);
|
|
178105
|
-
if (row !== null) out.push(row);
|
|
178106
|
-
}
|
|
178107
|
-
return out;
|
|
178108
|
-
}
|
|
178109
|
-
/** Create the record when the id is free; an existing record is never touched. */
|
|
178110
|
-
async putIfAbsent(source) {
|
|
178111
|
-
if (await this.kind.get(source.id) !== null) return false;
|
|
178112
|
-
await this.kind.put({
|
|
178113
|
-
id: source.id,
|
|
178114
|
-
data: bodyOf(source)
|
|
178115
|
-
}, { ifRev: 0 });
|
|
178116
|
-
return true;
|
|
178117
|
-
}
|
|
178118
|
-
/**
|
|
178119
|
-
* Make the records equal `sources`. A source whose credential the keychain did not serve is
|
|
178120
|
-
* left exactly as stored: rewriting it would save a marker as the credential.
|
|
178121
|
-
*/
|
|
178122
|
-
async replaceAll(sources) {
|
|
178123
|
-
const nextIds = new Set(sources.map((s) => s.id));
|
|
178124
|
-
for (const id of await this.listIds()) if (!nextIds.has(id)) await this.kind.delete(id);
|
|
178125
|
-
for (const source of sources) {
|
|
178126
|
-
if (hasUnresolvedCredential(source.args)) continue;
|
|
178127
|
-
await this.kind.put({
|
|
178128
|
-
id: source.id,
|
|
178129
|
-
data: bodyOf(source)
|
|
178130
|
-
});
|
|
178131
|
-
}
|
|
178098
|
+
function allocateSourceId(label, existingIds) {
|
|
178099
|
+
const taken = existingIds instanceof Set ? existingIds : new Set(existingIds);
|
|
178100
|
+
const baseRaw = slugifySourceLabel(label);
|
|
178101
|
+
const base = baseRaw.length > 0 ? baseRaw : "source";
|
|
178102
|
+
if (!taken.has(base) && SOURCE_ID_PATTERN.test(base)) return base;
|
|
178103
|
+
for (let n = 2; n < 1e4; n++) {
|
|
178104
|
+
const suffix = `-${n}`;
|
|
178105
|
+
const candidate = `${base.slice(0, Math.max(1, 64 - suffix.length)).replace(/-+$/g, "")}${suffix}`;
|
|
178106
|
+
if (SOURCE_ID_PATTERN.test(candidate) && !taken.has(candidate)) return candidate;
|
|
178132
178107
|
}
|
|
178133
|
-
|
|
178108
|
+
throw new Error("could not allocate a unique source id");
|
|
178109
|
+
}
|
|
178134
178110
|
//#endregion
|
|
178135
178111
|
//#region src/waste-source-instances.ts
|
|
178136
178112
|
/**
|
|
@@ -178492,7 +178468,7 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178492
178468
|
if (!plainBoot) await records.declare({ "waste-source": wasteSourceEntryKind });
|
|
178493
178469
|
const store = WasteSourceStore.of(records, plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178494
178470
|
this.sourceStore = store;
|
|
178495
|
-
await this.migrateLegacySources(
|
|
178471
|
+
await this.migrateLegacySources(plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178496
178472
|
this.activeSources = await this.loadPersistedSources();
|
|
178497
178473
|
const instances = plainBoot ? null : new WasteSourceInstances(records, {
|
|
178498
178474
|
onChanged: (id, change) => this.onInstanceChanged(id, change),
|
|
@@ -178540,23 +178516,13 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178540
178516
|
customActions: wasteCollectionsActions,
|
|
178541
178517
|
actionHandlers: {
|
|
178542
178518
|
fetchNow: async (input) => this.fetchNow(input),
|
|
178543
|
-
listSources: async () => this.listSources(),
|
|
178544
178519
|
listCountries: async () => listCatalogCountries(),
|
|
178545
178520
|
listServices: async (input) => listCatalogServices(input.countryId).map((s) => ({
|
|
178546
178521
|
id: s.id,
|
|
178547
178522
|
label: serviceDisplayLabel(s),
|
|
178548
178523
|
title: s.label,
|
|
178549
178524
|
module: s.module
|
|
178550
|
-
}))
|
|
178551
|
-
serviceFormSchema: async (input) => {
|
|
178552
|
-
const service = findCatalogService(input.serviceId);
|
|
178553
|
-
if (!service) throw new Error(`unknown service '${input.serviceId}'`);
|
|
178554
|
-
return serviceFormSchema(service);
|
|
178555
|
-
},
|
|
178556
|
-
createSource: async (input) => this.createSource(input),
|
|
178557
|
-
updateSource: async (input) => this.updateSource(input),
|
|
178558
|
-
deleteSource: async (input) => this.deleteSource(input),
|
|
178559
|
-
migrateSources: async (input) => this.migrateSources(input.apply)
|
|
178525
|
+
}))
|
|
178560
178526
|
}
|
|
178561
178527
|
};
|
|
178562
178528
|
}
|
|
@@ -178584,16 +178550,8 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178584
178550
|
* fails leaves the settings where they are and is logged by id: the sources stay unavailable
|
|
178585
178551
|
* until it lands, never half-copied.
|
|
178586
178552
|
*/
|
|
178587
|
-
async migrateLegacySources(
|
|
178588
|
-
const report = await this.runMigrations(
|
|
178589
|
-
readStored: (key) => this.readStoredGlobalKey(key),
|
|
178590
|
-
listSourceIds: () => store.listIds(),
|
|
178591
|
-
putSource: async (source) => {
|
|
178592
|
-
await store.putIfAbsent(source);
|
|
178593
|
-
},
|
|
178594
|
-
readSource: (id) => store.readResolved(id),
|
|
178595
|
-
warn: (message) => this.ctx.logger.warn(message)
|
|
178596
|
-
}));
|
|
178553
|
+
async migrateLegacySources(kind) {
|
|
178554
|
+
const report = await this.runMigrations(ADDON_MIGRATIONS, (api) => wasteSourcePortsOver(api, kind));
|
|
178597
178555
|
this.legacyServed = !report.declared || report.failed.includes("waste-collections/2026-11-06-sources-from-settings-and-argsjson") || report.blocked.includes("waste-collections/2026-11-06-sources-from-settings-and-argsjson");
|
|
178598
178556
|
if (this.legacyServed) this.ctx.logger.warn("waste-collections: the sources migration has not landed - the legacy sources not yet in records are served read-only beside the records until it does", { meta: {
|
|
178599
178557
|
declared: report.declared,
|
|
@@ -178623,17 +178581,6 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178623
178581
|
if (!this.legacyServed) return records;
|
|
178624
178582
|
return withUnmigratedLegacyRows(records, this.parseConfigSources(this.config.sources));
|
|
178625
178583
|
}
|
|
178626
|
-
async persistSources(sources) {
|
|
178627
|
-
this.activeSources = [...sources];
|
|
178628
|
-
const store = this.sourceStore;
|
|
178629
|
-
if (store !== null) {
|
|
178630
|
-
await store.replaceAll(sources);
|
|
178631
|
-
const kept = new Set(sources.map((s) => s.id));
|
|
178632
|
-
await this.pruneLegacyRows((legacyId) => kept.has(legacyId));
|
|
178633
|
-
return;
|
|
178634
|
-
}
|
|
178635
|
-
await this.updateGlobalSettings({ sources: [...sources] });
|
|
178636
|
-
}
|
|
178637
178584
|
/**
|
|
178638
178585
|
* A source deleted at the records leaves the legacy setting too, or it would reappear beside the
|
|
178639
178586
|
* records (while the migration is pending) or be copied back by it.
|
|
@@ -178648,195 +178595,6 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178648
178595
|
if (this.activeSources.length > 0 || this.sourceStore !== null) return this.activeSources;
|
|
178649
178596
|
return this.parseConfigSources(this.config.sources);
|
|
178650
178597
|
}
|
|
178651
|
-
listSources() {
|
|
178652
|
-
return this.configuredSources().map((s) => {
|
|
178653
|
-
let args = {};
|
|
178654
|
-
try {
|
|
178655
|
-
args = sourceArgs(s);
|
|
178656
|
-
} catch {
|
|
178657
|
-
args = { ...s.args };
|
|
178658
|
-
}
|
|
178659
|
-
const matched = matchCatalogService(s.module, args);
|
|
178660
|
-
const formValues = {
|
|
178661
|
-
label: s.label,
|
|
178662
|
-
enabled: s.enabled,
|
|
178663
|
-
...redactArgs(args)
|
|
178664
|
-
};
|
|
178665
|
-
if (matched) for (const key of Object.keys(matched.defaultArgs)) delete formValues[key];
|
|
178666
|
-
return {
|
|
178667
|
-
id: s.id,
|
|
178668
|
-
label: s.label,
|
|
178669
|
-
module: s.module,
|
|
178670
|
-
enabled: s.enabled,
|
|
178671
|
-
formValues,
|
|
178672
|
-
serviceId: matched?.id,
|
|
178673
|
-
countryId: matched?.countryId
|
|
178674
|
-
};
|
|
178675
|
-
});
|
|
178676
|
-
}
|
|
178677
|
-
async createSource(input) {
|
|
178678
|
-
const existing = this.configuredSources().map((s) => s.id);
|
|
178679
|
-
let id;
|
|
178680
|
-
if (input.id !== void 0) {
|
|
178681
|
-
if (!SOURCE_ID_PATTERN.test(input.id)) throw new Error("id must be a lowercase slug (e.g. ics-home, junker)");
|
|
178682
|
-
if (existing.includes(input.id)) throw new Error(`source '${input.id}' already exists`);
|
|
178683
|
-
id = input.id;
|
|
178684
|
-
} else id = allocateSourceId(input.label, existing);
|
|
178685
|
-
const service = findCatalogService(input.serviceId);
|
|
178686
|
-
if (!service) throw new Error(`unknown service '${input.serviceId}'`);
|
|
178687
|
-
const row = WasteSourceConfigSchema.parse({
|
|
178688
|
-
id,
|
|
178689
|
-
label: input.label,
|
|
178690
|
-
module: service.module,
|
|
178691
|
-
args: this.typedArgs(mergeServiceArgs(service, restoreRedactedArgs(input.args, {}))),
|
|
178692
|
-
enabled: input.enabled ?? true
|
|
178693
|
-
});
|
|
178694
|
-
await this.persistSources([...this.configuredSources(), row]);
|
|
178695
|
-
return { id };
|
|
178696
|
-
}
|
|
178697
|
-
async updateSource(input) {
|
|
178698
|
-
const sources = this.configuredSources();
|
|
178699
|
-
const idx = sources.findIndex((s) => s.id === input.id);
|
|
178700
|
-
if (idx < 0) throw new Error(`source '${input.id}' not found`);
|
|
178701
|
-
const prev = sources[idx];
|
|
178702
|
-
let prevArgs = {};
|
|
178703
|
-
try {
|
|
178704
|
-
prevArgs = sourceArgs(prev);
|
|
178705
|
-
} catch {
|
|
178706
|
-
prevArgs = { ...prev.args };
|
|
178707
|
-
}
|
|
178708
|
-
let module = prev.module;
|
|
178709
|
-
let args = prevArgs;
|
|
178710
|
-
if (input.serviceId !== void 0) {
|
|
178711
|
-
const service = findCatalogService(input.serviceId);
|
|
178712
|
-
if (!service) throw new Error(`unknown service '${input.serviceId}'`);
|
|
178713
|
-
module = service.module;
|
|
178714
|
-
args = mergeServiceArgs(service, restoreRedactedArgs(input.args ?? {}, prevArgs));
|
|
178715
|
-
} else if (input.args !== void 0) {
|
|
178716
|
-
const sent = restoreRedactedArgs(input.args, prevArgs);
|
|
178717
|
-
const matched = matchCatalogService(prev.module, prevArgs);
|
|
178718
|
-
args = matched ? mergeServiceArgs(matched, sent) : {
|
|
178719
|
-
...prevArgs,
|
|
178720
|
-
...sent
|
|
178721
|
-
};
|
|
178722
|
-
}
|
|
178723
|
-
const next = WasteSourceConfigSchema.parse({
|
|
178724
|
-
id: prev.id,
|
|
178725
|
-
label: input.label,
|
|
178726
|
-
module,
|
|
178727
|
-
args: this.typedArgs(args),
|
|
178728
|
-
enabled: input.enabled ?? prev.enabled
|
|
178729
|
-
});
|
|
178730
|
-
await this.persistSources(sources.map((s, i) => i === idx ? next : s));
|
|
178731
|
-
return { ok: true };
|
|
178732
|
-
}
|
|
178733
|
-
/** Validate an argument bag against the typed value schema (a nested object is refused). */
|
|
178734
|
-
typedArgs(args) {
|
|
178735
|
-
const out = {};
|
|
178736
|
-
for (const [key, value] of Object.entries(args)) out[key] = WasteArgValueSchema.parse(value);
|
|
178737
|
-
return out;
|
|
178738
|
-
}
|
|
178739
|
-
/**
|
|
178740
|
-
* D880: rewrite every legacy source (free `argsJson`) as typed `args`; its
|
|
178741
|
-
* credentials go to the keychain through the normal save path. Then every
|
|
178742
|
-
* credential is read back and compared with what the legacy row held; on any
|
|
178743
|
-
* difference the legacy rows are written back, so nothing is lost. Dry run
|
|
178744
|
-
* unless `apply`. Idempotent. Counts only - never an argument, a ref or an id.
|
|
178745
|
-
*/
|
|
178746
|
-
async migrateSources(apply) {
|
|
178747
|
-
const sources = this.configuredSources();
|
|
178748
|
-
const legacy = sources.filter((s) => s.argsJson !== void 0);
|
|
178749
|
-
const rawRows = legacy.filter((s) => {
|
|
178750
|
-
try {
|
|
178751
|
-
return hasCredential(sourceArgs(s));
|
|
178752
|
-
} catch {
|
|
178753
|
-
return false;
|
|
178754
|
-
}
|
|
178755
|
-
});
|
|
178756
|
-
const markerRows = sources.filter((s) => s.argsJson === void 0 && hasCredential(s.args)).length;
|
|
178757
|
-
const report = (over) => ({
|
|
178758
|
-
apply,
|
|
178759
|
-
rows: sources.length,
|
|
178760
|
-
raw: rawRows.length,
|
|
178761
|
-
markers: markerRows,
|
|
178762
|
-
migrated: 0,
|
|
178763
|
-
failed: 0,
|
|
178764
|
-
remainingRaw: rawRows.length,
|
|
178765
|
-
unresolvedMarkers: this.unresolvedSecrets.length,
|
|
178766
|
-
redundantCopies: legacy.length,
|
|
178767
|
-
verified: rawRows.length === 0 && this.unresolvedSecrets.length === 0,
|
|
178768
|
-
...over
|
|
178769
|
-
});
|
|
178770
|
-
if (!apply || legacy.length === 0) return report({});
|
|
178771
|
-
const expected = /* @__PURE__ */ new Map();
|
|
178772
|
-
const unconverted = /* @__PURE__ */ new Set();
|
|
178773
|
-
const migrated = sources.map((s) => {
|
|
178774
|
-
if (s.argsJson === void 0) return s;
|
|
178775
|
-
try {
|
|
178776
|
-
const args = sourceArgs(s);
|
|
178777
|
-
const row = WasteSourceConfigSchema.parse({
|
|
178778
|
-
id: s.id,
|
|
178779
|
-
label: s.label,
|
|
178780
|
-
module: s.module,
|
|
178781
|
-
args: this.typedArgs(args),
|
|
178782
|
-
enabled: s.enabled
|
|
178783
|
-
});
|
|
178784
|
-
expected.set(s.id, args);
|
|
178785
|
-
return row;
|
|
178786
|
-
} catch {
|
|
178787
|
-
unconverted.add(s.id);
|
|
178788
|
-
return s;
|
|
178789
|
-
}
|
|
178790
|
-
});
|
|
178791
|
-
const failedRows = unconverted.size;
|
|
178792
|
-
if (expected.size === 0) return report({
|
|
178793
|
-
failed: failedRows,
|
|
178794
|
-
remainingRaw: legacy.length
|
|
178795
|
-
});
|
|
178796
|
-
try {
|
|
178797
|
-
await this.persistSources(migrated);
|
|
178798
|
-
} catch {
|
|
178799
|
-
return report({ failed: legacy.length });
|
|
178800
|
-
}
|
|
178801
|
-
const back = new Map(this.configuredSources().map((s) => [s.id, s]));
|
|
178802
|
-
if (![...expected].every(([id, args]) => {
|
|
178803
|
-
const row = back.get(id);
|
|
178804
|
-
return row !== void 0 && row.argsJson === void 0 && JSON.stringify(sortedEntries(row.args)) === JSON.stringify(sortedEntries(args));
|
|
178805
|
-
}) || this.unresolvedSecrets.length > 0) {
|
|
178806
|
-
await this.rollbackMigration(sources, expected);
|
|
178807
|
-
return report({ failed: legacy.length });
|
|
178808
|
-
}
|
|
178809
|
-
return report({
|
|
178810
|
-
migrated: expected.size,
|
|
178811
|
-
failed: failedRows,
|
|
178812
|
-
remainingRaw: failedRows,
|
|
178813
|
-
unresolvedMarkers: 0,
|
|
178814
|
-
redundantCopies: legacy.length,
|
|
178815
|
-
verified: failedRows === 0
|
|
178816
|
-
});
|
|
178817
|
-
}
|
|
178818
|
-
/**
|
|
178819
|
-
* Put the legacy rows back. A converted row's `args` is written as `null`, which
|
|
178820
|
-
* the save path reads as "this object is gone": it deletes the keychain refs the
|
|
178821
|
-
* migration created (a plain `{}` would keep them). Never throws - the report
|
|
178822
|
-
* says failed either way, and the legacy JSON stays authoritative.
|
|
178823
|
-
*/
|
|
178824
|
-
async rollbackMigration(sources, converted) {
|
|
178825
|
-
try {
|
|
178826
|
-
await this.updateGlobalSettingsAsOperator({ sources: sources.map((s) => converted.has(s.id) ? {
|
|
178827
|
-
...s,
|
|
178828
|
-
args: null
|
|
178829
|
-
} : s) });
|
|
178830
|
-
} catch {
|
|
178831
|
-
this.ctx.logger.error("waste-collections: the migration rollback failed - keychain refs may be orphaned (System -> Secrets)");
|
|
178832
|
-
}
|
|
178833
|
-
}
|
|
178834
|
-
async deleteSource(input) {
|
|
178835
|
-
const sources = this.configuredSources();
|
|
178836
|
-
if (!sources.some((s) => s.id === input.id)) throw new Error(`source '${input.id}' not found`);
|
|
178837
|
-
await this.persistSources(sources.filter((s) => s.id !== input.id));
|
|
178838
|
-
return { ok: true };
|
|
178839
|
-
}
|
|
178840
178598
|
async fetchNow(input) {
|
|
178841
178599
|
const targets = this.configuredSources().filter((s) => {
|
|
178842
178600
|
if (!s.enabled) return false;
|
|
@@ -178937,32 +178695,10 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178937
178695
|
this.ctx.logger.warn("waste-collections: cache persist failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
178938
178696
|
}
|
|
178939
178697
|
}
|
|
178940
|
-
globalSettingsSchema() {
|
|
178941
|
-
return this.schema({ sections: [{
|
|
178942
|
-
id: "refresh",
|
|
178943
|
-
title: "Refresh",
|
|
178944
|
-
description: "Sources are managed in the table above (Add / Edit): pick a country and a service, then fill only that service’s fields. Each source becomes one calendar under System → Calendars.",
|
|
178945
|
-
columns: 2,
|
|
178946
|
-
fields: [{
|
|
178947
|
-
type: "addon-action-button",
|
|
178948
|
-
key: "_fetchNow",
|
|
178949
|
-
label: "Refresh",
|
|
178950
|
-
buttonLabel: "Fetch now",
|
|
178951
|
-
description: "Sources refresh once at boot and then every 24 hours; this refreshes every enabled source immediately.",
|
|
178952
|
-
addonId: "waste-collections",
|
|
178953
|
-
action: "fetchNow",
|
|
178954
|
-
variant: "primary",
|
|
178955
|
-
paramsStatic: {}
|
|
178956
|
-
}]
|
|
178957
|
-
}] });
|
|
178958
|
-
}
|
|
178959
178698
|
};
|
|
178960
178699
|
/** Re-parse helper for tests. */
|
|
178961
178700
|
function parseWasteConfig(raw) {
|
|
178962
178701
|
return WasteConfigSchema.parse(raw);
|
|
178963
178702
|
}
|
|
178964
|
-
function sortedEntries(args) {
|
|
178965
|
-
return Object.entries(args).toSorted(([a], [b]) => a < b ? -1 : 1);
|
|
178966
|
-
}
|
|
178967
178703
|
//#endregion
|
|
178968
178704
|
export { WasteCollectionsAddon, WasteCollectionsAddon as default, sourceArgs as a, wasteCollectionsActions as customActions, WasteConfigSchema as i, createWasteCalendarSource as n, parseWasteConfig, DEFAULT_WASTE_CONFIG as r, collectionsToEvents as t };
|