@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
|
@@ -6005,7 +6005,7 @@ var ZodIssueCode = {
|
|
|
6005
6005
|
var ZodFirstPartyTypeKind;
|
|
6006
6006
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
6007
6007
|
//#endregion
|
|
6008
|
-
//#region ../types/dist/sleep-
|
|
6008
|
+
//#region ../types/dist/sleep-CQP-REJp.mjs
|
|
6009
6009
|
/**
|
|
6010
6010
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6011
6011
|
* window to float samples (D455).
|
|
@@ -6369,6 +6369,59 @@ Object.fromEntries([
|
|
|
6369
6369
|
function isValuelessField(field) {
|
|
6370
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";
|
|
6371
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
|
+
}
|
|
6372
6425
|
function hydrateSchema(schema, values, options = {}) {
|
|
6373
6426
|
return {
|
|
6374
6427
|
...schema.tabs ? { tabs: [...schema.tabs] } : {},
|
|
@@ -7749,6 +7802,21 @@ var SCOPED_SETTINGS_SCOPES = [
|
|
|
7749
7802
|
"device-defaults",
|
|
7750
7803
|
"node"
|
|
7751
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
|
+
}
|
|
7752
7820
|
/** A write to a secret path could not be stored in the keychain, so it was refused. */
|
|
7753
7821
|
var KeychainWriteRefusedError = class extends Error {
|
|
7754
7822
|
fieldPath;
|
|
@@ -8324,7 +8392,16 @@ function createHandle(raw, opts) {
|
|
|
8324
8392
|
return blob;
|
|
8325
8393
|
}
|
|
8326
8394
|
},
|
|
8327
|
-
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
|
+
})
|
|
8328
8405
|
};
|
|
8329
8406
|
}
|
|
8330
8407
|
/** The context the addon sees, plus the private raw view and write outcome. */
|
|
@@ -8869,6 +8946,151 @@ var BaseAddon = class {
|
|
|
8869
8946
|
}
|
|
8870
8947
|
}
|
|
8871
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
|
+
/**
|
|
8872
9094
|
* What the create wizard and the integration edit page render for one instance: the addon's
|
|
8873
9095
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
8874
9096
|
*/
|
|
@@ -9020,6 +9242,9 @@ var BaseAddon = class {
|
|
|
9020
9242
|
* a keychain (an older kernel) it writes `''`, as such code always did.
|
|
9021
9243
|
*/
|
|
9022
9244
|
async clearSecrets(keys, nodeId) {
|
|
9245
|
+
await this.clearSecretKeys(keys, nodeId);
|
|
9246
|
+
}
|
|
9247
|
+
async clearSecretKeys(keys, nodeId, cap) {
|
|
9023
9248
|
const tree = this.addonSecretTree();
|
|
9024
9249
|
const keychain = this._ctx?.secrets !== void 0;
|
|
9025
9250
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
@@ -9032,25 +9257,27 @@ var BaseAddon = class {
|
|
|
9032
9257
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
9033
9258
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
9034
9259
|
}
|
|
9035
|
-
await this.applyGlobalPatch(patch, nodeId);
|
|
9260
|
+
await this.applyGlobalPatch(patch, nodeId, cap);
|
|
9036
9261
|
}
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
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);
|
|
9040
9266
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
9041
9267
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
9042
9268
|
const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
|
|
9043
9269
|
const handle = this._secretSettings;
|
|
9044
9270
|
if (handle !== null) {
|
|
9045
9271
|
const { changed } = await handle.writeAddonStore(scoped);
|
|
9046
|
-
if (!changed) return;
|
|
9272
|
+
if (!changed) return false;
|
|
9047
9273
|
} else await this._ctx?.settings?.writeAddonStore(scoped);
|
|
9048
|
-
if (target !== localNode) return;
|
|
9274
|
+
if (target !== localNode) return true;
|
|
9049
9275
|
await this.resolveConfig();
|
|
9050
9276
|
await this.onConfigChanged();
|
|
9051
9277
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
9052
9278
|
const legacy = this.globalSettingsSchema();
|
|
9053
9279
|
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
9280
|
+
return true;
|
|
9054
9281
|
}
|
|
9055
9282
|
/**
|
|
9056
9283
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -44885,6 +45112,8 @@ var SettingsSchemaWithValuesSchema = object({
|
|
|
44885
45112
|
});
|
|
44886
45113
|
/** Patch object — keys are field names, values are the new field values. */
|
|
44887
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);
|
|
44888
45117
|
/** Standard success response for update operations. */
|
|
44889
45118
|
var SettingsUpdateResultSchema = object({ success: literal(true) });
|
|
44890
45119
|
method(object({
|
|
@@ -44900,6 +45129,25 @@ method(object({
|
|
|
44900
45129
|
kind: "mutation",
|
|
44901
45130
|
auth: "admin"
|
|
44902
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({
|
|
44903
45151
|
addonId: string(),
|
|
44904
45152
|
nodeId: string().optional()
|
|
44905
45153
|
}), SettingsSchemaWithValuesSchema.nullable()), method(object({
|
|
@@ -76788,6 +77036,20 @@ Object.freeze({
|
|
|
76788
77036
|
access: "view",
|
|
76789
77037
|
role: "admin"
|
|
76790
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
|
+
},
|
|
76791
77053
|
"addonSettings.unsetStoredSettingsKeys": {
|
|
76792
77054
|
capName: "addon-settings",
|
|
76793
77055
|
capScope: "system",
|
|
@@ -76809,6 +77071,13 @@ Object.freeze({
|
|
|
76809
77071
|
access: "create",
|
|
76810
77072
|
role: "admin"
|
|
76811
77073
|
},
|
|
77074
|
+
"addonSettings.updateScopedSettings": {
|
|
77075
|
+
capName: "addon-settings",
|
|
77076
|
+
capScope: "system",
|
|
77077
|
+
addonId: null,
|
|
77078
|
+
access: "create",
|
|
77079
|
+
role: "admin"
|
|
77080
|
+
},
|
|
76812
77081
|
"addonWidgets.listWidgets": {
|
|
76813
77082
|
capName: "addon-widgets",
|
|
76814
77083
|
capScope: "system",
|
|
@@ -91427,42 +91696,6 @@ async function copySourcesToRecords(ports) {
|
|
|
91427
91696
|
}
|
|
91428
91697
|
};
|
|
91429
91698
|
}
|
|
91430
|
-
/** The `waste-collections` catalogue (D987): offered to the engine when the addon initialises. */
|
|
91431
|
-
var ADDON_MIGRATIONS = [{
|
|
91432
|
-
meta: {
|
|
91433
|
-
id: "waste-collections/2026-11-06-sources-from-settings-and-argsjson",
|
|
91434
|
-
owner: "waste-collections",
|
|
91435
|
-
level: "addon-state",
|
|
91436
|
-
seq: 1,
|
|
91437
|
-
dependsOn: [],
|
|
91438
|
-
store: "settings",
|
|
91439
|
-
scope: "cluster",
|
|
91440
|
-
onFailure: "tolerate",
|
|
91441
|
-
bodyKind: "procedure",
|
|
91442
|
-
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",
|
|
91443
|
-
fixtureMark: "the credential of a legacy argsJson row, read back from its record"
|
|
91444
|
-
},
|
|
91445
|
-
procedure: copySourcesToRecords
|
|
91446
|
-
}];
|
|
91447
|
-
//#endregion
|
|
91448
|
-
//#region src/python-paths.ts
|
|
91449
|
-
/**
|
|
91450
|
-
* Locate the addon's bundled `python/` dir at runtime (deployed package
|
|
91451
|
-
* root or workspace package root). Same candidate walk as detection-pipeline.
|
|
91452
|
-
*/
|
|
91453
|
-
function resolvePythonDir(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
91454
|
-
const here = (0, node_path.dirname)((0, node_url.fileURLToPath)(fromModuleUrl));
|
|
91455
|
-
const candidates = [
|
|
91456
|
-
(0, node_path.join)(here, "../python"),
|
|
91457
|
-
(0, node_path.join)(here, "../../python"),
|
|
91458
|
-
(0, node_path.join)(here, "python")
|
|
91459
|
-
];
|
|
91460
|
-
for (const c of candidates) if ((0, node_fs.existsSync)((0, node_path.join)(c, "fetch_source.py"))) return c;
|
|
91461
|
-
throw new Error(`addon-waste-collections: python/ dir not found. Searched:\n${candidates.join("\n")}`);
|
|
91462
|
-
}
|
|
91463
|
-
function resolveFetchScript(fromModuleUrl = require("url").pathToFileURL(__filename).href) {
|
|
91464
|
-
return (0, node_path.join)(resolvePythonDir(fromModuleUrl), "fetch_source.py");
|
|
91465
|
-
}
|
|
91466
91699
|
//#endregion
|
|
91467
91700
|
//#region src/waste-source-record-kinds.ts
|
|
91468
91701
|
var WASTE_COLLECTIONS_OWNER = "waste-collections";
|
|
@@ -91508,6 +91741,202 @@ var plainWasteSourceKind = defineEntryKind("waste-source", {
|
|
|
91508
91741
|
writeTrigger: WRITE_TRIGGER
|
|
91509
91742
|
});
|
|
91510
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
|
|
91511
91940
|
//#region src/seal-plaintext.ts
|
|
91512
91941
|
/**
|
|
91513
91942
|
* A credential an older version stored as plain text in a `waste-source` record is moved into the
|
|
@@ -177683,101 +178112,6 @@ function allocateSourceId(label, existingIds) {
|
|
|
177683
178112
|
throw new Error("could not allocate a unique source id");
|
|
177684
178113
|
}
|
|
177685
178114
|
//#endregion
|
|
177686
|
-
//#region src/waste-source-store.ts
|
|
177687
|
-
/**
|
|
177688
|
-
* A stored body as a source. A row read while the keychain is locked still carries its markers
|
|
177689
|
-
* in `args`; the config schema keeps them, so the source lists and its fetch refuses by name.
|
|
177690
|
-
*/
|
|
177691
|
-
function rowToSource(id, data) {
|
|
177692
|
-
const parsed = WasteSourceConfigSchema.safeParse({
|
|
177693
|
-
id,
|
|
177694
|
-
label: data.name,
|
|
177695
|
-
module: data.module,
|
|
177696
|
-
args: data.args,
|
|
177697
|
-
enabled: data.enabled
|
|
177698
|
-
});
|
|
177699
|
-
return parsed.success ? parsed.data : null;
|
|
177700
|
-
}
|
|
177701
|
-
function bodyOf(source) {
|
|
177702
|
-
const { id, label, module, args, enabled } = source;
|
|
177703
|
-
const body = {
|
|
177704
|
-
name: label,
|
|
177705
|
-
module,
|
|
177706
|
-
args,
|
|
177707
|
-
enabled
|
|
177708
|
-
};
|
|
177709
|
-
const parsed = wasteSourceEntryKind.schema.safeParse(body);
|
|
177710
|
-
if (!parsed.success) throw new Error(`invalid waste source '${id}': ${parsed.error.message}`);
|
|
177711
|
-
return parsed.data;
|
|
177712
|
-
}
|
|
177713
|
-
/**
|
|
177714
|
-
* While the sources migration has not landed: every record, plus the legacy rows that have no
|
|
177715
|
-
* record yet (a read-only view of those). A legacy row leaves the view as soon as a record has
|
|
177716
|
-
* its id, so the two never diverge; writes go to the records.
|
|
177717
|
-
*/
|
|
177718
|
-
function withUnmigratedLegacyRows(records, legacy) {
|
|
177719
|
-
const have = new Set(records.map((r) => r.id));
|
|
177720
|
-
return [...records, ...legacy.filter((r) => !have.has(r.id))];
|
|
177721
|
-
}
|
|
177722
|
-
var WasteSourceStore = class WasteSourceStore {
|
|
177723
|
-
kind;
|
|
177724
|
-
constructor(kind) {
|
|
177725
|
-
this.kind = kind;
|
|
177726
|
-
}
|
|
177727
|
-
static of(records, kind = wasteSourceEntryKind) {
|
|
177728
|
-
return new WasteSourceStore(records.kind(kind));
|
|
177729
|
-
}
|
|
177730
|
-
/** The record ids, whatever their content. */
|
|
177731
|
-
async listIds() {
|
|
177732
|
-
return (await this.kind.query({ limit: 1e3 })).records.map((record) => record.id);
|
|
177733
|
-
}
|
|
177734
|
-
/** One source with its credentials resolved; markers stay where the keychain cannot serve. */
|
|
177735
|
-
async readResolved(id) {
|
|
177736
|
-
const stored = await this.kind.get(id);
|
|
177737
|
-
if (stored === null) return null;
|
|
177738
|
-
let data = stored.data;
|
|
177739
|
-
try {
|
|
177740
|
-
const resolved = await this.kind.getResolved(id);
|
|
177741
|
-
if (resolved !== null) data = resolved.data;
|
|
177742
|
-
} catch (err) {
|
|
177743
|
-
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
177744
|
-
}
|
|
177745
|
-
return rowToSource(id, data);
|
|
177746
|
-
}
|
|
177747
|
-
async listResolved() {
|
|
177748
|
-
const out = [];
|
|
177749
|
-
for (const id of await this.listIds()) {
|
|
177750
|
-
const row = await this.readResolved(id);
|
|
177751
|
-
if (row !== null) out.push(row);
|
|
177752
|
-
}
|
|
177753
|
-
return out;
|
|
177754
|
-
}
|
|
177755
|
-
/** Create the record when the id is free; an existing record is never touched. */
|
|
177756
|
-
async putIfAbsent(source) {
|
|
177757
|
-
if (await this.kind.get(source.id) !== null) return false;
|
|
177758
|
-
await this.kind.put({
|
|
177759
|
-
id: source.id,
|
|
177760
|
-
data: bodyOf(source)
|
|
177761
|
-
}, { ifRev: 0 });
|
|
177762
|
-
return true;
|
|
177763
|
-
}
|
|
177764
|
-
/**
|
|
177765
|
-
* Make the records equal `sources`. A source whose credential the keychain did not serve is
|
|
177766
|
-
* left exactly as stored: rewriting it would save a marker as the credential.
|
|
177767
|
-
*/
|
|
177768
|
-
async replaceAll(sources) {
|
|
177769
|
-
const nextIds = new Set(sources.map((s) => s.id));
|
|
177770
|
-
for (const id of await this.listIds()) if (!nextIds.has(id)) await this.kind.delete(id);
|
|
177771
|
-
for (const source of sources) {
|
|
177772
|
-
if (hasUnresolvedCredential(source.args)) continue;
|
|
177773
|
-
await this.kind.put({
|
|
177774
|
-
id: source.id,
|
|
177775
|
-
data: bodyOf(source)
|
|
177776
|
-
});
|
|
177777
|
-
}
|
|
177778
|
-
}
|
|
177779
|
-
};
|
|
177780
|
-
//#endregion
|
|
177781
178115
|
//#region src/waste-source-instances.ts
|
|
177782
178116
|
/**
|
|
177783
178117
|
* The waste sources as `integration-instances` (planning#440 step 6): one instance per
|
|
@@ -178138,7 +178472,7 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178138
178472
|
if (!plainBoot) await records.declare({ "waste-source": wasteSourceEntryKind });
|
|
178139
178473
|
const store = WasteSourceStore.of(records, plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178140
178474
|
this.sourceStore = store;
|
|
178141
|
-
await this.migrateLegacySources(
|
|
178475
|
+
await this.migrateLegacySources(plainBoot ? plainWasteSourceKind : wasteSourceEntryKind);
|
|
178142
178476
|
this.activeSources = await this.loadPersistedSources();
|
|
178143
178477
|
const instances = plainBoot ? null : new WasteSourceInstances(records, {
|
|
178144
178478
|
onChanged: (id, change) => this.onInstanceChanged(id, change),
|
|
@@ -178220,16 +178554,8 @@ var WasteCollectionsAddon = class extends BaseAddon {
|
|
|
178220
178554
|
* fails leaves the settings where they are and is logged by id: the sources stay unavailable
|
|
178221
178555
|
* until it lands, never half-copied.
|
|
178222
178556
|
*/
|
|
178223
|
-
async migrateLegacySources(
|
|
178224
|
-
const report = await this.runMigrations(
|
|
178225
|
-
readStored: (key) => this.readStoredGlobalKey(key),
|
|
178226
|
-
listSourceIds: () => store.listIds(),
|
|
178227
|
-
putSource: async (source) => {
|
|
178228
|
-
await store.putIfAbsent(source);
|
|
178229
|
-
},
|
|
178230
|
-
readSource: (id) => store.readResolved(id),
|
|
178231
|
-
warn: (message) => this.ctx.logger.warn(message)
|
|
178232
|
-
}));
|
|
178557
|
+
async migrateLegacySources(kind) {
|
|
178558
|
+
const report = await this.runMigrations(ADDON_MIGRATIONS, (api) => wasteSourcePortsOver(api, kind));
|
|
178233
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");
|
|
178234
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: {
|
|
178235
178561
|
declared: report.declared,
|