@camstack/addon-waste-collections 0.1.37 → 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 +6 -0
- package/dist/waste-collections.addon.js +476 -150
- package/dist/waste-collections.addon.mjs +476 -150
- package/package.json +2 -2
|
@@ -6001,7 +6001,7 @@ var ZodIssueCode = {
|
|
|
6001
6001
|
var ZodFirstPartyTypeKind;
|
|
6002
6002
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6003
6003
|
//#endregion
|
|
6004
|
-
//#region ../types/dist/sleep-
|
|
6004
|
+
//#region ../types/dist/sleep-CQP-REJp.mjs
|
|
6005
6005
|
/**
|
|
6006
6006
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6007
6007
|
* window to float samples (D455).
|
|
@@ -6365,6 +6365,59 @@ Object.fromEntries([
|
|
|
6365
6365
|
function isValuelessField(field) {
|
|
6366
6366
|
return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button" || field.type === "account-sign-in" || field.type === "secret-presence";
|
|
6367
6367
|
}
|
|
6368
|
+
/**
|
|
6369
|
+
* Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
|
|
6370
|
+
* `key → { value, default }`, recursing through `group` and `sub-tabs`
|
|
6371
|
+
* layout fields. Keeping `value` and `default` SEPARATE (rather than
|
|
6372
|
+
* pre-resolving) lets callers both read the effective value AND detect
|
|
6373
|
+
* overrides (`value` differs from `default`). Structural over `unknown`;
|
|
6374
|
+
* malformed input yields an empty map, never throws.
|
|
6375
|
+
*/
|
|
6376
|
+
function collectHydratedFieldEntries(schema) {
|
|
6377
|
+
const out = /* @__PURE__ */ new Map();
|
|
6378
|
+
if (typeof schema !== "object" || schema === null) return out;
|
|
6379
|
+
const sections = schema.sections;
|
|
6380
|
+
if (!Array.isArray(sections)) return out;
|
|
6381
|
+
for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
|
|
6382
|
+
return out;
|
|
6383
|
+
}
|
|
6384
|
+
/** Recursive field walker for {@link collectHydratedFieldEntries}. */
|
|
6385
|
+
function collectFieldEntriesInto(fields, into) {
|
|
6386
|
+
if (!Array.isArray(fields)) return;
|
|
6387
|
+
for (const field of fields) {
|
|
6388
|
+
if (typeof field !== "object" || field === null) continue;
|
|
6389
|
+
const type = field.type;
|
|
6390
|
+
if (type === "group") {
|
|
6391
|
+
collectFieldEntriesInto(field.fields, into);
|
|
6392
|
+
continue;
|
|
6393
|
+
}
|
|
6394
|
+
if (type === "sub-tabs") {
|
|
6395
|
+
const tabs = field.tabs;
|
|
6396
|
+
if (Array.isArray(tabs)) {
|
|
6397
|
+
for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
|
|
6398
|
+
}
|
|
6399
|
+
continue;
|
|
6400
|
+
}
|
|
6401
|
+
const key = field.key;
|
|
6402
|
+
if (typeof key !== "string" || key.length === 0) continue;
|
|
6403
|
+
if (!("value" in field)) continue;
|
|
6404
|
+
into.set(key, {
|
|
6405
|
+
value: field.value,
|
|
6406
|
+
default: field.default
|
|
6407
|
+
});
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
/**
|
|
6411
|
+
* Flatten a hydrated settings payload into `key → EFFECTIVE value`
|
|
6412
|
+
* (`value ?? default`). The read-side inverse of `hydrateSchema` for the
|
|
6413
|
+
* common "just give me the resolved values" case. Built on
|
|
6414
|
+
* {@link collectHydratedFieldEntries}.
|
|
6415
|
+
*/
|
|
6416
|
+
function collectHydratedFieldValues(schema) {
|
|
6417
|
+
const out = /* @__PURE__ */ new Map();
|
|
6418
|
+
for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
|
|
6419
|
+
return out;
|
|
6420
|
+
}
|
|
6368
6421
|
function hydrateSchema(schema, values, options = {}) {
|
|
6369
6422
|
return {
|
|
6370
6423
|
...schema.tabs ? { tabs: [...schema.tabs] } : {},
|
|
@@ -7745,6 +7798,21 @@ var SCOPED_SETTINGS_SCOPES = [
|
|
|
7745
7798
|
"device-defaults",
|
|
7746
7799
|
"node"
|
|
7747
7800
|
];
|
|
7801
|
+
/** The one refusal of a key a scope did not declare. */
|
|
7802
|
+
function assertDeclaredKeys(keys, declared) {
|
|
7803
|
+
const undeclared = keys.filter((key) => !declared.has(key));
|
|
7804
|
+
if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
|
|
7805
|
+
}
|
|
7806
|
+
/** Build the handle over an addon's scoped read, write and unset. */
|
|
7807
|
+
function createScopedSettings(deps) {
|
|
7808
|
+
return {
|
|
7809
|
+
async get() {
|
|
7810
|
+
return deps.schema.parse(await deps.read());
|
|
7811
|
+
},
|
|
7812
|
+
patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
|
|
7813
|
+
unset: (keys) => deps.unset(keys)
|
|
7814
|
+
};
|
|
7815
|
+
}
|
|
7748
7816
|
/** A write to a secret path could not be stored in the keychain, so it was refused. */
|
|
7749
7817
|
var KeychainWriteRefusedError = class extends Error {
|
|
7750
7818
|
fieldPath;
|
|
@@ -8320,7 +8388,16 @@ function createHandle(raw, opts) {
|
|
|
8320
8388
|
return blob;
|
|
8321
8389
|
}
|
|
8322
8390
|
},
|
|
8323
|
-
writeAddonStore: writeAddon
|
|
8391
|
+
writeAddonStore: writeAddon,
|
|
8392
|
+
removeClearedKeys: (keys) => serialize("addon", async () => {
|
|
8393
|
+
const stored = await readStoredAddonBlob(raw);
|
|
8394
|
+
index.seen("addon", stored);
|
|
8395
|
+
const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
|
|
8396
|
+
if (removable.length === 0) return;
|
|
8397
|
+
const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
|
|
8398
|
+
await raw.writeAddonStore(patch);
|
|
8399
|
+
index.wrote("addon", patch);
|
|
8400
|
+
})
|
|
8324
8401
|
};
|
|
8325
8402
|
}
|
|
8326
8403
|
/** The context the addon sees, plus the private raw view and write outcome. */
|
|
@@ -8865,6 +8942,151 @@ var BaseAddon = class {
|
|
|
8865
8942
|
}
|
|
8866
8943
|
}
|
|
8867
8944
|
/**
|
|
8945
|
+
* Hook: values layered over the stored ones when the FORM of one scope is served
|
|
8946
|
+
* ({@link getScopedSettings}) — a preview that derives dependent options without writing.
|
|
8947
|
+
* Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
|
|
8948
|
+
*/
|
|
8949
|
+
async scopedSettingsOverlay(_scope, _cap, _nodeId) {
|
|
8950
|
+
return null;
|
|
8951
|
+
}
|
|
8952
|
+
/**
|
|
8953
|
+
* Hook: a write of one scope changed stored values (not called when the store reports no change,
|
|
8954
|
+
* e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
|
|
8955
|
+
*/
|
|
8956
|
+
async onScopedSettingsChanged(_scope, _cap, _keys) {}
|
|
8957
|
+
/** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
|
|
8958
|
+
clusterSettings(cap, schema) {
|
|
8959
|
+
return this.scopedHandle("cluster", cap, schema);
|
|
8960
|
+
}
|
|
8961
|
+
/** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
|
|
8962
|
+
deviceDefaults(cap, schema) {
|
|
8963
|
+
return this.scopedHandle("device-defaults", cap, schema);
|
|
8964
|
+
}
|
|
8965
|
+
/**
|
|
8966
|
+
* Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
|
|
8967
|
+
* with no row yields the schema defaults — never a bare-key fallback.
|
|
8968
|
+
*/
|
|
8969
|
+
nodeSettings(cap, schema, nodeId) {
|
|
8970
|
+
return this.scopedHandle("node", cap, schema, nodeId);
|
|
8971
|
+
}
|
|
8972
|
+
/**
|
|
8973
|
+
* {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
|
|
8974
|
+
* settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
|
|
8975
|
+
* central delete. `nodeId` omitted ⇒ the local node.
|
|
8976
|
+
*/
|
|
8977
|
+
nodeState(key, schema, fallback, nodeId) {
|
|
8978
|
+
this._declaredStateKeys.add(key);
|
|
8979
|
+
return createDurableState({
|
|
8980
|
+
key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
|
|
8981
|
+
schema,
|
|
8982
|
+
fallback,
|
|
8983
|
+
read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
|
|
8984
|
+
write: async (patch) => {
|
|
8985
|
+
await this._ctx?.settings?.writeAddonStore(patch);
|
|
8986
|
+
},
|
|
8987
|
+
onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
|
|
8988
|
+
addonId: this._ctx?.id,
|
|
8989
|
+
key: k,
|
|
8990
|
+
error: String(e)
|
|
8991
|
+
} })
|
|
8992
|
+
});
|
|
8993
|
+
}
|
|
8994
|
+
/** The loaded schema `cap` declares for `scope`, or `null`. */
|
|
8995
|
+
async declaredScopedSchema(scope, cap) {
|
|
8996
|
+
await this.loadScopedSchemas(cap);
|
|
8997
|
+
return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
|
|
8998
|
+
}
|
|
8999
|
+
async requireScopedSchema(scope, cap) {
|
|
9000
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9001
|
+
if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
|
|
9002
|
+
return schema;
|
|
9003
|
+
}
|
|
9004
|
+
scopedKeys(schema) {
|
|
9005
|
+
return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
|
|
9006
|
+
}
|
|
9007
|
+
/** `store` narrowed to the keys `schema` declares. */
|
|
9008
|
+
narrowToScope(store, schema) {
|
|
9009
|
+
const keys = this.scopedKeys(schema);
|
|
9010
|
+
return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
|
|
9011
|
+
}
|
|
9012
|
+
scopedHandle(scope, cap, schema, nodeId) {
|
|
9013
|
+
return createScopedSettings({
|
|
9014
|
+
schema,
|
|
9015
|
+
read: async () => {
|
|
9016
|
+
const declared = await this.requireScopedSchema(scope, cap);
|
|
9017
|
+
const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
|
|
9018
|
+
const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
|
|
9019
|
+
return {
|
|
9020
|
+
...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
|
|
9021
|
+
...stored
|
|
9022
|
+
};
|
|
9023
|
+
},
|
|
9024
|
+
write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
|
|
9025
|
+
unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
|
|
9026
|
+
});
|
|
9027
|
+
}
|
|
9028
|
+
/** The one write path of the scoped API: declared keys only, then the global patch. */
|
|
9029
|
+
async writeScoped(scope, cap, patch, nodeId) {
|
|
9030
|
+
assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
9031
|
+
if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
|
|
9032
|
+
}
|
|
9033
|
+
/**
|
|
9034
|
+
* Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
|
|
9035
|
+
* value is released exactly as there — and the emptied row is then removed; every other key is
|
|
9036
|
+
* removed by an `undefined` patch (which a secret transform would read as "keep").
|
|
9037
|
+
*/
|
|
9038
|
+
async unsetScoped(scope, cap, keys, nodeId) {
|
|
9039
|
+
assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
|
|
9040
|
+
const target = scope === "node" ? nodeId : void 0;
|
|
9041
|
+
const tree = this.addonSecretTree();
|
|
9042
|
+
const secretKeys = keys.filter((key) => tree.has(key));
|
|
9043
|
+
const plainKeys = keys.filter((key) => !tree.has(key));
|
|
9044
|
+
let changed = false;
|
|
9045
|
+
if (plainKeys.length > 0) {
|
|
9046
|
+
const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
|
|
9047
|
+
changed = await this.applyGlobalPatch(patch, target, cap) || changed;
|
|
9048
|
+
}
|
|
9049
|
+
if (secretKeys.length > 0) {
|
|
9050
|
+
await this.clearSecretKeys(secretKeys, target, cap);
|
|
9051
|
+
const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
|
|
9052
|
+
const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
|
|
9053
|
+
if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
|
|
9054
|
+
else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
|
|
9055
|
+
changed = true;
|
|
9056
|
+
}
|
|
9057
|
+
if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
|
|
9058
|
+
}
|
|
9059
|
+
/**
|
|
9060
|
+
* The hydrated form of one scope of one capability's settings (any
|
|
9061
|
+
* {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
|
|
9062
|
+
*/
|
|
9063
|
+
async getScopedSettings(scope, cap, nodeId) {
|
|
9064
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9065
|
+
if (schema === null) return null;
|
|
9066
|
+
const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
|
|
9067
|
+
const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
|
|
9068
|
+
return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
|
|
9069
|
+
}
|
|
9070
|
+
/** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
|
|
9071
|
+
async updateScopedSettings(scope, cap, patch, nodeId) {
|
|
9072
|
+
this._operatorWriteDepth++;
|
|
9073
|
+
try {
|
|
9074
|
+
await this.writeScoped(scope, cap, patch, nodeId);
|
|
9075
|
+
} finally {
|
|
9076
|
+
this._operatorWriteDepth--;
|
|
9077
|
+
}
|
|
9078
|
+
}
|
|
9079
|
+
/**
|
|
9080
|
+
* The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
|
|
9081
|
+
* its value. `null` when the capability declares nothing for the scope.
|
|
9082
|
+
*/
|
|
9083
|
+
async readScopedValues(scope, cap, nodeId) {
|
|
9084
|
+
const schema = await this.declaredScopedSchema(scope, cap);
|
|
9085
|
+
if (schema === null) return null;
|
|
9086
|
+
const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
|
|
9087
|
+
return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
|
|
9088
|
+
}
|
|
9089
|
+
/**
|
|
8868
9090
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
8869
9091
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
8870
9092
|
*/
|
|
@@ -9016,6 +9238,9 @@ var BaseAddon = class {
|
|
|
9016
9238
|
* a keychain (an older kernel) it writes `''`, as such code always did.
|
|
9017
9239
|
*/
|
|
9018
9240
|
async clearSecrets(keys, nodeId) {
|
|
9241
|
+
await this.clearSecretKeys(keys, nodeId);
|
|
9242
|
+
}
|
|
9243
|
+
async clearSecretKeys(keys, nodeId, cap) {
|
|
9019
9244
|
const tree = this.addonSecretTree();
|
|
9020
9245
|
const keychain = this._ctx?.secrets !== void 0;
|
|
9021
9246
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
@@ -9028,25 +9253,27 @@ var BaseAddon = class {
|
|
|
9028
9253
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
9029
9254
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
9030
9255
|
}
|
|
9031
|
-
await this.applyGlobalPatch(patch, nodeId);
|
|
9256
|
+
await this.applyGlobalPatch(patch, nodeId, cap);
|
|
9032
9257
|
}
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9258
|
+
/** Resolves `true` when the write changed stored values. */
|
|
9259
|
+
async applyGlobalPatch(barePatch, nodeId, cap) {
|
|
9260
|
+
await this.loadScopedSchemas(cap);
|
|
9261
|
+
const keys = this.perNodeKeys(cap);
|
|
9036
9262
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
9037
9263
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
9038
9264
|
const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
|
|
9039
9265
|
const handle = this._secretSettings;
|
|
9040
9266
|
if (handle !== null) {
|
|
9041
9267
|
const { changed } = await handle.writeAddonStore(scoped);
|
|
9042
|
-
if (!changed) return;
|
|
9268
|
+
if (!changed) return false;
|
|
9043
9269
|
} else await this._ctx?.settings?.writeAddonStore(scoped);
|
|
9044
|
-
if (target !== localNode) return;
|
|
9270
|
+
if (target !== localNode) return true;
|
|
9045
9271
|
await this.resolveConfig();
|
|
9046
9272
|
await this.onConfigChanged();
|
|
9047
9273
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
9048
9274
|
const legacy = this.globalSettingsSchema();
|
|
9049
9275
|
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
9276
|
+
return true;
|
|
9050
9277
|
}
|
|
9051
9278
|
/**
|
|
9052
9279
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -44881,6 +45108,8 @@ var SettingsSchemaWithValuesSchema = object({
|
|
|
44881
45108
|
});
|
|
44882
45109
|
/** Patch object — keys are field names, values are the new field values. */
|
|
44883
45110
|
var SettingsPatchSchema = record(string(), unknown());
|
|
45111
|
+
/** The scope of a scoped-settings call (D986): where the capability declared the values. */
|
|
45112
|
+
var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
|
|
44884
45113
|
/** Standard success response for update operations. */
|
|
44885
45114
|
var SettingsUpdateResultSchema = object({ success: literal(true) });
|
|
44886
45115
|
method(object({
|
|
@@ -44896,6 +45125,25 @@ method(object({
|
|
|
44896
45125
|
kind: "mutation",
|
|
44897
45126
|
auth: "admin"
|
|
44898
45127
|
}), method(object({
|
|
45128
|
+
addonId: string(),
|
|
45129
|
+
cap: string(),
|
|
45130
|
+
scope: ScopedSettingsScopeSchema,
|
|
45131
|
+
nodeId: string().optional()
|
|
45132
|
+
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
45133
|
+
addonId: string(),
|
|
45134
|
+
cap: string(),
|
|
45135
|
+
scope: ScopedSettingsScopeSchema,
|
|
45136
|
+
nodeId: string().optional(),
|
|
45137
|
+
patch: SettingsPatchSchema
|
|
45138
|
+
}), SettingsUpdateResultSchema, {
|
|
45139
|
+
kind: "mutation",
|
|
45140
|
+
auth: "admin"
|
|
45141
|
+
}), method(object({
|
|
45142
|
+
addonId: string(),
|
|
45143
|
+
cap: string(),
|
|
45144
|
+
scope: ScopedSettingsScopeSchema,
|
|
45145
|
+
nodeId: string().optional()
|
|
45146
|
+
}), record(string(), unknown()).nullable()), method(object({
|
|
44899
45147
|
addonId: string(),
|
|
44900
45148
|
nodeId: string().optional()
|
|
44901
45149
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
@@ -76784,6 +77032,20 @@ Object.freeze({
|
|
|
76784
77032
|
access: "view",
|
|
76785
77033
|
role: "admin"
|
|
76786
77034
|
},
|
|
77035
|
+
"addonSettings.getScopedSettings": {
|
|
77036
|
+
capName: "addon-settings",
|
|
77037
|
+
capScope: "system",
|
|
77038
|
+
addonId: null,
|
|
77039
|
+
access: "view",
|
|
77040
|
+
role: "admin"
|
|
77041
|
+
},
|
|
77042
|
+
"addonSettings.readScopedValues": {
|
|
77043
|
+
capName: "addon-settings",
|
|
77044
|
+
capScope: "system",
|
|
77045
|
+
addonId: null,
|
|
77046
|
+
access: "view",
|
|
77047
|
+
role: "admin"
|
|
77048
|
+
},
|
|
76787
77049
|
"addonSettings.unsetStoredSettingsKeys": {
|
|
76788
77050
|
capName: "addon-settings",
|
|
76789
77051
|
capScope: "system",
|
|
@@ -76805,6 +77067,13 @@ Object.freeze({
|
|
|
76805
77067
|
access: "create",
|
|
76806
77068
|
role: "admin"
|
|
76807
77069
|
},
|
|
77070
|
+
"addonSettings.updateScopedSettings": {
|
|
77071
|
+
capName: "addon-settings",
|
|
77072
|
+
capScope: "system",
|
|
77073
|
+
addonId: null,
|
|
77074
|
+
access: "create",
|
|
77075
|
+
role: "admin"
|
|
77076
|
+
},
|
|
76808
77077
|
"addonWidgets.listWidgets": {
|
|
76809
77078
|
capName: "addon-widgets",
|
|
76810
77079
|
capScope: "system",
|
|
@@ -91423,42 +91692,6 @@ async function copySourcesToRecords(ports) {
|
|
|
91423
91692
|
}
|
|
91424
91693
|
};
|
|
91425
91694
|
}
|
|
91426
|
-
/** The `waste-collections` catalogue (D987): offered to the engine when the addon initialises. */
|
|
91427
|
-
var ADDON_MIGRATIONS = [{
|
|
91428
|
-
meta: {
|
|
91429
|
-
id: "waste-collections/2026-11-06-sources-from-settings-and-argsjson",
|
|
91430
|
-
owner: "waste-collections",
|
|
91431
|
-
level: "addon-state",
|
|
91432
|
-
seq: 1,
|
|
91433
|
-
dependsOn: [],
|
|
91434
|
-
store: "settings",
|
|
91435
|
-
scope: "cluster",
|
|
91436
|
-
onFailure: "tolerate",
|
|
91437
|
-
bodyKind: "procedure",
|
|
91438
|
-
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",
|
|
91439
|
-
fixtureMark: "the credential of a legacy argsJson row, read back from its record"
|
|
91440
|
-
},
|
|
91441
|
-
procedure: copySourcesToRecords
|
|
91442
|
-
}];
|
|
91443
|
-
//#endregion
|
|
91444
|
-
//#region src/python-paths.ts
|
|
91445
|
-
/**
|
|
91446
|
-
* Locate the addon's bundled `python/` dir at runtime (deployed package
|
|
91447
|
-
* root or workspace package root). Same candidate walk as detection-pipeline.
|
|
91448
|
-
*/
|
|
91449
|
-
function resolvePythonDir(fromModuleUrl = import.meta.url) {
|
|
91450
|
-
const here = dirname(fileURLToPath(fromModuleUrl));
|
|
91451
|
-
const candidates = [
|
|
91452
|
-
join(here, "../python"),
|
|
91453
|
-
join(here, "../../python"),
|
|
91454
|
-
join(here, "python")
|
|
91455
|
-
];
|
|
91456
|
-
for (const c of candidates) if (existsSync(join(c, "fetch_source.py"))) return c;
|
|
91457
|
-
throw new Error(`addon-waste-collections: python/ dir not found. Searched:\n${candidates.join("\n")}`);
|
|
91458
|
-
}
|
|
91459
|
-
function resolveFetchScript(fromModuleUrl = import.meta.url) {
|
|
91460
|
-
return join(resolvePythonDir(fromModuleUrl), "fetch_source.py");
|
|
91461
|
-
}
|
|
91462
91695
|
//#endregion
|
|
91463
91696
|
//#region src/waste-source-record-kinds.ts
|
|
91464
91697
|
var WASTE_COLLECTIONS_OWNER = "waste-collections";
|
|
@@ -91504,6 +91737,202 @@ var plainWasteSourceKind = defineEntryKind("waste-source", {
|
|
|
91504
91737
|
writeTrigger: WRITE_TRIGGER
|
|
91505
91738
|
});
|
|
91506
91739
|
//#endregion
|
|
91740
|
+
//#region src/waste-source-store.ts
|
|
91741
|
+
/**
|
|
91742
|
+
* A stored body as a source. A row read while the keychain is locked still carries its markers
|
|
91743
|
+
* in `args`; the config schema keeps them, so the source lists and its fetch refuses by name.
|
|
91744
|
+
*/
|
|
91745
|
+
function rowToSource(id, data) {
|
|
91746
|
+
const parsed = WasteSourceConfigSchema.safeParse({
|
|
91747
|
+
id,
|
|
91748
|
+
label: data.name,
|
|
91749
|
+
module: data.module,
|
|
91750
|
+
args: data.args,
|
|
91751
|
+
enabled: data.enabled
|
|
91752
|
+
});
|
|
91753
|
+
return parsed.success ? parsed.data : null;
|
|
91754
|
+
}
|
|
91755
|
+
function bodyOf(source) {
|
|
91756
|
+
const { id, label, module, args, enabled } = source;
|
|
91757
|
+
const body = {
|
|
91758
|
+
name: label,
|
|
91759
|
+
module,
|
|
91760
|
+
args,
|
|
91761
|
+
enabled
|
|
91762
|
+
};
|
|
91763
|
+
const parsed = wasteSourceEntryKind.schema.safeParse(body);
|
|
91764
|
+
if (!parsed.success) throw new Error(`invalid waste source '${id}': ${parsed.error.message}`);
|
|
91765
|
+
return parsed.data;
|
|
91766
|
+
}
|
|
91767
|
+
/**
|
|
91768
|
+
* While the sources migration has not landed: every record, plus the legacy rows that have no
|
|
91769
|
+
* record yet (a read-only view of those). A legacy row leaves the view as soon as a record has
|
|
91770
|
+
* its id, so the two never diverge; writes go to the records.
|
|
91771
|
+
*/
|
|
91772
|
+
function withUnmigratedLegacyRows(records, legacy) {
|
|
91773
|
+
const have = new Set(records.map((r) => r.id));
|
|
91774
|
+
return [...records, ...legacy.filter((r) => !have.has(r.id))];
|
|
91775
|
+
}
|
|
91776
|
+
var WasteSourceStore = class WasteSourceStore {
|
|
91777
|
+
kind;
|
|
91778
|
+
constructor(kind) {
|
|
91779
|
+
this.kind = kind;
|
|
91780
|
+
}
|
|
91781
|
+
static of(records, kind = wasteSourceEntryKind) {
|
|
91782
|
+
return new WasteSourceStore(records.kind(kind));
|
|
91783
|
+
}
|
|
91784
|
+
/** The record ids, whatever their content. */
|
|
91785
|
+
async listIds() {
|
|
91786
|
+
return (await this.kind.query({ limit: 1e3 })).records.map((record) => record.id);
|
|
91787
|
+
}
|
|
91788
|
+
/** One source with its credentials resolved; markers stay where the keychain cannot serve. */
|
|
91789
|
+
async readResolved(id) {
|
|
91790
|
+
const stored = await this.kind.get(id);
|
|
91791
|
+
if (stored === null) return null;
|
|
91792
|
+
let data = stored.data;
|
|
91793
|
+
try {
|
|
91794
|
+
const resolved = await this.kind.getResolved(id);
|
|
91795
|
+
if (resolved !== null) data = resolved.data;
|
|
91796
|
+
} catch (err) {
|
|
91797
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
91798
|
+
}
|
|
91799
|
+
return rowToSource(id, data);
|
|
91800
|
+
}
|
|
91801
|
+
async listResolved() {
|
|
91802
|
+
const out = [];
|
|
91803
|
+
for (const id of await this.listIds()) {
|
|
91804
|
+
const row = await this.readResolved(id);
|
|
91805
|
+
if (row !== null) out.push(row);
|
|
91806
|
+
}
|
|
91807
|
+
return out;
|
|
91808
|
+
}
|
|
91809
|
+
/** Create the record when the id is free; an existing record is never touched. */
|
|
91810
|
+
async putIfAbsent(source) {
|
|
91811
|
+
if (await this.kind.get(source.id) !== null) return false;
|
|
91812
|
+
await this.kind.put({
|
|
91813
|
+
id: source.id,
|
|
91814
|
+
data: bodyOf(source)
|
|
91815
|
+
}, { ifRev: 0 });
|
|
91816
|
+
return true;
|
|
91817
|
+
}
|
|
91818
|
+
/**
|
|
91819
|
+
* Make the records equal `sources`. A source whose credential the keychain did not serve is
|
|
91820
|
+
* left exactly as stored: rewriting it would save a marker as the credential.
|
|
91821
|
+
*/
|
|
91822
|
+
async replaceAll(sources) {
|
|
91823
|
+
const nextIds = new Set(sources.map((s) => s.id));
|
|
91824
|
+
for (const id of await this.listIds()) if (!nextIds.has(id)) await this.kind.delete(id);
|
|
91825
|
+
for (const source of sources) {
|
|
91826
|
+
if (hasUnresolvedCredential(source.args)) continue;
|
|
91827
|
+
await this.kind.put({
|
|
91828
|
+
id: source.id,
|
|
91829
|
+
data: bodyOf(source)
|
|
91830
|
+
});
|
|
91831
|
+
}
|
|
91832
|
+
}
|
|
91833
|
+
};
|
|
91834
|
+
//#endregion
|
|
91835
|
+
//#region src/migrations/ports.ts
|
|
91836
|
+
var PAGE = 1e3;
|
|
91837
|
+
/**
|
|
91838
|
+
* The sources migration's ports over {@link MigrationApiV1}, and nothing else (planning#447): the
|
|
91839
|
+
* addon's own settings store, its keychain handle and its `waste-source` record kind.
|
|
91840
|
+
*/
|
|
91841
|
+
function wasteSourcePortsOver(api, kind) {
|
|
91842
|
+
const handle = api.records.kind(kind);
|
|
91843
|
+
const secrets = { resolve: async (refs) => {
|
|
91844
|
+
const { values } = await api.secrets.resolve({ refs: [...refs] });
|
|
91845
|
+
return Object.fromEntries(refs.map((ref) => {
|
|
91846
|
+
const value = values[ref];
|
|
91847
|
+
return [ref, value === null || value === void 0 ? { kind: "missing" } : {
|
|
91848
|
+
kind: "value",
|
|
91849
|
+
value,
|
|
91850
|
+
version: 0
|
|
91851
|
+
}];
|
|
91852
|
+
}));
|
|
91853
|
+
} };
|
|
91854
|
+
const readStored = async (key) => {
|
|
91855
|
+
let store;
|
|
91856
|
+
try {
|
|
91857
|
+
store = (await api.settings.read({})).store;
|
|
91858
|
+
} catch (err) {
|
|
91859
|
+
return {
|
|
91860
|
+
kind: "unreadable",
|
|
91861
|
+
reason: "store-read-failed",
|
|
91862
|
+
error: err instanceof Error ? err.message : String(err)
|
|
91863
|
+
};
|
|
91864
|
+
}
|
|
91865
|
+
if (!Object.hasOwn(store, key)) return { kind: "absent" };
|
|
91866
|
+
const { value } = await resolveSecretMarkers(store[key], secrets, { unresolved: "keep-marker" });
|
|
91867
|
+
return {
|
|
91868
|
+
kind: "present",
|
|
91869
|
+
value
|
|
91870
|
+
};
|
|
91871
|
+
};
|
|
91872
|
+
const asRow = (row) => rowToSource(row.id, row.data);
|
|
91873
|
+
return {
|
|
91874
|
+
readStored,
|
|
91875
|
+
listSourceIds: async () => (await handle.query({ limit: PAGE })).records.map((row) => row.id),
|
|
91876
|
+
putSource: async (source) => {
|
|
91877
|
+
if (await handle.get({ id: source.id }) !== null) return;
|
|
91878
|
+
await handle.put({
|
|
91879
|
+
id: source.id,
|
|
91880
|
+
data: bodyOf(source),
|
|
91881
|
+
ifRev: 0
|
|
91882
|
+
});
|
|
91883
|
+
},
|
|
91884
|
+
readSource: async (id) => {
|
|
91885
|
+
const stored = await handle.get({ id });
|
|
91886
|
+
if (stored === null) return null;
|
|
91887
|
+
try {
|
|
91888
|
+
return asRow(await handle.getResolved({ id }) ?? stored);
|
|
91889
|
+
} catch (err) {
|
|
91890
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
91891
|
+
return asRow(stored);
|
|
91892
|
+
}
|
|
91893
|
+
},
|
|
91894
|
+
warn: (message) => {
|
|
91895
|
+
api.log.warn({ message });
|
|
91896
|
+
}
|
|
91897
|
+
};
|
|
91898
|
+
}
|
|
91899
|
+
/** The `waste-collections` catalogue (D987): offered to the engine when the addon initialises. */
|
|
91900
|
+
var ADDON_MIGRATIONS = [{
|
|
91901
|
+
meta: {
|
|
91902
|
+
id: "waste-collections/2026-11-06-sources-from-settings-and-argsjson",
|
|
91903
|
+
owner: "waste-collections",
|
|
91904
|
+
level: "addon-state",
|
|
91905
|
+
seq: 1,
|
|
91906
|
+
dependsOn: [],
|
|
91907
|
+
store: "settings",
|
|
91908
|
+
scope: "cluster",
|
|
91909
|
+
onFailure: "tolerate",
|
|
91910
|
+
bodyKind: "procedure",
|
|
91911
|
+
description: "Copy the addon-level sources setting (typed args, or a legacy argsJson string) into waste-source records, credentials in the keychain, each read back; an existing record is never overwritten",
|
|
91912
|
+
fixtureMark: "the credential of a legacy argsJson row, read back from its record"
|
|
91913
|
+
},
|
|
91914
|
+
procedure: copySourcesToRecords
|
|
91915
|
+
}];
|
|
91916
|
+
//#endregion
|
|
91917
|
+
//#region src/python-paths.ts
|
|
91918
|
+
/**
|
|
91919
|
+
* Locate the addon's bundled `python/` dir at runtime (deployed package
|
|
91920
|
+
* root or workspace package root). Same candidate walk as detection-pipeline.
|
|
91921
|
+
*/
|
|
91922
|
+
function resolvePythonDir(fromModuleUrl = import.meta.url) {
|
|
91923
|
+
const here = dirname(fileURLToPath(fromModuleUrl));
|
|
91924
|
+
const candidates = [
|
|
91925
|
+
join(here, "../python"),
|
|
91926
|
+
join(here, "../../python"),
|
|
91927
|
+
join(here, "python")
|
|
91928
|
+
];
|
|
91929
|
+
for (const c of candidates) if (existsSync(join(c, "fetch_source.py"))) return c;
|
|
91930
|
+
throw new Error(`addon-waste-collections: python/ dir not found. Searched:\n${candidates.join("\n")}`);
|
|
91931
|
+
}
|
|
91932
|
+
function resolveFetchScript(fromModuleUrl = import.meta.url) {
|
|
91933
|
+
return join(resolvePythonDir(fromModuleUrl), "fetch_source.py");
|
|
91934
|
+
}
|
|
91935
|
+
//#endregion
|
|
91507
91936
|
//#region src/seal-plaintext.ts
|
|
91508
91937
|
/**
|
|
91509
91938
|
* A credential an older version stored as plain text in a `waste-source` record is moved into the
|
|
@@ -177679,101 +178108,6 @@ function allocateSourceId(label, existingIds) {
|
|
|
177679
178108
|
throw new Error("could not allocate a unique source id");
|
|
177680
178109
|
}
|
|
177681
178110
|
//#endregion
|
|
177682
|
-
//#region src/waste-source-store.ts
|
|
177683
|
-
/**
|
|
177684
|
-
* A stored body as a source. A row read while the keychain is locked still carries its markers
|
|
177685
|
-
* in `args`; the config schema keeps them, so the source lists and its fetch refuses by name.
|
|
177686
|
-
*/
|
|
177687
|
-
function rowToSource(id, data) {
|
|
177688
|
-
const parsed = WasteSourceConfigSchema.safeParse({
|
|
177689
|
-
id,
|
|
177690
|
-
label: data.name,
|
|
177691
|
-
module: data.module,
|
|
177692
|
-
args: data.args,
|
|
177693
|
-
enabled: data.enabled
|
|
177694
|
-
});
|
|
177695
|
-
return parsed.success ? parsed.data : null;
|
|
177696
|
-
}
|
|
177697
|
-
function bodyOf(source) {
|
|
177698
|
-
const { id, label, module, args, enabled } = source;
|
|
177699
|
-
const body = {
|
|
177700
|
-
name: label,
|
|
177701
|
-
module,
|
|
177702
|
-
args,
|
|
177703
|
-
enabled
|
|
177704
|
-
};
|
|
177705
|
-
const parsed = wasteSourceEntryKind.schema.safeParse(body);
|
|
177706
|
-
if (!parsed.success) throw new Error(`invalid waste source '${id}': ${parsed.error.message}`);
|
|
177707
|
-
return parsed.data;
|
|
177708
|
-
}
|
|
177709
|
-
/**
|
|
177710
|
-
* While the sources migration has not landed: every record, plus the legacy rows that have no
|
|
177711
|
-
* record yet (a read-only view of those). A legacy row leaves the view as soon as a record has
|
|
177712
|
-
* its id, so the two never diverge; writes go to the records.
|
|
177713
|
-
*/
|
|
177714
|
-
function withUnmigratedLegacyRows(records, legacy) {
|
|
177715
|
-
const have = new Set(records.map((r) => r.id));
|
|
177716
|
-
return [...records, ...legacy.filter((r) => !have.has(r.id))];
|
|
177717
|
-
}
|
|
177718
|
-
var WasteSourceStore = class WasteSourceStore {
|
|
177719
|
-
kind;
|
|
177720
|
-
constructor(kind) {
|
|
177721
|
-
this.kind = kind;
|
|
177722
|
-
}
|
|
177723
|
-
static of(records, kind = wasteSourceEntryKind) {
|
|
177724
|
-
return new WasteSourceStore(records.kind(kind));
|
|
177725
|
-
}
|
|
177726
|
-
/** The record ids, whatever their content. */
|
|
177727
|
-
async listIds() {
|
|
177728
|
-
return (await this.kind.query({ limit: 1e3 })).records.map((record) => record.id);
|
|
177729
|
-
}
|
|
177730
|
-
/** One source with its credentials resolved; markers stay where the keychain cannot serve. */
|
|
177731
|
-
async readResolved(id) {
|
|
177732
|
-
const stored = await this.kind.get(id);
|
|
177733
|
-
if (stored === null) return null;
|
|
177734
|
-
let data = stored.data;
|
|
177735
|
-
try {
|
|
177736
|
-
const resolved = await this.kind.getResolved(id);
|
|
177737
|
-
if (resolved !== null) data = resolved.data;
|
|
177738
|
-
} catch (err) {
|
|
177739
|
-
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
177740
|
-
}
|
|
177741
|
-
return rowToSource(id, data);
|
|
177742
|
-
}
|
|
177743
|
-
async listResolved() {
|
|
177744
|
-
const out = [];
|
|
177745
|
-
for (const id of await this.listIds()) {
|
|
177746
|
-
const row = await this.readResolved(id);
|
|
177747
|
-
if (row !== null) out.push(row);
|
|
177748
|
-
}
|
|
177749
|
-
return out;
|
|
177750
|
-
}
|
|
177751
|
-
/** Create the record when the id is free; an existing record is never touched. */
|
|
177752
|
-
async putIfAbsent(source) {
|
|
177753
|
-
if (await this.kind.get(source.id) !== null) return false;
|
|
177754
|
-
await this.kind.put({
|
|
177755
|
-
id: source.id,
|
|
177756
|
-
data: bodyOf(source)
|
|
177757
|
-
}, { ifRev: 0 });
|
|
177758
|
-
return true;
|
|
177759
|
-
}
|
|
177760
|
-
/**
|
|
177761
|
-
* Make the records equal `sources`. A source whose credential the keychain did not serve is
|
|
177762
|
-
* left exactly as stored: rewriting it would save a marker as the credential.
|
|
177763
|
-
*/
|
|
177764
|
-
async replaceAll(sources) {
|
|
177765
|
-
const nextIds = new Set(sources.map((s) => s.id));
|
|
177766
|
-
for (const id of await this.listIds()) if (!nextIds.has(id)) await this.kind.delete(id);
|
|
177767
|
-
for (const source of sources) {
|
|
177768
|
-
if (hasUnresolvedCredential(source.args)) continue;
|
|
177769
|
-
await this.kind.put({
|
|
177770
|
-
id: source.id,
|
|
177771
|
-
data: bodyOf(source)
|
|
177772
|
-
});
|
|
177773
|
-
}
|
|
177774
|
-
}
|
|
177775
|
-
};
|
|
177776
|
-
//#endregion
|
|
177777
178111
|
//#region src/waste-source-instances.ts
|
|
177778
178112
|
/**
|
|
177779
178113
|
* The waste sources as `integration-instances` (planning#440 step 6): one instance per
|
|
@@ -178134,7 +178468,7 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178134
178468
|
if (!plainBoot) await records.declare({ "waste-source": wasteSourceEntryKind });
|
|
178135
178469
|
const store = WasteSourceStore.of(records, plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178136
178470
|
this.sourceStore = store;
|
|
178137
|
-
await this.migrateLegacySources(
|
|
178471
|
+
await this.migrateLegacySources(plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178138
178472
|
this.activeSources = await this.loadPersistedSources();
|
|
178139
178473
|
const instances = plainBoot ? null : new WasteSourceInstances(records, {
|
|
178140
178474
|
onChanged: (id, change) => this.onInstanceChanged(id, change),
|
|
@@ -178216,16 +178550,8 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178216
178550
|
* fails leaves the settings where they are and is logged by id: the sources stay unavailable
|
|
178217
178551
|
* until it lands, never half-copied.
|
|
178218
178552
|
*/
|
|
178219
|
-
async migrateLegacySources(
|
|
178220
|
-
const report = await this.runMigrations(
|
|
178221
|
-
readStored: (key) => this.readStoredGlobalKey(key),
|
|
178222
|
-
listSourceIds: () => store.listIds(),
|
|
178223
|
-
putSource: async (source) => {
|
|
178224
|
-
await store.putIfAbsent(source);
|
|
178225
|
-
},
|
|
178226
|
-
readSource: (id) => store.readResolved(id),
|
|
178227
|
-
warn: (message) => this.ctx.logger.warn(message)
|
|
178228
|
-
}));
|
|
178553
|
+
async migrateLegacySources(kind) {
|
|
178554
|
+
const report = await this.runMigrations(ADDON_MIGRATIONS, (api) => wasteSourcePortsOver(api, kind));
|
|
178229
178555
|
this.legacyServed = !report.declared || report.failed.includes("waste-collections/2026-11-06-sources-from-settings-and-argsjson") || report.blocked.includes("waste-collections/2026-11-06-sources-from-settings-and-argsjson");
|
|
178230
178556
|
if (this.legacyServed) this.ctx.logger.warn("waste-collections: the sources migration has not landed - the legacy sources not yet in records are served read-only beside the records until it does", { meta: {
|
|
178231
178557
|
declared: report.declared,
|