@camstack/addon-import-alexa 0.2.191 → 0.2.192
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 +10 -0
- package/dist/addon.js +1398 -859
- package/dist/addon.mjs +1398 -859
- package/package.json +2 -2
- package/python/alexa_gateway.py +28 -8
- package/python/alexa_push.py +178 -0
- package/python/alexa_worker.py +7 -1
- package/python/test_alexa_gateway_echo.py +14 -0
- package/python/test_alexa_push.py +227 -0
package/dist/addon.mjs
CHANGED
|
@@ -6042,7 +6042,7 @@ function number(params) {
|
|
|
6042
6042
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
6043
6043
|
}
|
|
6044
6044
|
//#endregion
|
|
6045
|
-
//#region ../types/dist/sleep-
|
|
6045
|
+
//#region ../types/dist/sleep-CmjWurK5.mjs
|
|
6046
6046
|
/**
|
|
6047
6047
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
6048
6048
|
* window to float samples (D455).
|
|
@@ -6726,137 +6726,102 @@ async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
|
6726
6726
|
blockOwner
|
|
6727
6727
|
};
|
|
6728
6728
|
}
|
|
6729
|
-
/**
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
const set = async (next) => {
|
|
6748
|
-
const validated = deps.schema.parse(next);
|
|
6749
|
-
await deps.write({ [deps.key]: validated });
|
|
6750
|
-
};
|
|
6751
|
-
const update = async (fn) => {
|
|
6752
|
-
await set(fn(await get()));
|
|
6753
|
-
};
|
|
6729
|
+
/** The record calls the one-switch migration makes, over the migration API. */
|
|
6730
|
+
function switchRecordsOverMigrationApi(api) {
|
|
6731
|
+
return { kind: (definition) => {
|
|
6732
|
+
const handle = api.records.kind(definition);
|
|
6733
|
+
return {
|
|
6734
|
+
get: (id) => handle.get({ id }),
|
|
6735
|
+
put: (input, opts) => handle.put({
|
|
6736
|
+
...input,
|
|
6737
|
+
...opts?.ifRev === void 0 ? {} : { ifRev: opts.ifRev }
|
|
6738
|
+
}),
|
|
6739
|
+
patch: (id, partial) => handle.patch({
|
|
6740
|
+
id,
|
|
6741
|
+
partial
|
|
6742
|
+
})
|
|
6743
|
+
};
|
|
6744
|
+
} };
|
|
6745
|
+
}
|
|
6746
|
+
function integrationRowsApiOver(api) {
|
|
6754
6747
|
return {
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6748
|
+
list: async () => (await api.integrations.list({})).integrations,
|
|
6749
|
+
readLegacyIntegrationRows: (input) => api.legacyRows.read(input),
|
|
6750
|
+
clearLegacyIntegrationRows: (input) => api.legacyRows.clear(input)
|
|
6758
6751
|
};
|
|
6759
6752
|
}
|
|
6753
|
+
var PREFIX = "records/";
|
|
6754
|
+
/** The base of every records failure. `detail` is the human part of the message. */
|
|
6755
|
+
var RecordsError = class extends Error {
|
|
6756
|
+
code;
|
|
6757
|
+
detail;
|
|
6758
|
+
constructor(code, detail) {
|
|
6759
|
+
super(`${PREFIX}${code}: ${detail}`);
|
|
6760
|
+
this.name = "RecordsError";
|
|
6761
|
+
this.code = code;
|
|
6762
|
+
this.detail = detail;
|
|
6763
|
+
}
|
|
6764
|
+
};
|
|
6765
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
6766
|
+
var RevConflictError = class extends RecordsError {
|
|
6767
|
+
constructor(detail) {
|
|
6768
|
+
super("rev-conflict", detail);
|
|
6769
|
+
this.name = "RevConflictError";
|
|
6770
|
+
}
|
|
6771
|
+
};
|
|
6772
|
+
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
6773
|
+
var RecordSecretUnavailableError = class extends RecordsError {
|
|
6774
|
+
constructor(detail) {
|
|
6775
|
+
super("secret-unavailable", detail);
|
|
6776
|
+
this.name = "RecordSecretUnavailableError";
|
|
6777
|
+
}
|
|
6778
|
+
};
|
|
6779
|
+
var InvalidRecordError = class extends RecordsError {
|
|
6780
|
+
constructor(detail) {
|
|
6781
|
+
super("invalid-record", detail);
|
|
6782
|
+
this.name = "InvalidRecordError";
|
|
6783
|
+
}
|
|
6784
|
+
};
|
|
6785
|
+
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
6786
|
+
var DeclarationRefusedError = class extends RecordsError {
|
|
6787
|
+
constructor(detail) {
|
|
6788
|
+
super("declaration-refused", detail);
|
|
6789
|
+
this.name = "DeclarationRefusedError";
|
|
6790
|
+
}
|
|
6791
|
+
};
|
|
6760
6792
|
/**
|
|
6761
|
-
*
|
|
6762
|
-
*
|
|
6763
|
-
*
|
|
6764
|
-
* hub-routed — the hub instance answers for every node), so fields whose
|
|
6765
|
-
* value is a NODE fact (decoder backend, engine pick, probed hardware
|
|
6766
|
-
* capabilities) persist under the node-scoped key `<key>@<nodeId>` inside
|
|
6767
|
-
* the single shared blob. The SCHEMA field key stays bare: the write path
|
|
6768
|
-
* ({@link scopePatch}) maps the bare field onto the target node's scoped
|
|
6769
|
-
* key; the read/hydrate path ({@link projectStore}) maps THIS node's value
|
|
6770
|
-
* back, so UI forms and `resolveConfig` only ever see the bare key.
|
|
6771
|
-
*
|
|
6772
|
-
* Fields opt in via the `perNode: true` schema marker on `ConfigFieldBase`
|
|
6773
|
-
* (`interfaces/config-ui.ts`); `BaseAddon` derives the key set from the
|
|
6774
|
-
* schema and routes reads/writes through these helpers.
|
|
6775
|
-
*
|
|
6776
|
-
* ## No bare-key fallback — deliberate
|
|
6777
|
-
*
|
|
6778
|
-
* {@link readNodeValue} reads the node-scoped key ONLY. A node with no
|
|
6779
|
-
* scoped key resolves to `undefined` so the field's schema `default` wins —
|
|
6780
|
-
* NEVER `store[base]` and never another node's value. A bare legacy key in
|
|
6781
|
-
* the store is invisible to every node, hub included, so one node's
|
|
6782
|
-
* selection can never leak onto another. (This generalizes the
|
|
6783
|
-
* `decoder-backend-keys.ts` semantics — the no-fallback variant — over an
|
|
6784
|
-
* arbitrary set of per-node field keys.)
|
|
6785
|
-
*
|
|
6786
|
-
* Pure functions only — no I/O, no imports beyond the language. This is a
|
|
6787
|
-
* LEAF module: import it via its deep path, never from the root barrel.
|
|
6788
|
-
*/
|
|
6789
|
-
/**
|
|
6790
|
-
* Normalize a raw kernel node id to the bare node id used for scoping.
|
|
6791
|
-
* `localNodeId` can carry a `<node>/<addon>` suffix on forked child
|
|
6792
|
-
* processes; per-node settings are per-NODE, so strip the addon segment.
|
|
6793
|
-
* `undefined` / `null` / empty falls back to `'hub'`.
|
|
6794
|
-
*/
|
|
6795
|
-
function normalizeNodeId(raw) {
|
|
6796
|
-
if (raw === void 0 || raw === null || raw === "") return "hub";
|
|
6797
|
-
const slashIdx = raw.indexOf("/");
|
|
6798
|
-
if (slashIdx < 0) return raw;
|
|
6799
|
-
const bare = raw.slice(0, slashIdx);
|
|
6800
|
-
return bare === "" ? "hub" : bare;
|
|
6801
|
-
}
|
|
6802
|
-
/** The node-scoped store key for a per-node field: `<base>@<nodeId>`. */
|
|
6803
|
-
function nodeScopedKey(base, nodeId) {
|
|
6804
|
-
return `${base}@${normalizeNodeId(nodeId)}`;
|
|
6805
|
-
}
|
|
6806
|
-
/**
|
|
6807
|
-
* Read a node's value for a per-node field from the raw shared store:
|
|
6808
|
-
* the node-scoped key when present, otherwise `undefined`.
|
|
6809
|
-
* Deliberately NO bare-key fallback (see module doc) — the caller lets the
|
|
6810
|
-
* schema `default` win on `undefined`.
|
|
6793
|
+
* The link primitive's declaration half (D1013 rule 5): what a link field names and how a kind
|
|
6794
|
+
* records it. Split from `instance-link.ts` so `RecordIntegrationInstances` can read the links of
|
|
6795
|
+
* its kind without importing the builders that import it.
|
|
6811
6796
|
*/
|
|
6812
|
-
|
|
6813
|
-
|
|
6797
|
+
/** The body field of a link: the id of the instance it names; the target rides in the field meta. */
|
|
6798
|
+
function linkField(target) {
|
|
6799
|
+
return string().meta({ link: target });
|
|
6814
6800
|
}
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
* patch is not mutated.
|
|
6821
|
-
*/
|
|
6822
|
-
function scopePatch(patch, perNodeKeys, nodeId) {
|
|
6823
|
-
const out = {};
|
|
6824
|
-
for (const [key, value] of Object.entries(patch)) out[perNodeKeys.has(key) ? nodeScopedKey(key, nodeId) : key] = value;
|
|
6825
|
-
return out;
|
|
6801
|
+
function linkTargetOf(meta) {
|
|
6802
|
+
if (typeof meta !== "object" || meta === null) return void 0;
|
|
6803
|
+
const link = Reflect.get(meta, "link");
|
|
6804
|
+
const parsed = LinkTargetSchema.safeParse(link);
|
|
6805
|
+
return parsed.success ? parsed.data : void 0;
|
|
6826
6806
|
}
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
const out = {};
|
|
6843
|
-
for (const [key, value] of Object.entries(store)) {
|
|
6844
|
-
if (key.includes("@")) continue;
|
|
6845
|
-
if (perNodeKeys.has(key)) continue;
|
|
6846
|
-
out[key] = value;
|
|
6847
|
-
}
|
|
6848
|
-
for (const base of perNodeKeys) {
|
|
6849
|
-
const value = readNodeValue(store, base, nodeId);
|
|
6850
|
-
if (value !== void 0) out[base] = value;
|
|
6807
|
+
var LinkTargetSchema = object({
|
|
6808
|
+
cap: string().min(1),
|
|
6809
|
+
kind: string().min(1).optional()
|
|
6810
|
+
});
|
|
6811
|
+
/** The links a kind declares, read from the meta of its body fields. */
|
|
6812
|
+
function linksOfKind(kind) {
|
|
6813
|
+
const schema = kind.schema;
|
|
6814
|
+
if (!(schema instanceof ZodObject)) return [];
|
|
6815
|
+
const links = [];
|
|
6816
|
+
for (const [field, fieldSchema] of Object.entries(schema.shape)) {
|
|
6817
|
+
const to = linkTargetOf(globalRegistry.get(fieldSchema));
|
|
6818
|
+
if (to !== void 0) links.push({
|
|
6819
|
+
field,
|
|
6820
|
+
to
|
|
6821
|
+
});
|
|
6851
6822
|
}
|
|
6852
|
-
return
|
|
6823
|
+
return links;
|
|
6853
6824
|
}
|
|
6854
|
-
/** Every scope, in the order the guard and the docs list them. */
|
|
6855
|
-
var SCOPED_SETTINGS_SCOPES = [
|
|
6856
|
-
"cluster",
|
|
6857
|
-
"device-defaults",
|
|
6858
|
-
"node"
|
|
6859
|
-
];
|
|
6860
6825
|
/**
|
|
6861
6826
|
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6862
6827
|
*
|
|
@@ -6991,6 +6956,643 @@ function parseSecretRef(ref) {
|
|
|
6991
6956
|
fieldPath
|
|
6992
6957
|
};
|
|
6993
6958
|
}
|
|
6959
|
+
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
6960
|
+
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
6961
|
+
var RECORD_DATABASES = ["settings", "events"];
|
|
6962
|
+
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
6963
|
+
var RECORD_CLASSES = [
|
|
6964
|
+
"config",
|
|
6965
|
+
"ledger",
|
|
6966
|
+
"audit",
|
|
6967
|
+
"mirror",
|
|
6968
|
+
"ephemeral"
|
|
6969
|
+
];
|
|
6970
|
+
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
6971
|
+
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
6972
|
+
var RECORD_BATCH_MAX = 1e3;
|
|
6973
|
+
var RECORD_QUERY_MAX = 1e3;
|
|
6974
|
+
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
6975
|
+
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
6976
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
6977
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
6978
|
+
/**
|
|
6979
|
+
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
6980
|
+
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
6981
|
+
* (`auth.accessToken`, `headers[].value`).
|
|
6982
|
+
*
|
|
6983
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
6984
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
6985
|
+
* key releases exactly that key's secret.
|
|
6986
|
+
*
|
|
6987
|
+
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
6988
|
+
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
6989
|
+
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
6990
|
+
* leaf names its element: `headers[X-Api].value`.
|
|
6991
|
+
*
|
|
6992
|
+
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
6993
|
+
*/
|
|
6994
|
+
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
6995
|
+
function parseSecretPath(path) {
|
|
6996
|
+
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
6997
|
+
return path.split(".").map((raw) => {
|
|
6998
|
+
const each = raw.endsWith("[]");
|
|
6999
|
+
const map = raw.endsWith("{}");
|
|
7000
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
7001
|
+
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
7002
|
+
return {
|
|
7003
|
+
key,
|
|
7004
|
+
each,
|
|
7005
|
+
map
|
|
7006
|
+
};
|
|
7007
|
+
});
|
|
7008
|
+
}
|
|
7009
|
+
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
7010
|
+
function secretArrayPrefixes(path) {
|
|
7011
|
+
const segments = parseSecretPath(path);
|
|
7012
|
+
const prefixes = [];
|
|
7013
|
+
const walked = [];
|
|
7014
|
+
for (const segment of segments) {
|
|
7015
|
+
walked.push(segment.key);
|
|
7016
|
+
if (segment.each) prefixes.push(walked.join("."));
|
|
7017
|
+
}
|
|
7018
|
+
return prefixes;
|
|
7019
|
+
}
|
|
7020
|
+
function isRecord$6(value) {
|
|
7021
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7022
|
+
}
|
|
7023
|
+
function elementId(element, idKey, declared, prefix) {
|
|
7024
|
+
const id = isRecord$6(element) ? element[idKey] : void 0;
|
|
7025
|
+
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
7026
|
+
return id;
|
|
7027
|
+
}
|
|
7028
|
+
/**
|
|
7029
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
7030
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
7031
|
+
* result is a new body, `data` is untouched.
|
|
7032
|
+
*/
|
|
7033
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
7034
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
7035
|
+
if (!isRecord$6(node)) return node;
|
|
7036
|
+
const segment = segments[index];
|
|
7037
|
+
if (segment === void 0) return node;
|
|
7038
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
7039
|
+
const child = node[segment.key];
|
|
7040
|
+
const keys = [...walkedKeys, segment.key];
|
|
7041
|
+
const last = index === segments.length - 1;
|
|
7042
|
+
if (segment.each) {
|
|
7043
|
+
if (!Array.isArray(child)) return node;
|
|
7044
|
+
const prefix = keys.join(".");
|
|
7045
|
+
const idKey = arrayKeys[prefix];
|
|
7046
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
7047
|
+
const mapped = child.map((element) => {
|
|
7048
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
7049
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
7050
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
7051
|
+
});
|
|
7052
|
+
return {
|
|
7053
|
+
...node,
|
|
7054
|
+
[segment.key]: mapped
|
|
7055
|
+
};
|
|
7056
|
+
}
|
|
7057
|
+
if (segment.map) {
|
|
7058
|
+
if (!isRecord$6(child)) return node;
|
|
7059
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
7060
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
7061
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
7062
|
+
return [mapKey, visit({
|
|
7063
|
+
declared,
|
|
7064
|
+
concrete,
|
|
7065
|
+
value
|
|
7066
|
+
})];
|
|
7067
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
7068
|
+
return {
|
|
7069
|
+
...node,
|
|
7070
|
+
[segment.key]: Object.fromEntries(kept)
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
7074
|
+
if (last) {
|
|
7075
|
+
const replacement = visit({
|
|
7076
|
+
declared,
|
|
7077
|
+
concrete,
|
|
7078
|
+
value: child
|
|
7079
|
+
});
|
|
7080
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
7081
|
+
return {
|
|
7082
|
+
...node,
|
|
7083
|
+
[segment.key]: replacement
|
|
7084
|
+
};
|
|
7085
|
+
}
|
|
7086
|
+
return {
|
|
7087
|
+
...node,
|
|
7088
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
7089
|
+
};
|
|
7090
|
+
}
|
|
7091
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
7092
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
7093
|
+
let current = data;
|
|
7094
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
7095
|
+
return current;
|
|
7096
|
+
}
|
|
7097
|
+
/**
|
|
7098
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
7099
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
7100
|
+
* already holds during an adoption.
|
|
7101
|
+
*/
|
|
7102
|
+
function recordSecretCollection(kind) {
|
|
7103
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
7104
|
+
}
|
|
7105
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
7106
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
7107
|
+
return buildSecretRef({
|
|
7108
|
+
owner,
|
|
7109
|
+
scope: {
|
|
7110
|
+
kind: "row",
|
|
7111
|
+
collection: recordSecretCollection(kind),
|
|
7112
|
+
rowId
|
|
7113
|
+
},
|
|
7114
|
+
fieldPath: concretePath
|
|
7115
|
+
});
|
|
7116
|
+
}
|
|
7117
|
+
/**
|
|
7118
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
7119
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
7120
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
7121
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
7122
|
+
* stored secret (the records door reads a view as KEEP).
|
|
7123
|
+
*
|
|
7124
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
7125
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
7126
|
+
*/
|
|
7127
|
+
function isRecord$5(value) {
|
|
7128
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7129
|
+
}
|
|
7130
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
7131
|
+
if (decl.secretPaths.length === 0) return data;
|
|
7132
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
7133
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
7134
|
+
return {
|
|
7135
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
7136
|
+
v: 1,
|
|
7137
|
+
set: true
|
|
7138
|
+
};
|
|
7139
|
+
});
|
|
7140
|
+
return isRecord$5(shown) ? shown : data;
|
|
7141
|
+
}
|
|
7142
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
7143
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
7144
|
+
/**
|
|
7145
|
+
* The ONE reader of an instance's switch (D996): an absent field means on, so a row written
|
|
7146
|
+
* before the field existed needs no migration. Every consumer of an instance body asks this.
|
|
7147
|
+
*/
|
|
7148
|
+
function isInstanceEnabled(body) {
|
|
7149
|
+
return body.enabled !== false;
|
|
7150
|
+
}
|
|
7151
|
+
/** The body key of the switch; only {@link RecordIntegrationInstances.setInstanceEnabled} writes it. */
|
|
7152
|
+
var SWITCH_KEY = "enabled";
|
|
7153
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
7154
|
+
function topLevelSecretKeys(paths) {
|
|
7155
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
7156
|
+
}
|
|
7157
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
7158
|
+
function secretMapKeys(paths) {
|
|
7159
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
7160
|
+
}
|
|
7161
|
+
/**
|
|
7162
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
7163
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
7164
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
7165
|
+
*/
|
|
7166
|
+
function secretMapPatch(value) {
|
|
7167
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
7168
|
+
const out = {};
|
|
7169
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
7170
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
7171
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
7172
|
+
}
|
|
7173
|
+
var RecordIntegrationInstances = class {
|
|
7174
|
+
records;
|
|
7175
|
+
kind;
|
|
7176
|
+
hooks;
|
|
7177
|
+
handle;
|
|
7178
|
+
secretKeys;
|
|
7179
|
+
secretMaps;
|
|
7180
|
+
nameField;
|
|
7181
|
+
switchListeners = /* @__PURE__ */ new Set();
|
|
7182
|
+
constructor(records, kind, hooks) {
|
|
7183
|
+
this.records = records;
|
|
7184
|
+
this.kind = kind;
|
|
7185
|
+
this.hooks = hooks;
|
|
7186
|
+
this.handle = records.kind(kind);
|
|
7187
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
7188
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
7189
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
7190
|
+
}
|
|
7191
|
+
/**
|
|
7192
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
7193
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
7194
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
7195
|
+
*/
|
|
7196
|
+
async readResolved(id) {
|
|
7197
|
+
try {
|
|
7198
|
+
const record = await this.handle.getResolved(id);
|
|
7199
|
+
return record === null ? null : {
|
|
7200
|
+
id,
|
|
7201
|
+
body: record.data,
|
|
7202
|
+
enabled: isInstanceEnabled(record.data),
|
|
7203
|
+
secrets: "resolved"
|
|
7204
|
+
};
|
|
7205
|
+
} catch (err) {
|
|
7206
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
7207
|
+
const stored = await this.handle.get(id);
|
|
7208
|
+
if (stored === null) return null;
|
|
7209
|
+
return {
|
|
7210
|
+
id,
|
|
7211
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
7212
|
+
enabled: isInstanceEnabled(stored.data),
|
|
7213
|
+
secrets: "unavailable"
|
|
7214
|
+
};
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
7217
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
7218
|
+
async getResolved(id) {
|
|
7219
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
7220
|
+
}
|
|
7221
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
7222
|
+
async listResolved() {
|
|
7223
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
7224
|
+
const out = [];
|
|
7225
|
+
for (const record of page.records) {
|
|
7226
|
+
const read = await this.readResolved(record.id);
|
|
7227
|
+
if (read !== null) out.push(read);
|
|
7228
|
+
}
|
|
7229
|
+
return out;
|
|
7230
|
+
}
|
|
7231
|
+
viewOf(record) {
|
|
7232
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
7233
|
+
const rawName = record.data[this.nameField];
|
|
7234
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
7235
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
7236
|
+
return {
|
|
7237
|
+
id: record.id,
|
|
7238
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
7239
|
+
configured: this.hooks.configured(record.data),
|
|
7240
|
+
enabled: isInstanceEnabled(record.data),
|
|
7241
|
+
formValues: shown,
|
|
7242
|
+
...kind !== void 0 ? { kind } : {},
|
|
7243
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
7244
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
7245
|
+
};
|
|
7246
|
+
}
|
|
7247
|
+
describe = async () => {
|
|
7248
|
+
const declared = linksOfKind(this.kind);
|
|
7249
|
+
const links = this.hooks.description.links !== void 0 || declared.length === 0 ? {} : { links: declared.map((link) => ({
|
|
7250
|
+
kind: this.kind.name,
|
|
7251
|
+
field: link.field,
|
|
7252
|
+
to: link.to
|
|
7253
|
+
})) };
|
|
7254
|
+
return {
|
|
7255
|
+
...this.hooks.description,
|
|
7256
|
+
...links,
|
|
7257
|
+
...this.hooks.mintId === void 0 ? {} : { mintsId: true }
|
|
7258
|
+
};
|
|
7259
|
+
};
|
|
7260
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
7261
|
+
listInstances = async () => {
|
|
7262
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
7263
|
+
};
|
|
7264
|
+
getInstance = async ({ id }) => {
|
|
7265
|
+
const record = await this.handle.get(id);
|
|
7266
|
+
return record === null ? null : this.viewOf(record);
|
|
7267
|
+
};
|
|
7268
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
7269
|
+
createInstance = async ({ id, name, kind, values }) => {
|
|
7270
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
7271
|
+
name,
|
|
7272
|
+
...kind !== void 0 ? { kind } : {}
|
|
7273
|
+
});
|
|
7274
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
7275
|
+
await this.handle.put({
|
|
7276
|
+
id: minted,
|
|
7277
|
+
data: this.hooks.initialBody(name, kind)
|
|
7278
|
+
}, { ifRev: 0 });
|
|
7279
|
+
const patch = this.patchOf(values ?? {});
|
|
7280
|
+
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
7281
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
7282
|
+
return { id: minted };
|
|
7283
|
+
};
|
|
7284
|
+
updateInstance = async ({ id, name, values }) => {
|
|
7285
|
+
const patch = this.patchOf(values);
|
|
7286
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
7287
|
+
await this.handle.patch(id, patch);
|
|
7288
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
7289
|
+
return { ok: true };
|
|
7290
|
+
};
|
|
7291
|
+
/** The record patch a form `values` object means: secrets kept, cleared or replaced. */
|
|
7292
|
+
patchOf(values) {
|
|
7293
|
+
const patch = {};
|
|
7294
|
+
for (const [key, value] of Object.entries(values)) {
|
|
7295
|
+
if (value === void 0) continue;
|
|
7296
|
+
if (key === "enabled") continue;
|
|
7297
|
+
if (this.secretMaps.has(key)) {
|
|
7298
|
+
const entries = secretMapPatch(value);
|
|
7299
|
+
if (entries !== null) patch[key] = entries;
|
|
7300
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
7301
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
7302
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
7303
|
+
}
|
|
7304
|
+
return patch;
|
|
7305
|
+
}
|
|
7306
|
+
/**
|
|
7307
|
+
* Listen to the switch of ANY instance of this helper (in the process that serves it). The
|
|
7308
|
+
* durable replacement of the `integration.enabled` / `integration.disabled` events (D8): the
|
|
7309
|
+
* addon reconciles from the record, whichever way it flipped.
|
|
7310
|
+
*/
|
|
7311
|
+
onSwitched(listener) {
|
|
7312
|
+
this.switchListeners.add(listener);
|
|
7313
|
+
}
|
|
7314
|
+
/**
|
|
7315
|
+
* The ONE writer of the switch (D996, D1014): the record body's `enabled`, for every role. A
|
|
7316
|
+
* configured thing is an instance, so an id with no record yet (a default integration that was
|
|
7317
|
+
* never configured) gets its record from `initialBody` before the switch is written.
|
|
7318
|
+
*/
|
|
7319
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
7320
|
+
if (await this.handle.get(id) === null) await this.handle.put({
|
|
7321
|
+
id,
|
|
7322
|
+
data: this.hooks.initialBody(id)
|
|
7323
|
+
}, { ifRev: 0 });
|
|
7324
|
+
await this.handle.patch(id, { [SWITCH_KEY]: enabled });
|
|
7325
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
7326
|
+
for (const listener of this.switchListeners) await listener(id, enabled);
|
|
7327
|
+
return { ok: true };
|
|
7328
|
+
};
|
|
7329
|
+
/** The row and its keychain refs go together (the records door). */
|
|
7330
|
+
deleteInstance = async ({ id }) => {
|
|
7331
|
+
await this.handle.delete(id);
|
|
7332
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
7333
|
+
return { ok: true };
|
|
7334
|
+
};
|
|
7335
|
+
testInstance = async (input) => {
|
|
7336
|
+
if (this.hooks.test === void 0) return {
|
|
7337
|
+
success: false,
|
|
7338
|
+
status: "unsupported",
|
|
7339
|
+
error: "This integration declares no connection test"
|
|
7340
|
+
};
|
|
7341
|
+
return this.hooks.test(input);
|
|
7342
|
+
};
|
|
7343
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
7344
|
+
toProvider() {
|
|
7345
|
+
return {
|
|
7346
|
+
describe: this.describe,
|
|
7347
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
7348
|
+
listInstances: this.listInstances,
|
|
7349
|
+
getInstance: this.getInstance,
|
|
7350
|
+
createInstance: this.createInstance,
|
|
7351
|
+
updateInstance: this.updateInstance,
|
|
7352
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
7353
|
+
deleteInstance: this.deleteInstance,
|
|
7354
|
+
testInstance: this.testInstance
|
|
7355
|
+
};
|
|
7356
|
+
}
|
|
7357
|
+
};
|
|
7358
|
+
/**
|
|
7359
|
+
* The one-switch migration (epic #440, step 3). An integration's on/off state used to live in the
|
|
7360
|
+
* registry's `enabled` column for a device addon; it now lives only in the instance record's
|
|
7361
|
+
* body (`enabled`, D1007 superseded). This moves a registry `enabled = false` into the body, once.
|
|
7362
|
+
*
|
|
7363
|
+
* For every registry row of the addon that is OFF:
|
|
7364
|
+
* - no record under its id: the record is created from `initialBody(name)` with `enabled: false`
|
|
7365
|
+
* (a default integration that never had settings, Reolink's among them);
|
|
7366
|
+
* - a record whose body states no `enabled`: `enabled: false` is written into it;
|
|
7367
|
+
* - a record that already states `enabled` (either way) is the newer truth and is left alone.
|
|
7368
|
+
* A row that is ON writes nothing: an absent field reads as on. A rerun finds every off row with
|
|
7369
|
+
* a stated switch and does nothing, so nothing switched off comes back on and nothing switched on
|
|
7370
|
+
* goes off. The registry column itself is neither read afterwards nor written; its drop is step 5.
|
|
7371
|
+
*
|
|
7372
|
+
* It runs in the addon process, BEFORE the addon registers its `integration-instances` provider,
|
|
7373
|
+
* which is also why `integrations.list` still answers the stored column at that point.
|
|
7374
|
+
*/
|
|
7375
|
+
function registrySwitchToBodyId(addonId, date) {
|
|
7376
|
+
return `${addonId}/${date}-registry-switch-to-body`;
|
|
7377
|
+
}
|
|
7378
|
+
async function moveRegistrySwitchToBody(ports, spec) {
|
|
7379
|
+
const rows = await ports.listIntegrations(spec.addonId);
|
|
7380
|
+
let created = 0;
|
|
7381
|
+
let switched = 0;
|
|
7382
|
+
let kept = 0;
|
|
7383
|
+
for (const row of rows) {
|
|
7384
|
+
if (row.enabled) continue;
|
|
7385
|
+
const body = await ports.readBody(row.id);
|
|
7386
|
+
if (body === null) {
|
|
7387
|
+
if (await ports.createRecord(row.id, {
|
|
7388
|
+
...spec.initialBody(row.name),
|
|
7389
|
+
["enabled"]: false
|
|
7390
|
+
}) === "created") created += 1;
|
|
7391
|
+
else kept += 1;
|
|
7392
|
+
continue;
|
|
7393
|
+
}
|
|
7394
|
+
if ("enabled" in body) {
|
|
7395
|
+
kept += 1;
|
|
7396
|
+
continue;
|
|
7397
|
+
}
|
|
7398
|
+
await ports.switchOff(row.id);
|
|
7399
|
+
switched += 1;
|
|
7400
|
+
}
|
|
7401
|
+
const summary = {
|
|
7402
|
+
integrations: rows.length,
|
|
7403
|
+
off: rows.filter((r) => !r.enabled).length,
|
|
7404
|
+
created,
|
|
7405
|
+
switched,
|
|
7406
|
+
kept
|
|
7407
|
+
};
|
|
7408
|
+
return created === 0 && switched === 0 ? {
|
|
7409
|
+
outcome: "already-done",
|
|
7410
|
+
summary
|
|
7411
|
+
} : {
|
|
7412
|
+
outcome: "done",
|
|
7413
|
+
summary
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7416
|
+
/** A migration entry: an addon states its kind and how a new body starts, nothing else. */
|
|
7417
|
+
function registrySwitchToBodyMigration(spec) {
|
|
7418
|
+
return {
|
|
7419
|
+
meta: {
|
|
7420
|
+
id: registrySwitchToBodyId(spec.addonId, spec.date),
|
|
7421
|
+
owner: spec.addonId,
|
|
7422
|
+
level: "addon-state",
|
|
7423
|
+
seq: spec.seq,
|
|
7424
|
+
dependsOn: [],
|
|
7425
|
+
store: "settings",
|
|
7426
|
+
scope: "cluster",
|
|
7427
|
+
onFailure: "block-owner",
|
|
7428
|
+
bodyKind: "procedure",
|
|
7429
|
+
description: "Move the registry enabled flag of every switched-off integration into its instance record body, creating the record when there is none",
|
|
7430
|
+
fixtureMark: "enabled"
|
|
7431
|
+
},
|
|
7432
|
+
procedure: (ports) => moveRegistrySwitchToBody(ports, {
|
|
7433
|
+
addonId: spec.addonId,
|
|
7434
|
+
initialBody: spec.initialBody
|
|
7435
|
+
})
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
7438
|
+
/** The hub and records sides of {@link registrySwitchToBodyMigration}. */
|
|
7439
|
+
function registrySwitchPortsOver(deps) {
|
|
7440
|
+
const handle = deps.records.kind({
|
|
7441
|
+
name: deps.kind.name,
|
|
7442
|
+
declaration: deps.kind.declaration,
|
|
7443
|
+
schema: record(string(), unknown())
|
|
7444
|
+
});
|
|
7445
|
+
return {
|
|
7446
|
+
listIntegrations: async (addonId) => (await deps.api.list()).filter((i) => i.addonId === addonId).map((i) => ({
|
|
7447
|
+
id: i.id,
|
|
7448
|
+
name: i.name,
|
|
7449
|
+
enabled: i.enabled !== false
|
|
7450
|
+
})),
|
|
7451
|
+
readBody: async (id) => (await handle.get(id))?.data ?? null,
|
|
7452
|
+
createRecord: async (id, body) => {
|
|
7453
|
+
if (await handle.get(id) !== null) return "exists";
|
|
7454
|
+
await handle.put({
|
|
7455
|
+
id,
|
|
7456
|
+
data: body
|
|
7457
|
+
}, { ifRev: 0 });
|
|
7458
|
+
return "created";
|
|
7459
|
+
},
|
|
7460
|
+
switchOff: async (id) => {
|
|
7461
|
+
await handle.patch(id, { [SWITCH_KEY]: false });
|
|
7462
|
+
}
|
|
7463
|
+
};
|
|
7464
|
+
}
|
|
7465
|
+
/**
|
|
7466
|
+
* Build a {@link DurableState} over a single store key. Transport-agnostic:
|
|
7467
|
+
* `read`/`write` are the addon-store or device-store accessors. The whole
|
|
7468
|
+
* validated value round-trips on every read/write — the schema is the
|
|
7469
|
+
* single source of truth for what is persisted, so no hand-listed field
|
|
7470
|
+
* set can drop a key on save.
|
|
7471
|
+
*/
|
|
7472
|
+
function createDurableState(deps) {
|
|
7473
|
+
const get = async () => {
|
|
7474
|
+
const raw = (await deps.read())[deps.key];
|
|
7475
|
+
if (raw === void 0) return deps.fallback;
|
|
7476
|
+
const parsed = deps.schema.safeParse(raw);
|
|
7477
|
+
if (!parsed.success) {
|
|
7478
|
+
deps.onParseError?.(deps.key, parsed.error);
|
|
7479
|
+
return deps.fallback;
|
|
7480
|
+
}
|
|
7481
|
+
return parsed.data;
|
|
7482
|
+
};
|
|
7483
|
+
const set = async (next) => {
|
|
7484
|
+
const validated = deps.schema.parse(next);
|
|
7485
|
+
await deps.write({ [deps.key]: validated });
|
|
7486
|
+
};
|
|
7487
|
+
const update = async (fn) => {
|
|
7488
|
+
await set(fn(await get()));
|
|
7489
|
+
};
|
|
7490
|
+
return {
|
|
7491
|
+
get,
|
|
7492
|
+
set,
|
|
7493
|
+
update
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
/**
|
|
7497
|
+
* Per-node scoping for the shared addon-settings blob.
|
|
7498
|
+
*
|
|
7499
|
+
* An addon's settings store is hub-central (the `addon-settings` cap is
|
|
7500
|
+
* hub-routed — the hub instance answers for every node), so fields whose
|
|
7501
|
+
* value is a NODE fact (decoder backend, engine pick, probed hardware
|
|
7502
|
+
* capabilities) persist under the node-scoped key `<key>@<nodeId>` inside
|
|
7503
|
+
* the single shared blob. The SCHEMA field key stays bare: the write path
|
|
7504
|
+
* ({@link scopePatch}) maps the bare field onto the target node's scoped
|
|
7505
|
+
* key; the read/hydrate path ({@link projectStore}) maps THIS node's value
|
|
7506
|
+
* back, so UI forms and `resolveConfig` only ever see the bare key.
|
|
7507
|
+
*
|
|
7508
|
+
* Fields opt in via the `perNode: true` schema marker on `ConfigFieldBase`
|
|
7509
|
+
* (`interfaces/config-ui.ts`); `BaseAddon` derives the key set from the
|
|
7510
|
+
* schema and routes reads/writes through these helpers.
|
|
7511
|
+
*
|
|
7512
|
+
* ## No bare-key fallback — deliberate
|
|
7513
|
+
*
|
|
7514
|
+
* {@link readNodeValue} reads the node-scoped key ONLY. A node with no
|
|
7515
|
+
* scoped key resolves to `undefined` so the field's schema `default` wins —
|
|
7516
|
+
* NEVER `store[base]` and never another node's value. A bare legacy key in
|
|
7517
|
+
* the store is invisible to every node, hub included, so one node's
|
|
7518
|
+
* selection can never leak onto another. (This generalizes the
|
|
7519
|
+
* `decoder-backend-keys.ts` semantics — the no-fallback variant — over an
|
|
7520
|
+
* arbitrary set of per-node field keys.)
|
|
7521
|
+
*
|
|
7522
|
+
* Pure functions only — no I/O, no imports beyond the language. This is a
|
|
7523
|
+
* LEAF module: import it via its deep path, never from the root barrel.
|
|
7524
|
+
*/
|
|
7525
|
+
/**
|
|
7526
|
+
* Normalize a raw kernel node id to the bare node id used for scoping.
|
|
7527
|
+
* `localNodeId` can carry a `<node>/<addon>` suffix on forked child
|
|
7528
|
+
* processes; per-node settings are per-NODE, so strip the addon segment.
|
|
7529
|
+
* `undefined` / `null` / empty falls back to `'hub'`.
|
|
7530
|
+
*/
|
|
7531
|
+
function normalizeNodeId(raw) {
|
|
7532
|
+
if (raw === void 0 || raw === null || raw === "") return "hub";
|
|
7533
|
+
const slashIdx = raw.indexOf("/");
|
|
7534
|
+
if (slashIdx < 0) return raw;
|
|
7535
|
+
const bare = raw.slice(0, slashIdx);
|
|
7536
|
+
return bare === "" ? "hub" : bare;
|
|
7537
|
+
}
|
|
7538
|
+
/** The node-scoped store key for a per-node field: `<base>@<nodeId>`. */
|
|
7539
|
+
function nodeScopedKey(base, nodeId) {
|
|
7540
|
+
return `${base}@${normalizeNodeId(nodeId)}`;
|
|
7541
|
+
}
|
|
7542
|
+
/**
|
|
7543
|
+
* Read a node's value for a per-node field from the raw shared store:
|
|
7544
|
+
* the node-scoped key when present, otherwise `undefined`.
|
|
7545
|
+
* Deliberately NO bare-key fallback (see module doc) — the caller lets the
|
|
7546
|
+
* schema `default` win on `undefined`.
|
|
7547
|
+
*/
|
|
7548
|
+
function readNodeValue(store, base, nodeId) {
|
|
7549
|
+
return store[nodeScopedKey(base, nodeId)];
|
|
7550
|
+
}
|
|
7551
|
+
/**
|
|
7552
|
+
* Re-map a UI/settings patch so every bare perNode field persists under the
|
|
7553
|
+
* TARGET node's scoped key; all other keys pass through unchanged. Used on
|
|
7554
|
+
* the write path so a save for one node never clobbers another node's value
|
|
7555
|
+
* (and the bare key is never written). Returns a new object — the input
|
|
7556
|
+
* patch is not mutated.
|
|
7557
|
+
*/
|
|
7558
|
+
function scopePatch(patch, perNodeKeys, nodeId) {
|
|
7559
|
+
const out = {};
|
|
7560
|
+
for (const [key, value] of Object.entries(patch)) out[perNodeKeys.has(key) ? nodeScopedKey(key, nodeId) : key] = value;
|
|
7561
|
+
return out;
|
|
7562
|
+
}
|
|
7563
|
+
/**
|
|
7564
|
+
* Project the raw shared store onto the bare schema keys for ONE node so a
|
|
7565
|
+
* UI schema (whose field keys are bare) hydrates from that node's own
|
|
7566
|
+
* values:
|
|
7567
|
+
*
|
|
7568
|
+
* - EVERY `@`-scoped key (any node's) is dropped from the pass-through.
|
|
7569
|
+
* - Every bare perNode key is dropped from the pass-through (a stray bare
|
|
7570
|
+
* legacy key must never hydrate any node — no bare fallback).
|
|
7571
|
+
* - THIS node's effective value ({@link readNodeValue}) is then laid onto
|
|
7572
|
+
* each bare perNode key; when the node has no scoped key the bare key is
|
|
7573
|
+
* left ABSENT so the field's schema `default` wins.
|
|
7574
|
+
*
|
|
7575
|
+
* Returns a new object — the input store is not mutated.
|
|
7576
|
+
*/
|
|
7577
|
+
function projectStore(store, perNodeKeys, nodeId) {
|
|
7578
|
+
const out = {};
|
|
7579
|
+
for (const [key, value] of Object.entries(store)) {
|
|
7580
|
+
if (key.includes("@")) continue;
|
|
7581
|
+
if (perNodeKeys.has(key)) continue;
|
|
7582
|
+
out[key] = value;
|
|
7583
|
+
}
|
|
7584
|
+
for (const base of perNodeKeys) {
|
|
7585
|
+
const value = readNodeValue(store, base, nodeId);
|
|
7586
|
+
if (value !== void 0) out[base] = value;
|
|
7587
|
+
}
|
|
7588
|
+
return out;
|
|
7589
|
+
}
|
|
7590
|
+
/** Every scope, in the order the guard and the docs list them. */
|
|
7591
|
+
var SCOPED_SETTINGS_SCOPES = [
|
|
7592
|
+
"cluster",
|
|
7593
|
+
"device-defaults",
|
|
7594
|
+
"node"
|
|
7595
|
+
];
|
|
6994
7596
|
/** A write to a secret path could not be stored in the keychain, so it was refused. */
|
|
6995
7597
|
var KeychainWriteRefusedError = class extends Error {
|
|
6996
7598
|
fieldPath;
|
|
@@ -7001,7 +7603,7 @@ var KeychainWriteRefusedError = class extends Error {
|
|
|
7001
7603
|
}
|
|
7002
7604
|
};
|
|
7003
7605
|
var KEEP = { kind: "keep" };
|
|
7004
|
-
function isRecord$
|
|
7606
|
+
function isRecord$4$1(value) {
|
|
7005
7607
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7006
7608
|
}
|
|
7007
7609
|
function defaultMintId() {
|
|
@@ -7014,7 +7616,7 @@ function collectSecretMarkerRefs(value, out = []) {
|
|
|
7014
7616
|
return out;
|
|
7015
7617
|
}
|
|
7016
7618
|
if (Array.isArray(value)) for (const item of value) collectSecretMarkerRefs(item, out);
|
|
7017
|
-
else if (isRecord$
|
|
7619
|
+
else if (isRecord$4$1(value)) for (const child of Object.values(value)) collectSecretMarkerRefs(child, out);
|
|
7018
7620
|
return out;
|
|
7019
7621
|
}
|
|
7020
7622
|
async function storedValueEquals(secrets, marker, candidate) {
|
|
@@ -7069,7 +7671,7 @@ function rowId(row, idKey) {
|
|
|
7069
7671
|
return null;
|
|
7070
7672
|
}
|
|
7071
7673
|
async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
7072
|
-
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$
|
|
7674
|
+
const storedRows = Array.isArray(stored) ? stored.filter(isRecord$4$1) : [];
|
|
7073
7675
|
if (!Array.isArray(incoming)) {
|
|
7074
7676
|
ctx.deletes.push(...collectSecretMarkerRefs(storedRows));
|
|
7075
7677
|
return {
|
|
@@ -7085,7 +7687,7 @@ async function writeArray(ctx, node, incoming, stored, fieldPath) {
|
|
|
7085
7687
|
const kept = /* @__PURE__ */ new Set();
|
|
7086
7688
|
const rows = [];
|
|
7087
7689
|
for (const row of incoming) {
|
|
7088
|
-
if (!isRecord$
|
|
7690
|
+
if (!isRecord$4$1(row)) {
|
|
7089
7691
|
rows.push(row);
|
|
7090
7692
|
continue;
|
|
7091
7693
|
}
|
|
@@ -7110,7 +7712,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
7110
7712
|
case "leaf": return writeLeaf(ctx, incoming, stored, fieldPath);
|
|
7111
7713
|
case "array": return writeArray(ctx, node, incoming, stored, fieldPath);
|
|
7112
7714
|
case "object": {
|
|
7113
|
-
if (!isRecord$
|
|
7715
|
+
if (!isRecord$4$1(incoming)) {
|
|
7114
7716
|
if (incoming === void 0) return KEEP;
|
|
7115
7717
|
ctx.deletes.push(...collectSecretMarkerRefs(stored));
|
|
7116
7718
|
return {
|
|
@@ -7118,7 +7720,7 @@ async function writeNode(ctx, node, incoming, stored, fieldPath) {
|
|
|
7118
7720
|
value: incoming
|
|
7119
7721
|
};
|
|
7120
7722
|
}
|
|
7121
|
-
const storedObj = isRecord$
|
|
7723
|
+
const storedObj = isRecord$4$1(stored) ? stored : {};
|
|
7122
7724
|
return {
|
|
7123
7725
|
kind: "value",
|
|
7124
7726
|
value: await writeMembers(ctx, node.children, incoming, storedObj, (m) => `${fieldPath}.${m}`)
|
|
@@ -7241,7 +7843,7 @@ async function resolveSecretMarkers(value, secrets, options = {}) {
|
|
|
7241
7843
|
value: items
|
|
7242
7844
|
};
|
|
7243
7845
|
}
|
|
7244
|
-
if (isRecord$
|
|
7846
|
+
if (isRecord$4$1(v)) {
|
|
7245
7847
|
const out = {};
|
|
7246
7848
|
for (const [k, child] of Object.entries(v)) {
|
|
7247
7849
|
const s = substitute(child);
|
|
@@ -7273,7 +7875,7 @@ var SecretOverwriteRefusedError = class extends Error {
|
|
|
7273
7875
|
this.name = "SecretOverwriteRefusedError";
|
|
7274
7876
|
}
|
|
7275
7877
|
};
|
|
7276
|
-
function isRecord$
|
|
7878
|
+
function isRecord$3$1(value) {
|
|
7277
7879
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7278
7880
|
}
|
|
7279
7881
|
function resolvedValue(pass, marker) {
|
|
@@ -7302,7 +7904,7 @@ function inArray(pass, incoming, stored) {
|
|
|
7302
7904
|
return incoming.map((item, index) => {
|
|
7303
7905
|
const twin = stored[index];
|
|
7304
7906
|
if (isSecretMarker(twin)) return typeof item === "string" && item === resolvedValue(pass, twin) ? twin : item;
|
|
7305
|
-
if (isRecord$
|
|
7907
|
+
if (isRecord$3$1(item) && isRecord$3$1(twin)) return inArrayRow(pass, item, twin);
|
|
7306
7908
|
if (Array.isArray(item) && Array.isArray(twin)) return inArray(pass, item, twin);
|
|
7307
7909
|
return item;
|
|
7308
7910
|
});
|
|
@@ -7313,7 +7915,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
7313
7915
|
const twin = stored[key];
|
|
7314
7916
|
if (isSecretMarker(twin)) {
|
|
7315
7917
|
if (typeof value === "string" && value === resolvedValue(pass, twin)) out[key] = twin;
|
|
7316
|
-
} else if (isRecord$
|
|
7918
|
+
} else if (isRecord$3$1(value) && isRecord$3$1(twin)) out[key] = inArrayRow(pass, value, twin);
|
|
7317
7919
|
else if (Array.isArray(value) && Array.isArray(twin)) out[key] = inArray(pass, value, twin);
|
|
7318
7920
|
}
|
|
7319
7921
|
return out;
|
|
@@ -7321,7 +7923,7 @@ function inArrayRow(pass, incoming, stored) {
|
|
|
7321
7923
|
async function walk(pass, incoming, stored) {
|
|
7322
7924
|
if (isSecretMarker(stored)) return atMarker(pass, incoming, stored);
|
|
7323
7925
|
if (Array.isArray(incoming) && Array.isArray(stored)) return inArray(pass, incoming, stored);
|
|
7324
|
-
if (!isRecord$
|
|
7926
|
+
if (!isRecord$3$1(incoming) || !isRecord$3$1(stored)) return incoming;
|
|
7325
7927
|
const out = { ...incoming };
|
|
7326
7928
|
for (const [key, value] of Object.entries(incoming)) out[key] = await walk(pass, value, stored[key]);
|
|
7327
7929
|
return out;
|
|
@@ -7352,7 +7954,7 @@ async function restoreUnchangedSecrets(input) {
|
|
|
7352
7954
|
function isJsonEqual(a, b) {
|
|
7353
7955
|
if (Object.is(a, b)) return true;
|
|
7354
7956
|
if (Array.isArray(a)) return Array.isArray(b) && a.length === b.length && a.every((v, i) => isJsonEqual(v, b[i]));
|
|
7355
|
-
if (isRecord$
|
|
7957
|
+
if (isRecord$3$1(a) && isRecord$3$1(b)) {
|
|
7356
7958
|
const keys = Object.keys(a);
|
|
7357
7959
|
if (keys.length !== Object.keys(b).length) return false;
|
|
7358
7960
|
return keys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && isJsonEqual(a[k], b[k]));
|
|
@@ -7376,7 +7978,7 @@ function markerKeysOf(blob, owner) {
|
|
|
7376
7978
|
}
|
|
7377
7979
|
function nodeFromStored(value, owner) {
|
|
7378
7980
|
if (isSecretMarker(value)) return isSecretRefOwnedBy(value.$secret, owner) ? { kind: "leaf" } : null;
|
|
7379
|
-
if (!isRecord$
|
|
7981
|
+
if (!isRecord$3$1(value)) return null;
|
|
7380
7982
|
const children = /* @__PURE__ */ new Map();
|
|
7381
7983
|
for (const [key, child] of Object.entries(value)) {
|
|
7382
7984
|
const node = nodeFromStored(child, owner);
|
|
@@ -7873,13 +8475,48 @@ var BaseAddon = class {
|
|
|
7873
8475
|
if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
|
|
7874
8476
|
return this.runBound(sets.flatMap((set) => set.bind(api)), options);
|
|
7875
8477
|
}
|
|
7876
|
-
|
|
8478
|
+
/**
|
|
8479
|
+
* A device addon states its instance kind once, before its migrations run, and the one-switch
|
|
8480
|
+
* migration (registry `enabled` to the instance body, epic #440 step 3) joins whatever
|
|
8481
|
+
* catalogue the addon runs, in the SAME declaration (the engine replaces an owner's catalogue
|
|
8482
|
+
* on every declare).
|
|
8483
|
+
*/
|
|
8484
|
+
declareRegistrySwitch(spec) {
|
|
8485
|
+
this.registrySwitchSpec = spec;
|
|
8486
|
+
}
|
|
8487
|
+
registrySwitchSpec = null;
|
|
8488
|
+
registrySwitchBound() {
|
|
8489
|
+
const spec = this.registrySwitchSpec;
|
|
8490
|
+
if (spec === null) return [];
|
|
8491
|
+
const api = this.ctx.migrationApi;
|
|
8492
|
+
if (api === void 0) {
|
|
8493
|
+
this.ctx.logger.warn("the registry switch migration is skipped: the migration API is not available on this context");
|
|
8494
|
+
return [];
|
|
8495
|
+
}
|
|
8496
|
+
return migrationSet([registrySwitchToBodyMigration({
|
|
8497
|
+
addonId: this.ctx.id,
|
|
8498
|
+
date: "2026-11-06",
|
|
8499
|
+
seq: 1e3,
|
|
8500
|
+
initialBody: spec.initialBody
|
|
8501
|
+
})], (a) => registrySwitchPortsOver({
|
|
8502
|
+
api: integrationRowsApiOver(a),
|
|
8503
|
+
records: switchRecordsOverMigrationApi(a),
|
|
8504
|
+
kind: spec.kind
|
|
8505
|
+
})).bind(api);
|
|
8506
|
+
}
|
|
8507
|
+
async runBound(declared, options) {
|
|
8508
|
+
const added = this.registrySwitchBound();
|
|
8509
|
+
const migrations = [...declared, ...added];
|
|
8510
|
+
const phased = options?.only === void 0 || added.length === 0 ? options : {
|
|
8511
|
+
...options,
|
|
8512
|
+
only: [...options.only, ...added.map((m) => m.meta.id)]
|
|
8513
|
+
};
|
|
7877
8514
|
const api = this.ctx.api.migrations;
|
|
7878
8515
|
const report = await runAddonMigrations({
|
|
7879
8516
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7880
8517
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7881
8518
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7882
|
-
}, migrations, this.ctx.logger,
|
|
8519
|
+
}, migrations, this.ctx.logger, phased);
|
|
7883
8520
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7884
8521
|
return report;
|
|
7885
8522
|
}
|
|
@@ -8952,228 +9589,6 @@ function normalizeAddonInitResult(result) {
|
|
|
8952
9589
|
if (Array.isArray(result)) return { providers: result };
|
|
8953
9590
|
return result;
|
|
8954
9591
|
}
|
|
8955
|
-
var PREFIX = "records/";
|
|
8956
|
-
/** The base of every records failure. `detail` is the human part of the message. */
|
|
8957
|
-
var RecordsError = class extends Error {
|
|
8958
|
-
code;
|
|
8959
|
-
detail;
|
|
8960
|
-
constructor(code, detail) {
|
|
8961
|
-
super(`${PREFIX}${code}: ${detail}`);
|
|
8962
|
-
this.name = "RecordsError";
|
|
8963
|
-
this.code = code;
|
|
8964
|
-
this.detail = detail;
|
|
8965
|
-
}
|
|
8966
|
-
};
|
|
8967
|
-
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8968
|
-
var RevConflictError = class extends RecordsError {
|
|
8969
|
-
constructor(detail) {
|
|
8970
|
-
super("rev-conflict", detail);
|
|
8971
|
-
this.name = "RevConflictError";
|
|
8972
|
-
}
|
|
8973
|
-
};
|
|
8974
|
-
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8975
|
-
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8976
|
-
constructor(detail) {
|
|
8977
|
-
super("secret-unavailable", detail);
|
|
8978
|
-
this.name = "RecordSecretUnavailableError";
|
|
8979
|
-
}
|
|
8980
|
-
};
|
|
8981
|
-
var InvalidRecordError = class extends RecordsError {
|
|
8982
|
-
constructor(detail) {
|
|
8983
|
-
super("invalid-record", detail);
|
|
8984
|
-
this.name = "InvalidRecordError";
|
|
8985
|
-
}
|
|
8986
|
-
};
|
|
8987
|
-
/** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
|
|
8988
|
-
var DeclarationRefusedError = class extends RecordsError {
|
|
8989
|
-
constructor(detail) {
|
|
8990
|
-
super("declaration-refused", detail);
|
|
8991
|
-
this.name = "DeclarationRefusedError";
|
|
8992
|
-
}
|
|
8993
|
-
};
|
|
8994
|
-
/** Limits and names of `ctx.records` (D881). One authority; the engine and the facade read it. */
|
|
8995
|
-
/** The files a kind may live in. `history` is device-history's (D850), never records. */
|
|
8996
|
-
var RECORD_DATABASES = ["settings", "events"];
|
|
8997
|
-
/** The classes of a kind: the `@durable` vocabulary, one authority (replaces the annotation). */
|
|
8998
|
-
var RECORD_CLASSES = [
|
|
8999
|
-
"config",
|
|
9000
|
-
"ledger",
|
|
9001
|
-
"audit",
|
|
9002
|
-
"mirror",
|
|
9003
|
-
"ephemeral"
|
|
9004
|
-
];
|
|
9005
|
-
var RECORD_DEFAULT_MAX_DATA_BYTES = 64 * 1024;
|
|
9006
|
-
var RECORD_MAX_DATA_BYTES = 1024 * 1024;
|
|
9007
|
-
var RECORD_BATCH_MAX = 1e3;
|
|
9008
|
-
var RECORD_QUERY_MAX = 1e3;
|
|
9009
|
-
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
9010
|
-
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
9011
|
-
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
9012
|
-
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
9013
|
-
/**
|
|
9014
|
-
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
9015
|
-
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
9016
|
-
* (`auth.accessToken`, `headers[].value`).
|
|
9017
|
-
*
|
|
9018
|
-
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
9019
|
-
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
9020
|
-
* key releases exactly that key's secret.
|
|
9021
|
-
*
|
|
9022
|
-
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
9023
|
-
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
9024
|
-
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
9025
|
-
* leaf names its element: `headers[X-Api].value`.
|
|
9026
|
-
*
|
|
9027
|
-
* Pure and browser-safe. Nothing here mutates: every transform returns a new value.
|
|
9028
|
-
*/
|
|
9029
|
-
/** The array prefixes (declared form, e.g. `headers` or `a.headers`) of a path. */
|
|
9030
|
-
function parseSecretPath(path) {
|
|
9031
|
-
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
9032
|
-
return path.split(".").map((raw) => {
|
|
9033
|
-
const each = raw.endsWith("[]");
|
|
9034
|
-
const map = raw.endsWith("{}");
|
|
9035
|
-
const key = each || map ? raw.slice(0, -2) : raw;
|
|
9036
|
-
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
9037
|
-
return {
|
|
9038
|
-
key,
|
|
9039
|
-
each,
|
|
9040
|
-
map
|
|
9041
|
-
};
|
|
9042
|
-
});
|
|
9043
|
-
}
|
|
9044
|
-
/** The declared array prefixes of `path` that need an id key (`headers`, `a.rows`). */
|
|
9045
|
-
function secretArrayPrefixes(path) {
|
|
9046
|
-
const segments = parseSecretPath(path);
|
|
9047
|
-
const prefixes = [];
|
|
9048
|
-
const walked = [];
|
|
9049
|
-
for (const segment of segments) {
|
|
9050
|
-
walked.push(segment.key);
|
|
9051
|
-
if (segment.each) prefixes.push(walked.join("."));
|
|
9052
|
-
}
|
|
9053
|
-
return prefixes;
|
|
9054
|
-
}
|
|
9055
|
-
function isRecord$4$1(value) {
|
|
9056
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9057
|
-
}
|
|
9058
|
-
function elementId(element, idKey, declared, prefix) {
|
|
9059
|
-
const id = isRecord$4$1(element) ? element[idKey] : void 0;
|
|
9060
|
-
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
9061
|
-
return id;
|
|
9062
|
-
}
|
|
9063
|
-
/**
|
|
9064
|
-
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
9065
|
-
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
9066
|
-
* result is a new body, `data` is untouched.
|
|
9067
|
-
*/
|
|
9068
|
-
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
9069
|
-
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
9070
|
-
if (!isRecord$4$1(node)) return node;
|
|
9071
|
-
const segment = segments[index];
|
|
9072
|
-
if (segment === void 0) return node;
|
|
9073
|
-
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
9074
|
-
const child = node[segment.key];
|
|
9075
|
-
const keys = [...walkedKeys, segment.key];
|
|
9076
|
-
const last = index === segments.length - 1;
|
|
9077
|
-
if (segment.each) {
|
|
9078
|
-
if (!Array.isArray(child)) return node;
|
|
9079
|
-
const prefix = keys.join(".");
|
|
9080
|
-
const idKey = arrayKeys[prefix];
|
|
9081
|
-
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
9082
|
-
const mapped = child.map((element) => {
|
|
9083
|
-
const id = elementId(element, idKey, declared, prefix);
|
|
9084
|
-
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
9085
|
-
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
9086
|
-
});
|
|
9087
|
-
return {
|
|
9088
|
-
...node,
|
|
9089
|
-
[segment.key]: mapped
|
|
9090
|
-
};
|
|
9091
|
-
}
|
|
9092
|
-
if (segment.map) {
|
|
9093
|
-
if (!isRecord$4$1(child)) return node;
|
|
9094
|
-
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
9095
|
-
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
9096
|
-
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
9097
|
-
return [mapKey, visit({
|
|
9098
|
-
declared,
|
|
9099
|
-
concrete,
|
|
9100
|
-
value
|
|
9101
|
-
})];
|
|
9102
|
-
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
9103
|
-
return {
|
|
9104
|
-
...node,
|
|
9105
|
-
[segment.key]: Object.fromEntries(kept)
|
|
9106
|
-
};
|
|
9107
|
-
}
|
|
9108
|
-
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
9109
|
-
if (last) {
|
|
9110
|
-
const replacement = visit({
|
|
9111
|
-
declared,
|
|
9112
|
-
concrete,
|
|
9113
|
-
value: child
|
|
9114
|
-
});
|
|
9115
|
-
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
9116
|
-
return {
|
|
9117
|
-
...node,
|
|
9118
|
-
[segment.key]: replacement
|
|
9119
|
-
};
|
|
9120
|
-
}
|
|
9121
|
-
return {
|
|
9122
|
-
...node,
|
|
9123
|
-
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
9124
|
-
};
|
|
9125
|
-
}
|
|
9126
|
-
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
9127
|
-
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
9128
|
-
let current = data;
|
|
9129
|
-
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
9130
|
-
return current;
|
|
9131
|
-
}
|
|
9132
|
-
/**
|
|
9133
|
-
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
9134
|
-
* record's refs never collide with the refs a legacy collection of the same name
|
|
9135
|
-
* already holds during an adoption.
|
|
9136
|
-
*/
|
|
9137
|
-
function recordSecretCollection(kind) {
|
|
9138
|
-
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
9139
|
-
}
|
|
9140
|
-
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
9141
|
-
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
9142
|
-
return buildSecretRef({
|
|
9143
|
-
owner,
|
|
9144
|
-
scope: {
|
|
9145
|
-
kind: "row",
|
|
9146
|
-
collection: recordSecretCollection(kind),
|
|
9147
|
-
rowId
|
|
9148
|
-
},
|
|
9149
|
-
fieldPath: concretePath
|
|
9150
|
-
});
|
|
9151
|
-
}
|
|
9152
|
-
/**
|
|
9153
|
-
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
9154
|
-
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
9155
|
-
* token. The form hands it to the secret field, which reveals the value by ref
|
|
9156
|
-
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
9157
|
-
* stored secret (the records door reads a view as KEEP).
|
|
9158
|
-
*
|
|
9159
|
-
* ONE implementation for the entries service and for every addon that serves
|
|
9160
|
-
* its own instances (`RecordIntegrationInstances`).
|
|
9161
|
-
*/
|
|
9162
|
-
function isRecord$3$1(value) {
|
|
9163
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9164
|
-
}
|
|
9165
|
-
function secretViewsOf(owner, kind, id, decl, data) {
|
|
9166
|
-
if (decl.secretPaths.length === 0) return data;
|
|
9167
|
-
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
9168
|
-
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
9169
|
-
return {
|
|
9170
|
-
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
9171
|
-
v: 1,
|
|
9172
|
-
set: true
|
|
9173
|
-
};
|
|
9174
|
-
});
|
|
9175
|
-
return isRecord$3$1(shown) ? shown : data;
|
|
9176
|
-
}
|
|
9177
9592
|
/**
|
|
9178
9593
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
9179
9594
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -9427,6 +9842,7 @@ function defineRecordKinds(specs) {
|
|
|
9427
9842
|
function entryBody(shape) {
|
|
9428
9843
|
return object({
|
|
9429
9844
|
name: string().min(1),
|
|
9845
|
+
enabled: boolean().optional(),
|
|
9430
9846
|
...shape
|
|
9431
9847
|
});
|
|
9432
9848
|
}
|
|
@@ -11270,6 +11686,10 @@ Object.freeze({
|
|
|
11270
11686
|
"all",
|
|
11271
11687
|
"one"
|
|
11272
11688
|
]
|
|
11689
|
+
},
|
|
11690
|
+
{
|
|
11691
|
+
"path": "positionUpdatedAt",
|
|
11692
|
+
"valueType": "number"
|
|
11273
11693
|
}
|
|
11274
11694
|
],
|
|
11275
11695
|
"meter": [{
|
|
@@ -11524,22 +11944,6 @@ Object.freeze({
|
|
|
11524
11944
|
"path": "pumpRunning",
|
|
11525
11945
|
"valueType": "boolean"
|
|
11526
11946
|
},
|
|
11527
|
-
{
|
|
11528
|
-
"path": "lastDrink",
|
|
11529
|
-
"valueType": "number"
|
|
11530
|
-
},
|
|
11531
|
-
{
|
|
11532
|
-
"path": "currentHourDrink",
|
|
11533
|
-
"valueType": "number"
|
|
11534
|
-
},
|
|
11535
|
-
{
|
|
11536
|
-
"path": "last24hDrink",
|
|
11537
|
-
"valueType": "number"
|
|
11538
|
-
},
|
|
11539
|
-
{
|
|
11540
|
-
"path": "todayDrink",
|
|
11541
|
-
"valueType": "number"
|
|
11542
|
-
},
|
|
11543
11947
|
{
|
|
11544
11948
|
"path": "error",
|
|
11545
11949
|
"valueType": "string"
|
|
@@ -12105,12 +12509,66 @@ Object.freeze({
|
|
|
12105
12509
|
}
|
|
12106
12510
|
]
|
|
12107
12511
|
});
|
|
12512
|
+
Object.freeze({
|
|
12513
|
+
"actions": { "run": 363e4 },
|
|
12514
|
+
"audio-analyzer": { "classify": 3e4 },
|
|
12515
|
+
"custom-model-registry": { "importModelBundle": 18e5 },
|
|
12516
|
+
"database-snapshot": { "writeSnapshot": 18e5 },
|
|
12517
|
+
"device-manager": {
|
|
12518
|
+
"migrateDevice": 72e4,
|
|
12519
|
+
"runDiscoverySweep": 12e4
|
|
12520
|
+
},
|
|
12521
|
+
"device-provider": { "reloadDevice": 18e4 },
|
|
12522
|
+
"llm": {
|
|
12523
|
+
"generate": 6e5,
|
|
12524
|
+
"generateVision": 6e5,
|
|
12525
|
+
"testProfile": 6e5
|
|
12526
|
+
},
|
|
12527
|
+
"llm-runtime": {
|
|
12528
|
+
"complete": 6e5,
|
|
12529
|
+
"completeApple": 6e5,
|
|
12530
|
+
"ensureStarted": 6e5,
|
|
12531
|
+
"installModel": 36e5
|
|
12532
|
+
},
|
|
12533
|
+
"local-network": { "scanNetwork": 9e4 },
|
|
12534
|
+
"model-convert": { "convert": 6e5 },
|
|
12535
|
+
"pipeline-analytics": {
|
|
12536
|
+
"addRetrainFrameFromRecording": 6e4,
|
|
12537
|
+
"offerRetrainArchive": 12e4,
|
|
12538
|
+
"proposeRetrainAnnotationsWithLlm": 18e4,
|
|
12539
|
+
"resolveRetrainTrainingCorpus": 6e4,
|
|
12540
|
+
"writeRetrainArchive": 36e5
|
|
12541
|
+
},
|
|
12542
|
+
"server-management": { "applyServerUpdate": 96e4 },
|
|
12543
|
+
"terminal-session": { "pullOutput": 15e3 },
|
|
12544
|
+
"training": {
|
|
12545
|
+
"canaryResult": 12e4,
|
|
12546
|
+
"cancelJob": 12e4,
|
|
12547
|
+
"planJob": 6e4,
|
|
12548
|
+
"registerResult": 18e5,
|
|
12549
|
+
"rollbackCanary": 12e4,
|
|
12550
|
+
"startJob": 12e4,
|
|
12551
|
+
"startJobBatch": 12e4
|
|
12552
|
+
},
|
|
12553
|
+
"training-provider": {
|
|
12554
|
+
"cancelJob": 6e4,
|
|
12555
|
+
"cleanupJob": 3e5,
|
|
12556
|
+
"getJob": 6e4,
|
|
12557
|
+
"listJobEvents": 6e4,
|
|
12558
|
+
"listJobs": 12e4,
|
|
12559
|
+
"offerArtifact": 3e5,
|
|
12560
|
+
"readResult": 3e5,
|
|
12561
|
+
"submitJob": 18e5,
|
|
12562
|
+
"uploadDataset": 72e5,
|
|
12563
|
+
"validateCredentials": 3e4
|
|
12564
|
+
}
|
|
12565
|
+
});
|
|
12108
12566
|
object({
|
|
12109
12567
|
code: string().regex(/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/),
|
|
12110
12568
|
params: record(string(), union([string(), number$1()])).optional()
|
|
12111
12569
|
});
|
|
12112
12570
|
//#endregion
|
|
12113
|
-
//#region ../types/dist/composition-
|
|
12571
|
+
//#region ../types/dist/composition-BmBHuDTQ.mjs
|
|
12114
12572
|
/**
|
|
12115
12573
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
12116
12574
|
*
|
|
@@ -12414,7 +12872,7 @@ var table = {
|
|
|
12414
12872
|
},
|
|
12415
12873
|
dwell: {
|
|
12416
12874
|
minArgs: 3,
|
|
12417
|
-
maxArgs:
|
|
12875
|
+
maxArgs: 4,
|
|
12418
12876
|
apply: () => {
|
|
12419
12877
|
throw new ExpressionEvalError("dwell: stateful evaluation context is required");
|
|
12420
12878
|
}
|
|
@@ -13574,254 +14032,6 @@ function buildCapturingReply(envelope) {
|
|
|
13574
14032
|
};
|
|
13575
14033
|
return wrapper;
|
|
13576
14034
|
}
|
|
13577
|
-
/**
|
|
13578
|
-
* The link primitive's declaration half (D1013 rule 5): what a link field names and how a kind
|
|
13579
|
-
* records it. Split from `instance-link.ts` so `RecordIntegrationInstances` can read the links of
|
|
13580
|
-
* its kind without importing the builders that import it.
|
|
13581
|
-
*/
|
|
13582
|
-
/** The body field of a link: the id of the instance it names; the target rides in the field meta. */
|
|
13583
|
-
function linkField(target) {
|
|
13584
|
-
return string().meta({ link: target });
|
|
13585
|
-
}
|
|
13586
|
-
function linkTargetOf(meta) {
|
|
13587
|
-
if (typeof meta !== "object" || meta === null) return void 0;
|
|
13588
|
-
const link = Reflect.get(meta, "link");
|
|
13589
|
-
const parsed = LinkTargetSchema.safeParse(link);
|
|
13590
|
-
return parsed.success ? parsed.data : void 0;
|
|
13591
|
-
}
|
|
13592
|
-
var LinkTargetSchema = object({
|
|
13593
|
-
cap: string().min(1),
|
|
13594
|
-
kind: string().min(1).optional()
|
|
13595
|
-
});
|
|
13596
|
-
/** The links a kind declares, read from the meta of its body fields. */
|
|
13597
|
-
function linksOfKind(kind) {
|
|
13598
|
-
const schema = kind.schema;
|
|
13599
|
-
if (!(schema instanceof ZodObject)) return [];
|
|
13600
|
-
const links = [];
|
|
13601
|
-
for (const [field, fieldSchema] of Object.entries(schema.shape)) {
|
|
13602
|
-
const to = linkTargetOf(globalRegistry.get(fieldSchema));
|
|
13603
|
-
if (to !== void 0) links.push({
|
|
13604
|
-
field,
|
|
13605
|
-
to
|
|
13606
|
-
});
|
|
13607
|
-
}
|
|
13608
|
-
return links;
|
|
13609
|
-
}
|
|
13610
|
-
/**
|
|
13611
|
-
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13612
|
-
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13613
|
-
*
|
|
13614
|
-
* Every instance is one record of the addon's own kind, keyed by the
|
|
13615
|
-
* `integrations` registry id. The records door does the secret work: a field the
|
|
13616
|
-
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13617
|
-
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13618
|
-
*
|
|
13619
|
-
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13620
|
-
* token or the view the form was given KEEP the stored secret;
|
|
13621
|
-
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13622
|
-
*/
|
|
13623
|
-
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13624
|
-
var INSTANCES_LIST_MAX = 1e3;
|
|
13625
|
-
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13626
|
-
function topLevelSecretKeys(paths) {
|
|
13627
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13628
|
-
}
|
|
13629
|
-
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13630
|
-
function secretMapKeys(paths) {
|
|
13631
|
-
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13632
|
-
}
|
|
13633
|
-
/**
|
|
13634
|
-
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13635
|
-
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13636
|
-
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13637
|
-
*/
|
|
13638
|
-
function secretMapPatch(value) {
|
|
13639
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13640
|
-
const out = {};
|
|
13641
|
-
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13642
|
-
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13643
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
13644
|
-
}
|
|
13645
|
-
/** Does the kind's body schema carry an `enabled` field? */
|
|
13646
|
-
function declaresEnabled(kind) {
|
|
13647
|
-
const json = toJSONSchema(kind.schema, {
|
|
13648
|
-
unrepresentable: "any",
|
|
13649
|
-
io: "input"
|
|
13650
|
-
});
|
|
13651
|
-
const properties = Reflect.get(json, "properties");
|
|
13652
|
-
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13653
|
-
}
|
|
13654
|
-
var RecordIntegrationInstances = class {
|
|
13655
|
-
records;
|
|
13656
|
-
kind;
|
|
13657
|
-
hooks;
|
|
13658
|
-
handle;
|
|
13659
|
-
secretKeys;
|
|
13660
|
-
secretMaps;
|
|
13661
|
-
nameField;
|
|
13662
|
-
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13663
|
-
registryOwnsEnabled;
|
|
13664
|
-
constructor(records, kind, hooks) {
|
|
13665
|
-
this.records = records;
|
|
13666
|
-
this.kind = kind;
|
|
13667
|
-
this.hooks = hooks;
|
|
13668
|
-
this.handle = records.kind(kind);
|
|
13669
|
-
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13670
|
-
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13671
|
-
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13672
|
-
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13673
|
-
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13674
|
-
}
|
|
13675
|
-
/**
|
|
13676
|
-
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13677
|
-
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13678
|
-
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13679
|
-
*/
|
|
13680
|
-
async readResolved(id) {
|
|
13681
|
-
try {
|
|
13682
|
-
const record = await this.handle.getResolved(id);
|
|
13683
|
-
return record === null ? null : {
|
|
13684
|
-
id,
|
|
13685
|
-
body: record.data,
|
|
13686
|
-
enabled: record.data.enabled !== false,
|
|
13687
|
-
secrets: "resolved"
|
|
13688
|
-
};
|
|
13689
|
-
} catch (err) {
|
|
13690
|
-
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13691
|
-
const stored = await this.handle.get(id);
|
|
13692
|
-
if (stored === null) return null;
|
|
13693
|
-
return {
|
|
13694
|
-
id,
|
|
13695
|
-
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13696
|
-
enabled: stored.data.enabled !== false,
|
|
13697
|
-
secrets: "unavailable"
|
|
13698
|
-
};
|
|
13699
|
-
}
|
|
13700
|
-
}
|
|
13701
|
-
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13702
|
-
async getResolved(id) {
|
|
13703
|
-
return (await this.readResolved(id))?.body ?? null;
|
|
13704
|
-
}
|
|
13705
|
-
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13706
|
-
async listResolved() {
|
|
13707
|
-
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13708
|
-
const out = [];
|
|
13709
|
-
for (const record of page.records) {
|
|
13710
|
-
const read = await this.readResolved(record.id);
|
|
13711
|
-
if (read !== null) out.push(read);
|
|
13712
|
-
}
|
|
13713
|
-
return out;
|
|
13714
|
-
}
|
|
13715
|
-
viewOf(record) {
|
|
13716
|
-
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13717
|
-
const rawName = record.data[this.nameField];
|
|
13718
|
-
const kind = this.hooks.kindOf?.(record.data);
|
|
13719
|
-
const health = this.hooks.health?.(record.data, record.id);
|
|
13720
|
-
return {
|
|
13721
|
-
id: record.id,
|
|
13722
|
-
name: typeof rawName === "string" ? rawName : record.id,
|
|
13723
|
-
configured: this.hooks.configured(record.data),
|
|
13724
|
-
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13725
|
-
formValues: shown,
|
|
13726
|
-
...kind !== void 0 ? { kind } : {},
|
|
13727
|
-
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13728
|
-
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13729
|
-
};
|
|
13730
|
-
}
|
|
13731
|
-
describe = async () => {
|
|
13732
|
-
const declared = linksOfKind(this.kind);
|
|
13733
|
-
const links = this.hooks.description.links !== void 0 || declared.length === 0 ? {} : { links: declared.map((link) => ({
|
|
13734
|
-
kind: this.kind.name,
|
|
13735
|
-
field: link.field,
|
|
13736
|
-
to: link.to
|
|
13737
|
-
})) };
|
|
13738
|
-
return {
|
|
13739
|
-
...this.hooks.description,
|
|
13740
|
-
...links,
|
|
13741
|
-
...this.hooks.mintId === void 0 ? {} : { mintsId: true }
|
|
13742
|
-
};
|
|
13743
|
-
};
|
|
13744
|
-
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13745
|
-
listInstances = async () => {
|
|
13746
|
-
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13747
|
-
};
|
|
13748
|
-
getInstance = async ({ id }) => {
|
|
13749
|
-
const record = await this.handle.get(id);
|
|
13750
|
-
return record === null ? null : this.viewOf(record);
|
|
13751
|
-
};
|
|
13752
|
-
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13753
|
-
createInstance = async ({ id, name, kind, values }) => {
|
|
13754
|
-
const minted = id ?? this.hooks.mintId?.({
|
|
13755
|
-
name,
|
|
13756
|
-
...kind !== void 0 ? { kind } : {}
|
|
13757
|
-
});
|
|
13758
|
-
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13759
|
-
await this.handle.put({
|
|
13760
|
-
id: minted,
|
|
13761
|
-
data: this.hooks.initialBody(name, kind)
|
|
13762
|
-
}, { ifRev: 0 });
|
|
13763
|
-
const patch = this.patchOf(values ?? {});
|
|
13764
|
-
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
13765
|
-
await this.hooks.onChanged?.(minted, "created");
|
|
13766
|
-
return { id: minted };
|
|
13767
|
-
};
|
|
13768
|
-
updateInstance = async ({ id, name, values }) => {
|
|
13769
|
-
const patch = this.patchOf(values);
|
|
13770
|
-
if (name !== void 0) patch[this.nameField] = name;
|
|
13771
|
-
await this.handle.patch(id, patch);
|
|
13772
|
-
await this.hooks.onChanged?.(id, "updated");
|
|
13773
|
-
return { ok: true };
|
|
13774
|
-
};
|
|
13775
|
-
/** The record patch a form `values` object means: secrets kept, cleared or replaced. */
|
|
13776
|
-
patchOf(values) {
|
|
13777
|
-
const patch = {};
|
|
13778
|
-
for (const [key, value] of Object.entries(values)) {
|
|
13779
|
-
if (value === void 0) continue;
|
|
13780
|
-
if (this.secretMaps.has(key)) {
|
|
13781
|
-
const entries = secretMapPatch(value);
|
|
13782
|
-
if (entries !== null) patch[key] = entries;
|
|
13783
|
-
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13784
|
-
else if (isSecretClear(value)) patch[key] = null;
|
|
13785
|
-
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13786
|
-
}
|
|
13787
|
-
return patch;
|
|
13788
|
-
}
|
|
13789
|
-
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13790
|
-
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13791
|
-
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13792
|
-
await this.handle.patch(id, { enabled });
|
|
13793
|
-
await this.hooks.onChanged?.(id, "enabled");
|
|
13794
|
-
return { ok: true };
|
|
13795
|
-
};
|
|
13796
|
-
/** The row and its keychain refs go together (the records door). */
|
|
13797
|
-
deleteInstance = async ({ id }) => {
|
|
13798
|
-
await this.handle.delete(id);
|
|
13799
|
-
await this.hooks.onChanged?.(id, "deleted");
|
|
13800
|
-
return { ok: true };
|
|
13801
|
-
};
|
|
13802
|
-
testInstance = async (input) => {
|
|
13803
|
-
if (this.hooks.test === void 0) return {
|
|
13804
|
-
success: false,
|
|
13805
|
-
status: "unsupported",
|
|
13806
|
-
error: "This integration declares no connection test"
|
|
13807
|
-
};
|
|
13808
|
-
return this.hooks.test(input);
|
|
13809
|
-
};
|
|
13810
|
-
/** The flat provider object to hand to `registerProvider`. */
|
|
13811
|
-
toProvider() {
|
|
13812
|
-
return {
|
|
13813
|
-
describe: this.describe,
|
|
13814
|
-
instanceFormSchema: this.instanceFormSchema,
|
|
13815
|
-
listInstances: this.listInstances,
|
|
13816
|
-
getInstance: this.getInstance,
|
|
13817
|
-
createInstance: this.createInstance,
|
|
13818
|
-
updateInstance: this.updateInstance,
|
|
13819
|
-
setInstanceEnabled: this.setInstanceEnabled,
|
|
13820
|
-
deleteInstance: this.deleteInstance,
|
|
13821
|
-
testInstance: this.testInstance
|
|
13822
|
-
};
|
|
13823
|
-
}
|
|
13824
|
-
};
|
|
13825
14035
|
/** No part holds the id a call named. */
|
|
13826
14036
|
var UnknownInstanceError = class extends Error {
|
|
13827
14037
|
instanceId;
|
|
@@ -20188,31 +20398,21 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
20188
20398
|
"pet-fountain": {
|
|
20189
20399
|
scope: "device",
|
|
20190
20400
|
noun: t("Pet fountain", "Fontana per animali"),
|
|
20191
|
-
actions: [
|
|
20192
|
-
|
|
20193
|
-
|
|
20194
|
-
|
|
20195
|
-
|
|
20196
|
-
|
|
20197
|
-
|
|
20198
|
-
|
|
20199
|
-
|
|
20200
|
-
|
|
20201
|
-
|
|
20202
|
-
|
|
20203
|
-
|
|
20204
|
-
|
|
20205
|
-
|
|
20206
|
-
args: { on: false }
|
|
20207
|
-
},
|
|
20208
|
-
{
|
|
20209
|
-
id: "petFountain.resetDailyConsumption",
|
|
20210
|
-
method: "resetDailyConsumption",
|
|
20211
|
-
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
20212
|
-
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
20213
|
-
icon: "rotate-ccw"
|
|
20214
|
-
}
|
|
20215
|
-
],
|
|
20401
|
+
actions: [{
|
|
20402
|
+
id: "petFountain.pumpOn",
|
|
20403
|
+
method: "setPump",
|
|
20404
|
+
label: t("Pump on", "Pompa accesa"),
|
|
20405
|
+
phrase: t("Start the pump of {device}", "Avvia la pompa di {device}"),
|
|
20406
|
+
icon: "play",
|
|
20407
|
+
args: { on: true }
|
|
20408
|
+
}, {
|
|
20409
|
+
id: "petFountain.pumpOff",
|
|
20410
|
+
method: "setPump",
|
|
20411
|
+
label: t("Pump off", "Pompa spenta"),
|
|
20412
|
+
phrase: t("Stop the pump of {device}", "Ferma la pompa di {device}"),
|
|
20413
|
+
icon: "square",
|
|
20414
|
+
args: { on: false }
|
|
20415
|
+
}],
|
|
20216
20416
|
state: [{
|
|
20217
20417
|
kind: "boolean",
|
|
20218
20418
|
field: "empty",
|
|
@@ -44166,8 +44366,15 @@ var ActionInputSchema = object({
|
|
|
44166
44366
|
var ActionRunOriginSchema = object({
|
|
44167
44367
|
/** The automation block whose step started it. */
|
|
44168
44368
|
blockId: string().min(1).optional() });
|
|
44169
|
-
/**
|
|
44170
|
-
|
|
44369
|
+
/**
|
|
44370
|
+
* `started` = the caller did not ask to wait. `timed-out` = the run reached the
|
|
44371
|
+
* ceiling (`ACTION_RUN_MAX_MS`) and was stopped; it is never a `ran`.
|
|
44372
|
+
*/
|
|
44373
|
+
var ActionRunOutcomeSchema = union([
|
|
44374
|
+
AutomationRunOutcomeSchema,
|
|
44375
|
+
literal("started"),
|
|
44376
|
+
literal("timed-out")
|
|
44377
|
+
]);
|
|
44171
44378
|
var ActionRunResultSchema = object({
|
|
44172
44379
|
runId: string().min(1),
|
|
44173
44380
|
outcome: ActionRunOutcomeSchema,
|
|
@@ -44711,6 +44918,8 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
|
44711
44918
|
kind: "mutation",
|
|
44712
44919
|
auth: "admin"
|
|
44713
44920
|
});
|
|
44921
|
+
/** `timeoutMs` of `actions.run`: a waited call outlives any legal run. */
|
|
44922
|
+
var ACTION_RUN_TIMEOUT_MS = 363e4;
|
|
44714
44923
|
/**
|
|
44715
44924
|
* actions — the ONE surface for runnable actions, for the UI and for
|
|
44716
44925
|
* automations. Provided by the core-blocks builtin on the hub.
|
|
@@ -44729,6 +44938,12 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
|
44729
44938
|
* An admin, and an in-process caller (an automation block), may run any action.
|
|
44730
44939
|
* Every refusal is typed (`[not-linked] …`) and logged.
|
|
44731
44940
|
*
|
|
44941
|
+
* **A run lasts at most `ACTION_RUN_MAX_MS` (60 min).** The provider stops a
|
|
44942
|
+
* run that reaches it and answers `timed-out` (never `ran`); an action whose
|
|
44943
|
+
* `wait` steps alone add up to more is refused at create/update
|
|
44944
|
+
* (`[invalid-steps]`). `run` declares a `timeoutMs` above that ceiling, so a
|
|
44945
|
+
* waited call (an automation step) is never cut by the 60 s transport default.
|
|
44946
|
+
*
|
|
44732
44947
|
* A second `run` of an action that is already running is REFUSED — traced and
|
|
44733
44948
|
* logged — never queued.
|
|
44734
44949
|
*/
|
|
@@ -44755,7 +44970,8 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
|
44755
44970
|
role: "viewer",
|
|
44756
44971
|
auth: "protected",
|
|
44757
44972
|
kind: "mutation",
|
|
44758
|
-
caller: "required"
|
|
44973
|
+
caller: "required",
|
|
44974
|
+
timeoutMs: ACTION_RUN_TIMEOUT_MS
|
|
44759
44975
|
}), method(ActionIdInput, _void(), {
|
|
44760
44976
|
kind: "mutation",
|
|
44761
44977
|
auth: "admin"
|
|
@@ -49846,6 +50062,9 @@ var MediaPlayerStatusSchema = object({
|
|
|
49846
50062
|
/** Repeat mode. Null when no repeat surface.
|
|
49847
50063
|
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
49848
50064
|
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
50065
|
+
/** Ms epoch when `position` was read from the device. Null when the driver
|
|
50066
|
+
* does not say; a UI that advances the position while playing counts from it. */
|
|
50067
|
+
positionUpdatedAt: number$1().nullable().default(null),
|
|
49849
50068
|
/** Ms epoch when the slice was last updated. */
|
|
49850
50069
|
lastChangedAt: number$1()
|
|
49851
50070
|
});
|
|
@@ -50481,7 +50700,9 @@ var ThresholdTransformDraftSchema = object({
|
|
|
50481
50700
|
comparison: _enum(["gte", "lte"]),
|
|
50482
50701
|
threshold: number$1(),
|
|
50483
50702
|
startDwellMs: number$1().nonnegative(),
|
|
50484
|
-
stopDwellMs: number$1().nonnegative()
|
|
50703
|
+
stopDwellMs: number$1().nonnegative(),
|
|
50704
|
+
/** Samples a start needs; absent = the compiler's default for the source (2 for running power, else 1). */
|
|
50705
|
+
minSamples: number$1().int().min(1).optional()
|
|
50485
50706
|
});
|
|
50486
50707
|
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
50487
50708
|
IdentityTransformDraftSchema,
|
|
@@ -56358,6 +56579,7 @@ var IntegrationWithStateSchema = object({
|
|
|
56358
56579
|
id: string(),
|
|
56359
56580
|
addonId: string(),
|
|
56360
56581
|
name: string(),
|
|
56582
|
+
/** A projection of the instance body's switch (`setInstanceEnabled` is the only writer). */
|
|
56361
56583
|
enabled: boolean(),
|
|
56362
56584
|
info: record(string(), unknown()).readonly(),
|
|
56363
56585
|
/** The discovery candidate this integration was set up from (D984). */
|
|
@@ -56370,6 +56592,7 @@ var IntegrationLiteSchema = object({
|
|
|
56370
56592
|
id: string(),
|
|
56371
56593
|
addonId: string(),
|
|
56372
56594
|
name: string(),
|
|
56595
|
+
/** A projection of the instance body's switch (`setInstanceEnabled` is the only writer). */
|
|
56373
56596
|
enabled: boolean(),
|
|
56374
56597
|
info: record(string(), unknown()).readonly(),
|
|
56375
56598
|
/** The discovery candidate this integration was set up from (D984). */
|
|
@@ -56469,7 +56692,6 @@ var TestConnectionResultSchema = object({
|
|
|
56469
56692
|
var CreateIntegrationInputSchema = object({
|
|
56470
56693
|
addonId: string(),
|
|
56471
56694
|
name: string(),
|
|
56472
|
-
enabled: boolean().optional(),
|
|
56473
56695
|
info: record(string(), unknown()).optional(),
|
|
56474
56696
|
/** `stableId` of the discovery candidate the form was seeded from (D984). */
|
|
56475
56697
|
originStableId: string().optional(),
|
|
@@ -56478,6 +56700,11 @@ var CreateIntegrationInputSchema = object({
|
|
|
56478
56700
|
var UpdateIntegrationInputSchema = object({
|
|
56479
56701
|
id: string(),
|
|
56480
56702
|
name: string().optional(),
|
|
56703
|
+
/**
|
|
56704
|
+
* Accepted for a FIXED builtin only (`info.fixed`: alarm panel, camera grid, core blocks,
|
|
56705
|
+
* terminal), which keeps no instance record: the registry flag is its one store. Any other
|
|
56706
|
+
* integration is switched through `integration-instances.setInstanceEnabled`.
|
|
56707
|
+
*/
|
|
56481
56708
|
enabled: boolean().optional(),
|
|
56482
56709
|
info: record(string(), unknown()).optional()
|
|
56483
56710
|
});
|
|
@@ -56525,7 +56752,7 @@ var IntegrationInstanceIdSchema = string().min(1);
|
|
|
56525
56752
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
56526
56753
|
/**
|
|
56527
56754
|
* What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
|
|
56528
|
-
* instance is an integration that brings devices (
|
|
56755
|
+
* instance is an integration that brings devices (its record body owns the on/off switch);
|
|
56529
56756
|
* `services`: a connection configured from the addon's own function page, never listed on
|
|
56530
56757
|
* Integrations.
|
|
56531
56758
|
* It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
|
|
@@ -56642,8 +56869,8 @@ var IntegrationInstanceSchema = object({
|
|
|
56642
56869
|
configured: boolean(),
|
|
56643
56870
|
/**
|
|
56644
56871
|
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56645
|
-
* so a row written before the field existed is on).
|
|
56646
|
-
*
|
|
56872
|
+
* so a row written before the field existed is on). The same for every role, `devices` included:
|
|
56873
|
+
* the body is the one store, `setInstanceEnabled` the one writer (epic #440, step 3).
|
|
56647
56874
|
*/
|
|
56648
56875
|
enabled: boolean().optional(),
|
|
56649
56876
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
@@ -56693,7 +56920,8 @@ var integrationInstancesCapability = {
|
|
|
56693
56920
|
auth: "admin"
|
|
56694
56921
|
}),
|
|
56695
56922
|
/**
|
|
56696
|
-
* Switch ONE instance on or off
|
|
56923
|
+
* Switch ONE instance on or off, for every role; a missing record is created from the addon's
|
|
56924
|
+
* initial body. A row control, not a form field: `updateInstance.values` is the
|
|
56697
56925
|
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
56698
56926
|
*/
|
|
56699
56927
|
setInstanceEnabled: method(object({
|
|
@@ -59756,6 +59984,8 @@ var MigrationIntegrationSchema = object({
|
|
|
59756
59984
|
addonId: string(),
|
|
59757
59985
|
name: string(),
|
|
59758
59986
|
info: Rows,
|
|
59987
|
+
/** The registry's stored switch: read by the one-switch migration, nothing else. */
|
|
59988
|
+
enabled: boolean().optional(),
|
|
59759
59989
|
originStableId: string().optional()
|
|
59760
59990
|
});
|
|
59761
59991
|
var MigrationDeviceSchema = object({
|
|
@@ -69885,14 +70115,6 @@ var PetFountainStatusSchema = object({
|
|
|
69885
70115
|
empty: boolean().nullable(),
|
|
69886
70116
|
/** True while the pump is running. Null while unknown. */
|
|
69887
70117
|
pumpRunning: boolean().nullable(),
|
|
69888
|
-
/** Water drunk in the last drink, in litres. */
|
|
69889
|
-
lastDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
69890
|
-
/** Water drunk in the current hour, in litres. */
|
|
69891
|
-
currentHourDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
69892
|
-
/** Water drunk over the last 24 hours (rolling), in litres. */
|
|
69893
|
-
last24hDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
69894
|
-
/** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
|
|
69895
|
-
todayDrink: number$1().min(0).nullable().meta(readingMeta("volume")),
|
|
69896
70118
|
/** Decoded fault message. Null means healthy; a non-null string is an active fault. */
|
|
69897
70119
|
error: string().nullable(),
|
|
69898
70120
|
/** Ms epoch when the slice was last refreshed. */
|
|
@@ -69905,21 +70127,15 @@ var petFountainCapability = {
|
|
|
69905
70127
|
deviceNative: true,
|
|
69906
70128
|
mode: "singleton",
|
|
69907
70129
|
deviceTypes: [DeviceType.PetFountain],
|
|
69908
|
-
methods: {
|
|
69909
|
-
|
|
69910
|
-
|
|
69911
|
-
|
|
69912
|
-
|
|
69913
|
-
|
|
69914
|
-
|
|
69915
|
-
|
|
69916
|
-
|
|
69917
|
-
/** Clear the rolling daily consumption counters on the device. */
|
|
69918
|
-
resetDailyConsumption: method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
|
|
69919
|
-
kind: "mutation",
|
|
69920
|
-
auth: "protected"
|
|
69921
|
-
})
|
|
69922
|
-
},
|
|
70130
|
+
methods: {
|
|
70131
|
+
/** Start or stop the pump. */
|
|
70132
|
+
setPump: method(object({
|
|
70133
|
+
deviceId: DeviceIdSchema$1,
|
|
70134
|
+
on: boolean()
|
|
70135
|
+
}), _void(), {
|
|
70136
|
+
kind: "mutation",
|
|
70137
|
+
auth: "protected"
|
|
70138
|
+
}) },
|
|
69923
70139
|
status: {
|
|
69924
70140
|
schema: PetFountainStatusSchema,
|
|
69925
70141
|
kind: "push"
|
|
@@ -69943,14 +70159,6 @@ var petFountainCapability = {
|
|
|
69943
70159
|
waterVolume: {
|
|
69944
70160
|
kind: "gauge",
|
|
69945
70161
|
unit: "L"
|
|
69946
|
-
},
|
|
69947
|
-
todayDrink: {
|
|
69948
|
-
kind: "gauge",
|
|
69949
|
-
unit: "L"
|
|
69950
|
-
},
|
|
69951
|
-
last24hDrink: {
|
|
69952
|
-
kind: "gauge",
|
|
69953
|
-
unit: "L"
|
|
69954
70162
|
}
|
|
69955
70163
|
} }
|
|
69956
70164
|
};
|
|
@@ -88399,13 +88607,6 @@ Object.freeze({
|
|
|
88399
88607
|
access: "view",
|
|
88400
88608
|
role: "viewer"
|
|
88401
88609
|
},
|
|
88402
|
-
"petFountain.resetDailyConsumption": {
|
|
88403
|
-
capName: "pet-fountain",
|
|
88404
|
-
capScope: "device",
|
|
88405
|
-
addonId: null,
|
|
88406
|
-
access: "delete",
|
|
88407
|
-
role: "viewer"
|
|
88408
|
-
},
|
|
88409
88610
|
"petFountain.setPump": {
|
|
88410
88611
|
capName: "pet-fountain",
|
|
88411
88612
|
capScope: "device",
|
|
@@ -94802,11 +95003,6 @@ Object.freeze({
|
|
|
94802
95003
|
form: "single",
|
|
94803
95004
|
optional: false
|
|
94804
95005
|
}],
|
|
94805
|
-
"petFountain.resetDailyConsumption": [{
|
|
94806
|
-
name: "deviceId",
|
|
94807
|
-
form: "single",
|
|
94808
|
-
optional: false
|
|
94809
|
-
}],
|
|
94810
95006
|
"petFountain.setPump": [{
|
|
94811
95007
|
name: "deviceId",
|
|
94812
95008
|
form: "single",
|
|
@@ -98524,6 +98720,20 @@ async function assertInstanceUnused(instanceId, finders) {
|
|
|
98524
98720
|
for (const finder of finders) users.push(...await finder.find(instanceId));
|
|
98525
98721
|
if (users.length > 0) throw new InstanceInUseError(instanceId, users);
|
|
98526
98722
|
}
|
|
98723
|
+
/**
|
|
98724
|
+
* A provider whose `deleteInstance` is refused while anything still uses the instance. The
|
|
98725
|
+
* account is an `integration-instances` kind (D1013), so the refusal lives on the delete the
|
|
98726
|
+
* instances page calls.
|
|
98727
|
+
*/
|
|
98728
|
+
function guardInstanceDelete(base, users) {
|
|
98729
|
+
return {
|
|
98730
|
+
...base,
|
|
98731
|
+
deleteInstance: async (input) => {
|
|
98732
|
+
await assertInstanceUnused(input.id, users());
|
|
98733
|
+
return base.deleteInstance(input);
|
|
98734
|
+
}
|
|
98735
|
+
};
|
|
98736
|
+
}
|
|
98527
98737
|
/** The `instance-select` field of a link, keyed like the body field it fills. */
|
|
98528
98738
|
function linkFieldDescriptor(key, target, labels) {
|
|
98529
98739
|
return {
|
|
@@ -98537,108 +98747,71 @@ function linkFieldDescriptor(key, target, labels) {
|
|
|
98537
98747
|
...target.kind !== void 0 ? { kind: target.kind } : {}
|
|
98538
98748
|
};
|
|
98539
98749
|
}
|
|
98540
|
-
|
|
98541
|
-
|
|
98542
|
-
|
|
98543
|
-
|
|
98544
|
-
|
|
98545
|
-
|
|
98546
|
-
|
|
98547
|
-
|
|
98548
|
-
try {
|
|
98549
|
-
await assertInstanceUnused(brokerId, finders);
|
|
98550
|
-
} catch (err) {
|
|
98551
|
-
if (err instanceof InstanceInUseError) throw new BrokerInUseError(err.instanceId, err.users);
|
|
98552
|
-
throw err;
|
|
98553
|
-
}
|
|
98554
|
-
}
|
|
98555
|
-
/** @deprecated use `guardInstanceDelete`. */
|
|
98556
|
-
function guardBrokerDelete(base, users) {
|
|
98557
|
-
return {
|
|
98558
|
-
...base,
|
|
98559
|
-
deleteInstance: async (input) => {
|
|
98560
|
-
await assertBrokerUnused(input.id, users());
|
|
98561
|
-
return base.deleteInstance(input);
|
|
98562
|
-
}
|
|
98563
|
-
};
|
|
98564
|
-
}
|
|
98565
|
-
/**
|
|
98566
|
-
* The generic binding of a service to a broker (D1012). A service (an integration today; a
|
|
98567
|
-
* notifier target, a calendar source or an exporter later) names the broker it runs through by id
|
|
98568
|
-
* and keeps only its OWN link settings: never a copy of the broker's host, sign-in or secrets,
|
|
98569
|
-
* which stay with the broker and are edited on the Brokers page.
|
|
98570
|
-
*
|
|
98571
|
-
* One primitive serves every consumer:
|
|
98572
|
-
* - {@link declareBrokerLinkKind}: the record kind, body `{ name, brokerId, ...linkFields }`;
|
|
98573
|
-
* - {@link buildBrokerLinkForm}: the form, a `broker-select` picker plus the service's link
|
|
98574
|
-
* sections, which may be DYNAMIC (a list read from the chosen broker, like HA calendars);
|
|
98575
|
-
* - {@link buildBrokerLinkInstances}: the `integration-instances` provider of an integration;
|
|
98576
|
-
* - {@link resolveBrokerId} / {@link brokerLinkUserFinder}: which broker a record names and which
|
|
98577
|
-
* records name a broker, so a delete of a broker still in use can name its users generically.
|
|
98578
|
-
*/
|
|
98579
|
-
/** The record kind an integration is stored under. */
|
|
98580
|
-
var BROKER_LINK_KIND_NAME = "connections";
|
|
98581
|
-
/** What every broker-linked record carries; `brokerId` is always present, `''` is a row whose broker is not picked yet; link fields of the service come on top. */
|
|
98582
|
-
var BrokerLinkBodySchema = entryBody({ brokerId: string() });
|
|
98583
|
-
function declareBrokerLinkKind(spec) {
|
|
98584
|
-
return defineEntryKind(BROKER_LINK_KIND_NAME, {
|
|
98750
|
+
function declareLinkKind(spec) {
|
|
98751
|
+
const linkShape = {};
|
|
98752
|
+
for (const [field, target] of Object.entries(spec.links)) linkShape[field] = linkField(target);
|
|
98753
|
+
const typed = entryBody({
|
|
98754
|
+
...linkShape,
|
|
98755
|
+
...spec.fields
|
|
98756
|
+
});
|
|
98757
|
+
return defineEntryKind(spec.name, {
|
|
98585
98758
|
title: spec.title,
|
|
98586
|
-
schema:
|
|
98759
|
+
schema: typed,
|
|
98587
98760
|
writeTrigger: spec.writeTrigger
|
|
98588
98761
|
});
|
|
98589
98762
|
}
|
|
98590
|
-
/** The
|
|
98591
|
-
|
|
98592
|
-
const
|
|
98593
|
-
return
|
|
98594
|
-
{
|
|
98595
|
-
id: "broker-link",
|
|
98596
|
-
title: spec.brokerLabel,
|
|
98597
|
-
description: `Each integration links to a ${spec.brokerLabel} connection (Connectivity → Brokers). The connection and its sign-in are edited there.`,
|
|
98598
|
-
columns: 1,
|
|
98599
|
-
fields: [{
|
|
98600
|
-
type: "broker-select",
|
|
98601
|
-
key: "brokerId",
|
|
98602
|
-
label: spec.brokerLabel,
|
|
98603
|
-
brokerKind: spec.brokerKind
|
|
98604
|
-
}]
|
|
98605
|
-
},
|
|
98606
|
-
...spec.linkSections ?? [],
|
|
98607
|
-
...dynamic
|
|
98608
|
-
] };
|
|
98609
|
-
}
|
|
98610
|
-
/** The broker a record names; undefined while it names none. */
|
|
98611
|
-
function resolveBrokerId(record) {
|
|
98612
|
-
return record.brokerId === void 0 || record.brokerId === "" ? void 0 : record.brokerId;
|
|
98763
|
+
/** The id a record holds in a link field; undefined while it names none. */
|
|
98764
|
+
function linkedId(body, field) {
|
|
98765
|
+
const value = body[field];
|
|
98766
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
98613
98767
|
}
|
|
98614
|
-
/** The
|
|
98615
|
-
async function
|
|
98768
|
+
/** The instance an integration's record links to through `field`; undefined while it names none. */
|
|
98769
|
+
async function linkedIdOf(instances, integrationId, field) {
|
|
98616
98770
|
const body = await instances.getResolved(integrationId);
|
|
98617
|
-
return body === null ? void 0 :
|
|
98771
|
+
return body === null ? void 0 : linkedId(body, field);
|
|
98618
98772
|
}
|
|
98619
|
-
/** Every record of the kind
|
|
98620
|
-
function
|
|
98621
|
-
return { find: async (
|
|
98773
|
+
/** Every record of the kind whose `field` names `instanceId`, as users of that instance. */
|
|
98774
|
+
function linkUserFinder(instances, field, userKind = "integration") {
|
|
98775
|
+
return { find: async (instanceId) => (await instances.listResolved()).filter((entry) => linkedId(entry.body, field) === instanceId).map((entry) => ({
|
|
98622
98776
|
kind: userKind,
|
|
98623
98777
|
id: entry.id,
|
|
98624
|
-
name: entry.body.name
|
|
98778
|
+
name: typeof entry.body["name"] === "string" ? entry.body["name"] : entry.id
|
|
98625
98779
|
})) };
|
|
98626
98780
|
}
|
|
98627
|
-
/** The `integration-instances` provider of
|
|
98628
|
-
function
|
|
98781
|
+
/** The `integration-instances` provider of a service whose records link to other instances. */
|
|
98782
|
+
function buildLinkedInstances(records, spec) {
|
|
98783
|
+
const links = linksOfKind(spec.kind);
|
|
98784
|
+
const kindId = spec.instanceKind ?? spec.kind.name;
|
|
98629
98785
|
const instances = new RecordIntegrationInstances(records, spec.kind, {
|
|
98630
98786
|
description: {
|
|
98631
|
-
|
|
98632
|
-
|
|
98633
|
-
|
|
98634
|
-
|
|
98787
|
+
...spec.description,
|
|
98788
|
+
links: links.map((link) => ({
|
|
98789
|
+
kind: kindId,
|
|
98790
|
+
field: link.field,
|
|
98791
|
+
to: link.to
|
|
98792
|
+
}))
|
|
98793
|
+
},
|
|
98794
|
+
formSchema: async (id) => {
|
|
98795
|
+
const first = links[0];
|
|
98796
|
+
const chosen = id === null || first === void 0 || spec.dynamicSections === void 0 ? void 0 : linkedId(await instances.getResolved(id) ?? {}, first.field);
|
|
98797
|
+
const dynamic = chosen !== void 0 && spec.dynamicSections !== void 0 ? await spec.dynamicSections(chosen) : [];
|
|
98798
|
+
return { sections: [
|
|
98799
|
+
{
|
|
98800
|
+
id: "links",
|
|
98801
|
+
title: "Linked instances",
|
|
98802
|
+
columns: 1,
|
|
98803
|
+
fields: links.map((link) => linkFieldDescriptor(link.field, link.to, spec.labels?.[link.field] ?? { label: link.field }))
|
|
98804
|
+
},
|
|
98805
|
+
...spec.linkSections ?? [],
|
|
98806
|
+
...dynamic
|
|
98807
|
+
] };
|
|
98635
98808
|
},
|
|
98636
|
-
formSchema: async (id) => buildBrokerLinkForm(spec, id === null ? void 0 : await brokerIdOf(instances, id)),
|
|
98637
98809
|
initialBody: (name) => spec.kind.schema.parse({
|
|
98638
98810
|
name,
|
|
98639
|
-
|
|
98811
|
+
...Object.fromEntries(links.map((link) => [link.field, ""]))
|
|
98640
98812
|
}),
|
|
98641
|
-
configured: (body) =>
|
|
98813
|
+
configured: (body) => links.every((link) => linkedId(body, link.field) !== void 0) && (spec.alsoConfigured?.(body) ?? true),
|
|
98814
|
+
...spec.mintId !== void 0 ? { mintId: spec.mintId } : {},
|
|
98642
98815
|
...spec.onChanged !== void 0 ? { onChanged: spec.onChanged } : {}
|
|
98643
98816
|
});
|
|
98644
98817
|
return instances;
|
|
@@ -98720,8 +98893,15 @@ var ALEXA_ACCOUNT_LINK = {
|
|
|
98720
98893
|
var DEVICE_REFRESH_MS = 5 * 6e4;
|
|
98721
98894
|
/** Where the addon serves the Alexa mark; `listTargetKinds` stamps it as `iconUrl`. */
|
|
98722
98895
|
var ALEXA_SPEAK_ICON_URL = `/addon/${ALEXA_SPEAK_ADDON_ID}/icons/${ALEXA_SPEAK_KIND}`;
|
|
98723
|
-
/** How often an adopted Echo's player state is read
|
|
98896
|
+
/** How often an adopted Echo's player state is read while the push channel is down. */
|
|
98724
98897
|
var ECHO_POLL_MS = 3e4;
|
|
98898
|
+
/**
|
|
98899
|
+
* The same read while the push channel is up: a safety net for a missed event, not the
|
|
98900
|
+
* way changes arrive. Back to `ECHO_POLL_MS` the moment the channel goes down.
|
|
98901
|
+
*/
|
|
98902
|
+
var ECHO_POLL_PUSH_HEALTHY_MS = 5 * 6e4;
|
|
98903
|
+
/** Push events for one Echo inside this window are one read, not one per event. */
|
|
98904
|
+
var ECHO_PUSH_READ_GAP_MS = 2e3;
|
|
98725
98905
|
/** One more read after a command, once Amazon has had time to apply it. */
|
|
98726
98906
|
var ECHO_REREAD_MS = 1500;
|
|
98727
98907
|
/** After a 429 the account is left alone for this long, doubling up to the cap. */
|
|
@@ -143050,7 +143230,7 @@ var AlexaBrokerRegistry = class {
|
|
|
143050
143230
|
async restore(entries) {
|
|
143051
143231
|
for (const entry of entries) this.#seedCounter(entry.id);
|
|
143052
143232
|
for (const entry of entries) {
|
|
143053
|
-
if (entry
|
|
143233
|
+
if (!isInstanceEnabled(entry)) continue;
|
|
143054
143234
|
try {
|
|
143055
143235
|
await this.#startManager(entry);
|
|
143056
143236
|
} catch (err) {
|
|
@@ -144016,6 +144196,9 @@ function isKnownCap(cap) {
|
|
|
144016
144196
|
return KNOWN_CAPS.has(cap);
|
|
144017
144197
|
}
|
|
144018
144198
|
var EchoSessionRegistry = class {
|
|
144199
|
+
#channels = /* @__PURE__ */ new Map();
|
|
144200
|
+
#pushListeners = /* @__PURE__ */ new Map();
|
|
144201
|
+
#channelListeners = /* @__PURE__ */ new Map();
|
|
144019
144202
|
#resolve = null;
|
|
144020
144203
|
#dnd = /* @__PURE__ */ new Map();
|
|
144021
144204
|
#volumes = /* @__PURE__ */ new Map();
|
|
@@ -144057,12 +144240,54 @@ var EchoSessionRegistry = class {
|
|
|
144057
144240
|
});
|
|
144058
144241
|
return pending;
|
|
144059
144242
|
}
|
|
144243
|
+
/** Forget the shared volume list: a list fetched before a set must not answer a poll after it. */
|
|
144244
|
+
invalidateVolumes(brokerId) {
|
|
144245
|
+
this.#volumes.delete(brokerId);
|
|
144246
|
+
}
|
|
144247
|
+
/** Forget the shared Do Not Disturb list, for the same reason. */
|
|
144248
|
+
invalidateDnd(brokerId) {
|
|
144249
|
+
this.#dnd.delete(brokerId);
|
|
144250
|
+
}
|
|
144251
|
+
/** Whether the account's push channel is open; `down` when nothing was ever reported. */
|
|
144252
|
+
pushChannel(brokerId) {
|
|
144253
|
+
return this.#channels.get(brokerId) ?? "down";
|
|
144254
|
+
}
|
|
144255
|
+
channel(brokerId, state) {
|
|
144256
|
+
if (this.pushChannel(brokerId) === state) return;
|
|
144257
|
+
this.#channels.set(brokerId, state);
|
|
144258
|
+
for (const listener of [...this.#channelListeners.get(brokerId) ?? []]) listener(state);
|
|
144259
|
+
}
|
|
144260
|
+
event(brokerId, serial) {
|
|
144261
|
+
this.invalidateVolumes(brokerId);
|
|
144262
|
+
this.invalidateDnd(brokerId);
|
|
144263
|
+
for (const listener of [...this.#pushListeners.get(`${brokerId}\u0000${serial}`) ?? []]) listener();
|
|
144264
|
+
}
|
|
144265
|
+
/** Called when Amazon pushes something about this Echo. Returns the unsubscribe. */
|
|
144266
|
+
onEcho(brokerId, serial, listener) {
|
|
144267
|
+
return subscribe(this.#pushListeners, `${brokerId}\u0000${serial}`, listener);
|
|
144268
|
+
}
|
|
144269
|
+
/** Called when the account's channel goes up or down. Returns the unsubscribe. */
|
|
144270
|
+
onChannel(brokerId, listener) {
|
|
144271
|
+
return subscribe(this.#channelListeners, brokerId, listener);
|
|
144272
|
+
}
|
|
144060
144273
|
clear() {
|
|
144274
|
+
this.#channels.clear();
|
|
144275
|
+
this.#pushListeners.clear();
|
|
144276
|
+
this.#channelListeners.clear();
|
|
144061
144277
|
this.#resolve = null;
|
|
144062
144278
|
this.#dnd.clear();
|
|
144063
144279
|
this.#volumes.clear();
|
|
144064
144280
|
}
|
|
144065
144281
|
};
|
|
144282
|
+
function subscribe(map, key, listener) {
|
|
144283
|
+
const set = map.get(key) ?? /* @__PURE__ */ new Set();
|
|
144284
|
+
set.add(listener);
|
|
144285
|
+
map.set(key, set);
|
|
144286
|
+
return () => {
|
|
144287
|
+
set.delete(listener);
|
|
144288
|
+
if (set.size === 0 && map.get(key) === set) map.delete(key);
|
|
144289
|
+
};
|
|
144290
|
+
}
|
|
144066
144291
|
/** The single in-process registry shared across the addon. */
|
|
144067
144292
|
var alexaEchoSessions = new EchoSessionRegistry();
|
|
144068
144293
|
//#endregion
|
|
@@ -144082,6 +144307,7 @@ function emptyEchoStatus(state, now) {
|
|
|
144082
144307
|
duration: null,
|
|
144083
144308
|
shuffle: null,
|
|
144084
144309
|
repeat: null,
|
|
144310
|
+
positionUpdatedAt: null,
|
|
144085
144311
|
lastChangedAt: now
|
|
144086
144312
|
};
|
|
144087
144313
|
}
|
|
@@ -144115,12 +144341,24 @@ function repeatOf(value) {
|
|
|
144115
144341
|
if (value === "UNSELECTED") return "off";
|
|
144116
144342
|
return null;
|
|
144117
144343
|
}
|
|
144344
|
+
/** The streaming service playing: its display name, else its raw name; empty when Amazon names none. */
|
|
144345
|
+
function sourceOf(info) {
|
|
144346
|
+
return nonEmpty(info.provider?.providerDisplayName) ?? nonEmpty(info.provider?.providerName) ?? "";
|
|
144347
|
+
}
|
|
144348
|
+
/**
|
|
144349
|
+
* What is playing. Amazon puts the title in `infoText.title`, but a radio station
|
|
144350
|
+
* often names itself only in the first sub text, and some players send artwork and
|
|
144351
|
+
* a provider with no text at all: each still counts as now playing. Nothing to
|
|
144352
|
+
* title it by (no text, and artwork with no provider name) is null.
|
|
144353
|
+
*/
|
|
144118
144354
|
function currentMediaOf(info) {
|
|
144119
|
-
const
|
|
144120
|
-
|
|
144121
|
-
const artist = nonEmpty(info.infoText?.subText1);
|
|
144122
|
-
const album = nonEmpty(info.infoText?.subText2);
|
|
144355
|
+
const first = nonEmpty(info.infoText?.title);
|
|
144356
|
+
const second = nonEmpty(info.infoText?.subText1);
|
|
144123
144357
|
const imageUrl = nonEmpty(info.mainArt?.url);
|
|
144358
|
+
const album = nonEmpty(info.infoText?.subText2);
|
|
144359
|
+
const title = first ?? second ?? (imageUrl !== null ? nonEmpty(sourceOf(info)) : null);
|
|
144360
|
+
if (title === null) return null;
|
|
144361
|
+
const artist = first !== null ? second : null;
|
|
144124
144362
|
return {
|
|
144125
144363
|
type: "unknown",
|
|
144126
144364
|
title,
|
|
@@ -144132,12 +144370,15 @@ function currentMediaOf(info) {
|
|
|
144132
144370
|
function mapPlayerInfoToStatus(info, input) {
|
|
144133
144371
|
if (!input.online) return emptyEchoStatus("off", input.now);
|
|
144134
144372
|
const length = secondsToMs(info.progress?.mediaLength);
|
|
144373
|
+
const position = secondsToMs(info.progress?.mediaProgress);
|
|
144135
144374
|
return {
|
|
144136
144375
|
...emptyEchoStatus(mapEchoPlaybackState(info.state), input.now),
|
|
144137
144376
|
volumeLevel: percent(info.volume?.volume),
|
|
144138
144377
|
isMuted: typeof info.volume?.muted === "boolean" ? info.volume.muted : null,
|
|
144378
|
+
source: sourceOf(info),
|
|
144139
144379
|
currentMedia: currentMediaOf(info),
|
|
144140
|
-
position
|
|
144380
|
+
position,
|
|
144381
|
+
positionUpdatedAt: position === null ? null : input.now,
|
|
144141
144382
|
duration: length === 0 ? null : length,
|
|
144142
144383
|
shuffle: shuffleOf(info.transport?.shuffle),
|
|
144143
144384
|
repeat: repeatOf(info.transport?.repeat)
|
|
@@ -144191,6 +144432,35 @@ var WorkerLineSchema = union([object({
|
|
|
144191
144432
|
/** A typed reason the worker names; `rate-limited` is Amazon's 429. */
|
|
144192
144433
|
code: string().optional()
|
|
144193
144434
|
})]);
|
|
144435
|
+
var WorkerPushSchema = discriminatedUnion("push", [object({
|
|
144436
|
+
push: literal("channel"),
|
|
144437
|
+
state: _enum(["up", "down"]),
|
|
144438
|
+
reason: string().default(""),
|
|
144439
|
+
/** How long until the worker opens the channel again; 0 when it is up. */
|
|
144440
|
+
retryInMs: number$1().int().nonnegative().default(0)
|
|
144441
|
+
}), object({
|
|
144442
|
+
push: literal("event"),
|
|
144443
|
+
command: string().min(1),
|
|
144444
|
+
/** The Echo the event is about; null when Amazon named none. */
|
|
144445
|
+
serial: string().min(1).nullable()
|
|
144446
|
+
})]);
|
|
144447
|
+
/** Push events that change what an Echo's slice shows: the addon reads that Echo once. */
|
|
144448
|
+
var PUSH_READ_COMMANDS = new Set([
|
|
144449
|
+
"PUSH_AUDIO_PLAYER_STATE",
|
|
144450
|
+
"PUSH_MEDIA_CHANGE",
|
|
144451
|
+
"PUSH_VOLUME_CHANGE",
|
|
144452
|
+
"PUSH_DOPPLER_CONNECTION_CHANGE"
|
|
144453
|
+
]);
|
|
144454
|
+
/**
|
|
144455
|
+
* Push events Amazon sends that the slice does not need: progress ticks every few
|
|
144456
|
+
* seconds while playing (the position is derived from `positionUpdatedAt`), and the
|
|
144457
|
+
* notification / equalizer changes touch nothing the media-player shows.
|
|
144458
|
+
*/
|
|
144459
|
+
var PUSH_IGNORED_COMMANDS = new Set([
|
|
144460
|
+
"PUSH_MEDIA_PROGRESS_CHANGE",
|
|
144461
|
+
"PUSH_NOTIFICATION_CHANGE",
|
|
144462
|
+
"PUSH_EQUALIZER_STATE_CHANGE"
|
|
144463
|
+
]);
|
|
144194
144464
|
var RATE_LIMITED_CODE = "rate-limited";
|
|
144195
144465
|
/** Amazon answered 429. Never an empty answer: the caller backs off and keeps what it had. */
|
|
144196
144466
|
var RateLimitedError = class extends Error {
|
|
@@ -144233,35 +144503,75 @@ function parseSessionStatus(result) {
|
|
|
144233
144503
|
return parsed.data;
|
|
144234
144504
|
}
|
|
144235
144505
|
/**
|
|
144506
|
+
* A reading Amazon sent in a shape we did not expect is a reading we do not have
|
|
144507
|
+
* (null), never a reason to discard the rest of the player: one odd field used to
|
|
144508
|
+
* fail the whole read and the Echo kept showing nothing.
|
|
144509
|
+
*/
|
|
144510
|
+
var text = string().nullish().catch(null);
|
|
144511
|
+
var flag = boolean().nullish().catch(null);
|
|
144512
|
+
var count = number$1().nullish().catch(null);
|
|
144513
|
+
/**
|
|
144236
144514
|
* The raw `playerInfo` of `/api/np/player`. Amazon omits what it does not know,
|
|
144237
144515
|
* so every field is nullable; the mapping to the media-player cap turns a missing
|
|
144238
|
-
* value into null, never into 0 (D393).
|
|
144516
|
+
* value into null, never into 0 (D393). Every block and every leaf is read on its
|
|
144517
|
+
* own: a block of the wrong type is null, the others still count.
|
|
144239
144518
|
*/
|
|
144240
144519
|
var PlayerInfoSchema = object({
|
|
144241
|
-
state:
|
|
144520
|
+
state: text,
|
|
144242
144521
|
volume: object({
|
|
144243
|
-
volume:
|
|
144244
|
-
muted:
|
|
144245
|
-
}).nullish(),
|
|
144522
|
+
volume: count,
|
|
144523
|
+
muted: flag
|
|
144524
|
+
}).nullish().catch(null),
|
|
144246
144525
|
infoText: object({
|
|
144247
|
-
title:
|
|
144248
|
-
subText1:
|
|
144249
|
-
subText2:
|
|
144250
|
-
}).nullish(),
|
|
144251
|
-
mainArt: object({ url:
|
|
144526
|
+
title: text,
|
|
144527
|
+
subText1: text,
|
|
144528
|
+
subText2: text
|
|
144529
|
+
}).nullish().catch(null),
|
|
144530
|
+
mainArt: object({ url: text }).nullish().catch(null),
|
|
144252
144531
|
progress: object({
|
|
144253
|
-
mediaLength:
|
|
144254
|
-
mediaProgress:
|
|
144255
|
-
}).nullish(),
|
|
144532
|
+
mediaLength: count,
|
|
144533
|
+
mediaProgress: count
|
|
144534
|
+
}).nullish().catch(null),
|
|
144535
|
+
provider: object({
|
|
144536
|
+
providerName: text,
|
|
144537
|
+
providerDisplayName: text
|
|
144538
|
+
}).nullish().catch(null),
|
|
144256
144539
|
transport: object({
|
|
144257
|
-
shuffle:
|
|
144258
|
-
repeat:
|
|
144259
|
-
}).nullish()
|
|
144260
|
-
});
|
|
144540
|
+
shuffle: text,
|
|
144541
|
+
repeat: text
|
|
144542
|
+
}).nullish().catch(null)
|
|
144543
|
+
});
|
|
144544
|
+
/** The playback states Amazon is known to send. */
|
|
144545
|
+
var KNOWN_PLAYER_STATES = new Set([
|
|
144546
|
+
"PLAYING",
|
|
144547
|
+
"PAUSED",
|
|
144548
|
+
"BUFFERING",
|
|
144549
|
+
"IDLE"
|
|
144550
|
+
]);
|
|
144551
|
+
/**
|
|
144552
|
+
* A lenient read must not turn garbage into a healthy idle player: when Amazon sent a
|
|
144553
|
+
* state we do not recognise (or of the wrong type) and not one block survived, the read
|
|
144554
|
+
* is a failure, exactly as before the leniency. A body that says nothing (`{}`, null
|
|
144555
|
+
* state) stays an idle player.
|
|
144556
|
+
*/
|
|
144261
144557
|
function parsePlayerState(result) {
|
|
144262
144558
|
const parsed = object({ playerInfo: PlayerInfoSchema.nullish() }).safeParse(result);
|
|
144263
144559
|
if (!parsed.success) throw new Error("worker returned an unexpected player state");
|
|
144264
|
-
|
|
144560
|
+
const info = parsed.data.playerInfo ?? {};
|
|
144561
|
+
const raw = object({ playerInfo: object({ state: unknown() }).nullish() }).safeParse(result);
|
|
144562
|
+
const rawState = raw.success ? raw.data.playerInfo?.state : void 0;
|
|
144563
|
+
const saidSomething = rawState !== void 0 && rawState !== null;
|
|
144564
|
+
const knownState = typeof info.state === "string" && KNOWN_PLAYER_STATES.has(info.state);
|
|
144565
|
+
const noBlock = [
|
|
144566
|
+
info.volume,
|
|
144567
|
+
info.infoText,
|
|
144568
|
+
info.mainArt,
|
|
144569
|
+
info.progress,
|
|
144570
|
+
info.provider,
|
|
144571
|
+
info.transport
|
|
144572
|
+
].every((block) => block === null || block === void 0);
|
|
144573
|
+
if (saidSomething && !knownState && noBlock) throw new Error("worker returned an unreadable player state");
|
|
144574
|
+
return info;
|
|
144265
144575
|
}
|
|
144266
144576
|
var DndEntrySchema = object({
|
|
144267
144577
|
serial: string().min(1),
|
|
@@ -144312,21 +144622,32 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144312
144622
|
features = [
|
|
144313
144623
|
DeviceFeature.MediaPlayerVolume,
|
|
144314
144624
|
DeviceFeature.MediaPlayerNext,
|
|
144315
|
-
DeviceFeature.MediaPlayerPrevious
|
|
144625
|
+
DeviceFeature.MediaPlayerPrevious,
|
|
144626
|
+
DeviceFeature.MediaPlayerShuffle,
|
|
144627
|
+
DeviceFeature.MediaPlayerRepeat
|
|
144316
144628
|
];
|
|
144317
144629
|
brokerId;
|
|
144318
144630
|
serial;
|
|
144319
144631
|
amazonDeviceType;
|
|
144320
144632
|
timer = null;
|
|
144321
144633
|
rereadTimer = null;
|
|
144634
|
+
pushReadTimer = null;
|
|
144635
|
+
lastPushReadAt = 0;
|
|
144636
|
+
repollAfter = false;
|
|
144637
|
+
unsubscribers = [];
|
|
144322
144638
|
polling = false;
|
|
144323
144639
|
failureLogged = false;
|
|
144324
144640
|
volumeFailureLogged = false;
|
|
144325
144641
|
health = "ok";
|
|
144326
|
-
/**
|
|
144327
|
-
|
|
144328
|
-
|
|
144329
|
-
|
|
144642
|
+
/** When this device last wrote its volume / Do Not Disturb optimistically; a read that began earlier must not overwrite it. */
|
|
144643
|
+
volumeSetAt = 0;
|
|
144644
|
+
dndSetAt = 0;
|
|
144645
|
+
/** Counts this device's sets; a refused set reverts only while it is still the latest. */
|
|
144646
|
+
volumeSeq = 0;
|
|
144647
|
+
dndSeq = 0;
|
|
144648
|
+
/** The level Amazon last took (a good set, or a read newer than every set); undefined until one is known. */
|
|
144649
|
+
volumeSettled = void 0;
|
|
144650
|
+
dndSettled = void 0;
|
|
144330
144651
|
constructor(ctx) {
|
|
144331
144652
|
const persisted = alexaEchoDeviceSchema.parse(ctx.persistedConfig ?? {});
|
|
144332
144653
|
super(ctx, alexaEchoDeviceSchema, { type: DeviceType.MediaPlayer });
|
|
@@ -144345,14 +144666,54 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144345
144666
|
this.runtimeState.setCapState("media-player", emptyEchoStatus("off", 0));
|
|
144346
144667
|
this.runtimeState.setCapState("do-not-disturb", COLD_DND);
|
|
144347
144668
|
this.poll();
|
|
144348
|
-
this.
|
|
144669
|
+
this.armPollTimer();
|
|
144670
|
+
this.unsubscribers = [alexaEchoSessions.onEcho(this.brokerId, this.serial, () => this.onPushEvent()), alexaEchoSessions.onChannel(this.brokerId, (state) => this.onChannel(state))];
|
|
144671
|
+
}
|
|
144672
|
+
/** The poll pace now: slow while the push channel is up, normal when it is down. */
|
|
144673
|
+
get pollIntervalMs() {
|
|
144674
|
+
return alexaEchoSessions.pushChannel(this.brokerId) === "up" ? ECHO_POLL_PUSH_HEALTHY_MS : ECHO_POLL_MS;
|
|
144675
|
+
}
|
|
144676
|
+
armPollTimer() {
|
|
144677
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
144678
|
+
this.timer = setInterval(() => void this.poll(), this.pollIntervalMs);
|
|
144349
144679
|
this.timer.unref();
|
|
144350
144680
|
}
|
|
144681
|
+
onChannel(state) {
|
|
144682
|
+
this.armPollTimer();
|
|
144683
|
+
this.ctx.logger.debug("alexa echo: push channel changed, poll pace follows", {
|
|
144684
|
+
tags: { deviceId: this.id },
|
|
144685
|
+
meta: {
|
|
144686
|
+
channel: state,
|
|
144687
|
+
pollMs: this.pollIntervalMs
|
|
144688
|
+
}
|
|
144689
|
+
});
|
|
144690
|
+
this.poll();
|
|
144691
|
+
}
|
|
144692
|
+
/**
|
|
144693
|
+
* Amazon pushed a change for this Echo: read it once, no sooner than the gap after the
|
|
144694
|
+
* last push read. The read is the ordinary one, so the optimistic-set guard applies as
|
|
144695
|
+
* it does to a poll: a read that began before a set never undoes it.
|
|
144696
|
+
*/
|
|
144697
|
+
onPushEvent() {
|
|
144698
|
+
if (this.pushReadTimer !== null) return;
|
|
144699
|
+
const wait = Math.max(0, this.lastPushReadAt + ECHO_PUSH_READ_GAP_MS - Date.now());
|
|
144700
|
+
this.pushReadTimer = setTimeout(() => {
|
|
144701
|
+
this.pushReadTimer = null;
|
|
144702
|
+
this.lastPushReadAt = Date.now();
|
|
144703
|
+
if (this.polling) this.repollAfter = true;
|
|
144704
|
+
else this.poll();
|
|
144705
|
+
}, wait);
|
|
144706
|
+
this.pushReadTimer.unref();
|
|
144707
|
+
}
|
|
144351
144708
|
async removeDevice() {
|
|
144352
144709
|
if (this.timer !== null) clearInterval(this.timer);
|
|
144353
144710
|
if (this.rereadTimer !== null) clearTimeout(this.rereadTimer);
|
|
144711
|
+
if (this.pushReadTimer !== null) clearTimeout(this.pushReadTimer);
|
|
144712
|
+
for (const off of this.unsubscribers) off();
|
|
144713
|
+
this.unsubscribers = [];
|
|
144354
144714
|
this.timer = null;
|
|
144355
144715
|
this.rereadTimer = null;
|
|
144716
|
+
this.pushReadTimer = null;
|
|
144356
144717
|
await super.removeDevice();
|
|
144357
144718
|
}
|
|
144358
144719
|
session() {
|
|
@@ -144381,8 +144742,13 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144381
144742
|
} finally {
|
|
144382
144743
|
this.polling = false;
|
|
144383
144744
|
}
|
|
144745
|
+
if (this.repollAfter) {
|
|
144746
|
+
this.repollAfter = false;
|
|
144747
|
+
await this.poll();
|
|
144748
|
+
}
|
|
144384
144749
|
}
|
|
144385
144750
|
async readOnce() {
|
|
144751
|
+
const readStartedAt = Date.now();
|
|
144386
144752
|
const session = alexaEchoSessions.get(this.brokerId);
|
|
144387
144753
|
if (session === null || session.state !== "ready") return;
|
|
144388
144754
|
if (session.rateLimitedUntil !== null) {
|
|
@@ -144398,21 +144764,21 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144398
144764
|
}
|
|
144399
144765
|
const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
|
|
144400
144766
|
try {
|
|
144401
|
-
const
|
|
144402
|
-
const info = await session.playerState(this.serial, deviceType, this.id);
|
|
144403
|
-
this.polledFrom = readStartedAt;
|
|
144404
|
-
const fromPlayer = mapPlayerInfoToStatus(info, {
|
|
144767
|
+
const fromPlayer = mapPlayerInfoToStatus(await session.playerState(this.serial, deviceType, this.id), {
|
|
144405
144768
|
online: true,
|
|
144406
144769
|
now: Date.now()
|
|
144407
144770
|
});
|
|
144408
|
-
|
|
144771
|
+
const read = await this.completeVolume(fromPlayer);
|
|
144772
|
+
const kept = this.keepNewerVolume(read, readStartedAt);
|
|
144773
|
+
if (kept === read) this.volumeSettled = read.volumeLevel;
|
|
144774
|
+
this.runtimeState.setCapState("media-player", kept);
|
|
144409
144775
|
this.markOnline(true);
|
|
144410
144776
|
this.noteRecovered();
|
|
144411
144777
|
} catch (err) {
|
|
144412
144778
|
this.noteReadFailure(err);
|
|
144413
144779
|
return;
|
|
144414
144780
|
}
|
|
144415
|
-
await this.readDoNotDisturb();
|
|
144781
|
+
await this.readDoNotDisturb(readStartedAt);
|
|
144416
144782
|
}
|
|
144417
144783
|
/**
|
|
144418
144784
|
* An idle Echo's player read carries no volume. Ask the account's volume list
|
|
@@ -144437,10 +144803,26 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144437
144803
|
return status;
|
|
144438
144804
|
}
|
|
144439
144805
|
}
|
|
144440
|
-
|
|
144806
|
+
/**
|
|
144807
|
+
* A read that began before this device last set its volume carries the level from
|
|
144808
|
+
* before the set: keep the level the operator chose (and its mute), take the rest.
|
|
144809
|
+
*/
|
|
144810
|
+
keepNewerVolume(read, readStartedAt) {
|
|
144811
|
+
if (this.volumeSetAt < readStartedAt) return read;
|
|
144812
|
+
const current = this.runtimeState.getCapState("media-player");
|
|
144813
|
+
if (current === void 0) return read;
|
|
144814
|
+
return {
|
|
144815
|
+
...read,
|
|
144816
|
+
volumeLevel: current.volumeLevel,
|
|
144817
|
+
isMuted: current.isMuted
|
|
144818
|
+
};
|
|
144819
|
+
}
|
|
144820
|
+
async readDoNotDisturb(readStartedAt) {
|
|
144441
144821
|
try {
|
|
144442
144822
|
const enabled = (await alexaEchoSessions.dnd(this.brokerId, Date.now())).get(this.serial);
|
|
144443
144823
|
if (enabled === void 0) return;
|
|
144824
|
+
if (this.dndSetAt >= readStartedAt) return;
|
|
144825
|
+
this.dndSettled = enabled;
|
|
144444
144826
|
this.runtimeState.setCapState("do-not-disturb", {
|
|
144445
144827
|
enabled,
|
|
144446
144828
|
lastChangedAt: Date.now()
|
|
@@ -144472,23 +144854,56 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144472
144854
|
await this.session().npCommand(this.serial, command, this.id);
|
|
144473
144855
|
this.rereadSoon();
|
|
144474
144856
|
}
|
|
144857
|
+
/**
|
|
144858
|
+
* The slice shows the new level at once; Amazon is told next. On a refusal the
|
|
144859
|
+
* slice goes back to the level Amazon last took (a good set, else the last read,
|
|
144860
|
+
* else what the slice held), with a new stamp, and the error is rethrown
|
|
144861
|
+
* (RateLimitedError included) after a warning. Only the LATEST set reverts: when
|
|
144862
|
+
* a newer set has superseded this one, that set owns the slice, so an earlier
|
|
144863
|
+
* refusal never writes over it, and a later refusal falls back to what Amazon
|
|
144864
|
+
* actually took, never to a level it did not. A cold slice (no read yet) gets no
|
|
144865
|
+
* optimistic write, so no state is invented.
|
|
144866
|
+
*/
|
|
144475
144867
|
async sendVolume(level) {
|
|
144476
|
-
|
|
144477
|
-
|
|
144478
|
-
|
|
144479
|
-
|
|
144480
|
-
|
|
144481
|
-
|
|
144868
|
+
const previous = this.runtimeState.getCapState("media-player");
|
|
144869
|
+
const setAt = Date.now();
|
|
144870
|
+
const seq = ++this.volumeSeq;
|
|
144871
|
+
this.volumeSetAt = setAt;
|
|
144872
|
+
if (previous !== void 0) this.runtimeState.setCapState("media-player", {
|
|
144873
|
+
...previous,
|
|
144874
|
+
volumeLevel: level,
|
|
144875
|
+
lastChangedAt: setAt
|
|
144876
|
+
});
|
|
144877
|
+
alexaEchoSessions.invalidateVolumes(this.brokerId);
|
|
144878
|
+
try {
|
|
144879
|
+
await this.session().setVolume(this.serial, level, this.id);
|
|
144880
|
+
this.volumeSettled = level;
|
|
144881
|
+
} catch (err) {
|
|
144882
|
+
const back = this.volumeSettled !== void 0 ? this.volumeSettled : previous?.volumeLevel ?? null;
|
|
144883
|
+
const current = this.runtimeState.getCapState("media-player");
|
|
144884
|
+
if (seq === this.volumeSeq && current !== void 0) this.runtimeState.setCapState("media-player", {
|
|
144885
|
+
...current,
|
|
144886
|
+
volumeLevel: back,
|
|
144887
|
+
lastChangedAt: Date.now()
|
|
144888
|
+
});
|
|
144889
|
+
this.ctx.logger.warn("alexa echo: Amazon refused the volume, back to the previous level", {
|
|
144890
|
+
tags: { deviceId: this.id },
|
|
144891
|
+
meta: {
|
|
144892
|
+
from: back,
|
|
144893
|
+
to: level,
|
|
144894
|
+
superseded: seq !== this.volumeSeq,
|
|
144895
|
+
error: err instanceof Error ? err.message : String(err)
|
|
144896
|
+
}
|
|
144897
|
+
});
|
|
144898
|
+
throw err;
|
|
144899
|
+
}
|
|
144482
144900
|
}
|
|
144483
144901
|
/**
|
|
144484
|
-
* Amazon has no step command: move from the level
|
|
144485
|
-
*
|
|
144486
|
-
* there is none.
|
|
144902
|
+
* Amazon has no step command: move from the level in the slice (it already holds
|
|
144903
|
+
* the last level we set); refuse when there is none.
|
|
144487
144904
|
*/
|
|
144488
144905
|
async stepVolume(delta) {
|
|
144489
|
-
const
|
|
144490
|
-
const set = this.lastSet;
|
|
144491
|
-
const level = set !== null && set.at >= this.polledFrom ? set.level : polled;
|
|
144906
|
+
const level = this.runtimeState.getCapState("media-player")?.volumeLevel;
|
|
144492
144907
|
if (level === null || level === void 0) throw new Error("the Echo volume is not known yet");
|
|
144493
144908
|
await this.sendVolume(Math.max(0, Math.min(100, level + delta)));
|
|
144494
144909
|
}
|
|
@@ -144509,22 +144924,51 @@ var AlexaEchoDevice = class extends BaseDevice {
|
|
|
144509
144924
|
channelDown: async () => unsupported("channel steps"),
|
|
144510
144925
|
seek: async () => unsupported("seek"),
|
|
144511
144926
|
setMute: async () => unsupported("mute"),
|
|
144512
|
-
setShuffle:
|
|
144513
|
-
setRepeat: async () =>
|
|
144927
|
+
setShuffle: ({ shuffle }) => this.command(shuffle ? "shuffle-on" : "shuffle-off"),
|
|
144928
|
+
setRepeat: async ({ repeat }) => {
|
|
144929
|
+
if (repeat === "one") unsupported("repeat one");
|
|
144930
|
+
await this.command(repeat === "all" ? "repeat-on" : "repeat-off");
|
|
144931
|
+
},
|
|
144514
144932
|
selectSource: async () => unsupported("source selection"),
|
|
144515
144933
|
playMedia: async () => unsupported("playing media")
|
|
144516
144934
|
});
|
|
144517
144935
|
}
|
|
144936
|
+
/** Optimistic like the volume, with the same rules: only the latest refused set reverts, to what Amazon last took. */
|
|
144937
|
+
async sendDnd(enabled) {
|
|
144938
|
+
const previous = this.runtimeState.getCapState("do-not-disturb") ?? COLD_DND;
|
|
144939
|
+
const setAt = Date.now();
|
|
144940
|
+
const seq = ++this.dndSeq;
|
|
144941
|
+
this.dndSetAt = setAt;
|
|
144942
|
+
this.runtimeState.setCapState("do-not-disturb", {
|
|
144943
|
+
enabled,
|
|
144944
|
+
lastChangedAt: setAt
|
|
144945
|
+
});
|
|
144946
|
+
alexaEchoSessions.invalidateDnd(this.brokerId);
|
|
144947
|
+
try {
|
|
144948
|
+
await this.session().setDnd(this.serial, enabled, this.id);
|
|
144949
|
+
this.dndSettled = enabled;
|
|
144950
|
+
} catch (err) {
|
|
144951
|
+
const back = this.dndSettled !== void 0 ? this.dndSettled : previous.enabled;
|
|
144952
|
+
if (seq === this.dndSeq) this.runtimeState.setCapState("do-not-disturb", {
|
|
144953
|
+
enabled: back,
|
|
144954
|
+
lastChangedAt: Date.now()
|
|
144955
|
+
});
|
|
144956
|
+
this.ctx.logger.warn("alexa echo: Amazon refused Do Not Disturb, back to the previous state", {
|
|
144957
|
+
tags: { deviceId: this.id },
|
|
144958
|
+
meta: {
|
|
144959
|
+
from: back,
|
|
144960
|
+
to: enabled,
|
|
144961
|
+
superseded: seq !== this.dndSeq,
|
|
144962
|
+
error: err instanceof Error ? err.message : String(err)
|
|
144963
|
+
}
|
|
144964
|
+
});
|
|
144965
|
+
throw err;
|
|
144966
|
+
}
|
|
144967
|
+
}
|
|
144518
144968
|
registerDoNotDisturb() {
|
|
144519
144969
|
this.ctx.registerNativeCap(doNotDisturbCapability, {
|
|
144520
144970
|
getStatus: async () => this.runtimeState.getCapState("do-not-disturb") ?? COLD_DND,
|
|
144521
|
-
setEnabled: async ({ enabled }) =>
|
|
144522
|
-
await this.session().setDnd(this.serial, enabled, this.id);
|
|
144523
|
-
this.runtimeState.setCapState("do-not-disturb", {
|
|
144524
|
-
enabled,
|
|
144525
|
-
lastChangedAt: Date.now()
|
|
144526
|
-
});
|
|
144527
|
-
}
|
|
144971
|
+
setEnabled: async ({ enabled }) => this.sendDnd(enabled)
|
|
144528
144972
|
});
|
|
144529
144973
|
}
|
|
144530
144974
|
};
|
|
@@ -144545,7 +144989,9 @@ var alexaSpeakSessionsEntryKind = defineEntryKind("speak-sessions", {
|
|
|
144545
144989
|
writeTrigger: "an Echo worker signs in as an Alexa account broker"
|
|
144546
144990
|
});
|
|
144547
144991
|
/** An Alexa integration: the link to the Alexa account (broker) it imports devices from. */
|
|
144548
|
-
var alexaConnectionsEntryKind =
|
|
144992
|
+
var alexaConnectionsEntryKind = declareLinkKind({
|
|
144993
|
+
name: "connections",
|
|
144994
|
+
links: { brokerId: ALEXA_ACCOUNT_LINK },
|
|
144549
144995
|
title: "Alexa integrations",
|
|
144550
144996
|
writeTrigger: "an operator links an Alexa integration to an Alexa account"
|
|
144551
144997
|
});
|
|
@@ -144647,11 +145093,15 @@ function buildAlexaAccountInstances(records, deps) {
|
|
|
144647
145093
|
* record, edited on the Brokers page). The account's sign-in and secrets are never copied here.
|
|
144648
145094
|
*/
|
|
144649
145095
|
function buildAlexaInstances(records) {
|
|
144650
|
-
return
|
|
145096
|
+
return buildLinkedInstances(records, {
|
|
144651
145097
|
kind: alexaConnectionsEntryKind,
|
|
144652
|
-
|
|
144653
|
-
|
|
144654
|
-
|
|
145098
|
+
description: {
|
|
145099
|
+
role: "devices",
|
|
145100
|
+
instanceMode: "multiple",
|
|
145101
|
+
kind: "connection",
|
|
145102
|
+
canTest: false
|
|
145103
|
+
},
|
|
145104
|
+
labels: { brokerId: { label: "Alexa account" } }
|
|
144655
145105
|
});
|
|
144656
145106
|
}
|
|
144657
145107
|
//#endregion
|
|
@@ -144944,8 +145394,67 @@ var BrokerEcho = class {
|
|
|
144944
145394
|
currentDevices = [];
|
|
144945
145395
|
backoffMs = 0;
|
|
144946
145396
|
limitedUntil = 0;
|
|
145397
|
+
channelState = "down";
|
|
145398
|
+
channelWasUp = false;
|
|
145399
|
+
secrets = [];
|
|
145400
|
+
unknownPushLogged = /* @__PURE__ */ new Set();
|
|
144947
145401
|
constructor(deps) {
|
|
144948
145402
|
this.deps = deps;
|
|
145403
|
+
deps.worker.onPush((push) => this.onPush(push));
|
|
145404
|
+
}
|
|
145405
|
+
/** Whether Amazon's push channel is open for this account right now. */
|
|
145406
|
+
get pushChannel() {
|
|
145407
|
+
return this.channelState;
|
|
145408
|
+
}
|
|
145409
|
+
/** A reason is text from the library: capped, then scrubbed of the secrets in play. */
|
|
145410
|
+
safeReason(reason) {
|
|
145411
|
+
return scrubSecrets(reason.slice(0, 200), this.secrets);
|
|
145412
|
+
}
|
|
145413
|
+
onPush(push) {
|
|
145414
|
+
const tags = { tags: { brokerId: this.deps.brokerId } };
|
|
145415
|
+
if (push.push === "channel") {
|
|
145416
|
+
if (push.state === this.channelState) {
|
|
145417
|
+
if (push.state === "down") this.deps.logger.debug("alexa echo: push channel still down", {
|
|
145418
|
+
...tags,
|
|
145419
|
+
meta: {
|
|
145420
|
+
reason: this.safeReason(push.reason),
|
|
145421
|
+
retryInMs: push.retryInMs
|
|
145422
|
+
}
|
|
145423
|
+
});
|
|
145424
|
+
return;
|
|
145425
|
+
}
|
|
145426
|
+
this.channelState = push.state;
|
|
145427
|
+
if (push.state === "up") {
|
|
145428
|
+
this.deps.logger.info(this.channelWasUp ? "alexa echo: push channel reconnected, polling slows down" : "alexa echo: push channel connected, polling slows down", tags);
|
|
145429
|
+
this.channelWasUp = true;
|
|
145430
|
+
} else this.deps.logger.warn("alexa echo: push channel down, polling at the normal pace", {
|
|
145431
|
+
...tags,
|
|
145432
|
+
meta: {
|
|
145433
|
+
reason: this.safeReason(push.reason),
|
|
145434
|
+
retryInMs: push.retryInMs
|
|
145435
|
+
}
|
|
145436
|
+
});
|
|
145437
|
+
this.deps.push?.channel(this.deps.brokerId, push.state);
|
|
145438
|
+
return;
|
|
145439
|
+
}
|
|
145440
|
+
if (PUSH_READ_COMMANDS.has(push.command)) {
|
|
145441
|
+
if (push.serial === null) {
|
|
145442
|
+
this.deps.logger.debug("alexa echo: push event without an Echo, ignored", {
|
|
145443
|
+
...tags,
|
|
145444
|
+
meta: { command: push.command }
|
|
145445
|
+
});
|
|
145446
|
+
return;
|
|
145447
|
+
}
|
|
145448
|
+
this.deps.push?.event(this.deps.brokerId, push.serial);
|
|
145449
|
+
return;
|
|
145450
|
+
}
|
|
145451
|
+
if (PUSH_IGNORED_COMMANDS.has(push.command)) return;
|
|
145452
|
+
if (this.unknownPushLogged.has(push.command)) return;
|
|
145453
|
+
this.unknownPushLogged.add(push.command);
|
|
145454
|
+
this.deps.logger.info("alexa echo: unknown push event, ignored (logged once)", {
|
|
145455
|
+
...tags,
|
|
145456
|
+
meta: { command: push.command }
|
|
145457
|
+
});
|
|
144949
145458
|
}
|
|
144950
145459
|
get id() {
|
|
144951
145460
|
return this.deps.brokerId;
|
|
@@ -144971,6 +145480,10 @@ var BrokerEcho = class {
|
|
|
144971
145480
|
}
|
|
144972
145481
|
stop() {
|
|
144973
145482
|
this.deps.worker.stop();
|
|
145483
|
+
if (this.channelState === "up") {
|
|
145484
|
+
this.channelState = "down";
|
|
145485
|
+
this.deps.push?.channel(this.deps.brokerId, "down");
|
|
145486
|
+
}
|
|
144974
145487
|
}
|
|
144975
145488
|
/** Sign the worker in as the broker's account. Never throws; the outcome is `state`. */
|
|
144976
145489
|
async signIn() {
|
|
@@ -144997,6 +145510,7 @@ var BrokerEcho = class {
|
|
|
144997
145510
|
}
|
|
144998
145511
|
secrets = seed === null ? [] : [seed.refreshToken];
|
|
144999
145512
|
worker.noteSecrets(secrets);
|
|
145513
|
+
this.secrets = secrets;
|
|
145000
145514
|
let outcome = parseLoginResult({
|
|
145001
145515
|
status: "failed",
|
|
145002
145516
|
error: "worker gave no answer"
|
|
@@ -145815,6 +146329,14 @@ function startAlexaWorker(deps) {
|
|
|
145815
146329
|
let secrets = [];
|
|
145816
146330
|
let pending = null;
|
|
145817
146331
|
const queue = [];
|
|
146332
|
+
const pushListeners = [];
|
|
146333
|
+
function deliverPush(push) {
|
|
146334
|
+
for (const listener of pushListeners) try {
|
|
146335
|
+
listener(push);
|
|
146336
|
+
} catch (err) {
|
|
146337
|
+
logger.warn("alexa worker push listener failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
146338
|
+
}
|
|
146339
|
+
}
|
|
145818
146340
|
function scrub(text) {
|
|
145819
146341
|
return scrubSecrets(text, secrets);
|
|
145820
146342
|
}
|
|
@@ -145892,6 +146414,12 @@ function startAlexaWorker(deps) {
|
|
|
145892
146414
|
logger.warn("alexa worker non-JSON line", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
145893
146415
|
return;
|
|
145894
146416
|
}
|
|
146417
|
+
if (typeof parsed === "object" && parsed !== null && "push" in parsed) {
|
|
146418
|
+
const push = WorkerPushSchema.safeParse(parsed);
|
|
146419
|
+
if (push.success) deliverPush(push.data);
|
|
146420
|
+
else logger.warn("alexa worker push line failed schema", { meta: { error: push.error.message } });
|
|
146421
|
+
return;
|
|
146422
|
+
}
|
|
145895
146423
|
const checked = WorkerLineSchema.safeParse(parsed);
|
|
145896
146424
|
if (!checked.success) {
|
|
145897
146425
|
logger.warn("alexa worker line failed schema", { meta: { error: checked.error.message } });
|
|
@@ -145918,6 +146446,12 @@ function startAlexaWorker(deps) {
|
|
|
145918
146446
|
}
|
|
145919
146447
|
function onExit(code) {
|
|
145920
146448
|
child = null;
|
|
146449
|
+
deliverPush({
|
|
146450
|
+
push: "channel",
|
|
146451
|
+
state: "down",
|
|
146452
|
+
reason: "worker exited",
|
|
146453
|
+
retryInMs: 0
|
|
146454
|
+
});
|
|
145921
146455
|
if (stopped) {
|
|
145922
146456
|
failPending(/* @__PURE__ */ new Error("alexa worker stopped"));
|
|
145923
146457
|
return;
|
|
@@ -146024,6 +146558,9 @@ function startAlexaWorker(deps) {
|
|
|
146024
146558
|
}
|
|
146025
146559
|
return {
|
|
146026
146560
|
request,
|
|
146561
|
+
onPush(listener) {
|
|
146562
|
+
pushListeners.push(listener);
|
|
146563
|
+
},
|
|
146027
146564
|
noteSecrets(next) {
|
|
146028
146565
|
secrets = [...next];
|
|
146029
146566
|
},
|
|
@@ -146174,7 +146711,8 @@ var AlexaSpeakController = class {
|
|
|
146174
146711
|
readSeed: () => this.host.readSeed(ref.id),
|
|
146175
146712
|
sessions: this.host.sessions,
|
|
146176
146713
|
logger: this.host.logger,
|
|
146177
|
-
now: this.now
|
|
146714
|
+
now: this.now,
|
|
146715
|
+
...this.host.push === void 0 ? {} : { push: this.host.push }
|
|
146178
146716
|
}));
|
|
146179
146717
|
await Promise.all(refs.map(async (ref) => {
|
|
146180
146718
|
const echo = this.echoes.get(ref.id);
|
|
@@ -146588,7 +147126,8 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
146588
147126
|
return entry === void 0 ? null : readBrokerSeed(entry);
|
|
146589
147127
|
},
|
|
146590
147128
|
sessions: RecordsSpeakSessionStore.of(records),
|
|
146591
|
-
ensurePython: () => this.ctx.deps.ensurePython()
|
|
147129
|
+
ensurePython: () => this.ctx.deps.ensurePython(),
|
|
147130
|
+
push: alexaEchoSessions
|
|
146592
147131
|
});
|
|
146593
147132
|
this.speak = speak;
|
|
146594
147133
|
alexaEchoSessions.setResolver((brokerId) => speak.echoFor(brokerId));
|
|
@@ -146652,7 +147191,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
146652
147191
|
methods: ["device-code", "proxy"]
|
|
146653
147192
|
}
|
|
146654
147193
|
},
|
|
146655
|
-
provider:
|
|
147194
|
+
provider: guardInstanceDelete(this.accountInstances.toProvider(), () => this.brokerUsers())
|
|
146656
147195
|
}, {
|
|
146657
147196
|
kind: {
|
|
146658
147197
|
id: "integration",
|
|
@@ -146685,7 +147224,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
146685
147224
|
return;
|
|
146686
147225
|
}
|
|
146687
147226
|
const configured = entry.connection.cookie.length > 0 || entry.connection.refreshToken.length > 0;
|
|
146688
|
-
if (entry
|
|
147227
|
+
if (!isInstanceEnabled(entry) || !configured) {
|
|
146689
147228
|
await reg.removeEntry(id);
|
|
146690
147229
|
this.speak?.brokersChanged();
|
|
146691
147230
|
return;
|
|
@@ -146777,7 +147316,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
146777
147316
|
} });
|
|
146778
147317
|
}
|
|
146779
147318
|
for (const entry of persisted) {
|
|
146780
|
-
if (entry
|
|
147319
|
+
if (!isInstanceEnabled(entry)) continue;
|
|
146781
147320
|
if (entry.id && liveIds.has(entry.id)) {
|
|
146782
147321
|
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
146783
147322
|
try {
|
|
@@ -146952,7 +147491,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
146952
147491
|
}
|
|
146953
147492
|
/** Who still points at an Alexa account: its integrations and the Echo targets that speak through it. */
|
|
146954
147493
|
brokerUsers() {
|
|
146955
|
-
return [
|
|
147494
|
+
return [linkUserFinder(this.requireInstances(), "brokerId"), { find: async (brokerId) => readEchoTargets(this.config).filter((target) => target.brokerId === brokerId).map((target) => ({
|
|
146956
147495
|
kind: "notification-target",
|
|
146957
147496
|
id: target.id,
|
|
146958
147497
|
name: target.name
|
|
@@ -146983,7 +147522,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
|
|
|
146983
147522
|
},
|
|
146984
147523
|
logger: this.ctx.logger,
|
|
146985
147524
|
getBrokerIdForIntegration: async (id) => {
|
|
146986
|
-
const brokerId = await
|
|
147525
|
+
const brokerId = await linkedIdOf(this.requireInstances(), id, "brokerId");
|
|
146987
147526
|
if (brokerId === void 0) throw new Error(`integration ${id} has no broker`);
|
|
146988
147527
|
return brokerId;
|
|
146989
147528
|
},
|