@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
|
@@ -6,7 +6,7 @@ let node_fs = require("node:fs");
|
|
|
6
6
|
let node_path = require("node:path");
|
|
7
7
|
let node_child_process = require("node:child_process");
|
|
8
8
|
let node_url = require("node:url");
|
|
9
|
-
//#region ../types/dist/event-category-
|
|
9
|
+
//#region ../types/dist/event-category-78tcBemo.mjs
|
|
10
10
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
11
11
|
EventCategory["SystemBoot"] = "system.boot";
|
|
12
12
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -127,6 +127,23 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
127
127
|
*/
|
|
128
128
|
EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
|
|
129
129
|
/**
|
|
130
|
+
* An addon is RUNNING although its installed package is below what the
|
|
131
|
+
* running server requires (`camstack.requires.addons` of the server, or the
|
|
132
|
+
* addon's own `requires.server`). Raised once per streak by the unsupported-
|
|
133
|
+
* addon tracker, on the hub and on agents; AlertCenter turns it into one
|
|
134
|
+
* operator alert per node and package. The addon is never held back for it.
|
|
135
|
+
*
|
|
136
|
+
* Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
|
|
137
|
+
*/
|
|
138
|
+
EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
|
|
139
|
+
/**
|
|
140
|
+
* The package of an {@link AddonUnsupportedVersion} alert reached a supported
|
|
141
|
+
* version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
|
|
142
|
+
*
|
|
143
|
+
* Payload: `{ packageName, addonId, nodeId }`.
|
|
144
|
+
*/
|
|
145
|
+
EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
|
|
146
|
+
/**
|
|
130
147
|
* Monitor scheduled the next retry for a failed addon. Transient —
|
|
131
148
|
* surfaced to the UI for live-updating the "next retry in Ns"
|
|
132
149
|
* countdown on the Addons page row, NOT persisted as an alert.
|
|
@@ -237,7 +254,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
237
254
|
EventCategory["ProcessRestarted"] = "process.restarted";
|
|
238
255
|
/**
|
|
239
256
|
* The SET of storage locations changed — one was created, edited, enabled,
|
|
240
|
-
* disabled or deleted through
|
|
257
|
+
* disabled or deleted through the location instances or the forced-delete action.
|
|
241
258
|
*
|
|
242
259
|
* Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
|
|
243
260
|
* it must also converge on its own periodic path, because a dropped event
|
|
@@ -5988,7 +6005,7 @@ var ZodIssueCode = {
|
|
|
5988
6005
|
var ZodFirstPartyTypeKind;
|
|
5989
6006
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5990
6007
|
//#endregion
|
|
5991
|
-
//#region ../types/dist/sleep-
|
|
6008
|
+
//#region ../types/dist/sleep-CQP-REJp.mjs
|
|
5992
6009
|
/**
|
|
5993
6010
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5994
6011
|
* window to float samples (D455).
|
|
@@ -6083,16 +6100,6 @@ var AddonMigrationsBlockedError = class extends Error {
|
|
|
6083
6100
|
this.migrationIds = migrationIds;
|
|
6084
6101
|
}
|
|
6085
6102
|
};
|
|
6086
|
-
/**
|
|
6087
|
-
* The value a redacted secret is replaced with on every read surface.
|
|
6088
|
-
*
|
|
6089
|
-
* It is also the WRITE-BACK token: an `upsertLocation` that sends this value
|
|
6090
|
-
* back for a key means "keep what is stored", which is what lets an operator
|
|
6091
|
-
* rename a location without retyping its password. A literal an operator could
|
|
6092
|
-
* plausibly choose as a real password would turn that convenience into a way
|
|
6093
|
-
* to lock yourself out, hence the sentinel shape.
|
|
6094
|
-
*/
|
|
6095
|
-
var REDACTED_SECRET = "__camstack_redacted__";
|
|
6096
6103
|
/** Is this field's stored value a credential? */
|
|
6097
6104
|
function isSecretConfigField(field) {
|
|
6098
6105
|
if (field.type === "password") return true;
|
|
@@ -6362,6 +6369,59 @@ Object.fromEntries([
|
|
|
6362
6369
|
function isValuelessField(field) {
|
|
6363
6370
|
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";
|
|
6364
6371
|
}
|
|
6372
|
+
/**
|
|
6373
|
+
* Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
|
|
6374
|
+
* `key → { value, default }`, recursing through `group` and `sub-tabs`
|
|
6375
|
+
* layout fields. Keeping `value` and `default` SEPARATE (rather than
|
|
6376
|
+
* pre-resolving) lets callers both read the effective value AND detect
|
|
6377
|
+
* overrides (`value` differs from `default`). Structural over `unknown`;
|
|
6378
|
+
* malformed input yields an empty map, never throws.
|
|
6379
|
+
*/
|
|
6380
|
+
function collectHydratedFieldEntries(schema) {
|
|
6381
|
+
const out = /* @__PURE__ */ new Map();
|
|
6382
|
+
if (typeof schema !== "object" || schema === null) return out;
|
|
6383
|
+
const sections = schema.sections;
|
|
6384
|
+
if (!Array.isArray(sections)) return out;
|
|
6385
|
+
for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
|
|
6386
|
+
return out;
|
|
6387
|
+
}
|
|
6388
|
+
/** Recursive field walker for {@link collectHydratedFieldEntries}. */
|
|
6389
|
+
function collectFieldEntriesInto(fields, into) {
|
|
6390
|
+
if (!Array.isArray(fields)) return;
|
|
6391
|
+
for (const field of fields) {
|
|
6392
|
+
if (typeof field !== "object" || field === null) continue;
|
|
6393
|
+
const type = field.type;
|
|
6394
|
+
if (type === "group") {
|
|
6395
|
+
collectFieldEntriesInto(field.fields, into);
|
|
6396
|
+
continue;
|
|
6397
|
+
}
|
|
6398
|
+
if (type === "sub-tabs") {
|
|
6399
|
+
const tabs = field.tabs;
|
|
6400
|
+
if (Array.isArray(tabs)) {
|
|
6401
|
+
for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
|
|
6402
|
+
}
|
|
6403
|
+
continue;
|
|
6404
|
+
}
|
|
6405
|
+
const key = field.key;
|
|
6406
|
+
if (typeof key !== "string" || key.length === 0) continue;
|
|
6407
|
+
if (!("value" in field)) continue;
|
|
6408
|
+
into.set(key, {
|
|
6409
|
+
value: field.value,
|
|
6410
|
+
default: field.default
|
|
6411
|
+
});
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
/**
|
|
6415
|
+
* Flatten a hydrated settings payload into `key → EFFECTIVE value`
|
|
6416
|
+
* (`value ?? default`). The read-side inverse of `hydrateSchema` for the
|
|
6417
|
+
* common "just give me the resolved values" case. Built on
|
|
6418
|
+
* {@link collectHydratedFieldEntries}.
|
|
6419
|
+
*/
|
|
6420
|
+
function collectHydratedFieldValues(schema) {
|
|
6421
|
+
const out = /* @__PURE__ */ new Map();
|
|
6422
|
+
for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
|
|
6423
|
+
return out;
|
|
6424
|
+
}
|
|
6365
6425
|
function hydrateSchema(schema, values, options = {}) {
|
|
6366
6426
|
return {
|
|
6367
6427
|
...schema.tabs ? { tabs: [...schema.tabs] } : {},
|
|
@@ -7403,17 +7463,35 @@ var RecordIntegrationInstances = class {
|
|
|
7403
7463
|
...kind !== void 0 ? { kind } : {}
|
|
7404
7464
|
});
|
|
7405
7465
|
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
7466
|
+
const initial = this.hooks.initialBody(name, kind);
|
|
7467
|
+
const patch = this.patchOf(values ?? {});
|
|
7468
|
+
if (Object.keys(patch).length > 0) Object.assign(patch, await this.hooks.validateWrite?.({
|
|
7469
|
+
kind,
|
|
7470
|
+
body: {
|
|
7471
|
+
...initial,
|
|
7472
|
+
...patch
|
|
7473
|
+
}
|
|
7474
|
+
}));
|
|
7406
7475
|
await this.handle.put({
|
|
7407
7476
|
id: minted,
|
|
7408
|
-
data:
|
|
7477
|
+
data: initial
|
|
7409
7478
|
}, { ifRev: 0 });
|
|
7410
|
-
const patch = this.patchOf(values ?? {});
|
|
7411
7479
|
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
7412
7480
|
await this.hooks.onChanged?.(minted, "created");
|
|
7413
7481
|
return { id: minted };
|
|
7414
7482
|
};
|
|
7415
7483
|
updateInstance = async ({ id, name, values }) => {
|
|
7416
7484
|
const patch = this.patchOf(values);
|
|
7485
|
+
if (this.hooks.validateWrite !== void 0 && Object.keys(patch).length > 0) {
|
|
7486
|
+
const stored = await this.handle.get(id);
|
|
7487
|
+
if (stored !== null) Object.assign(patch, await this.hooks.validateWrite({
|
|
7488
|
+
kind: void 0,
|
|
7489
|
+
body: {
|
|
7490
|
+
...stored.data,
|
|
7491
|
+
...patch
|
|
7492
|
+
}
|
|
7493
|
+
}));
|
|
7494
|
+
}
|
|
7417
7495
|
if (name !== void 0) patch[this.nameField] = name;
|
|
7418
7496
|
await this.handle.patch(id, patch);
|
|
7419
7497
|
await this.hooks.onChanged?.(id, "updated");
|
|
@@ -7724,6 +7802,21 @@ var SCOPED_SETTINGS_SCOPES = [
|
|
|
7724
7802
|
"device-defaults",
|
|
7725
7803
|
"node"
|
|
7726
7804
|
];
|
|
7805
|
+
/** The one refusal of a key a scope did not declare. */
|
|
7806
|
+
function assertDeclaredKeys(keys, declared) {
|
|
7807
|
+
const undeclared = keys.filter((key) => !declared.has(key));
|
|
7808
|
+
if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
|
|
7809
|
+
}
|
|
7810
|
+
/** Build the handle over an addon's scoped read, write and unset. */
|
|
7811
|
+
function createScopedSettings(deps) {
|
|
7812
|
+
return {
|
|
7813
|
+
async get() {
|
|
7814
|
+
return deps.schema.parse(await deps.read());
|
|
7815
|
+
},
|
|
7816
|
+
patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
|
|
7817
|
+
unset: (keys) => deps.unset(keys)
|
|
7818
|
+
};
|
|
7819
|
+
}
|
|
7727
7820
|
/** A write to a secret path could not be stored in the keychain, so it was refused. */
|
|
7728
7821
|
var KeychainWriteRefusedError = class extends Error {
|
|
7729
7822
|
fieldPath;
|
|
@@ -8299,7 +8392,16 @@ function createHandle(raw, opts) {
|
|
|
8299
8392
|
return blob;
|
|
8300
8393
|
}
|
|
8301
8394
|
},
|
|
8302
|
-
writeAddonStore: writeAddon
|
|
8395
|
+
writeAddonStore: writeAddon,
|
|
8396
|
+
removeClearedKeys: (keys) => serialize("addon", async () => {
|
|
8397
|
+
const stored = await readStoredAddonBlob(raw);
|
|
8398
|
+
index.seen("addon", stored);
|
|
8399
|
+
const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
|
|
8400
|
+
if (removable.length === 0) return;
|
|
8401
|
+
const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
|
|
8402
|
+
await raw.writeAddonStore(patch);
|
|
8403
|
+
index.wrote("addon", patch);
|
|
8404
|
+
})
|
|
8303
8405
|
};
|
|
8304
8406
|
}
|
|
8305
8407
|
/** The context the addon sees, plus the private raw view and write outcome. */
|
|
@@ -8844,6 +8946,151 @@ var BaseAddon = class {
|
|
|
8844
8946
|
}
|
|
8845
8947
|
}
|
|
8846
8948
|
/**
|
|
8949
|
+
* Hook: values layered over the stored ones when the FORM of one scope is served
|
|
8950
|
+
* ({@link getScopedSettings}) — a preview that derives dependent options without writing.
|
|
8951
|
+
* Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
|
|
8952
|
+
*/
|
|
8953
|
+
async scopedSettingsOverlay(_scope, _cap, _nodeId) {
|
|
8954
|
+
return null;
|
|
8955
|
+
}
|
|
8956
|
+
/**
|
|
8957
|
+
* Hook: a write of one scope changed stored values (not called when the store reports no change,
|
|
8958
|
+
* e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
|
|
8959
|
+
*/
|
|
8960
|
+
async onScopedSettingsChanged(_scope, _cap, _keys) {}
|
|
8961
|
+
/** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
|
|
8962
|
+
clusterSettings(cap, schema) {
|
|
8963
|
+
return this.scopedHandle("cluster", cap, schema);
|
|
8964
|
+
}
|
|
8965
|
+
/** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
|
|
8966
|
+
deviceDefaults(cap, schema) {
|
|
8967
|
+
return this.scopedHandle("device-defaults", cap, schema);
|
|
8968
|
+
}
|
|
8969
|
+
/**
|
|
8970
|
+
* Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
|
|
8971
|
+
* with no row yields the schema defaults — never a bare-key fallback.
|
|
8972
|
+
*/
|
|
8973
|
+
nodeSettings(cap, schema, nodeId) {
|
|
8974
|
+
return this.scopedHandle("node", cap, schema, nodeId);
|
|
8975
|
+
}
|
|
8976
|
+
/**
|
|
8977
|
+
* {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
|
|
8978
|
+
* settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
|
|
8979
|
+
* central delete. `nodeId` omitted ⇒ the local node.
|
|
8980
|
+
*/
|
|
8981
|
+
nodeState(key, schema, fallback, nodeId) {
|
|
8982
|
+
this._declaredStateKeys.add(key);
|
|
8983
|
+
return createDurableState({
|
|
8984
|
+
key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
|
|
8985
|
+
schema,
|
|
8986
|
+
fallback,
|
|
8987
|
+
read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
|
|
8988
|
+
write: async (patch) => {
|
|
8989
|
+
await this._ctx?.settings?.writeAddonStore(patch);
|
|
8990
|
+
},
|
|
8991
|
+
onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
|
|
8992
|
+
addonId: this._ctx?.id,
|
|
8993
|
+
key: k,
|
|
8994
|
+
error: String(e)
|
|
8995
|
+
} })
|
|
8996
|
+
});
|
|
8997
|
+
}
|
|
8998
|
+
/** The loaded schema `cap` declares for `scope`, or `null`. */
|
|
8999
|
+
async declaredScopedSchema(scope, cap) {
|
|
9000
|
+
await this.loadScopedSchemas(cap);
|
|
9001
|
+
return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
|
|
9002
|
+
}
|
|
9003
|
+
async requireScopedSchema(scope, cap) {
|
|
9004
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9005
|
+
if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
|
|
9006
|
+
return schema;
|
|
9007
|
+
}
|
|
9008
|
+
scopedKeys(schema) {
|
|
9009
|
+
return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
|
|
9010
|
+
}
|
|
9011
|
+
/** `store` narrowed to the keys `schema` declares. */
|
|
9012
|
+
narrowToScope(store, schema) {
|
|
9013
|
+
const keys = this.scopedKeys(schema);
|
|
9014
|
+
return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
|
|
9015
|
+
}
|
|
9016
|
+
scopedHandle(scope, cap, schema, nodeId) {
|
|
9017
|
+
return createScopedSettings({
|
|
9018
|
+
schema,
|
|
9019
|
+
read: async () => {
|
|
9020
|
+
const declared = await this.requireScopedSchema(scope, cap);
|
|
9021
|
+
const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
|
|
9022
|
+
const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
|
|
9023
|
+
return {
|
|
9024
|
+
...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
|
|
9025
|
+
...stored
|
|
9026
|
+
};
|
|
9027
|
+
},
|
|
9028
|
+
write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
|
|
9029
|
+
unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
|
|
9030
|
+
});
|
|
9031
|
+
}
|
|
9032
|
+
/** The one write path of the scoped API: declared keys only, then the global patch. */
|
|
9033
|
+
async writeScoped(scope, cap, patch, nodeId) {
|
|
9034
|
+
assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
9035
|
+
if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
|
|
9036
|
+
}
|
|
9037
|
+
/**
|
|
9038
|
+
* Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
|
|
9039
|
+
* value is released exactly as there — and the emptied row is then removed; every other key is
|
|
9040
|
+
* removed by an `undefined` patch (which a secret transform would read as "keep").
|
|
9041
|
+
*/
|
|
9042
|
+
async unsetScoped(scope, cap, keys, nodeId) {
|
|
9043
|
+
assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
9044
|
+
const target = scope === "node" ? nodeId : void 0;
|
|
9045
|
+
const tree = this.addonSecretTree();
|
|
9046
|
+
const secretKeys = keys.filter((key) => tree.has(key));
|
|
9047
|
+
const plainKeys = keys.filter((key) => !tree.has(key));
|
|
9048
|
+
let changed = false;
|
|
9049
|
+
if (plainKeys.length > 0) {
|
|
9050
|
+
const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
|
|
9051
|
+
changed = await this.applyGlobalPatch(patch, target, cap) || changed;
|
|
9052
|
+
}
|
|
9053
|
+
if (secretKeys.length > 0) {
|
|
9054
|
+
await this.clearSecretKeys(secretKeys, target, cap);
|
|
9055
|
+
const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
|
|
9056
|
+
const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
|
|
9057
|
+
if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
|
|
9058
|
+
else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
|
|
9059
|
+
changed = true;
|
|
9060
|
+
}
|
|
9061
|
+
if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
|
|
9062
|
+
}
|
|
9063
|
+
/**
|
|
9064
|
+
* The hydrated form of one scope of one capability's settings (any
|
|
9065
|
+
* {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
|
|
9066
|
+
*/
|
|
9067
|
+
async getScopedSettings(scope, cap, nodeId) {
|
|
9068
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9069
|
+
if (schema === null) return null;
|
|
9070
|
+
const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
|
|
9071
|
+
const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
|
|
9072
|
+
return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
|
|
9073
|
+
}
|
|
9074
|
+
/** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
|
|
9075
|
+
async updateScopedSettings(scope, cap, patch, nodeId) {
|
|
9076
|
+
this._operatorWriteDepth++;
|
|
9077
|
+
try {
|
|
9078
|
+
await this.writeScoped(scope, cap, patch, nodeId);
|
|
9079
|
+
} finally {
|
|
9080
|
+
this._operatorWriteDepth--;
|
|
9081
|
+
}
|
|
9082
|
+
}
|
|
9083
|
+
/**
|
|
9084
|
+
* The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
|
|
9085
|
+
* its value. `null` when the capability declares nothing for the scope.
|
|
9086
|
+
*/
|
|
9087
|
+
async readScopedValues(scope, cap, nodeId) {
|
|
9088
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9089
|
+
if (schema === null) return null;
|
|
9090
|
+
const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
|
|
9091
|
+
return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
|
|
9092
|
+
}
|
|
9093
|
+
/**
|
|
8847
9094
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
8848
9095
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
8849
9096
|
*/
|
|
@@ -8995,6 +9242,9 @@ var BaseAddon = class {
|
|
|
8995
9242
|
* a keychain (an older kernel) it writes `''`, as such code always did.
|
|
8996
9243
|
*/
|
|
8997
9244
|
async clearSecrets(keys, nodeId) {
|
|
9245
|
+
await this.clearSecretKeys(keys, nodeId);
|
|
9246
|
+
}
|
|
9247
|
+
async clearSecretKeys(keys, nodeId, cap) {
|
|
8998
9248
|
const tree = this.addonSecretTree();
|
|
8999
9249
|
const keychain = this._ctx?.secrets !== void 0;
|
|
9000
9250
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
@@ -9007,25 +9257,27 @@ var BaseAddon = class {
|
|
|
9007
9257
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
9008
9258
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
9009
9259
|
}
|
|
9010
|
-
await this.applyGlobalPatch(patch, nodeId);
|
|
9260
|
+
await this.applyGlobalPatch(patch, nodeId, cap);
|
|
9011
9261
|
}
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9262
|
+
/** Resolves `true` when the write changed stored values. */
|
|
9263
|
+
async applyGlobalPatch(barePatch, nodeId, cap) {
|
|
9264
|
+
await this.loadScopedSchemas(cap);
|
|
9265
|
+
const keys = this.perNodeKeys(cap);
|
|
9015
9266
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
9016
9267
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
9017
9268
|
const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
|
|
9018
9269
|
const handle = this._secretSettings;
|
|
9019
9270
|
if (handle !== null) {
|
|
9020
9271
|
const { changed } = await handle.writeAddonStore(scoped);
|
|
9021
|
-
if (!changed) return;
|
|
9272
|
+
if (!changed) return false;
|
|
9022
9273
|
} else await this._ctx?.settings?.writeAddonStore(scoped);
|
|
9023
|
-
if (target !== localNode) return;
|
|
9274
|
+
if (target !== localNode) return true;
|
|
9024
9275
|
await this.resolveConfig();
|
|
9025
9276
|
await this.onConfigChanged();
|
|
9026
9277
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
9027
9278
|
const legacy = this.globalSettingsSchema();
|
|
9028
9279
|
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
9280
|
+
return true;
|
|
9029
9281
|
}
|
|
9030
9282
|
/**
|
|
9031
9283
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -10777,7 +11029,6 @@ var CAP_NAMES = Object.freeze([
|
|
|
10777
11029
|
"notifier",
|
|
10778
11030
|
"numeric-sensor",
|
|
10779
11031
|
"oauth-integration",
|
|
10780
|
-
"oidc-connections",
|
|
10781
11032
|
"osd",
|
|
10782
11033
|
"osd-manager",
|
|
10783
11034
|
"pet-feeder",
|
|
@@ -12669,8 +12920,7 @@ Object.freeze({
|
|
|
12669
12920
|
"device-provider": { "reloadDevice": 18e4 },
|
|
12670
12921
|
"llm": {
|
|
12671
12922
|
"generate": 6e5,
|
|
12672
|
-
"generateVision": 6e5
|
|
12673
|
-
"testProfile": 6e5
|
|
12923
|
+
"generateVision": 6e5
|
|
12674
12924
|
},
|
|
12675
12925
|
"llm-runtime": {
|
|
12676
12926
|
"complete": 6e5,
|
|
@@ -14073,17 +14323,18 @@ var IntegrationInstanceRefSchema = object({
|
|
|
14073
14323
|
* message to what the kind supports — callers never special-case a service.
|
|
14074
14324
|
*
|
|
14075
14325
|
* DESIGN DECISIONS (locked):
|
|
14076
|
-
* -
|
|
14077
|
-
*
|
|
14078
|
-
*
|
|
14079
|
-
*
|
|
14080
|
-
*
|
|
14081
|
-
*
|
|
14082
|
-
*
|
|
14083
|
-
*
|
|
14084
|
-
*
|
|
14085
|
-
* routable. `send` / `testTarget` /
|
|
14086
|
-
* `
|
|
14326
|
+
* - A target is an INSTANCE of this function cap (epic #440, step 6): the
|
|
14327
|
+
* providers serve their targets as `integration-instances` records, one
|
|
14328
|
+
* record kind per target kind, and the admin lists, adds, edits, switches
|
|
14329
|
+
* and deletes them through that cap. This cap keeps what an instance cannot
|
|
14330
|
+
* say: the kind catalogue (`listTargetKinds`: capabilities, delivery
|
|
14331
|
+
* parameters, brand icon), dispatch (`send`, `testTarget`) and the form
|
|
14332
|
+
* helpers (`discoverTargets`, `probeTargetConfig`).
|
|
14333
|
+
* - `listTargetKinds` / `discoverTargets` return arrays → the generated
|
|
14334
|
+
* cap-mount auto-`concatCollection`-fans them across every registered
|
|
14335
|
+
* provider so one catalog is routable. `send` / `testTarget` /
|
|
14336
|
+
* `probeTargetConfig` route to ONE provider by the `addonId` the generated
|
|
14337
|
+
* collection router extracts from the call input.
|
|
14087
14338
|
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
14088
14339
|
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
14089
14340
|
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
@@ -14328,7 +14579,7 @@ var TargetKindCapsSchema = object({
|
|
|
14328
14579
|
* the union is large and not meant for runtime validation here; the exported
|
|
14329
14580
|
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
14330
14581
|
*/
|
|
14331
|
-
var ConfigSchemaPassthrough$
|
|
14582
|
+
var ConfigSchemaPassthrough$3 = unknown();
|
|
14332
14583
|
var TargetKindSchema = object({
|
|
14333
14584
|
kind: string(),
|
|
14334
14585
|
label: string(),
|
|
@@ -14365,7 +14616,7 @@ var TargetKindSchema = object({
|
|
|
14365
14616
|
* its raster path, which is the safe default for an unknown image.
|
|
14366
14617
|
*/
|
|
14367
14618
|
iconMediaType: string().optional(),
|
|
14368
|
-
configSchema: ConfigSchemaPassthrough$
|
|
14619
|
+
configSchema: ConfigSchemaPassthrough$3,
|
|
14369
14620
|
/**
|
|
14370
14621
|
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
14371
14622
|
* base URL, instance, chat, recipient). A save that changes any of them
|
|
@@ -14387,9 +14638,11 @@ var TargetKindSchema = object({
|
|
|
14387
14638
|
caps: TargetKindCapsSchema
|
|
14388
14639
|
});
|
|
14389
14640
|
/**
|
|
14390
|
-
* A
|
|
14391
|
-
*
|
|
14392
|
-
*
|
|
14641
|
+
* A target as a provider's own code sees it (the notification-center bridge, the
|
|
14642
|
+
* send path): the flat record body split into the name, the switch and the
|
|
14643
|
+
* kind's fields. `config` holds secrets; a reader that serves it to a client
|
|
14644
|
+
* REDACTS secret fields (a presence marker only) — never round-trip a stored
|
|
14645
|
+
* secret to the UI.
|
|
14393
14646
|
*/
|
|
14394
14647
|
var TargetSchema = object({
|
|
14395
14648
|
id: string(),
|
|
@@ -14432,13 +14685,13 @@ var ProbeTargetConfigResultSchema = object({
|
|
|
14432
14685
|
stateInstance: string().optional(),
|
|
14433
14686
|
error: string().optional()
|
|
14434
14687
|
});
|
|
14435
|
-
method(object({}), array(TargetKindSchema)), method(object({
|
|
14688
|
+
method(object({}), array(TargetKindSchema)), method(object({
|
|
14436
14689
|
kind: string(),
|
|
14437
14690
|
config: record(string(), unknown()).optional(),
|
|
14438
14691
|
/**
|
|
14439
14692
|
* When editing / duplicating, the blank password field never round-trips
|
|
14440
14693
|
* the stored token. Pass the source target id so the provider can restore
|
|
14441
|
-
* secrets server-side before dialling
|
|
14694
|
+
* secrets server-side before dialling.
|
|
14442
14695
|
*/
|
|
14443
14696
|
targetId: string().optional()
|
|
14444
14697
|
}), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
|
|
@@ -14461,27 +14714,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
|
|
|
14461
14714
|
}), TestResultSchema, {
|
|
14462
14715
|
kind: "mutation",
|
|
14463
14716
|
auth: "admin"
|
|
14464
|
-
}), method(object({
|
|
14465
|
-
target: TargetSchema,
|
|
14466
|
-
/**
|
|
14467
|
-
* Duplicate flow: create a NEW target id while copying secrets from an
|
|
14468
|
-
* existing row. The UI never holds the stored token; blank / redacted
|
|
14469
|
-
* password fields are restored from this source (D634 still applies if
|
|
14470
|
-
* destinations diverge without a re-typed secret).
|
|
14471
|
-
*/
|
|
14472
|
-
cloneSecretsFrom: string().optional()
|
|
14473
|
-
}), TargetSchema, {
|
|
14474
|
-
kind: "mutation",
|
|
14475
|
-
auth: "admin"
|
|
14476
|
-
}), method(object({ targetId: string() }), _void(), {
|
|
14477
|
-
kind: "mutation",
|
|
14478
|
-
auth: "admin"
|
|
14479
|
-
}), method(object({
|
|
14480
|
-
targetId: string(),
|
|
14481
|
-
enabled: boolean()
|
|
14482
|
-
}), _void(), {
|
|
14483
|
-
kind: "mutation",
|
|
14484
|
-
auth: "admin"
|
|
14485
14717
|
});
|
|
14486
14718
|
looseObject({
|
|
14487
14719
|
name: string().min(1),
|
|
@@ -22739,7 +22971,6 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
22739
22971
|
"notification-output": system("notification delivery — reached through notifier devices and rules"),
|
|
22740
22972
|
"notification-rules": system("notification rules — authored on their own page"),
|
|
22741
22973
|
"oauth-integration": system(IDENTITY),
|
|
22742
|
-
"oidc-connections": system(IDENTITY),
|
|
22743
22974
|
"osd-manager": system(PIPELINE),
|
|
22744
22975
|
"pipeline-executor": system(PIPELINE),
|
|
22745
22976
|
"pipeline-orchestrator": system(PIPELINE),
|
|
@@ -44881,6 +45112,8 @@ var SettingsSchemaWithValuesSchema = object({
|
|
|
44881
45112
|
});
|
|
44882
45113
|
/** Patch object — keys are field names, values are the new field values. */
|
|
44883
45114
|
var SettingsPatchSchema = record(string(), unknown());
|
|
45115
|
+
/** The scope of a scoped-settings call (D986): where the capability declared the values. */
|
|
45116
|
+
var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
|
|
44884
45117
|
/** Standard success response for update operations. */
|
|
44885
45118
|
var SettingsUpdateResultSchema = object({ success: literal(true) });
|
|
44886
45119
|
method(object({
|
|
@@ -44896,6 +45129,25 @@ method(object({
|
|
|
44896
45129
|
kind: "mutation",
|
|
44897
45130
|
auth: "admin"
|
|
44898
45131
|
}), method(object({
|
|
45132
|
+
addonId: string(),
|
|
45133
|
+
cap: string(),
|
|
45134
|
+
scope: ScopedSettingsScopeSchema,
|
|
45135
|
+
nodeId: string().optional()
|
|
45136
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
45137
|
+
addonId: string(),
|
|
45138
|
+
cap: string(),
|
|
45139
|
+
scope: ScopedSettingsScopeSchema,
|
|
45140
|
+
nodeId: string().optional(),
|
|
45141
|
+
patch: SettingsPatchSchema
|
|
45142
|
+
}), SettingsUpdateResultSchema, {
|
|
45143
|
+
kind: "mutation",
|
|
45144
|
+
auth: "admin"
|
|
45145
|
+
}), method(object({
|
|
45146
|
+
addonId: string(),
|
|
45147
|
+
cap: string(),
|
|
45148
|
+
scope: ScopedSettingsScopeSchema,
|
|
45149
|
+
nodeId: string().optional()
|
|
45150
|
+
}), record(string(), unknown()).nullable()), method(object({
|
|
44899
45151
|
addonId: string(),
|
|
44900
45152
|
nodeId: string().optional()
|
|
44901
45153
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
@@ -54427,7 +54679,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54427
54679
|
* or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
|
|
54428
54680
|
* clears it.
|
|
54429
54681
|
*/
|
|
54430
|
-
var ConfigSchemaPassthrough$
|
|
54682
|
+
var ConfigSchemaPassthrough$2 = custom();
|
|
54431
54683
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54432
54684
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54433
54685
|
/**
|
|
@@ -54473,7 +54725,16 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54473
54725
|
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
54474
54726
|
connectPath: string().min(1).optional(),
|
|
54475
54727
|
/** Relative hub path to disconnect that account. */
|
|
54476
|
-
disconnectPath: string().min(1).optional()
|
|
54728
|
+
disconnectPath: string().min(1).optional(),
|
|
54729
|
+
/**
|
|
54730
|
+
* Parts of the edit dialog the flat form cannot draw (a node picker, a model source), named by a
|
|
54731
|
+
* GENERIC widget id the client maps to a component; a client that knows none of them shows none.
|
|
54732
|
+
* `connection` widgets write connection values, `model` widgets own the model choice.
|
|
54733
|
+
*/
|
|
54734
|
+
widgets: array(object({
|
|
54735
|
+
id: string().min(1),
|
|
54736
|
+
placement: _enum(["connection", "model"])
|
|
54737
|
+
})).optional()
|
|
54477
54738
|
});
|
|
54478
54739
|
/**
|
|
54479
54740
|
* How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
|
|
@@ -54578,7 +54839,7 @@ var integrationInstancesCapability = {
|
|
|
54578
54839
|
id: IntegrationInstanceIdSchema.nullable(),
|
|
54579
54840
|
/** The kind of the create form (`id: null`) of a provider with several. */
|
|
54580
54841
|
kind: string().min(1).optional()
|
|
54581
|
-
}), ConfigSchemaPassthrough$
|
|
54842
|
+
}), ConfigSchemaPassthrough$2, { auth: "admin" }),
|
|
54582
54843
|
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54583
54844
|
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54584
54845
|
/**
|
|
@@ -55529,12 +55790,12 @@ var LlmModelInfoSchema = object({
|
|
|
55529
55790
|
});
|
|
55530
55791
|
/**
|
|
55531
55792
|
* The provider-specific part of a profile, validated by the provider that owns
|
|
55532
|
-
* the kind (
|
|
55793
|
+
* the kind (saving an instance refuses what it does not accept). A field its
|
|
55533
55794
|
* `connectionSchema` declares `type: 'password'` is a secret: it is held in the
|
|
55534
55795
|
* keychain, never in the row, and reads back as the redaction marker.
|
|
55535
55796
|
*/
|
|
55536
55797
|
var LlmProviderConfigSchema = record(string(), unknown());
|
|
55537
|
-
|
|
55798
|
+
object({
|
|
55538
55799
|
id: string(),
|
|
55539
55800
|
name: string(),
|
|
55540
55801
|
/** The provider-declared kind (`LlmProfileKindDescriptor.kind`). */
|
|
@@ -55628,8 +55889,8 @@ var LlmProviderWidgetSchema = object({
|
|
|
55628
55889
|
/** ConfigUISchema tree passed through untyped on the wire (the
|
|
55629
55890
|
* notification-output `ConfigSchemaPassthrough` precedent at
|
|
55630
55891
|
* notification-output.cap.ts:151); the exported TS type re-tightens it. */
|
|
55631
|
-
var ConfigSchemaPassthrough$
|
|
55632
|
-
|
|
55892
|
+
var ConfigSchemaPassthrough$1 = unknown();
|
|
55893
|
+
object({
|
|
55633
55894
|
kind: LlmProfileKindSchema,
|
|
55634
55895
|
label: string(),
|
|
55635
55896
|
/** A generic icon name (`cloud`, `plug`, `cpu`) — never a glyph or a logo. */
|
|
@@ -55637,9 +55898,9 @@ var LlmProfileKindDescriptorSchema = object({
|
|
|
55637
55898
|
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
55638
55899
|
addonId: string(),
|
|
55639
55900
|
/** The provider's own fields (`providerConfig`); `password` fields are keychain-held. */
|
|
55640
|
-
connectionSchema: ConfigSchemaPassthrough$
|
|
55901
|
+
connectionSchema: ConfigSchemaPassthrough$1,
|
|
55641
55902
|
/** Provider fields that belong under "Advanced" (extra headers, runtime tuning). */
|
|
55642
|
-
advancedSchema: ConfigSchemaPassthrough$
|
|
55903
|
+
advancedSchema: ConfigSchemaPassthrough$1.optional(),
|
|
55643
55904
|
/** The kind's defaults; a model from `listModels` may override any of them. */
|
|
55644
55905
|
features: LlmModelFeaturesSchema,
|
|
55645
55906
|
/** `false` = the provider stamps `supportsVision` itself (from what a node reports). */
|
|
@@ -55768,20 +56029,7 @@ method(LlmGenerateBaseInputSchema, LlmGenerateResultSchema, {
|
|
|
55768
56029
|
}), method(object({
|
|
55769
56030
|
addonId: string().optional(),
|
|
55770
56031
|
requestId: string()
|
|
55771
|
-
}), _void(), { kind: "mutation" }), method(
|
|
55772
|
-
kind: "mutation",
|
|
55773
|
-
auth: "admin"
|
|
55774
|
-
}), method(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
55775
|
-
kind: "mutation",
|
|
55776
|
-
auth: "admin"
|
|
55777
|
-
}), method(ProfileRefInputSchema, _void(), {
|
|
55778
|
-
kind: "mutation",
|
|
55779
|
-
auth: "admin"
|
|
55780
|
-
}), method(ProfileRefInputSchema, LlmGenerateResultSchema, {
|
|
55781
|
-
kind: "mutation",
|
|
55782
|
-
auth: "admin",
|
|
55783
|
-
timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
|
|
55784
|
-
}), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
|
|
56032
|
+
}), _void(), { kind: "mutation" }), method(LlmListModelsInputSchema, LlmListModelsResultSchema, {
|
|
55785
56033
|
kind: "mutation",
|
|
55786
56034
|
auth: "admin"
|
|
55787
56035
|
}), method(object({}), array(LlmDefaultSchema)), method(object({
|
|
@@ -56776,20 +57024,7 @@ method(_void(), MeshStatusSchema), method(object({
|
|
|
56776
57024
|
/** Optional hostname override the host should advertise once joined. */
|
|
56777
57025
|
hostname: string().optional() }), object({
|
|
56778
57026
|
/** Authentication URL the operator should open in a browser. */
|
|
56779
|
-
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() }))
|
|
56780
|
-
/** Optional auth key — when provided, probes the key validity
|
|
56781
|
-
* against the provider's API. Omit when already joined to
|
|
56782
|
-
* just ping the daemon. */
|
|
56783
|
-
authKey: string().optional() }), object({
|
|
56784
|
-
ok: boolean(),
|
|
56785
|
-
/** Provider-side identifier resolved by the probe (tailnet
|
|
56786
|
-
* name for Tailscale, network id for ZeroTier, etc.). */
|
|
56787
|
-
tenant: string().optional(),
|
|
56788
|
-
/** Daemon binary version, when reachable. */
|
|
56789
|
-
daemonVersion: string().optional(),
|
|
56790
|
-
/** Human-readable error when `ok: false`. */
|
|
56791
|
-
error: string().optional()
|
|
56792
|
-
}), { kind: "mutation" });
|
|
57027
|
+
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() }));
|
|
56793
57028
|
object({
|
|
56794
57029
|
value: number().nullable().meta(readingMeta("device", { label: "Total" })),
|
|
56795
57030
|
lastFetchedAt: number()
|
|
@@ -66447,61 +66682,6 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
66447
66682
|
});
|
|
66448
66683
|
method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
|
|
66449
66684
|
/**
|
|
66450
|
-
* `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
|
|
66451
|
-
*
|
|
66452
|
-
* One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
|
|
66453
|
-
* ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
|
|
66454
|
-
* Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
|
|
66455
|
-
* immutable after create.
|
|
66456
|
-
*/
|
|
66457
|
-
var ConfigSchemaPassthrough$1 = unknown();
|
|
66458
|
-
var OidcConnectionSchema = object({
|
|
66459
|
-
id: string().min(1),
|
|
66460
|
-
displayName: string(),
|
|
66461
|
-
icon: string(),
|
|
66462
|
-
issuerUrl: string(),
|
|
66463
|
-
clientId: string(),
|
|
66464
|
-
/** Client id + secret present — never the secret value. */
|
|
66465
|
-
configured: boolean(),
|
|
66466
|
-
/**
|
|
66467
|
-
* Values for the connection form schema. `clientSecret` is a keychain
|
|
66468
|
-
* {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
|
|
66469
|
-
*/
|
|
66470
|
-
formValues: record(string(), unknown()).optional()
|
|
66471
|
-
});
|
|
66472
|
-
/**
|
|
66473
|
-
* Patch one connection. Omitted or empty `clientSecret` keeps the stored
|
|
66474
|
-
* secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
|
|
66475
|
-
*/
|
|
66476
|
-
var OidcConnectionUpdateInputSchema = object({
|
|
66477
|
-
id: string().min(1),
|
|
66478
|
-
displayName: string().min(1),
|
|
66479
|
-
icon: string().optional(),
|
|
66480
|
-
issuerUrl: string().optional(),
|
|
66481
|
-
clientId: string().optional(),
|
|
66482
|
-
clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
|
|
66483
|
-
redirectUri: string().optional(),
|
|
66484
|
-
scopes: string().optional(),
|
|
66485
|
-
usernameClaim: _enum([
|
|
66486
|
-
"preferred_username",
|
|
66487
|
-
"email",
|
|
66488
|
-
"sub"
|
|
66489
|
-
]).optional()
|
|
66490
|
-
});
|
|
66491
|
-
method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
|
|
66492
|
-
id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
|
|
66493
|
-
displayName: string().min(1)
|
|
66494
|
-
}), object({ id: string() }), {
|
|
66495
|
-
kind: "mutation",
|
|
66496
|
-
auth: "admin"
|
|
66497
|
-
}), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
|
|
66498
|
-
kind: "mutation",
|
|
66499
|
-
auth: "admin"
|
|
66500
|
-
}), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
|
|
66501
|
-
kind: "mutation",
|
|
66502
|
-
auth: "admin"
|
|
66503
|
-
});
|
|
66504
|
-
/**
|
|
66505
66685
|
* Generic on-screen-display (video overlay) cap. Each camera exposes
|
|
66506
66686
|
* a firmware-defined list of overlays (channel name, timestamp,
|
|
66507
66687
|
* watermark, logo, …); the cap models them as a homogeneous array of
|
|
@@ -72674,21 +72854,6 @@ method(object({
|
|
|
72674
72854
|
*/
|
|
72675
72855
|
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
72676
72856
|
}), object({ token: string() }), { auth: "admin" }), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable(), { auth: "admin" });
|
|
72677
|
-
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
72678
|
-
providerId: string().min(1),
|
|
72679
|
-
displayName: string().min(1),
|
|
72680
|
-
configSchema: unknown(),
|
|
72681
|
-
nodeLocal: boolean(),
|
|
72682
|
-
shouldSaveDiskSpace: literal(true),
|
|
72683
|
-
minFreePercent: number().min(0).max(100)
|
|
72684
|
-
}), object({
|
|
72685
|
-
providerId: string().min(1),
|
|
72686
|
-
displayName: string().min(1),
|
|
72687
|
-
configSchema: unknown(),
|
|
72688
|
-
nodeLocal: boolean(),
|
|
72689
|
-
shouldSaveDiskSpace: literal(false),
|
|
72690
|
-
minFreePercent: literal(null)
|
|
72691
|
-
})]);
|
|
72692
72857
|
method(object({
|
|
72693
72858
|
location: StorageLocationRefSchema,
|
|
72694
72859
|
relativePath: string()
|
|
@@ -72750,42 +72915,7 @@ method(object({
|
|
|
72750
72915
|
}), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
|
|
72751
72916
|
kind: "mutation",
|
|
72752
72917
|
auth: "admin"
|
|
72753
|
-
}), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(
|
|
72754
|
-
createdAt: true,
|
|
72755
|
-
updatedAt: true
|
|
72756
|
-
}), StorageLocationSchema, {
|
|
72757
|
-
kind: "mutation",
|
|
72758
|
-
auth: "admin"
|
|
72759
|
-
}), method(object({
|
|
72760
|
-
id: string(),
|
|
72761
|
-
force: boolean().optional()
|
|
72762
|
-
}), _void(), {
|
|
72763
|
-
kind: "mutation",
|
|
72764
|
-
auth: "admin"
|
|
72765
|
-
}), method(_void(), array(StorageDrainProgressSchema).readonly()), method(object({ id: string() }), object({
|
|
72766
|
-
ok: boolean(),
|
|
72767
|
-
error: string().optional()
|
|
72768
|
-
}), { auth: "admin" }), method(_void(), array(ProviderListEntrySchema).readonly()), method(object({
|
|
72769
|
-
providerId: string(),
|
|
72770
|
-
/**
|
|
72771
|
-
* The location this config is an UNSAVED edit of, when there is one.
|
|
72772
|
-
*
|
|
72773
|
-
* `listLocations` replaces every declared secret with the redaction
|
|
72774
|
-
* sentinel, so the edit modal's form state holds the sentinel for any
|
|
72775
|
-
* credential the operator did not retype — and posting that here
|
|
72776
|
-
* without a way to resolve it makes the provider try to authenticate
|
|
72777
|
-
* as `__camstack_redacted__` and report the operator's own working
|
|
72778
|
-
* password as wrong. Given this id, the orchestrator restores each
|
|
72779
|
-
* sentinel from the stored config (same rule as `upsertLocation`)
|
|
72780
|
-
* before dispatching. Omitted by the "Add location" wizard, where
|
|
72781
|
-
* every value was typed just now and nothing is stored yet.
|
|
72782
|
-
*/
|
|
72783
|
-
locationId: string().optional(),
|
|
72784
|
-
config: record(string(), unknown())
|
|
72785
|
-
}), object({
|
|
72786
|
-
ok: boolean(),
|
|
72787
|
-
error: string().optional()
|
|
72788
|
-
}), { auth: "admin" });
|
|
72918
|
+
}), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(_void(), array(StorageDrainProgressSchema).readonly());
|
|
72789
72919
|
/** One table of the settings database. */
|
|
72790
72920
|
var StorageTableSchema = object({
|
|
72791
72921
|
/** The table's full name, e.g. `recorder:addon-settings` or `addon-device-settings`. */
|
|
@@ -76906,6 +77036,20 @@ Object.freeze({
|
|
|
76906
77036
|
access: "view",
|
|
76907
77037
|
role: "admin"
|
|
76908
77038
|
},
|
|
77039
|
+
"addonSettings.getScopedSettings": {
|
|
77040
|
+
capName: "addon-settings",
|
|
77041
|
+
capScope: "system",
|
|
77042
|
+
addonId: null,
|
|
77043
|
+
access: "view",
|
|
77044
|
+
role: "admin"
|
|
77045
|
+
},
|
|
77046
|
+
"addonSettings.readScopedValues": {
|
|
77047
|
+
capName: "addon-settings",
|
|
77048
|
+
capScope: "system",
|
|
77049
|
+
addonId: null,
|
|
77050
|
+
access: "view",
|
|
77051
|
+
role: "admin"
|
|
77052
|
+
},
|
|
76909
77053
|
"addonSettings.unsetStoredSettingsKeys": {
|
|
76910
77054
|
capName: "addon-settings",
|
|
76911
77055
|
capScope: "system",
|
|
@@ -76927,6 +77071,13 @@ Object.freeze({
|
|
|
76927
77071
|
access: "create",
|
|
76928
77072
|
role: "admin"
|
|
76929
77073
|
},
|
|
77074
|
+
"addonSettings.updateScopedSettings": {
|
|
77075
|
+
capName: "addon-settings",
|
|
77076
|
+
capScope: "system",
|
|
77077
|
+
addonId: null,
|
|
77078
|
+
access: "create",
|
|
77079
|
+
role: "admin"
|
|
77080
|
+
},
|
|
76930
77081
|
"addonWidgets.listWidgets": {
|
|
76931
77082
|
capName: "addon-widgets",
|
|
76932
77083
|
capScope: "system",
|
|
@@ -80378,13 +80529,6 @@ Object.freeze({
|
|
|
80378
80529
|
access: "delete",
|
|
80379
80530
|
role: "admin"
|
|
80380
80531
|
},
|
|
80381
|
-
"llm.deleteProfile": {
|
|
80382
|
-
capName: "llm",
|
|
80383
|
-
capScope: "system",
|
|
80384
|
-
addonId: null,
|
|
80385
|
-
access: "delete",
|
|
80386
|
-
role: "admin"
|
|
80387
|
-
},
|
|
80388
80532
|
"llm.generate": {
|
|
80389
80533
|
capName: "llm",
|
|
80390
80534
|
capScope: "system",
|
|
@@ -80448,20 +80592,6 @@ Object.freeze({
|
|
|
80448
80592
|
access: "view",
|
|
80449
80593
|
role: "admin"
|
|
80450
80594
|
},
|
|
80451
|
-
"llm.listProfileKinds": {
|
|
80452
|
-
capName: "llm",
|
|
80453
|
-
capScope: "system",
|
|
80454
|
-
addonId: null,
|
|
80455
|
-
access: "view",
|
|
80456
|
-
role: "admin"
|
|
80457
|
-
},
|
|
80458
|
-
"llm.listProfiles": {
|
|
80459
|
-
capName: "llm",
|
|
80460
|
-
capScope: "system",
|
|
80461
|
-
addonId: null,
|
|
80462
|
-
access: "view",
|
|
80463
|
-
role: "admin"
|
|
80464
|
-
},
|
|
80465
80595
|
"llm.listRuntimeNodes": {
|
|
80466
80596
|
capName: "llm",
|
|
80467
80597
|
capScope: "system",
|
|
@@ -80469,13 +80599,6 @@ Object.freeze({
|
|
|
80469
80599
|
access: "view",
|
|
80470
80600
|
role: "admin"
|
|
80471
80601
|
},
|
|
80472
|
-
"llm.migrateRawSecrets": {
|
|
80473
|
-
capName: "llm",
|
|
80474
|
-
capScope: "system",
|
|
80475
|
-
addonId: null,
|
|
80476
|
-
access: "create",
|
|
80477
|
-
role: "admin"
|
|
80478
|
-
},
|
|
80479
80602
|
"llm.resolveModelRef": {
|
|
80480
80603
|
capName: "llm",
|
|
80481
80604
|
capScope: "system",
|
|
@@ -80504,20 +80627,6 @@ Object.freeze({
|
|
|
80504
80627
|
access: "create",
|
|
80505
80628
|
role: "admin"
|
|
80506
80629
|
},
|
|
80507
|
-
"llm.testProfile": {
|
|
80508
|
-
capName: "llm",
|
|
80509
|
-
capScope: "system",
|
|
80510
|
-
addonId: null,
|
|
80511
|
-
access: "create",
|
|
80512
|
-
role: "admin"
|
|
80513
|
-
},
|
|
80514
|
-
"llm.upsertProfile": {
|
|
80515
|
-
capName: "llm",
|
|
80516
|
-
capScope: "system",
|
|
80517
|
-
addonId: null,
|
|
80518
|
-
access: "create",
|
|
80519
|
-
role: "admin"
|
|
80520
|
-
},
|
|
80521
80630
|
"llmRuntime.appleAvailability": {
|
|
80522
80631
|
capName: "llm-runtime",
|
|
80523
80632
|
capScope: "system",
|
|
@@ -81022,13 +81131,6 @@ Object.freeze({
|
|
|
81022
81131
|
access: "create",
|
|
81023
81132
|
role: "admin"
|
|
81024
81133
|
},
|
|
81025
|
-
"meshNetwork.testConnection": {
|
|
81026
|
-
capName: "mesh-network",
|
|
81027
|
-
capScope: "system",
|
|
81028
|
-
addonId: null,
|
|
81029
|
-
access: "create",
|
|
81030
|
-
role: "admin"
|
|
81031
|
-
},
|
|
81032
81134
|
"meter.getStatus": {
|
|
81033
81135
|
capName: "meter",
|
|
81034
81136
|
capScope: "device",
|
|
@@ -81687,13 +81789,6 @@ Object.freeze({
|
|
|
81687
81789
|
access: "create",
|
|
81688
81790
|
role: "admin"
|
|
81689
81791
|
},
|
|
81690
|
-
"notificationOutput.deleteTarget": {
|
|
81691
|
-
capName: "notification-output",
|
|
81692
|
-
capScope: "system",
|
|
81693
|
-
addonId: null,
|
|
81694
|
-
access: "delete",
|
|
81695
|
-
role: "admin"
|
|
81696
|
-
},
|
|
81697
81792
|
"notificationOutput.discoverTargets": {
|
|
81698
81793
|
capName: "notification-output",
|
|
81699
81794
|
capScope: "system",
|
|
@@ -81701,13 +81796,6 @@ Object.freeze({
|
|
|
81701
81796
|
access: "view",
|
|
81702
81797
|
role: "admin"
|
|
81703
81798
|
},
|
|
81704
|
-
"notificationOutput.getTarget": {
|
|
81705
|
-
capName: "notification-output",
|
|
81706
|
-
capScope: "system",
|
|
81707
|
-
addonId: null,
|
|
81708
|
-
access: "view",
|
|
81709
|
-
role: "admin"
|
|
81710
|
-
},
|
|
81711
81799
|
"notificationOutput.listTargetKinds": {
|
|
81712
81800
|
capName: "notification-output",
|
|
81713
81801
|
capScope: "system",
|
|
@@ -81715,13 +81803,6 @@ Object.freeze({
|
|
|
81715
81803
|
access: "view",
|
|
81716
81804
|
role: "admin"
|
|
81717
81805
|
},
|
|
81718
|
-
"notificationOutput.listTargets": {
|
|
81719
|
-
capName: "notification-output",
|
|
81720
|
-
capScope: "system",
|
|
81721
|
-
addonId: null,
|
|
81722
|
-
access: "view",
|
|
81723
|
-
role: "admin"
|
|
81724
|
-
},
|
|
81725
81806
|
"notificationOutput.probeTargetConfig": {
|
|
81726
81807
|
capName: "notification-output",
|
|
81727
81808
|
capScope: "system",
|
|
@@ -81736,13 +81817,6 @@ Object.freeze({
|
|
|
81736
81817
|
access: "create",
|
|
81737
81818
|
role: "admin"
|
|
81738
81819
|
},
|
|
81739
|
-
"notificationOutput.setTargetEnabled": {
|
|
81740
|
-
capName: "notification-output",
|
|
81741
|
-
capScope: "system",
|
|
81742
|
-
addonId: null,
|
|
81743
|
-
access: "create",
|
|
81744
|
-
role: "admin"
|
|
81745
|
-
},
|
|
81746
81820
|
"notificationOutput.testTarget": {
|
|
81747
81821
|
capName: "notification-output",
|
|
81748
81822
|
capScope: "system",
|
|
@@ -81750,13 +81824,6 @@ Object.freeze({
|
|
|
81750
81824
|
access: "create",
|
|
81751
81825
|
role: "admin"
|
|
81752
81826
|
},
|
|
81753
|
-
"notificationOutput.upsertTarget": {
|
|
81754
|
-
capName: "notification-output",
|
|
81755
|
-
capScope: "system",
|
|
81756
|
-
addonId: null,
|
|
81757
|
-
access: "create",
|
|
81758
|
-
role: "admin"
|
|
81759
|
-
},
|
|
81760
81827
|
"notificationRules.activateAlarmProfile": {
|
|
81761
81828
|
capName: "notification-rules",
|
|
81762
81829
|
capScope: "system",
|
|
@@ -82016,41 +82083,6 @@ Object.freeze({
|
|
|
82016
82083
|
access: "view",
|
|
82017
82084
|
role: "admin"
|
|
82018
82085
|
},
|
|
82019
|
-
"oidcConnections.connectionFormSchema": {
|
|
82020
|
-
capName: "oidc-connections",
|
|
82021
|
-
capScope: "system",
|
|
82022
|
-
addonId: null,
|
|
82023
|
-
access: "view",
|
|
82024
|
-
role: "admin"
|
|
82025
|
-
},
|
|
82026
|
-
"oidcConnections.createConnection": {
|
|
82027
|
-
capName: "oidc-connections",
|
|
82028
|
-
capScope: "system",
|
|
82029
|
-
addonId: null,
|
|
82030
|
-
access: "create",
|
|
82031
|
-
role: "admin"
|
|
82032
|
-
},
|
|
82033
|
-
"oidcConnections.deleteConnection": {
|
|
82034
|
-
capName: "oidc-connections",
|
|
82035
|
-
capScope: "system",
|
|
82036
|
-
addonId: null,
|
|
82037
|
-
access: "delete",
|
|
82038
|
-
role: "admin"
|
|
82039
|
-
},
|
|
82040
|
-
"oidcConnections.listConnections": {
|
|
82041
|
-
capName: "oidc-connections",
|
|
82042
|
-
capScope: "system",
|
|
82043
|
-
addonId: null,
|
|
82044
|
-
access: "view",
|
|
82045
|
-
role: "admin"
|
|
82046
|
-
},
|
|
82047
|
-
"oidcConnections.updateConnection": {
|
|
82048
|
-
capName: "oidc-connections",
|
|
82049
|
-
capScope: "system",
|
|
82050
|
-
addonId: null,
|
|
82051
|
-
access: "create",
|
|
82052
|
-
role: "admin"
|
|
82053
|
-
},
|
|
82054
82086
|
"osd.getStatus": {
|
|
82055
82087
|
capName: "osd",
|
|
82056
82088
|
capScope: "device",
|
|
@@ -85082,13 +85114,6 @@ Object.freeze({
|
|
|
85082
85114
|
access: "delete",
|
|
85083
85115
|
role: "admin"
|
|
85084
85116
|
},
|
|
85085
|
-
"storage.deleteLocation": {
|
|
85086
|
-
capName: "storage",
|
|
85087
|
-
capScope: "system",
|
|
85088
|
-
addonId: null,
|
|
85089
|
-
access: "delete",
|
|
85090
|
-
role: "admin"
|
|
85091
|
-
},
|
|
85092
85117
|
"storage.endDownload": {
|
|
85093
85118
|
capName: "storage",
|
|
85094
85119
|
capScope: "system",
|
|
@@ -85145,13 +85170,6 @@ Object.freeze({
|
|
|
85145
85170
|
access: "view",
|
|
85146
85171
|
role: "admin"
|
|
85147
85172
|
},
|
|
85148
|
-
"storage.listProviders": {
|
|
85149
|
-
capName: "storage",
|
|
85150
|
-
capScope: "system",
|
|
85151
|
-
addonId: null,
|
|
85152
|
-
access: "view",
|
|
85153
|
-
role: "admin"
|
|
85154
|
-
},
|
|
85155
85173
|
"storage.read": {
|
|
85156
85174
|
capName: "storage",
|
|
85157
85175
|
capScope: "system",
|
|
@@ -85173,27 +85191,6 @@ Object.freeze({
|
|
|
85173
85191
|
access: "view",
|
|
85174
85192
|
role: "admin"
|
|
85175
85193
|
},
|
|
85176
|
-
"storage.testConfig": {
|
|
85177
|
-
capName: "storage",
|
|
85178
|
-
capScope: "system",
|
|
85179
|
-
addonId: null,
|
|
85180
|
-
access: "view",
|
|
85181
|
-
role: "admin"
|
|
85182
|
-
},
|
|
85183
|
-
"storage.testLocation": {
|
|
85184
|
-
capName: "storage",
|
|
85185
|
-
capScope: "system",
|
|
85186
|
-
addonId: null,
|
|
85187
|
-
access: "view",
|
|
85188
|
-
role: "admin"
|
|
85189
|
-
},
|
|
85190
|
-
"storage.upsertLocation": {
|
|
85191
|
-
capName: "storage",
|
|
85192
|
-
capScope: "system",
|
|
85193
|
-
addonId: null,
|
|
85194
|
-
access: "create",
|
|
85195
|
-
role: "admin"
|
|
85196
|
-
},
|
|
85197
85194
|
"storage.write": {
|
|
85198
85195
|
capName: "storage",
|
|
85199
85196
|
capScope: "system",
|
|
@@ -91346,20 +91343,6 @@ object({
|
|
|
91346
91343
|
});
|
|
91347
91344
|
//#endregion
|
|
91348
91345
|
//#region src/custom-actions.ts
|
|
91349
|
-
var SourceIdSchema = string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug");
|
|
91350
|
-
/**
|
|
91351
|
-
* A source as the admin list shows it. No `args`: a credential never leaves the
|
|
91352
|
-
* addon - `formValues` carries the redaction token where one is stored (D880).
|
|
91353
|
-
*/
|
|
91354
|
-
var WasteSourceRowSchema = object({
|
|
91355
|
-
id: string(),
|
|
91356
|
-
label: string(),
|
|
91357
|
-
module: string(),
|
|
91358
|
-
enabled: boolean(),
|
|
91359
|
-
formValues: record(string(), unknown()).optional(),
|
|
91360
|
-
serviceId: string().optional(),
|
|
91361
|
-
countryId: string().optional()
|
|
91362
|
-
});
|
|
91363
91346
|
var wasteCollectionsActions = defineCustomActions({
|
|
91364
91347
|
fetchNow: customAction(object({
|
|
91365
91348
|
/** When set, refresh only this configured source id; otherwise all enabled. */
|
|
@@ -91374,10 +91357,6 @@ sourceId: string().min(1).optional() }).optional(), object({
|
|
|
91374
91357
|
kind: "mutation",
|
|
91375
91358
|
auth: "admin"
|
|
91376
91359
|
}),
|
|
91377
|
-
listSources: customAction(object({}), array(WasteSourceRowSchema).readonly(), {
|
|
91378
|
-
kind: "mutation",
|
|
91379
|
-
auth: "admin"
|
|
91380
|
-
}),
|
|
91381
91360
|
listCountries: customAction(object({}), array(object({
|
|
91382
91361
|
id: string(),
|
|
91383
91362
|
label: string()
|
|
@@ -91395,43 +91374,6 @@ sourceId: string().min(1).optional() }).optional(), object({
|
|
|
91395
91374
|
})).readonly(), {
|
|
91396
91375
|
kind: "mutation",
|
|
91397
91376
|
auth: "admin"
|
|
91398
|
-
}),
|
|
91399
|
-
serviceFormSchema: customAction(object({ serviceId: string().min(1) }), unknown(), {
|
|
91400
|
-
kind: "mutation",
|
|
91401
|
-
auth: "admin"
|
|
91402
|
-
}),
|
|
91403
|
-
createSource: customAction(object({
|
|
91404
|
-
/** Optional override; when omitted the id is derived from `label`. */
|
|
91405
|
-
id: SourceIdSchema.optional(),
|
|
91406
|
-
label: string().min(1),
|
|
91407
|
-
serviceId: string().min(1),
|
|
91408
|
-
args: record(string(), unknown()).default({}),
|
|
91409
|
-
enabled: boolean().optional()
|
|
91410
|
-
}), object({ id: string() }), {
|
|
91411
|
-
kind: "mutation",
|
|
91412
|
-
auth: "admin"
|
|
91413
|
-
}),
|
|
91414
|
-
updateSource: customAction(object({
|
|
91415
|
-
id: string().min(1),
|
|
91416
|
-
label: string().min(1),
|
|
91417
|
-
serviceId: string().min(1).optional(),
|
|
91418
|
-
args: record(string(), unknown()).optional(),
|
|
91419
|
-
enabled: boolean().optional()
|
|
91420
|
-
}), object({ ok: literal(true) }), {
|
|
91421
|
-
kind: "mutation",
|
|
91422
|
-
auth: "admin"
|
|
91423
|
-
}),
|
|
91424
|
-
/**
|
|
91425
|
-
* D880: rewrite every legacy source (free `argsJson`) as typed `args`, moving its
|
|
91426
|
-
* credentials into the keychain. Dry run unless `apply`; idempotent; counts only.
|
|
91427
|
-
*/
|
|
91428
|
-
migrateSources: customAction(RawSecretMigrationInputSchema, RawSecretMigrationReportSchema, {
|
|
91429
|
-
kind: "mutation",
|
|
91430
|
-
auth: "admin"
|
|
91431
|
-
}),
|
|
91432
|
-
deleteSource: customAction(object({ id: string().min(1) }), object({ ok: literal(true) }), {
|
|
91433
|
-
kind: "mutation",
|
|
91434
|
-
auth: "admin"
|
|
91435
91377
|
})
|
|
91436
91378
|
});
|
|
91437
91379
|
//#endregion
|
|
@@ -91465,33 +91407,6 @@ var WASTE_SECRET_STATE_PATHS = SECRET_ARG_KEYS.map((key) => `sources[id].args.${
|
|
|
91465
91407
|
function isSecretArg(key) {
|
|
91466
91408
|
return SECRET_ARG_KEYS.includes(key.toLowerCase());
|
|
91467
91409
|
}
|
|
91468
|
-
/** Is a credential stored for this argument (a value, or a marker the keychain could not serve)? */
|
|
91469
|
-
function isSet(value) {
|
|
91470
|
-
return isSecretMarker(value) || typeof value === "string" && value.length > 0;
|
|
91471
|
-
}
|
|
91472
|
-
/** The arguments as a read surface may show them: a stored credential is the redaction token. */
|
|
91473
|
-
function redactArgs(args) {
|
|
91474
|
-
const out = {};
|
|
91475
|
-
for (const [key, value] of Object.entries(args)) out[key] = isSecretArg(key) && isSet(value) ? REDACTED_SECRET : value;
|
|
91476
|
-
return out;
|
|
91477
|
-
}
|
|
91478
|
-
/**
|
|
91479
|
-
* The write-back of {@link redactArgs}: a credential sent back as the redaction
|
|
91480
|
-
* token means "keep what is stored", so it takes the stored (resolved) value; one
|
|
91481
|
-
* with nothing stored is dropped, never saved as the token.
|
|
91482
|
-
*/
|
|
91483
|
-
function restoreRedactedArgs(incoming, stored) {
|
|
91484
|
-
const out = {};
|
|
91485
|
-
for (const [key, value] of Object.entries(incoming)) {
|
|
91486
|
-
if (isSecretArg(key) && value === "__camstack_redacted__") {
|
|
91487
|
-
const previous = stored[key];
|
|
91488
|
-
if (previous !== void 0) out[key] = previous;
|
|
91489
|
-
continue;
|
|
91490
|
-
}
|
|
91491
|
-
out[key] = value;
|
|
91492
|
-
}
|
|
91493
|
-
return out;
|
|
91494
|
-
}
|
|
91495
91410
|
/**
|
|
91496
91411
|
* Does this argument bag hold a credential that is NOT a usable value: a MARKER the keychain did
|
|
91497
91412
|
* not resolve, or the redaction token (a row read without its credential)? Either would reach a
|
|
@@ -91500,10 +91415,6 @@ function restoreRedactedArgs(incoming, stored) {
|
|
|
91500
91415
|
function hasUnresolvedCredential(args) {
|
|
91501
91416
|
return Object.entries(args).some(([key, value]) => isSecretArg(key) && (isSecretMarker(value) || value === "__camstack_redacted__"));
|
|
91502
91417
|
}
|
|
91503
|
-
/** Does it hold a non-empty credential (any form)? */
|
|
91504
|
-
function hasCredential(args) {
|
|
91505
|
-
return Object.entries(args).some(([key, value]) => isSecretArg(key) && isSet(value));
|
|
91506
|
-
}
|
|
91507
91418
|
//#endregion
|
|
91508
91419
|
//#region src/waste-types.ts
|
|
91509
91420
|
/**
|
|
@@ -91785,42 +91696,6 @@ async function copySourcesToRecords(ports) {
|
|
|
91785
91696
|
}
|
|
91786
91697
|
};
|
|
91787
91698
|
}
|
|
91788
|
-
/** The `waste-collections` catalogue (D987): offered to the engine when the addon initialises. */
|
|
91789
|
-
var ADDON_MIGRATIONS = [{
|
|
91790
|
-
meta: {
|
|
91791
|
-
id: "waste-collections/2026-11-06-sources-from-settings-and-argsjson",
|
|
91792
|
-
owner: "waste-collections",
|
|
91793
|
-
level: "addon-state",
|
|
91794
|
-
seq: 1,
|
|
91795
|
-
dependsOn: [],
|
|
91796
|
-
store: "settings",
|
|
91797
|
-
scope: "cluster",
|
|
91798
|
-
onFailure: "tolerate",
|
|
91799
|
-
bodyKind: "procedure",
|
|
91800
|
-
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",
|
|
91801
|
-
fixtureMark: "the credential of a legacy argsJson row, read back from its record"
|
|
91802
|
-
},
|
|
91803
|
-
procedure: copySourcesToRecords
|
|
91804
|
-
}];
|
|
91805
|
-
//#endregion
|
|
91806
|
-
//#region src/python-paths.ts
|
|
91807
|
-
/**
|
|
91808
|
-
* Locate the addon's bundled `python/` dir at runtime (deployed package
|
|
91809
|
-
* root or workspace package root). Same candidate walk as detection-pipeline.
|
|
91810
|
-
*/
|
|
91811
|
-
function resolvePythonDir(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
91812
|
-
const here = (0, node_path.dirname)((0, node_url.fileURLToPath)(fromModuleUrl));
|
|
91813
|
-
const candidates = [
|
|
91814
|
-
(0, node_path.join)(here, "../python"),
|
|
91815
|
-
(0, node_path.join)(here, "../../python"),
|
|
91816
|
-
(0, node_path.join)(here, "python")
|
|
91817
|
-
];
|
|
91818
|
-
for (const c of candidates) if ((0, node_fs.existsSync)((0, node_path.join)(c, "fetch_source.py"))) return c;
|
|
91819
|
-
throw new Error(`addon-waste-collections: python/ dir not found. Searched:\n${candidates.join("\n")}`);
|
|
91820
|
-
}
|
|
91821
|
-
function resolveFetchScript(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
91822
|
-
return (0, node_path.join)(resolvePythonDir(fromModuleUrl), "fetch_source.py");
|
|
91823
|
-
}
|
|
91824
91699
|
//#endregion
|
|
91825
91700
|
//#region src/waste-source-record-kinds.ts
|
|
91826
91701
|
var WASTE_COLLECTIONS_OWNER = "waste-collections";
|
|
@@ -91866,6 +91741,202 @@ var plainWasteSourceKind = defineEntryKind("waste-source", {
|
|
|
91866
91741
|
writeTrigger: WRITE_TRIGGER
|
|
91867
91742
|
});
|
|
91868
91743
|
//#endregion
|
|
91744
|
+
//#region src/waste-source-store.ts
|
|
91745
|
+
/**
|
|
91746
|
+
* A stored body as a source. A row read while the keychain is locked still carries its markers
|
|
91747
|
+
* in `args`; the config schema keeps them, so the source lists and its fetch refuses by name.
|
|
91748
|
+
*/
|
|
91749
|
+
function rowToSource(id, data) {
|
|
91750
|
+
const parsed = WasteSourceConfigSchema.safeParse({
|
|
91751
|
+
id,
|
|
91752
|
+
label: data.name,
|
|
91753
|
+
module: data.module,
|
|
91754
|
+
args: data.args,
|
|
91755
|
+
enabled: data.enabled
|
|
91756
|
+
});
|
|
91757
|
+
return parsed.success ? parsed.data : null;
|
|
91758
|
+
}
|
|
91759
|
+
function bodyOf(source) {
|
|
91760
|
+
const { id, label, module, args, enabled } = source;
|
|
91761
|
+
const body = {
|
|
91762
|
+
name: label,
|
|
91763
|
+
module,
|
|
91764
|
+
args,
|
|
91765
|
+
enabled
|
|
91766
|
+
};
|
|
91767
|
+
const parsed = wasteSourceEntryKind.schema.safeParse(body);
|
|
91768
|
+
if (!parsed.success) throw new Error(`invalid waste source '${id}': ${parsed.error.message}`);
|
|
91769
|
+
return parsed.data;
|
|
91770
|
+
}
|
|
91771
|
+
/**
|
|
91772
|
+
* While the sources migration has not landed: every record, plus the legacy rows that have no
|
|
91773
|
+
* record yet (a read-only view of those). A legacy row leaves the view as soon as a record has
|
|
91774
|
+
* its id, so the two never diverge; writes go to the records.
|
|
91775
|
+
*/
|
|
91776
|
+
function withUnmigratedLegacyRows(records, legacy) {
|
|
91777
|
+
const have = new Set(records.map((r) => r.id));
|
|
91778
|
+
return [...records, ...legacy.filter((r) => !have.has(r.id))];
|
|
91779
|
+
}
|
|
91780
|
+
var WasteSourceStore = class WasteSourceStore {
|
|
91781
|
+
kind;
|
|
91782
|
+
constructor(kind) {
|
|
91783
|
+
this.kind = kind;
|
|
91784
|
+
}
|
|
91785
|
+
static of(records, kind = wasteSourceEntryKind) {
|
|
91786
|
+
return new WasteSourceStore(records.kind(kind));
|
|
91787
|
+
}
|
|
91788
|
+
/** The record ids, whatever their content. */
|
|
91789
|
+
async listIds() {
|
|
91790
|
+
return (await this.kind.query({ limit: 1e3 })).records.map((record) => record.id);
|
|
91791
|
+
}
|
|
91792
|
+
/** One source with its credentials resolved; markers stay where the keychain cannot serve. */
|
|
91793
|
+
async readResolved(id) {
|
|
91794
|
+
const stored = await this.kind.get(id);
|
|
91795
|
+
if (stored === null) return null;
|
|
91796
|
+
let data = stored.data;
|
|
91797
|
+
try {
|
|
91798
|
+
const resolved = await this.kind.getResolved(id);
|
|
91799
|
+
if (resolved !== null) data = resolved.data;
|
|
91800
|
+
} catch (err) {
|
|
91801
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
91802
|
+
}
|
|
91803
|
+
return rowToSource(id, data);
|
|
91804
|
+
}
|
|
91805
|
+
async listResolved() {
|
|
91806
|
+
const out = [];
|
|
91807
|
+
for (const id of await this.listIds()) {
|
|
91808
|
+
const row = await this.readResolved(id);
|
|
91809
|
+
if (row !== null) out.push(row);
|
|
91810
|
+
}
|
|
91811
|
+
return out;
|
|
91812
|
+
}
|
|
91813
|
+
/** Create the record when the id is free; an existing record is never touched. */
|
|
91814
|
+
async putIfAbsent(source) {
|
|
91815
|
+
if (await this.kind.get(source.id) !== null) return false;
|
|
91816
|
+
await this.kind.put({
|
|
91817
|
+
id: source.id,
|
|
91818
|
+
data: bodyOf(source)
|
|
91819
|
+
}, { ifRev: 0 });
|
|
91820
|
+
return true;
|
|
91821
|
+
}
|
|
91822
|
+
/**
|
|
91823
|
+
* Make the records equal `sources`. A source whose credential the keychain did not serve is
|
|
91824
|
+
* left exactly as stored: rewriting it would save a marker as the credential.
|
|
91825
|
+
*/
|
|
91826
|
+
async replaceAll(sources) {
|
|
91827
|
+
const nextIds = new Set(sources.map((s) => s.id));
|
|
91828
|
+
for (const id of await this.listIds()) if (!nextIds.has(id)) await this.kind.delete(id);
|
|
91829
|
+
for (const source of sources) {
|
|
91830
|
+
if (hasUnresolvedCredential(source.args)) continue;
|
|
91831
|
+
await this.kind.put({
|
|
91832
|
+
id: source.id,
|
|
91833
|
+
data: bodyOf(source)
|
|
91834
|
+
});
|
|
91835
|
+
}
|
|
91836
|
+
}
|
|
91837
|
+
};
|
|
91838
|
+
//#endregion
|
|
91839
|
+
//#region src/migrations/ports.ts
|
|
91840
|
+
var PAGE = 1e3;
|
|
91841
|
+
/**
|
|
91842
|
+
* The sources migration's ports over {@link MigrationApiV1}, and nothing else (planning#447): the
|
|
91843
|
+
* addon's own settings store, its keychain handle and its `waste-source` record kind.
|
|
91844
|
+
*/
|
|
91845
|
+
function wasteSourcePortsOver(api, kind) {
|
|
91846
|
+
const handle = api.records.kind(kind);
|
|
91847
|
+
const secrets = { resolve: async (refs) => {
|
|
91848
|
+
const { values } = await api.secrets.resolve({ refs: [...refs] });
|
|
91849
|
+
return Object.fromEntries(refs.map((ref) => {
|
|
91850
|
+
const value = values[ref];
|
|
91851
|
+
return [ref, value === null || value === void 0 ? { kind: "missing" } : {
|
|
91852
|
+
kind: "value",
|
|
91853
|
+
value,
|
|
91854
|
+
version: 0
|
|
91855
|
+
}];
|
|
91856
|
+
}));
|
|
91857
|
+
} };
|
|
91858
|
+
const readStored = async (key) => {
|
|
91859
|
+
let store;
|
|
91860
|
+
try {
|
|
91861
|
+
store = (await api.settings.read({})).store;
|
|
91862
|
+
} catch (err) {
|
|
91863
|
+
return {
|
|
91864
|
+
kind: "unreadable",
|
|
91865
|
+
reason: "store-read-failed",
|
|
91866
|
+
error: err instanceof Error ? err.message : String(err)
|
|
91867
|
+
};
|
|
91868
|
+
}
|
|
91869
|
+
if (!Object.hasOwn(store, key)) return { kind: "absent" };
|
|
91870
|
+
const { value } = await resolveSecretMarkers(store[key], secrets, { unresolved: "keep-marker" });
|
|
91871
|
+
return {
|
|
91872
|
+
kind: "present",
|
|
91873
|
+
value
|
|
91874
|
+
};
|
|
91875
|
+
};
|
|
91876
|
+
const asRow = (row) => rowToSource(row.id, row.data);
|
|
91877
|
+
return {
|
|
91878
|
+
readStored,
|
|
91879
|
+
listSourceIds: async () => (await handle.query({ limit: PAGE })).records.map((row) => row.id),
|
|
91880
|
+
putSource: async (source) => {
|
|
91881
|
+
if (await handle.get({ id: source.id }) !== null) return;
|
|
91882
|
+
await handle.put({
|
|
91883
|
+
id: source.id,
|
|
91884
|
+
data: bodyOf(source),
|
|
91885
|
+
ifRev: 0
|
|
91886
|
+
});
|
|
91887
|
+
},
|
|
91888
|
+
readSource: async (id) => {
|
|
91889
|
+
const stored = await handle.get({ id });
|
|
91890
|
+
if (stored === null) return null;
|
|
91891
|
+
try {
|
|
91892
|
+
return asRow(await handle.getResolved({ id }) ?? stored);
|
|
91893
|
+
} catch (err) {
|
|
91894
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
91895
|
+
return asRow(stored);
|
|
91896
|
+
}
|
|
91897
|
+
},
|
|
91898
|
+
warn: (message) => {
|
|
91899
|
+
api.log.warn({ message });
|
|
91900
|
+
}
|
|
91901
|
+
};
|
|
91902
|
+
}
|
|
91903
|
+
/** The `waste-collections` catalogue (D987): offered to the engine when the addon initialises. */
|
|
91904
|
+
var ADDON_MIGRATIONS = [{
|
|
91905
|
+
meta: {
|
|
91906
|
+
id: "waste-collections/2026-11-06-sources-from-settings-and-argsjson",
|
|
91907
|
+
owner: "waste-collections",
|
|
91908
|
+
level: "addon-state",
|
|
91909
|
+
seq: 1,
|
|
91910
|
+
dependsOn: [],
|
|
91911
|
+
store: "settings",
|
|
91912
|
+
scope: "cluster",
|
|
91913
|
+
onFailure: "tolerate",
|
|
91914
|
+
bodyKind: "procedure",
|
|
91915
|
+
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",
|
|
91916
|
+
fixtureMark: "the credential of a legacy argsJson row, read back from its record"
|
|
91917
|
+
},
|
|
91918
|
+
procedure: copySourcesToRecords
|
|
91919
|
+
}];
|
|
91920
|
+
//#endregion
|
|
91921
|
+
//#region src/python-paths.ts
|
|
91922
|
+
/**
|
|
91923
|
+
* Locate the addon's bundled `python/` dir at runtime (deployed package
|
|
91924
|
+
* root or workspace package root). Same candidate walk as detection-pipeline.
|
|
91925
|
+
*/
|
|
91926
|
+
function resolvePythonDir(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
91927
|
+
const here = (0, node_path.dirname)((0, node_url.fileURLToPath)(fromModuleUrl));
|
|
91928
|
+
const candidates = [
|
|
91929
|
+
(0, node_path.join)(here, "../python"),
|
|
91930
|
+
(0, node_path.join)(here, "../../python"),
|
|
91931
|
+
(0, node_path.join)(here, "python")
|
|
91932
|
+
];
|
|
91933
|
+
for (const c of candidates) if ((0, node_fs.existsSync)((0, node_path.join)(c, "fetch_source.py"))) return c;
|
|
91934
|
+
throw new Error(`addon-waste-collections: python/ dir not found. Searched:\n${candidates.join("\n")}`);
|
|
91935
|
+
}
|
|
91936
|
+
function resolveFetchScript(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
91937
|
+
return (0, node_path.join)(resolvePythonDir(fromModuleUrl), "fetch_source.py");
|
|
91938
|
+
}
|
|
91939
|
+
//#endregion
|
|
91869
91940
|
//#region src/seal-plaintext.ts
|
|
91870
91941
|
/**
|
|
91871
91942
|
* A credential an older version stored as plain text in a `waste-source` record is moved into the
|
|
@@ -91968,35 +92039,6 @@ function reportFailed(log, failures) {
|
|
|
91968
92039
|
};
|
|
91969
92040
|
}
|
|
91970
92041
|
//#endregion
|
|
91971
|
-
//#region src/source-id.ts
|
|
91972
|
-
/**
|
|
91973
|
-
* Stable calendar source ids derived from the operator label.
|
|
91974
|
-
* Pattern: `^[a-z0-9][a-z0-9-]{0,63}$` (max 64 chars).
|
|
91975
|
-
*/
|
|
91976
|
-
var SOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
91977
|
-
/** Slugify a display label into a candidate source id (may be empty). */
|
|
91978
|
-
function slugifySourceLabel(label) {
|
|
91979
|
-
const s = label.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
91980
|
-
if (s.length === 0) return "";
|
|
91981
|
-
return s.slice(0, 64);
|
|
91982
|
-
}
|
|
91983
|
-
/**
|
|
91984
|
-
* Pick an unused id from `label`. Collisions append `-2`, `-3`, …
|
|
91985
|
-
* (truncated so the result stays within the 64-char pattern).
|
|
91986
|
-
*/
|
|
91987
|
-
function allocateSourceId(label, existingIds) {
|
|
91988
|
-
const taken = existingIds instanceof Set ? existingIds : new Set(existingIds);
|
|
91989
|
-
const baseRaw = slugifySourceLabel(label);
|
|
91990
|
-
const base = baseRaw.length > 0 ? baseRaw : "source";
|
|
91991
|
-
if (!taken.has(base) && SOURCE_ID_PATTERN.test(base)) return base;
|
|
91992
|
-
for (let n = 2; n < 1e4; n++) {
|
|
91993
|
-
const suffix = `-${n}`;
|
|
91994
|
-
const candidate = `${base.slice(0, Math.max(1, 64 - suffix.length)).replace(/-+$/g, "")}${suffix}`;
|
|
91995
|
-
if (SOURCE_ID_PATTERN.test(candidate) && !taken.has(candidate)) return candidate;
|
|
91996
|
-
}
|
|
91997
|
-
throw new Error("could not allocate a unique source id");
|
|
91998
|
-
}
|
|
91999
|
-
//#endregion
|
|
92000
92042
|
//#region src/waste-calendar-source.ts
|
|
92001
92043
|
function startOfUtcDayMs(isoDate) {
|
|
92002
92044
|
const [y, m, d] = isoDate.split("-").map((p) => Number(p));
|
|
@@ -178041,100 +178083,34 @@ function mergeServiceArgs(service, formValues) {
|
|
|
178041
178083
|
return out;
|
|
178042
178084
|
}
|
|
178043
178085
|
//#endregion
|
|
178044
|
-
//#region src/
|
|
178086
|
+
//#region src/source-id.ts
|
|
178045
178087
|
/**
|
|
178046
|
-
*
|
|
178047
|
-
*
|
|
178088
|
+
* Stable calendar source ids derived from the operator label.
|
|
178089
|
+
* Pattern: `^[a-z0-9][a-z0-9-]{0,63}$` (max 64 chars).
|
|
178048
178090
|
*/
|
|
178049
|
-
|
|
178050
|
-
|
|
178051
|
-
|
|
178052
|
-
|
|
178053
|
-
|
|
178054
|
-
|
|
178055
|
-
enabled: data.enabled
|
|
178056
|
-
});
|
|
178057
|
-
return parsed.success ? parsed.data : null;
|
|
178058
|
-
}
|
|
178059
|
-
function bodyOf(source) {
|
|
178060
|
-
const { id, label, module, args, enabled } = source;
|
|
178061
|
-
const body = {
|
|
178062
|
-
name: label,
|
|
178063
|
-
module,
|
|
178064
|
-
args,
|
|
178065
|
-
enabled
|
|
178066
|
-
};
|
|
178067
|
-
const parsed = wasteSourceEntryKind.schema.safeParse(body);
|
|
178068
|
-
if (!parsed.success) throw new Error(`invalid waste source '${id}': ${parsed.error.message}`);
|
|
178069
|
-
return parsed.data;
|
|
178091
|
+
var SOURCE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
178092
|
+
/** Slugify a display label into a candidate source id (may be empty). */
|
|
178093
|
+
function slugifySourceLabel(label) {
|
|
178094
|
+
const s = label.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
178095
|
+
if (s.length === 0) return "";
|
|
178096
|
+
return s.slice(0, 64);
|
|
178070
178097
|
}
|
|
178071
178098
|
/**
|
|
178072
|
-
*
|
|
178073
|
-
*
|
|
178074
|
-
* its id, so the two never diverge; writes go to the records.
|
|
178099
|
+
* Pick an unused id from `label`. Collisions append `-2`, `-3`, …
|
|
178100
|
+
* (truncated so the result stays within the 64-char pattern).
|
|
178075
178101
|
*/
|
|
178076
|
-
function
|
|
178077
|
-
const
|
|
178078
|
-
|
|
178079
|
-
|
|
178080
|
-
|
|
178081
|
-
|
|
178082
|
-
|
|
178083
|
-
|
|
178084
|
-
|
|
178085
|
-
static of(records, kind = wasteSourceEntryKind) {
|
|
178086
|
-
return new WasteSourceStore(records.kind(kind));
|
|
178087
|
-
}
|
|
178088
|
-
/** The record ids, whatever their content. */
|
|
178089
|
-
async listIds() {
|
|
178090
|
-
return (await this.kind.query({ limit: 1e3 })).records.map((record) => record.id);
|
|
178091
|
-
}
|
|
178092
|
-
/** One source with its credentials resolved; markers stay where the keychain cannot serve. */
|
|
178093
|
-
async readResolved(id) {
|
|
178094
|
-
const stored = await this.kind.get(id);
|
|
178095
|
-
if (stored === null) return null;
|
|
178096
|
-
let data = stored.data;
|
|
178097
|
-
try {
|
|
178098
|
-
const resolved = await this.kind.getResolved(id);
|
|
178099
|
-
if (resolved !== null) data = resolved.data;
|
|
178100
|
-
} catch (err) {
|
|
178101
|
-
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
178102
|
-
}
|
|
178103
|
-
return rowToSource(id, data);
|
|
178104
|
-
}
|
|
178105
|
-
async listResolved() {
|
|
178106
|
-
const out = [];
|
|
178107
|
-
for (const id of await this.listIds()) {
|
|
178108
|
-
const row = await this.readResolved(id);
|
|
178109
|
-
if (row !== null) out.push(row);
|
|
178110
|
-
}
|
|
178111
|
-
return out;
|
|
178112
|
-
}
|
|
178113
|
-
/** Create the record when the id is free; an existing record is never touched. */
|
|
178114
|
-
async putIfAbsent(source) {
|
|
178115
|
-
if (await this.kind.get(source.id) !== null) return false;
|
|
178116
|
-
await this.kind.put({
|
|
178117
|
-
id: source.id,
|
|
178118
|
-
data: bodyOf(source)
|
|
178119
|
-
}, { ifRev: 0 });
|
|
178120
|
-
return true;
|
|
178121
|
-
}
|
|
178122
|
-
/**
|
|
178123
|
-
* Make the records equal `sources`. A source whose credential the keychain did not serve is
|
|
178124
|
-
* left exactly as stored: rewriting it would save a marker as the credential.
|
|
178125
|
-
*/
|
|
178126
|
-
async replaceAll(sources) {
|
|
178127
|
-
const nextIds = new Set(sources.map((s) => s.id));
|
|
178128
|
-
for (const id of await this.listIds()) if (!nextIds.has(id)) await this.kind.delete(id);
|
|
178129
|
-
for (const source of sources) {
|
|
178130
|
-
if (hasUnresolvedCredential(source.args)) continue;
|
|
178131
|
-
await this.kind.put({
|
|
178132
|
-
id: source.id,
|
|
178133
|
-
data: bodyOf(source)
|
|
178134
|
-
});
|
|
178135
|
-
}
|
|
178102
|
+
function allocateSourceId(label, existingIds) {
|
|
178103
|
+
const taken = existingIds instanceof Set ? existingIds : new Set(existingIds);
|
|
178104
|
+
const baseRaw = slugifySourceLabel(label);
|
|
178105
|
+
const base = baseRaw.length > 0 ? baseRaw : "source";
|
|
178106
|
+
if (!taken.has(base) && SOURCE_ID_PATTERN.test(base)) return base;
|
|
178107
|
+
for (let n = 2; n < 1e4; n++) {
|
|
178108
|
+
const suffix = `-${n}`;
|
|
178109
|
+
const candidate = `${base.slice(0, Math.max(1, 64 - suffix.length)).replace(/-+$/g, "")}${suffix}`;
|
|
178110
|
+
if (SOURCE_ID_PATTERN.test(candidate) && !taken.has(candidate)) return candidate;
|
|
178136
178111
|
}
|
|
178137
|
-
|
|
178112
|
+
throw new Error("could not allocate a unique source id");
|
|
178113
|
+
}
|
|
178138
178114
|
//#endregion
|
|
178139
178115
|
//#region src/waste-source-instances.ts
|
|
178140
178116
|
/**
|
|
@@ -178496,7 +178472,7 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178496
178472
|
if (!plainBoot) await records.declare({ "waste-source": wasteSourceEntryKind });
|
|
178497
178473
|
const store = WasteSourceStore.of(records, plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178498
178474
|
this.sourceStore = store;
|
|
178499
|
-
await this.migrateLegacySources(
|
|
178475
|
+
await this.migrateLegacySources(plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178500
178476
|
this.activeSources = await this.loadPersistedSources();
|
|
178501
178477
|
const instances = plainBoot ? null : new WasteSourceInstances(records, {
|
|
178502
178478
|
onChanged: (id, change) => this.onInstanceChanged(id, change),
|
|
@@ -178544,23 +178520,13 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178544
178520
|
customActions: wasteCollectionsActions,
|
|
178545
178521
|
actionHandlers: {
|
|
178546
178522
|
fetchNow: async (input) => this.fetchNow(input),
|
|
178547
|
-
listSources: async () => this.listSources(),
|
|
178548
178523
|
listCountries: async () => listCatalogCountries(),
|
|
178549
178524
|
listServices: async (input) => listCatalogServices(input.countryId).map((s) => ({
|
|
178550
178525
|
id: s.id,
|
|
178551
178526
|
label: serviceDisplayLabel(s),
|
|
178552
178527
|
title: s.label,
|
|
178553
178528
|
module: s.module
|
|
178554
|
-
}))
|
|
178555
|
-
serviceFormSchema: async (input) => {
|
|
178556
|
-
const service = findCatalogService(input.serviceId);
|
|
178557
|
-
if (!service) throw new Error(`unknown service '${input.serviceId}'`);
|
|
178558
|
-
return serviceFormSchema(service);
|
|
178559
|
-
},
|
|
178560
|
-
createSource: async (input) => this.createSource(input),
|
|
178561
|
-
updateSource: async (input) => this.updateSource(input),
|
|
178562
|
-
deleteSource: async (input) => this.deleteSource(input),
|
|
178563
|
-
migrateSources: async (input) => this.migrateSources(input.apply)
|
|
178529
|
+
}))
|
|
178564
178530
|
}
|
|
178565
178531
|
};
|
|
178566
178532
|
}
|
|
@@ -178588,16 +178554,8 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178588
178554
|
* fails leaves the settings where they are and is logged by id: the sources stay unavailable
|
|
178589
178555
|
* until it lands, never half-copied.
|
|
178590
178556
|
*/
|
|
178591
|
-
async migrateLegacySources(
|
|
178592
|
-
const report = await this.runMigrations(
|
|
178593
|
-
readStored: (key) => this.readStoredGlobalKey(key),
|
|
178594
|
-
listSourceIds: () => store.listIds(),
|
|
178595
|
-
putSource: async (source) => {
|
|
178596
|
-
await store.putIfAbsent(source);
|
|
178597
|
-
},
|
|
178598
|
-
readSource: (id) => store.readResolved(id),
|
|
178599
|
-
warn: (message) => this.ctx.logger.warn(message)
|
|
178600
|
-
}));
|
|
178557
|
+
async migrateLegacySources(kind) {
|
|
178558
|
+
const report = await this.runMigrations(ADDON_MIGRATIONS, (api) => wasteSourcePortsOver(api, kind));
|
|
178601
178559
|
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");
|
|
178602
178560
|
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: {
|
|
178603
178561
|
declared: report.declared,
|
|
@@ -178627,17 +178585,6 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178627
178585
|
if (!this.legacyServed) return records;
|
|
178628
178586
|
return withUnmigratedLegacyRows(records, this.parseConfigSources(this.config.sources));
|
|
178629
178587
|
}
|
|
178630
|
-
async persistSources(sources) {
|
|
178631
|
-
this.activeSources = [...sources];
|
|
178632
|
-
const store = this.sourceStore;
|
|
178633
|
-
if (store !== null) {
|
|
178634
|
-
await store.replaceAll(sources);
|
|
178635
|
-
const kept = new Set(sources.map((s) => s.id));
|
|
178636
|
-
await this.pruneLegacyRows((legacyId) => kept.has(legacyId));
|
|
178637
|
-
return;
|
|
178638
|
-
}
|
|
178639
|
-
await this.updateGlobalSettings({ sources: [...sources] });
|
|
178640
|
-
}
|
|
178641
178588
|
/**
|
|
178642
178589
|
* A source deleted at the records leaves the legacy setting too, or it would reappear beside the
|
|
178643
178590
|
* records (while the migration is pending) or be copied back by it.
|
|
@@ -178652,195 +178599,6 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178652
178599
|
if (this.activeSources.length > 0 || this.sourceStore !== null) return this.activeSources;
|
|
178653
178600
|
return this.parseConfigSources(this.config.sources);
|
|
178654
178601
|
}
|
|
178655
|
-
listSources() {
|
|
178656
|
-
return this.configuredSources().map((s) => {
|
|
178657
|
-
let args = {};
|
|
178658
|
-
try {
|
|
178659
|
-
args = sourceArgs(s);
|
|
178660
|
-
} catch {
|
|
178661
|
-
args = { ...s.args };
|
|
178662
|
-
}
|
|
178663
|
-
const matched = matchCatalogService(s.module, args);
|
|
178664
|
-
const formValues = {
|
|
178665
|
-
label: s.label,
|
|
178666
|
-
enabled: s.enabled,
|
|
178667
|
-
...redactArgs(args)
|
|
178668
|
-
};
|
|
178669
|
-
if (matched) for (const key of Object.keys(matched.defaultArgs)) delete formValues[key];
|
|
178670
|
-
return {
|
|
178671
|
-
id: s.id,
|
|
178672
|
-
label: s.label,
|
|
178673
|
-
module: s.module,
|
|
178674
|
-
enabled: s.enabled,
|
|
178675
|
-
formValues,
|
|
178676
|
-
serviceId: matched?.id,
|
|
178677
|
-
countryId: matched?.countryId
|
|
178678
|
-
};
|
|
178679
|
-
});
|
|
178680
|
-
}
|
|
178681
|
-
async createSource(input) {
|
|
178682
|
-
const existing = this.configuredSources().map((s) => s.id);
|
|
178683
|
-
let id;
|
|
178684
|
-
if (input.id !== void 0) {
|
|
178685
|
-
if (!SOURCE_ID_PATTERN.test(input.id)) throw new Error("id must be a lowercase slug (e.g. ics-home, junker)");
|
|
178686
|
-
if (existing.includes(input.id)) throw new Error(`source '${input.id}' already exists`);
|
|
178687
|
-
id = input.id;
|
|
178688
|
-
} else id = allocateSourceId(input.label, existing);
|
|
178689
|
-
const service = findCatalogService(input.serviceId);
|
|
178690
|
-
if (!service) throw new Error(`unknown service '${input.serviceId}'`);
|
|
178691
|
-
const row = WasteSourceConfigSchema.parse({
|
|
178692
|
-
id,
|
|
178693
|
-
label: input.label,
|
|
178694
|
-
module: service.module,
|
|
178695
|
-
args: this.typedArgs(mergeServiceArgs(service, restoreRedactedArgs(input.args, {}))),
|
|
178696
|
-
enabled: input.enabled ?? true
|
|
178697
|
-
});
|
|
178698
|
-
await this.persistSources([...this.configuredSources(), row]);
|
|
178699
|
-
return { id };
|
|
178700
|
-
}
|
|
178701
|
-
async updateSource(input) {
|
|
178702
|
-
const sources = this.configuredSources();
|
|
178703
|
-
const idx = sources.findIndex((s) => s.id === input.id);
|
|
178704
|
-
if (idx < 0) throw new Error(`source '${input.id}' not found`);
|
|
178705
|
-
const prev = sources[idx];
|
|
178706
|
-
let prevArgs = {};
|
|
178707
|
-
try {
|
|
178708
|
-
prevArgs = sourceArgs(prev);
|
|
178709
|
-
} catch {
|
|
178710
|
-
prevArgs = { ...prev.args };
|
|
178711
|
-
}
|
|
178712
|
-
let module = prev.module;
|
|
178713
|
-
let args = prevArgs;
|
|
178714
|
-
if (input.serviceId !== void 0) {
|
|
178715
|
-
const service = findCatalogService(input.serviceId);
|
|
178716
|
-
if (!service) throw new Error(`unknown service '${input.serviceId}'`);
|
|
178717
|
-
module = service.module;
|
|
178718
|
-
args = mergeServiceArgs(service, restoreRedactedArgs(input.args ?? {}, prevArgs));
|
|
178719
|
-
} else if (input.args !== void 0) {
|
|
178720
|
-
const sent = restoreRedactedArgs(input.args, prevArgs);
|
|
178721
|
-
const matched = matchCatalogService(prev.module, prevArgs);
|
|
178722
|
-
args = matched ? mergeServiceArgs(matched, sent) : {
|
|
178723
|
-
...prevArgs,
|
|
178724
|
-
...sent
|
|
178725
|
-
};
|
|
178726
|
-
}
|
|
178727
|
-
const next = WasteSourceConfigSchema.parse({
|
|
178728
|
-
id: prev.id,
|
|
178729
|
-
label: input.label,
|
|
178730
|
-
module,
|
|
178731
|
-
args: this.typedArgs(args),
|
|
178732
|
-
enabled: input.enabled ?? prev.enabled
|
|
178733
|
-
});
|
|
178734
|
-
await this.persistSources(sources.map((s, i) => i === idx ? next : s));
|
|
178735
|
-
return { ok: true };
|
|
178736
|
-
}
|
|
178737
|
-
/** Validate an argument bag against the typed value schema (a nested object is refused). */
|
|
178738
|
-
typedArgs(args) {
|
|
178739
|
-
const out = {};
|
|
178740
|
-
for (const [key, value] of Object.entries(args)) out[key] = WasteArgValueSchema.parse(value);
|
|
178741
|
-
return out;
|
|
178742
|
-
}
|
|
178743
|
-
/**
|
|
178744
|
-
* D880: rewrite every legacy source (free `argsJson`) as typed `args`; its
|
|
178745
|
-
* credentials go to the keychain through the normal save path. Then every
|
|
178746
|
-
* credential is read back and compared with what the legacy row held; on any
|
|
178747
|
-
* difference the legacy rows are written back, so nothing is lost. Dry run
|
|
178748
|
-
* unless `apply`. Idempotent. Counts only - never an argument, a ref or an id.
|
|
178749
|
-
*/
|
|
178750
|
-
async migrateSources(apply) {
|
|
178751
|
-
const sources = this.configuredSources();
|
|
178752
|
-
const legacy = sources.filter((s) => s.argsJson !== void 0);
|
|
178753
|
-
const rawRows = legacy.filter((s) => {
|
|
178754
|
-
try {
|
|
178755
|
-
return hasCredential(sourceArgs(s));
|
|
178756
|
-
} catch {
|
|
178757
|
-
return false;
|
|
178758
|
-
}
|
|
178759
|
-
});
|
|
178760
|
-
const markerRows = sources.filter((s) => s.argsJson === void 0 && hasCredential(s.args)).length;
|
|
178761
|
-
const report = (over) => ({
|
|
178762
|
-
apply,
|
|
178763
|
-
rows: sources.length,
|
|
178764
|
-
raw: rawRows.length,
|
|
178765
|
-
markers: markerRows,
|
|
178766
|
-
migrated: 0,
|
|
178767
|
-
failed: 0,
|
|
178768
|
-
remainingRaw: rawRows.length,
|
|
178769
|
-
unresolvedMarkers: this.unresolvedSecrets.length,
|
|
178770
|
-
redundantCopies: legacy.length,
|
|
178771
|
-
verified: rawRows.length === 0 && this.unresolvedSecrets.length === 0,
|
|
178772
|
-
...over
|
|
178773
|
-
});
|
|
178774
|
-
if (!apply || legacy.length === 0) return report({});
|
|
178775
|
-
const expected = /* @__PURE__ */ new Map();
|
|
178776
|
-
const unconverted = /* @__PURE__ */ new Set();
|
|
178777
|
-
const migrated = sources.map((s) => {
|
|
178778
|
-
if (s.argsJson === void 0) return s;
|
|
178779
|
-
try {
|
|
178780
|
-
const args = sourceArgs(s);
|
|
178781
|
-
const row = WasteSourceConfigSchema.parse({
|
|
178782
|
-
id: s.id,
|
|
178783
|
-
label: s.label,
|
|
178784
|
-
module: s.module,
|
|
178785
|
-
args: this.typedArgs(args),
|
|
178786
|
-
enabled: s.enabled
|
|
178787
|
-
});
|
|
178788
|
-
expected.set(s.id, args);
|
|
178789
|
-
return row;
|
|
178790
|
-
} catch {
|
|
178791
|
-
unconverted.add(s.id);
|
|
178792
|
-
return s;
|
|
178793
|
-
}
|
|
178794
|
-
});
|
|
178795
|
-
const failedRows = unconverted.size;
|
|
178796
|
-
if (expected.size === 0) return report({
|
|
178797
|
-
failed: failedRows,
|
|
178798
|
-
remainingRaw: legacy.length
|
|
178799
|
-
});
|
|
178800
|
-
try {
|
|
178801
|
-
await this.persistSources(migrated);
|
|
178802
|
-
} catch {
|
|
178803
|
-
return report({ failed: legacy.length });
|
|
178804
|
-
}
|
|
178805
|
-
const back = new Map(this.configuredSources().map((s) => [s.id, s]));
|
|
178806
|
-
if (![...expected].every(([id, args]) => {
|
|
178807
|
-
const row = back.get(id);
|
|
178808
|
-
return row !== void 0 && row.argsJson === void 0 && JSON.stringify(sortedEntries(row.args)) === JSON.stringify(sortedEntries(args));
|
|
178809
|
-
}) || this.unresolvedSecrets.length > 0) {
|
|
178810
|
-
await this.rollbackMigration(sources, expected);
|
|
178811
|
-
return report({ failed: legacy.length });
|
|
178812
|
-
}
|
|
178813
|
-
return report({
|
|
178814
|
-
migrated: expected.size,
|
|
178815
|
-
failed: failedRows,
|
|
178816
|
-
remainingRaw: failedRows,
|
|
178817
|
-
unresolvedMarkers: 0,
|
|
178818
|
-
redundantCopies: legacy.length,
|
|
178819
|
-
verified: failedRows === 0
|
|
178820
|
-
});
|
|
178821
|
-
}
|
|
178822
|
-
/**
|
|
178823
|
-
* Put the legacy rows back. A converted row's `args` is written as `null`, which
|
|
178824
|
-
* the save path reads as "this object is gone": it deletes the keychain refs the
|
|
178825
|
-
* migration created (a plain `{}` would keep them). Never throws - the report
|
|
178826
|
-
* says failed either way, and the legacy JSON stays authoritative.
|
|
178827
|
-
*/
|
|
178828
|
-
async rollbackMigration(sources, converted) {
|
|
178829
|
-
try {
|
|
178830
|
-
await this.updateGlobalSettingsAsOperator({ sources: sources.map((s) => converted.has(s.id) ? {
|
|
178831
|
-
...s,
|
|
178832
|
-
args: null
|
|
178833
|
-
} : s) });
|
|
178834
|
-
} catch {
|
|
178835
|
-
this.ctx.logger.error("waste-collections: the migration rollback failed - keychain refs may be orphaned (System -> Secrets)");
|
|
178836
|
-
}
|
|
178837
|
-
}
|
|
178838
|
-
async deleteSource(input) {
|
|
178839
|
-
const sources = this.configuredSources();
|
|
178840
|
-
if (!sources.some((s) => s.id === input.id)) throw new Error(`source '${input.id}' not found`);
|
|
178841
|
-
await this.persistSources(sources.filter((s) => s.id !== input.id));
|
|
178842
|
-
return { ok: true };
|
|
178843
|
-
}
|
|
178844
178602
|
async fetchNow(input) {
|
|
178845
178603
|
const targets = this.configuredSources().filter((s) => {
|
|
178846
178604
|
if (!s.enabled) return false;
|
|
@@ -178941,33 +178699,11 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178941
178699
|
this.ctx.logger.warn("waste-collections: cache persist failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
178942
178700
|
}
|
|
178943
178701
|
}
|
|
178944
|
-
globalSettingsSchema() {
|
|
178945
|
-
return this.schema({ sections: [{
|
|
178946
|
-
id: "refresh",
|
|
178947
|
-
title: "Refresh",
|
|
178948
|
-
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.",
|
|
178949
|
-
columns: 2,
|
|
178950
|
-
fields: [{
|
|
178951
|
-
type: "addon-action-button",
|
|
178952
|
-
key: "_fetchNow",
|
|
178953
|
-
label: "Refresh",
|
|
178954
|
-
buttonLabel: "Fetch now",
|
|
178955
|
-
description: "Sources refresh once at boot and then every 24 hours; this refreshes every enabled source immediately.",
|
|
178956
|
-
addonId: "waste-collections",
|
|
178957
|
-
action: "fetchNow",
|
|
178958
|
-
variant: "primary",
|
|
178959
|
-
paramsStatic: {}
|
|
178960
|
-
}]
|
|
178961
|
-
}] });
|
|
178962
|
-
}
|
|
178963
178702
|
};
|
|
178964
178703
|
/** Re-parse helper for tests. */
|
|
178965
178704
|
function parseWasteConfig(raw) {
|
|
178966
178705
|
return WasteConfigSchema.parse(raw);
|
|
178967
178706
|
}
|
|
178968
|
-
function sortedEntries(args) {
|
|
178969
|
-
return Object.entries(args).toSorted(([a], [b]) => a < b ? -1 : 1);
|
|
178970
|
-
}
|
|
178971
178707
|
//#endregion
|
|
178972
178708
|
exports.DEFAULT_WASTE_CONFIG = DEFAULT_WASTE_CONFIG;
|
|
178973
178709
|
exports.WasteCollectionsAddon = WasteCollectionsAddon;
|