@camstack/addon-google-services 0.2.37 → 0.2.39
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 +13 -0
- package/dist/google-services.addon.js +942 -800
- package/dist/google-services.addon.mjs +942 -800
- package/package.json +2 -2
|
@@ -5985,7 +5985,7 @@ var ZodIssueCode = {
|
|
|
5985
5985
|
var ZodFirstPartyTypeKind;
|
|
5986
5986
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5987
5987
|
//#endregion
|
|
5988
|
-
//#region ../types/dist/sleep-
|
|
5988
|
+
//#region ../types/dist/sleep-BX_MqzUe.mjs
|
|
5989
5989
|
/**
|
|
5990
5990
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5991
5991
|
* window to float samples (D455).
|
|
@@ -6585,7 +6585,7 @@ var NOT_RUN = {
|
|
|
6585
6585
|
* throws: an unreachable or refusing engine is logged and reported `declared: false`,
|
|
6586
6586
|
* because the hub (and this addon's boot) must not hang on a migration.
|
|
6587
6587
|
*/
|
|
6588
|
-
async function runAddonMigrations(host, migrations, logger) {
|
|
6588
|
+
async function runAddonMigrations(host, migrations, logger, options = {}) {
|
|
6589
6589
|
const first = migrations[0];
|
|
6590
6590
|
if (first === void 0) return NOT_RUN;
|
|
6591
6591
|
const owner = first.meta.owner;
|
|
@@ -6614,7 +6614,9 @@ async function runAddonMigrations(host, migrations, logger) {
|
|
|
6614
6614
|
else if (status === "blocked") blocked.push(id);
|
|
6615
6615
|
else skipped.push(id);
|
|
6616
6616
|
};
|
|
6617
|
+
const selected = options.only === void 0 ? null : new Set(options.only);
|
|
6617
6618
|
for (const step of declaration.steps) {
|
|
6619
|
+
if (selected !== null && !selected.has(step.id)) continue;
|
|
6618
6620
|
const bound = byId.get(step.id);
|
|
6619
6621
|
if (bound === void 0 || step.action === "skip") {
|
|
6620
6622
|
skipped.push(step.id);
|
|
@@ -6792,6 +6794,12 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6792
6794
|
}
|
|
6793
6795
|
return out;
|
|
6794
6796
|
}
|
|
6797
|
+
/** Every scope, in the order the guard and the docs list them. */
|
|
6798
|
+
var SCOPED_SETTINGS_SCOPES = [
|
|
6799
|
+
"cluster",
|
|
6800
|
+
"device-defaults",
|
|
6801
|
+
"node"
|
|
6802
|
+
];
|
|
6795
6803
|
/**
|
|
6796
6804
|
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6797
6805
|
*
|
|
@@ -7606,6 +7614,28 @@ var SECRET_RETRY_MAX_MS = 5 * 6e4;
|
|
|
7606
7614
|
/** Coalesces a burst of `keychain.changed` events into one re-resolve. */
|
|
7607
7615
|
var SECRET_CHANGE_DEBOUNCE_MS = 250;
|
|
7608
7616
|
/**
|
|
7617
|
+
* Pair a catalogue with the function that builds its ports FROM THE MIGRATION API. The one
|
|
7618
|
+
* place a catalogue is bound: a migration reaches nothing a ports binder cannot reach
|
|
7619
|
+
* through `api` (guard `check-migration-ports-surface`).
|
|
7620
|
+
*/
|
|
7621
|
+
function migrationSet(catalogue, bindPorts) {
|
|
7622
|
+
return { bind: (api) => defineAddonMigrations(catalogue, bindPorts(api)) };
|
|
7623
|
+
}
|
|
7624
|
+
/** A mixed array is a mistake: the other kind would be dropped without a word. */
|
|
7625
|
+
function refuseMixed(form, kept, dropped) {
|
|
7626
|
+
if (dropped > 0) throw new Error(`runMigrations(${form}): ${dropped} of ${kept + dropped} entries are of the other kind and would be dropped; pass one kind per call`);
|
|
7627
|
+
}
|
|
7628
|
+
function catalogueOf(list) {
|
|
7629
|
+
const kept = list.filter((entry) => !("execute" in entry));
|
|
7630
|
+
refuseMixed("catalogue, (api) => ports", kept.length, list.length - kept.length);
|
|
7631
|
+
return kept;
|
|
7632
|
+
}
|
|
7633
|
+
function boundOf(list) {
|
|
7634
|
+
const kept = list.filter((entry) => "execute" in entry);
|
|
7635
|
+
refuseMixed("bound migrations", kept.length, list.length - kept.length);
|
|
7636
|
+
return kept;
|
|
7637
|
+
}
|
|
7638
|
+
/**
|
|
7609
7639
|
* Base class for CamStack addons. Eliminates settings boilerplate:
|
|
7610
7640
|
*
|
|
7611
7641
|
* - Typed `config` property with automatic resolution from store + defaults
|
|
@@ -7683,6 +7713,13 @@ var BaseAddon = class {
|
|
|
7683
7713
|
*/
|
|
7684
7714
|
secretStatePaths = [];
|
|
7685
7715
|
_addonSecretTree = null;
|
|
7716
|
+
/**
|
|
7717
|
+
* D986 M4: the scoped schemas this addon declared, resolved once per capability
|
|
7718
|
+
* (`loadScopedSchemas`) so the sync readers (`perNodeKeys`, the secret tree, the restart check)
|
|
7719
|
+
* see them. Empty until loaded: the readers then fall back to {@link globalSettingsSchema}.
|
|
7720
|
+
*/
|
|
7721
|
+
_scopedByCap = /* @__PURE__ */ new Map();
|
|
7722
|
+
_nodeKeysByCap = /* @__PURE__ */ new Map();
|
|
7686
7723
|
_deviceSecretTree = null;
|
|
7687
7724
|
/** Refs the last resolved config pointed at — a change to one re-resolves. */
|
|
7688
7725
|
_configSecretRefs = /* @__PURE__ */ new Set();
|
|
@@ -7752,6 +7789,7 @@ var BaseAddon = class {
|
|
|
7752
7789
|
this._ctx = context;
|
|
7753
7790
|
this._secretSettings = null;
|
|
7754
7791
|
}
|
|
7792
|
+
await this.loadScopedSchemas();
|
|
7755
7793
|
await this.resolveConfig();
|
|
7756
7794
|
if (secrets !== void 0) this._secretChangeUnsub = secrets.onChange((ref) => {
|
|
7757
7795
|
if (this._configSecretRefs.has(ref)) this.scheduleSecretReresolve(SECRET_CHANGE_DEBOUNCE_MS);
|
|
@@ -7761,27 +7799,30 @@ var BaseAddon = class {
|
|
|
7761
7799
|
const normalized = normalizeAddonInitResult(result);
|
|
7762
7800
|
const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
|
|
7763
7801
|
this._registeredCapNames = providers.map((p) => p.capability.name);
|
|
7802
|
+
await this.loadScopedSchemas();
|
|
7764
7803
|
return normalized;
|
|
7765
7804
|
}
|
|
7805
|
+
async runMigrations(first, second, third) {
|
|
7806
|
+
if (typeof second === "function") return this.runMigrationSets([migrationSet(catalogueOf(first), second)], third);
|
|
7807
|
+
return this.runBound(boundOf(first), second);
|
|
7808
|
+
}
|
|
7766
7809
|
/**
|
|
7767
|
-
*
|
|
7768
|
-
*
|
|
7769
|
-
*
|
|
7770
|
-
* restored devices — with the catalogue bound to the addon's ports
|
|
7771
|
-
* (`defineAddonMigrations(ADDON_MIGRATIONS, ports)`).
|
|
7772
|
-
*
|
|
7773
|
-
* An engine that cannot be reached or refuses the catalogue is logged and runs
|
|
7774
|
-
* nothing; the boot continues. A migration that FAILED (or was blocked by one)
|
|
7775
|
-
* and declares `onFailure: 'block-owner'` makes this throw, so the addon does not
|
|
7776
|
-
* serve data whose shape it cannot trust; `tolerate` migrations never do.
|
|
7810
|
+
* Several catalogues of ONE owner in one declaration (the engine replaces an owner's
|
|
7811
|
+
* catalogue on every declare, so two calls would each judge the other's ids "newer").
|
|
7812
|
+
* Build each entry with `migrationSet(catalogue, (api) => ports)`.
|
|
7777
7813
|
*/
|
|
7778
|
-
async
|
|
7814
|
+
async runMigrationSets(sets, options) {
|
|
7815
|
+
const api = this.ctx.migrationApi;
|
|
7816
|
+
if (api === void 0) throw new Error(`${this.ctx.id}: the migration API is not available on this context`);
|
|
7817
|
+
return this.runBound(sets.flatMap((set) => set.bind(api)), options);
|
|
7818
|
+
}
|
|
7819
|
+
async runBound(migrations, options) {
|
|
7779
7820
|
const api = this.ctx.api.migrations;
|
|
7780
7821
|
const report = await runAddonMigrations({
|
|
7781
7822
|
declareOwner: (input) => api.declareOwner.mutate(input),
|
|
7782
7823
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7783
7824
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7784
|
-
}, migrations, this.ctx.logger);
|
|
7825
|
+
}, migrations, this.ctx.logger, options);
|
|
7785
7826
|
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7786
7827
|
return report;
|
|
7787
7828
|
}
|
|
@@ -7845,6 +7886,16 @@ var BaseAddon = class {
|
|
|
7845
7886
|
schema(schema) {
|
|
7846
7887
|
return schema;
|
|
7847
7888
|
}
|
|
7889
|
+
/**
|
|
7890
|
+
* The capability names whose scoped settings schemas ({@link clusterSettingsSchema},
|
|
7891
|
+
* {@link nodeSettingsSchema}, {@link deviceDefaultsSchema}) this addon declares. Read BEFORE
|
|
7892
|
+
* `onInitialize`, when the providers it registers are not known yet: the first config resolve
|
|
7893
|
+
* must already know which keys are per node. Names the providers register at init are added
|
|
7894
|
+
* once known.
|
|
7895
|
+
*/
|
|
7896
|
+
scopedSettingsCapabilities() {
|
|
7897
|
+
return [];
|
|
7898
|
+
}
|
|
7848
7899
|
/** Override to provide global-level settings UI schema. */
|
|
7849
7900
|
globalSettingsSchema(_cap) {
|
|
7850
7901
|
return null;
|
|
@@ -7888,7 +7939,8 @@ var BaseAddon = class {
|
|
|
7888
7939
|
return [];
|
|
7889
7940
|
}
|
|
7890
7941
|
async getGlobalSettings(overlay, cap, nodeId) {
|
|
7891
|
-
|
|
7942
|
+
await this.loadScopedSchemas(cap);
|
|
7943
|
+
const schema = this.globalSettingsSchema(cap) ?? this.scopedFormSchema(cap);
|
|
7892
7944
|
if (!schema) return { sections: [] };
|
|
7893
7945
|
const projected = await this.readRawProjectedStore(nodeId, cap);
|
|
7894
7946
|
return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(projected, overlay) : projected, { inlineSecretsAsViews: true });
|
|
@@ -7923,10 +7975,9 @@ var BaseAddon = class {
|
|
|
7923
7975
|
*
|
|
7924
7976
|
* Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
|
|
7925
7977
|
* needs its connection fields, the default integration the addon's own sections. It is the
|
|
7926
|
-
* SHAPE of the form only — the values live
|
|
7927
|
-
* unchanged (`integrations.setSettings` for an account integration).
|
|
7978
|
+
* SHAPE of the form only — the values live on the instance record.
|
|
7928
7979
|
*/
|
|
7929
|
-
async
|
|
7980
|
+
async integrationFormSchema(_instance) {
|
|
7930
7981
|
return null;
|
|
7931
7982
|
}
|
|
7932
7983
|
/**
|
|
@@ -7972,7 +8023,7 @@ var BaseAddon = class {
|
|
|
7972
8023
|
* per-instance schema when it declares one, else its global schema (what they always showed).
|
|
7973
8024
|
*/
|
|
7974
8025
|
async getIntegrationFormSchema(instance, nodeId) {
|
|
7975
|
-
const scoped = await this.
|
|
8026
|
+
const scoped = await this.integrationFormSchema(instance);
|
|
7976
8027
|
if (scoped === null) return this.getGlobalSettings(void 0, void 0, nodeId);
|
|
7977
8028
|
return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
|
|
7978
8029
|
}
|
|
@@ -8020,6 +8071,7 @@ var BaseAddon = class {
|
|
|
8020
8071
|
* resolved blob back is a no-op for every unchanged secret.
|
|
8021
8072
|
*/
|
|
8022
8073
|
async resolveGlobalStore(nodeId, cap) {
|
|
8074
|
+
await this.loadScopedSchemas(cap);
|
|
8023
8075
|
const raw = await this._ctx?.settings?.readAddonStore() ?? {};
|
|
8024
8076
|
return this.projectForNode(raw, nodeId, cap);
|
|
8025
8077
|
}
|
|
@@ -8030,6 +8082,7 @@ var BaseAddon = class {
|
|
|
8030
8082
|
}
|
|
8031
8083
|
/** The PROJECTED store with its markers — the form hydrate only (D798). */
|
|
8032
8084
|
async readRawProjectedStore(nodeId, cap) {
|
|
8085
|
+
await this.loadScopedSchemas(cap);
|
|
8033
8086
|
const raw = await this.rawSettings()?.readAddonStore() ?? {};
|
|
8034
8087
|
return this.projectForNode(raw, nodeId, cap);
|
|
8035
8088
|
}
|
|
@@ -8132,6 +8185,7 @@ var BaseAddon = class {
|
|
|
8132
8185
|
await this.applyGlobalPatch(patch, nodeId);
|
|
8133
8186
|
}
|
|
8134
8187
|
async applyGlobalPatch(barePatch, nodeId) {
|
|
8188
|
+
await this.loadScopedSchemas();
|
|
8135
8189
|
const keys = this.perNodeKeys();
|
|
8136
8190
|
const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
|
|
8137
8191
|
const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
|
|
@@ -8145,7 +8199,8 @@ var BaseAddon = class {
|
|
|
8145
8199
|
await this.resolveConfig();
|
|
8146
8200
|
await this.onConfigChanged();
|
|
8147
8201
|
this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
|
|
8148
|
-
|
|
8202
|
+
const legacy = this.globalSettingsSchema();
|
|
8203
|
+
this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
|
|
8149
8204
|
}
|
|
8150
8205
|
/**
|
|
8151
8206
|
* The set of field keys the global settings schema declares `perNode: true`
|
|
@@ -8153,14 +8208,67 @@ var BaseAddon = class {
|
|
|
8153
8208
|
* declarations). Empty set ⇒ every per-node code path is bypassed and the
|
|
8154
8209
|
* settings API behaves exactly like the legacy node-agnostic one.
|
|
8155
8210
|
*/
|
|
8156
|
-
|
|
8211
|
+
/**
|
|
8212
|
+
* Load the scoped schemas of every capability this addon declares (the override and the
|
|
8213
|
+
* providers registered at init, plus `extraCap`) that is not loaded yet, then drop what was
|
|
8214
|
+
* derived from the previous set. Idempotent.
|
|
8215
|
+
*/
|
|
8216
|
+
async loadScopedSchemas(extraCap) {
|
|
8217
|
+
const caps = new Set([
|
|
8218
|
+
...this.scopedSettingsCapabilities(),
|
|
8219
|
+
...this._registeredCapNames,
|
|
8220
|
+
...extraCap === void 0 ? [] : [extraCap]
|
|
8221
|
+
]);
|
|
8222
|
+
let added = false;
|
|
8223
|
+
for (const cap of caps) {
|
|
8224
|
+
if (this._scopedByCap.has(cap)) continue;
|
|
8225
|
+
const loaded = [];
|
|
8226
|
+
for (const scope of SCOPED_SETTINGS_SCOPES) {
|
|
8227
|
+
const schema = await this.getScopedSettingsSchema(scope, cap);
|
|
8228
|
+
if (schema !== null) loaded.push({
|
|
8229
|
+
scope,
|
|
8230
|
+
schema
|
|
8231
|
+
});
|
|
8232
|
+
}
|
|
8233
|
+
this._scopedByCap.set(cap, loaded);
|
|
8234
|
+
added = true;
|
|
8235
|
+
}
|
|
8236
|
+
if (!added) return;
|
|
8237
|
+
this._nodeKeysByCap = /* @__PURE__ */ new Map();
|
|
8238
|
+
this._addonSecretTree = null;
|
|
8239
|
+
}
|
|
8240
|
+
/**
|
|
8241
|
+
* The form the settings API serves while an addon declares no {@link globalSettingsSchema}: the
|
|
8242
|
+
* sections of its scoped schemas, so dropping the old hook does not blank the form.
|
|
8243
|
+
*/
|
|
8244
|
+
scopedFormSchema(cap) {
|
|
8245
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8246
|
+
for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
|
|
8247
|
+
return byId.size === 0 ? null : { sections: [...byId.values()] };
|
|
8248
|
+
}
|
|
8249
|
+
/** Every loaded scoped schema of `cap`, or of every loaded capability. */
|
|
8250
|
+
loadedScopedSchemas(cap) {
|
|
8251
|
+
if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
|
|
8252
|
+
return [...this._scopedByCap.values()].flat();
|
|
8253
|
+
}
|
|
8254
|
+
/**
|
|
8255
|
+
* The field keys that are stored per node: every field of a {@link nodeSettingsSchema}, plus
|
|
8256
|
+
* the fields the legacy {@link globalSettingsSchema} still marks `perNode: true`. Derived once
|
|
8257
|
+
* per `cap` argument and memoized (schemas are static declarations). Empty set ⇒ every
|
|
8258
|
+
* per-node code path is bypassed and the settings API behaves like the node-agnostic one.
|
|
8259
|
+
* Call {@link loadScopedSchemas} first (the async seams do).
|
|
8260
|
+
*/
|
|
8157
8261
|
perNodeKeys(cap) {
|
|
8158
8262
|
const cacheKey = cap ?? "";
|
|
8159
|
-
const cached = this.
|
|
8263
|
+
const cached = this._nodeKeysByCap.get(cacheKey);
|
|
8160
8264
|
if (cached) return cached;
|
|
8161
|
-
const
|
|
8162
|
-
const keys = new Set(
|
|
8163
|
-
this.
|
|
8265
|
+
const legacy = this.globalSettingsSchema(cap);
|
|
8266
|
+
const keys = new Set(legacy ? legacy.sections.flatMap((section) => collectPerNodeFieldKeys(section.fields)) : []);
|
|
8267
|
+
for (const { scope, schema } of this.loadedScopedSchemas(cap)) {
|
|
8268
|
+
if (scope !== "node") continue;
|
|
8269
|
+
for (const section of schema.sections) for (const key of collectValueFieldKeys(section.fields)) keys.add(key);
|
|
8270
|
+
}
|
|
8271
|
+
this._nodeKeysByCap.set(cacheKey, keys);
|
|
8164
8272
|
return keys;
|
|
8165
8273
|
}
|
|
8166
8274
|
/**
|
|
@@ -8170,10 +8278,9 @@ var BaseAddon = class {
|
|
|
8170
8278
|
* time to return its response before the addon is torn down and
|
|
8171
8279
|
* re-initialised by `AddonRegistryService.restartAddon`.
|
|
8172
8280
|
*/
|
|
8173
|
-
maybeAutoRestart(patchKeys,
|
|
8174
|
-
if (!schema) return;
|
|
8281
|
+
maybeAutoRestart(patchKeys, schemas) {
|
|
8175
8282
|
const restartKeys = /* @__PURE__ */ new Set();
|
|
8176
|
-
for (const section of schema.sections) for (const field of section.fields) {
|
|
8283
|
+
for (const schema of schemas) for (const section of schema.sections) for (const field of section.fields) {
|
|
8177
8284
|
if (field.type === "separator" || field.type === "info") continue;
|
|
8178
8285
|
if (field.requiresRestart) restartKeys.add(field.key);
|
|
8179
8286
|
}
|
|
@@ -8369,12 +8476,14 @@ var BaseAddon = class {
|
|
|
8369
8476
|
* here, in the addon's own process, so the hub keeps no copy of it.
|
|
8370
8477
|
*/
|
|
8371
8478
|
async getDeclaredSettingsKeys() {
|
|
8479
|
+
await this.loadScopedSchemas();
|
|
8372
8480
|
return { keys: [...this.declaredKeySet()].toSorted() };
|
|
8373
8481
|
}
|
|
8374
8482
|
declaredKeySet() {
|
|
8375
8483
|
const keys = new Set(Object.keys(this.defaults));
|
|
8376
8484
|
const schema = this.globalSettingsSchema();
|
|
8377
8485
|
if (schema) for (const section of schema.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
8486
|
+
for (const { schema: scoped } of this.loadedScopedSchemas()) for (const section of scoped.sections) for (const key of collectFieldKeys(section.fields)) keys.add(key);
|
|
8378
8487
|
for (const key of this._declaredStateKeys) keys.add(key);
|
|
8379
8488
|
for (const path of this.secretStatePaths) {
|
|
8380
8489
|
const root = path.split(/[.[]/, 1)[0];
|
|
@@ -8470,7 +8579,11 @@ var BaseAddon = class {
|
|
|
8470
8579
|
}
|
|
8471
8580
|
/** The addon store's secret tree: schema fields by TYPE, plus `secretStatePaths`. */
|
|
8472
8581
|
addonSecretTree() {
|
|
8473
|
-
this._addonSecretTree
|
|
8582
|
+
if (this._addonSecretTree === null) {
|
|
8583
|
+
const tree = new Map(secretTreeFromSchema(this.globalSettingsSchema()));
|
|
8584
|
+
for (const { schema } of this.loadedScopedSchemas()) for (const [key, node] of secretTreeFromSchema(schema)) tree.set(key, node);
|
|
8585
|
+
this._addonSecretTree = mergeSecretStatePaths(tree, this.secretStatePaths);
|
|
8586
|
+
}
|
|
8474
8587
|
return this._addonSecretTree;
|
|
8475
8588
|
}
|
|
8476
8589
|
deviceSecretTree() {
|
|
@@ -8720,6 +8833,23 @@ function dropPerNodeFields(fields) {
|
|
|
8720
8833
|
}
|
|
8721
8834
|
return kept;
|
|
8722
8835
|
}
|
|
8836
|
+
/** The keys of the value-holding fields, recursing into layout containers. */
|
|
8837
|
+
function collectValueFieldKeys(fields) {
|
|
8838
|
+
const collected = [];
|
|
8839
|
+
for (const field of fields) {
|
|
8840
|
+
if (field.type === "group") {
|
|
8841
|
+
collected.push(...collectValueFieldKeys(field.fields));
|
|
8842
|
+
continue;
|
|
8843
|
+
}
|
|
8844
|
+
if (field.type === "sub-tabs") {
|
|
8845
|
+
for (const tab of field.tabs) collected.push(...collectValueFieldKeys(tab.fields));
|
|
8846
|
+
continue;
|
|
8847
|
+
}
|
|
8848
|
+
if (field.type === "separator" || field.type === "info") continue;
|
|
8849
|
+
if ("key" in field && typeof field.key === "string") collected.push(field.key);
|
|
8850
|
+
}
|
|
8851
|
+
return collected;
|
|
8852
|
+
}
|
|
8723
8853
|
function collectFieldKeys(fields) {
|
|
8724
8854
|
const collected = [];
|
|
8725
8855
|
for (const field of fields) {
|
|
@@ -8821,6 +8951,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8821
8951
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8822
8952
|
* (`auth.accessToken`, `headers[].value`).
|
|
8823
8953
|
*
|
|
8954
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8955
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8956
|
+
* key releases exactly that key's secret.
|
|
8957
|
+
*
|
|
8824
8958
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8825
8959
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8826
8960
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8833,11 +8967,13 @@ function parseSecretPath(path) {
|
|
|
8833
8967
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8834
8968
|
return path.split(".").map((raw) => {
|
|
8835
8969
|
const each = raw.endsWith("[]");
|
|
8836
|
-
const
|
|
8970
|
+
const map = raw.endsWith("{}");
|
|
8971
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8837
8972
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8838
8973
|
return {
|
|
8839
8974
|
key,
|
|
8840
|
-
each
|
|
8975
|
+
each,
|
|
8976
|
+
map
|
|
8841
8977
|
};
|
|
8842
8978
|
});
|
|
8843
8979
|
}
|
|
@@ -8889,6 +9025,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8889
9025
|
[segment.key]: mapped
|
|
8890
9026
|
};
|
|
8891
9027
|
}
|
|
9028
|
+
if (segment.map) {
|
|
9029
|
+
if (!isRecord$4(child)) return node;
|
|
9030
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
9031
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
9032
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
9033
|
+
return [mapKey, visit({
|
|
9034
|
+
declared,
|
|
9035
|
+
concrete,
|
|
9036
|
+
value
|
|
9037
|
+
})];
|
|
9038
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
9039
|
+
return {
|
|
9040
|
+
...node,
|
|
9041
|
+
[segment.key]: Object.fromEntries(kept)
|
|
9042
|
+
};
|
|
9043
|
+
}
|
|
8892
9044
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8893
9045
|
if (last) {
|
|
8894
9046
|
const replacement = visit({
|
|
@@ -9030,7 +9182,8 @@ function isZodSecret(schema) {
|
|
|
9030
9182
|
}
|
|
9031
9183
|
/**
|
|
9032
9184
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9033
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9185
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9186
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9034
9187
|
* object or array is reported as one path and not descended into — everything
|
|
9035
9188
|
* under it is secret.
|
|
9036
9189
|
*/
|
|
@@ -9041,6 +9194,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9041
9194
|
const def = defOf(layer);
|
|
9042
9195
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9043
9196
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9197
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9044
9198
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9045
9199
|
else if (def.type === "intersection") {
|
|
9046
9200
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -9222,6 +9376,7 @@ function defineEntryKind(name, spec) {
|
|
|
9222
9376
|
class: "config",
|
|
9223
9377
|
writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
|
|
9224
9378
|
retention: { none: "an entry goes only when the operator deletes it" },
|
|
9379
|
+
...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
|
|
9225
9380
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
9226
9381
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
9227
9382
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
@@ -11901,7 +12056,7 @@ function userError(code, params, englishFallback) {
|
|
|
11901
12056
|
return new CamstackError(code, params, englishFallback);
|
|
11902
12057
|
}
|
|
11903
12058
|
//#endregion
|
|
11904
|
-
//#region ../types/dist/composition-
|
|
12059
|
+
//#region ../types/dist/composition-pOHbTzRB.mjs
|
|
11905
12060
|
/**
|
|
11906
12061
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11907
12062
|
*
|
|
@@ -13365,11 +13520,49 @@ function buildCapturingReply(envelope) {
|
|
|
13365
13520
|
};
|
|
13366
13521
|
return wrapper;
|
|
13367
13522
|
}
|
|
13523
|
+
/**
|
|
13524
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13525
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13526
|
+
*
|
|
13527
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13528
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13529
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13530
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13531
|
+
*
|
|
13532
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13533
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13534
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13535
|
+
*/
|
|
13368
13536
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13369
13537
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13370
13538
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13371
13539
|
function topLevelSecretKeys(paths) {
|
|
13372
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13540
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13541
|
+
}
|
|
13542
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13543
|
+
function secretMapKeys(paths) {
|
|
13544
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13545
|
+
}
|
|
13546
|
+
/**
|
|
13547
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13548
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13549
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13550
|
+
*/
|
|
13551
|
+
function secretMapPatch(value) {
|
|
13552
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13553
|
+
const out = {};
|
|
13554
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13555
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13556
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13557
|
+
}
|
|
13558
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13559
|
+
function declaresEnabled(kind) {
|
|
13560
|
+
const json = toJSONSchema(kind.schema, {
|
|
13561
|
+
unrepresentable: "any",
|
|
13562
|
+
io: "input"
|
|
13563
|
+
});
|
|
13564
|
+
const properties = Reflect.get(json, "properties");
|
|
13565
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13373
13566
|
}
|
|
13374
13567
|
var RecordIntegrationInstances = class {
|
|
13375
13568
|
records;
|
|
@@ -13377,14 +13570,20 @@ var RecordIntegrationInstances = class {
|
|
|
13377
13570
|
hooks;
|
|
13378
13571
|
handle;
|
|
13379
13572
|
secretKeys;
|
|
13573
|
+
secretMaps;
|
|
13380
13574
|
nameField;
|
|
13575
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13576
|
+
registryOwnsEnabled;
|
|
13381
13577
|
constructor(records, kind, hooks) {
|
|
13382
13578
|
this.records = records;
|
|
13383
13579
|
this.kind = kind;
|
|
13384
13580
|
this.hooks = hooks;
|
|
13385
13581
|
this.handle = records.kind(kind);
|
|
13386
13582
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13583
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13387
13584
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13585
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13586
|
+
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"`);
|
|
13388
13587
|
}
|
|
13389
13588
|
/**
|
|
13390
13589
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13435,7 +13634,7 @@ var RecordIntegrationInstances = class {
|
|
|
13435
13634
|
id: record.id,
|
|
13436
13635
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13437
13636
|
configured: this.hooks.configured(record.data),
|
|
13438
|
-
enabled: record.data.enabled !== false,
|
|
13637
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13439
13638
|
formValues: shown,
|
|
13440
13639
|
...kind !== void 0 ? { kind } : {},
|
|
13441
13640
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13455,7 +13654,7 @@ var RecordIntegrationInstances = class {
|
|
|
13455
13654
|
return record === null ? null : this.viewOf(record);
|
|
13456
13655
|
};
|
|
13457
13656
|
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13458
|
-
createInstance = async ({ id, name, kind }) => {
|
|
13657
|
+
createInstance = async ({ id, name, kind, values }) => {
|
|
13459
13658
|
const minted = id ?? this.hooks.mintId?.({
|
|
13460
13659
|
name,
|
|
13461
13660
|
...kind !== void 0 ? { kind } : {}
|
|
@@ -13465,32 +13664,43 @@ var RecordIntegrationInstances = class {
|
|
|
13465
13664
|
id: minted,
|
|
13466
13665
|
data: this.hooks.initialBody(name, kind)
|
|
13467
13666
|
}, { ifRev: 0 });
|
|
13468
|
-
|
|
13667
|
+
const patch = this.patchOf(values ?? {});
|
|
13668
|
+
if (Object.keys(patch).length > 0) await this.handle.patch(minted, patch);
|
|
13669
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13469
13670
|
return { id: minted };
|
|
13470
13671
|
};
|
|
13471
13672
|
updateInstance = async ({ id, name, values }) => {
|
|
13673
|
+
const patch = this.patchOf(values);
|
|
13674
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13675
|
+
await this.handle.patch(id, patch);
|
|
13676
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13677
|
+
return { ok: true };
|
|
13678
|
+
};
|
|
13679
|
+
/** The record patch a form `values` object means: secrets kept, cleared or replaced. */
|
|
13680
|
+
patchOf(values) {
|
|
13472
13681
|
const patch = {};
|
|
13473
13682
|
for (const [key, value] of Object.entries(values)) {
|
|
13474
13683
|
if (value === void 0) continue;
|
|
13475
|
-
if (
|
|
13684
|
+
if (this.secretMaps.has(key)) {
|
|
13685
|
+
const entries = secretMapPatch(value);
|
|
13686
|
+
if (entries !== null) patch[key] = entries;
|
|
13687
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13476
13688
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13477
13689
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13478
13690
|
}
|
|
13479
|
-
|
|
13480
|
-
|
|
13481
|
-
await this.hooks.onChanged?.(id);
|
|
13482
|
-
return { ok: true };
|
|
13483
|
-
};
|
|
13691
|
+
return patch;
|
|
13692
|
+
}
|
|
13484
13693
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13485
13694
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13695
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13486
13696
|
await this.handle.patch(id, { enabled });
|
|
13487
|
-
await this.hooks.onChanged?.(id);
|
|
13697
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13488
13698
|
return { ok: true };
|
|
13489
13699
|
};
|
|
13490
13700
|
/** The row and its keychain refs go together (the records door). */
|
|
13491
13701
|
deleteInstance = async ({ id }) => {
|
|
13492
13702
|
await this.handle.delete(id);
|
|
13493
|
-
await this.hooks.onChanged?.(id);
|
|
13703
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13494
13704
|
return { ok: true };
|
|
13495
13705
|
};
|
|
13496
13706
|
testInstance = async (input) => {
|
|
@@ -14561,7 +14771,13 @@ var MigrationLedgerEntrySchema = object({
|
|
|
14561
14771
|
* joined by the engine from the catalogues it knows this boot. Absent when no
|
|
14562
14772
|
* catalogue lists the id (a newer database).
|
|
14563
14773
|
*/
|
|
14564
|
-
meta: MigrationMetaSchema.optional()
|
|
14774
|
+
meta: MigrationMetaSchema.optional(),
|
|
14775
|
+
/**
|
|
14776
|
+
* Set when this is an applied row of an addon owner whose catalogue the answering
|
|
14777
|
+
* process has not loaded (the owner has not declared yet): the row is applied, the
|
|
14778
|
+
* process just cannot judge it. Never `skipped-newer` — that needs the owner loaded.
|
|
14779
|
+
*/
|
|
14780
|
+
ownerNotLoaded: boolean().optional()
|
|
14565
14781
|
});
|
|
14566
14782
|
/** A catalogued migration that is not applied, with where it stands. */
|
|
14567
14783
|
var MigrationPendingEntrySchema = object({
|
|
@@ -15682,6 +15898,39 @@ var UnstampedEventMediaCountSchema = object({
|
|
|
15682
15898
|
/** Sum across both, or `null` when either lane could not be counted. */
|
|
15683
15899
|
total: number().int().nonnegative().nullable()
|
|
15684
15900
|
}).nullable();
|
|
15901
|
+
/**
|
|
15902
|
+
* Where the stored event media sits, per storage location: how many media rows
|
|
15903
|
+
* and how many bytes carry each location id, how many carry none (legacy,
|
|
15904
|
+
* resolved to the default `eventMedia` location), and how many name a location
|
|
15905
|
+
* that was not asked about (a deleted or never-listed one).
|
|
15906
|
+
*
|
|
15907
|
+
* One covering-index aggregate per asked location plus the NULL one, and one
|
|
15908
|
+
* unfiltered total that scans the covering index: O(rows), not a walk of the
|
|
15909
|
+
* rows themselves. `unlisted` is total minus the ASKED locations minus NULL, so
|
|
15910
|
+
* it is a deleted location only when every location id was asked.
|
|
15911
|
+
* `null` is "could not be measured", never zero.
|
|
15912
|
+
*/
|
|
15913
|
+
var MediaLocationCensusInputSchema = object({ locationIds: array(string().min(1)).max(50).readonly() });
|
|
15914
|
+
var MediaLocationCensusSchema = object({
|
|
15915
|
+
locations: array(object({
|
|
15916
|
+
locationId: string(),
|
|
15917
|
+
rows: number().int().nonnegative(),
|
|
15918
|
+
bytes: number().nonnegative()
|
|
15919
|
+
})).readonly(),
|
|
15920
|
+
unstamped: object({
|
|
15921
|
+
rows: number().int().nonnegative(),
|
|
15922
|
+
bytes: number().nonnegative()
|
|
15923
|
+
}),
|
|
15924
|
+
/** Rows stamped with a location id that was not in the asked list. */
|
|
15925
|
+
unlisted: object({
|
|
15926
|
+
rows: number().int().nonnegative(),
|
|
15927
|
+
bytes: number().nonnegative()
|
|
15928
|
+
}),
|
|
15929
|
+
total: object({
|
|
15930
|
+
rows: number().int().nonnegative(),
|
|
15931
|
+
bytes: number().nonnegative()
|
|
15932
|
+
})
|
|
15933
|
+
});
|
|
15685
15934
|
var StorageMigrationMediaMoveInputSchema = RelocateMediaInputSchema.extend({ leaseId: string().min(1) });
|
|
15686
15935
|
/** The independently selectable logical storage classes — every class
|
|
15687
15936
|
* `storage.listLocationDeclarations` reports, so an operator never meets a
|
|
@@ -46221,52 +46470,7 @@ DeviceType.Light, method(object({
|
|
|
46221
46470
|
level: number().min(0).max(100),
|
|
46222
46471
|
lastChangedAt: number()
|
|
46223
46472
|
});
|
|
46224
|
-
|
|
46225
|
-
* `broker` — unified pub/sub broker registry, system-scoped collection.
|
|
46226
|
-
*
|
|
46227
|
-
* The cap models any kind-tagged message broker the user wants to
|
|
46228
|
-
* register and that other addons might consume. The first two kinds
|
|
46229
|
-
* are MQTT (mosquitto / aedes embedded / cloud bridge) and
|
|
46230
|
-
* Home Assistant (HA WebSocket — subscribe_entities + call_service).
|
|
46231
|
-
* Future kinds (Zigbee2MQTT bridge, ZHA, KNX, Telegram, …) plug in
|
|
46232
|
-
* the same surface.
|
|
46233
|
-
*
|
|
46234
|
-
* Why one cap, not one cap per kind:
|
|
46235
|
-
* - The integrations page wants a single table of "every broker"
|
|
46236
|
-
* across kinds; a unified cap drives it without join logic.
|
|
46237
|
-
* - Generic admin operations (add / remove / test / status) are
|
|
46238
|
-
* identical across kinds — duplicating them per cap was busywork.
|
|
46239
|
-
* - The `network-access` cap precedent: same interface, many
|
|
46240
|
-
* `providerKind: 'ingress'` implementations (Tailscale, ngrok, …).
|
|
46241
|
-
*
|
|
46242
|
-
* What stays kind-specific:
|
|
46243
|
-
* - `getBrokerConfig` payload (kind decides whether it returns
|
|
46244
|
-
* mqtt URL+creds, HA baseUrl+token, …) — typed as
|
|
46245
|
-
* `Record<string, unknown>` at the wire; consumers narrow.
|
|
46246
|
-
* - `publish` / `subscribe` / `getState` arguments (target / filter /
|
|
46247
|
-
* key) — also typed as `Record<string, unknown>` so each kind can
|
|
46248
|
-
* evolve its surface (MQTT topic+qos vs HA entity_id+domain)
|
|
46249
|
-
* without rebreaking the cap signature.
|
|
46250
|
-
* - `add` `settings` payload (different fields per kind, validated by
|
|
46251
|
-
* the kind-specific provider).
|
|
46252
|
-
*
|
|
46253
|
-
* Bidirectionality:
|
|
46254
|
-
* - `publish` is RPC — synchronous result (provider-defined).
|
|
46255
|
-
* - `subscribe` returns a subscription handle; the actual message
|
|
46256
|
-
* stream flows over the typed event-bus as `broker.message` events
|
|
46257
|
-
* keyed by `(brokerId, subscriptionId)`. Consumers filter in their
|
|
46258
|
-
* event handler. Subscriptions persist across reconnects — the
|
|
46259
|
-
* provider re-subscribes upstream after a transport drop.
|
|
46260
|
-
*
|
|
46261
|
-
* Why event-bus push (not a subscription RPC stream):
|
|
46262
|
-
* - Matches the rest of CamStack's D8 contract — events for telemetry,
|
|
46263
|
-
* RPC for loss-is-a-bug. Broker messages are telemetry (a single
|
|
46264
|
-
* drop is recoverable via `getState({key})`).
|
|
46265
|
-
* - Frees `broker.subscribe` from holding a long-lived RPC channel.
|
|
46266
|
-
* - Cross-process: events already route through `$event-bus`; no
|
|
46267
|
-
* per-broker wire-shaping needed.
|
|
46268
|
-
*/
|
|
46269
|
-
var BrokerStatusEnum = _enum([
|
|
46473
|
+
_enum([
|
|
46270
46474
|
"connected",
|
|
46271
46475
|
"disconnected",
|
|
46272
46476
|
"connecting",
|
|
@@ -46274,141 +46478,12 @@ var BrokerStatusEnum = _enum([
|
|
|
46274
46478
|
"unreachable",
|
|
46275
46479
|
"error"
|
|
46276
46480
|
]);
|
|
46277
|
-
var BrokerInfoSchema = object({
|
|
46278
|
-
/** Stable broker id. Persisted; survives addon restarts. */
|
|
46279
|
-
id: string(),
|
|
46280
|
-
/** Addon id of the provider that OWNS this broker.
|
|
46281
|
-
*
|
|
46282
|
-
* The `broker` cap is a system-scoped collection: several addons
|
|
46283
|
-
* register a `broker` provider, each owning a DISJOINT set of
|
|
46284
|
-
* brokers (mqtt-broker owns `mqtt_*`, provider-homeassistant owns
|
|
46285
|
-
* `ha_*`). A broker therefore belongs to exactly one addon — like a
|
|
46286
|
-
* device belongs to one integration. The admin UI threads this id
|
|
46287
|
-
* back as the `{ addonId }` system-collection selector on every
|
|
46288
|
-
* id-keyed call (`get` / `getSettings` / `setSettings` / `remove` /
|
|
46289
|
-
* `testConnection`), so the call routes to the OWNING provider
|
|
46290
|
-
* instead of defaulting to the first-registered one. */
|
|
46291
|
-
addonId: string(),
|
|
46292
|
-
/** Human-readable name (operator-chosen at add-time). */
|
|
46293
|
-
name: string(),
|
|
46294
|
-
/** Provider-defined kind tag — `mqtt` / `home-assistant` / future. */
|
|
46295
|
-
kind: string(),
|
|
46296
|
-
status: BrokerStatusEnum,
|
|
46297
|
-
/** Free-form provider-specific info (HA version, MQTT broker
|
|
46298
|
-
* flavour, latency, mTLS active, …). */
|
|
46299
|
-
info: record(string(), unknown()),
|
|
46300
|
-
/** Ms epoch of the last connection probe / status update. */
|
|
46301
|
-
lastCheckedAt: number().nullable(),
|
|
46302
|
-
/** Last error message, when `status` indicates failure. */
|
|
46303
|
-
error: string().nullable()
|
|
46304
|
-
});
|
|
46305
46481
|
var RegistryStatusSchema = object({
|
|
46306
46482
|
brokerCount: number().int().nonnegative(),
|
|
46307
46483
|
connectedCount: number().int().nonnegative()
|
|
46308
46484
|
});
|
|
46309
|
-
/**
|
|
46310
|
-
* One entry per `broker` provider — which addon provides which broker
|
|
46311
|
-
* kind(s). The unified create picker reads this to know, for each
|
|
46312
|
-
* (addonId, kind) pair the operator can pick, where to route the
|
|
46313
|
-
* follow-up `getSettingsSchema` / `testSettings` / `add` calls (via the
|
|
46314
|
-
* `{ addonId }` selector). A provider only advertises a kind here when
|
|
46315
|
-
* it implements `getSettingsSchema` for that kind (so the picker never
|
|
46316
|
-
* offers a kind whose creation form can't be rendered).
|
|
46317
|
-
*/
|
|
46318
|
-
var BrokerProviderInfoSchema = object({
|
|
46319
|
-
/** Addon id of the `broker` provider this entry describes. */
|
|
46320
|
-
addonId: string(),
|
|
46321
|
-
/** Broker kinds this provider can create, with a display label. */
|
|
46322
|
-
kinds: array(object({
|
|
46323
|
-
kind: string(),
|
|
46324
|
-
label: string(),
|
|
46325
|
-
/**
|
|
46326
|
-
* Relative hub path to start an interactive account link (OAuth popup).
|
|
46327
|
-
* May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
|
|
46328
|
-
*/
|
|
46329
|
-
interactiveConnectPath: string().min(1).optional(),
|
|
46330
|
-
/** Relative hub path to disconnect the linked account (same popup pattern). */
|
|
46331
|
-
interactiveDisconnectPath: string().min(1).optional(),
|
|
46332
|
-
/**
|
|
46333
|
-
* The kind signs in through a server-held interactive session instead of
|
|
46334
|
-
* (or before) typed credentials. `basePath` is a hub-relative route set
|
|
46335
|
-
* served by the owning addon (`/addon/<id>/...`) speaking the sign-in
|
|
46336
|
-
* contract: `POST {basePath}/start` (body: the form's settings) answers
|
|
46337
|
-
* `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
|
|
46338
|
-
* `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
|
|
46339
|
-
* 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
|
|
46340
|
-
* (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
|
|
46341
|
-
* existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
|
|
46342
|
-
* (`{ sessionId }`) drops the session. The dialog that edits a broker
|
|
46343
|
-
* offers it as the default path; the fields the session fills are
|
|
46344
|
-
* marked `filledBySignIn` in the kind's settings schema.
|
|
46345
|
-
*
|
|
46346
|
-
* `methods` lists the ways the session can sign in, recommended first:
|
|
46347
|
-
* `'device-code'` adds `POST {basePath}/code/start` (body: the form's
|
|
46348
|
-
* settings) answering `{ sessionId, userCode, verificationUrl,
|
|
46349
|
-
* expiresAt }` — the operator types `userCode` at `verificationUrl` on
|
|
46350
|
-
* any device, and the same `status` / `commit` / `cancel` routes follow
|
|
46351
|
-
* (status then carries `method`, `userCode`, `verificationUrl`,
|
|
46352
|
-
* `expiresAt` and the phases `'awaiting-code'` / `'expired'`);
|
|
46353
|
-
* `'proxy'` is the `{basePath}/start` flow above. Absent means
|
|
46354
|
-
* `['proxy']`.
|
|
46355
|
-
*/
|
|
46356
|
-
signIn: object({
|
|
46357
|
-
basePath: string().min(1),
|
|
46358
|
-
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
46359
|
-
}).optional()
|
|
46360
|
-
}))
|
|
46361
|
-
});
|
|
46362
|
-
var ListInputSchema = object({
|
|
46363
|
-
/** Optional kind filter — `list({kind:'home-assistant'})` returns
|
|
46364
|
-
* only HA brokers. Omit to list every broker. */
|
|
46365
|
-
kind: string().optional() });
|
|
46366
46485
|
var GetInputSchema = object({ id: string() });
|
|
46367
|
-
var AddInputSchema = object({
|
|
46368
|
-
kind: string().min(1),
|
|
46369
|
-
name: string().min(1),
|
|
46370
|
-
/**
|
|
46371
|
-
* ADOPT an existing id instead of minting a new one.
|
|
46372
|
-
*
|
|
46373
|
-
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
46374
|
-
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
46375
|
-
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
46376
|
-
* bound to a broker that no longer exists. Re-entering the password under the
|
|
46377
|
-
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
46378
|
-
* password.
|
|
46379
|
-
*
|
|
46380
|
-
* A provider MUST refuse an id that is already in use — adopting a live
|
|
46381
|
-
* broker's id would silently take it over.
|
|
46382
|
-
*/
|
|
46383
|
-
id: string().min(1).optional(),
|
|
46384
|
-
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
46385
|
-
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
46386
|
-
* branch on receipt — invalid shape rejects the add. */
|
|
46387
|
-
settings: record(string(), unknown())
|
|
46388
|
-
});
|
|
46389
|
-
var AddResultSchema = object({ id: string() });
|
|
46390
|
-
var RemoveInputSchema = object({ id: string() });
|
|
46391
|
-
var TestConnectionResultSchema = discriminatedUnion("ok", [object({
|
|
46392
|
-
ok: literal(true),
|
|
46393
|
-
latencyMs: number().nonnegative()
|
|
46394
|
-
}), object({
|
|
46395
|
-
ok: literal(false),
|
|
46396
|
-
error: string()
|
|
46397
|
-
})]);
|
|
46398
46486
|
var SettingsRecordSchema$1 = record(string(), unknown());
|
|
46399
|
-
var SettingsSchemaInputSchema = object({ kind: string() });
|
|
46400
|
-
var TestSettingsInputSchema = object({
|
|
46401
|
-
kind: string(),
|
|
46402
|
-
settings: SettingsRecordSchema$1
|
|
46403
|
-
});
|
|
46404
|
-
var TestSettingsResultSchema = discriminatedUnion("ok", [object({
|
|
46405
|
-
ok: literal(true),
|
|
46406
|
-
latencyMs: number().nonnegative().optional()
|
|
46407
|
-
}).strict(), object({
|
|
46408
|
-
ok: literal(false),
|
|
46409
|
-
error: string()
|
|
46410
|
-
})]);
|
|
46411
|
-
var SettingsSchemaResultSchema = unknown().nullable();
|
|
46412
46487
|
var PublishInputSchema = object({
|
|
46413
46488
|
brokerId: string(),
|
|
46414
46489
|
/** Kind-specific routing target.
|
|
@@ -46452,51 +46527,12 @@ var brokerCapability = {
|
|
|
46452
46527
|
kind: "poll"
|
|
46453
46528
|
},
|
|
46454
46529
|
methods: {
|
|
46455
|
-
list: method(ListInputSchema, array(BrokerInfoSchema)),
|
|
46456
|
-
get: method(GetInputSchema, BrokerInfoSchema.nullable()),
|
|
46457
|
-
/** Enumerate which addon provides which broker kind(s) for the
|
|
46458
|
-
* unified create picker. The auto-mount fans this array across
|
|
46459
|
-
* every registered `broker` provider (array-output method), so the
|
|
46460
|
-
* picker sees every kind from every provider in one call. */
|
|
46461
|
-
listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
|
|
46462
|
-
add: method(AddInputSchema, AddResultSchema, {
|
|
46463
|
-
kind: "mutation",
|
|
46464
|
-
auth: "admin"
|
|
46465
|
-
}),
|
|
46466
|
-
remove: method(RemoveInputSchema, _void(), {
|
|
46467
|
-
kind: "mutation",
|
|
46468
|
-
auth: "admin"
|
|
46469
|
-
}),
|
|
46470
|
-
testConnection: method(GetInputSchema, TestConnectionResultSchema, {
|
|
46471
|
-
kind: "mutation",
|
|
46472
|
-
auth: "admin"
|
|
46473
|
-
}),
|
|
46474
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
46475
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
46476
|
-
* when the broker id is unknown to the provider (the collection
|
|
46477
|
-
* fallback may route a foreign id to the first provider). */
|
|
46478
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46479
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
46480
|
-
* provider validates the shape and applies the change (reconnects
|
|
46481
|
-
* if credentials changed). */
|
|
46482
|
-
setSettings: method(object({
|
|
46483
|
-
id: string(),
|
|
46484
|
-
settings: SettingsRecordSchema$1
|
|
46485
|
-
}), _void(), {
|
|
46486
|
-
kind: "mutation",
|
|
46487
|
-
auth: "admin"
|
|
46488
|
-
}),
|
|
46489
46530
|
/** Returns the kind-specific connection config the consumer needs
|
|
46490
46531
|
* to open its own client (MQTT pattern: `{url, username, password,
|
|
46491
46532
|
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
46492
46533
|
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
46493
46534
|
* Returns `null` when the broker id is unknown to the provider. */
|
|
46494
46535
|
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
46495
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
46496
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
46497
|
-
kind: "mutation",
|
|
46498
|
-
auth: "admin"
|
|
46499
|
-
}),
|
|
46500
46536
|
publish: method(PublishInputSchema, unknown(), {
|
|
46501
46537
|
kind: "mutation",
|
|
46502
46538
|
auth: "admin"
|
|
@@ -49130,6 +49166,9 @@ var RunningProposalEvidenceSchema = object({
|
|
|
49130
49166
|
runsFound: number().int().nonnegative(),
|
|
49131
49167
|
pausesFound: number().int().nonnegative(),
|
|
49132
49168
|
longestPauseMs: number().nonnegative(),
|
|
49169
|
+
/** Episodes shorter than ~90 s (an anti-crease pulse, a display waking): never a run, never a pause. */
|
|
49170
|
+
pulsesFound: number().int().nonnegative(),
|
|
49171
|
+
longestPulseMs: number().nonnegative(),
|
|
49133
49172
|
idleFloorW: number(),
|
|
49134
49173
|
noiseCeilingW: number(),
|
|
49135
49174
|
plateauW: number(),
|
|
@@ -50576,7 +50615,14 @@ var DiscoveryCandidateSchema = object({
|
|
|
50576
50615
|
/** `stableId` of the gateway candidate this device was found BEHIND. */
|
|
50577
50616
|
parentStableId: string().optional(),
|
|
50578
50617
|
/** `false` = shown for context but not pickable. Default true. */
|
|
50579
|
-
selectable: boolean().optional()
|
|
50618
|
+
selectable: boolean().optional(),
|
|
50619
|
+
/**
|
|
50620
|
+
* Keys of the provider's creation form (`getChildCreationSchema`) this candidate
|
|
50621
|
+
* cannot be adopted without — a key for an encrypted board, a pairing code. The
|
|
50622
|
+
* admin asks for exactly these, on the creation form prefilled with
|
|
50623
|
+
* `prefilledConfig`, before it adopts. Absent or empty = adopt as it is.
|
|
50624
|
+
*/
|
|
50625
|
+
needsParams: array(string()).optional()
|
|
50580
50626
|
});
|
|
50581
50627
|
/**
|
|
50582
50628
|
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
@@ -50635,12 +50681,24 @@ method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind:
|
|
|
50635
50681
|
}), array(DiscoveryCandidateSchema), {
|
|
50636
50682
|
kind: "mutation",
|
|
50637
50683
|
auth: "admin"
|
|
50638
|
-
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50684
|
+
}), method(object({}), CreationSchemaOutputSchema), method(object({
|
|
50685
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50686
|
+
integrationId: string().optional() }), object({ deviceType: _enum(DeviceType).nullable() })), method(object({
|
|
50687
|
+
candidate: DiscoveryCandidateSchema,
|
|
50688
|
+
/** The integration the operator adopts into; a provider with several instances needs it. */
|
|
50689
|
+
integrationId: string().optional()
|
|
50690
|
+
}), DeviceSummarySchema, {
|
|
50639
50691
|
kind: "mutation",
|
|
50640
50692
|
auth: "admin"
|
|
50641
|
-
}), method(object({}), boolean()), method(object({
|
|
50693
|
+
}), method(object({}), boolean()), method(object({
|
|
50642
50694
|
type: _enum(DeviceType),
|
|
50643
|
-
|
|
50695
|
+
/** The integration asked about; a provider whose integrations differ answers per one. */
|
|
50696
|
+
integrationId: string().optional()
|
|
50697
|
+
}), CreationSchemaOutputSchema), method(object({
|
|
50698
|
+
type: _enum(DeviceType),
|
|
50699
|
+
config: record(string(), unknown()),
|
|
50700
|
+
/** The integration the operator adds the device to; a provider with several instances needs it. */
|
|
50701
|
+
integrationId: string().optional()
|
|
50644
50702
|
}), DeviceSummarySchema, {
|
|
50645
50703
|
kind: "mutation",
|
|
50646
50704
|
auth: "admin"
|
|
@@ -51350,7 +51408,10 @@ method(object({
|
|
|
51350
51408
|
auth: "admin"
|
|
51351
51409
|
}), method(object({
|
|
51352
51410
|
addonId: string(),
|
|
51353
|
-
type: _enum(DeviceType)
|
|
51411
|
+
type: _enum(DeviceType),
|
|
51412
|
+
/** The integration the form is for; a provider whose integrations differ (a hub
|
|
51413
|
+
* integration takes no manual add) answers per integration. Absent = the addon's own. */
|
|
51414
|
+
integrationId: string().optional()
|
|
51354
51415
|
}), unknown().nullable()), method(object({
|
|
51355
51416
|
addonId: string(),
|
|
51356
51417
|
type: _enum(DeviceType),
|
|
@@ -51434,7 +51495,11 @@ method(object({
|
|
|
51434
51495
|
kind: "mutation",
|
|
51435
51496
|
auth: "admin",
|
|
51436
51497
|
timeoutMs: 12e4
|
|
51437
|
-
}), method(object({
|
|
51498
|
+
}), method(object({
|
|
51499
|
+
addonId: string(),
|
|
51500
|
+
/** The integration the Add-Device dialog is open on; absent = the addon's own. */
|
|
51501
|
+
integrationId: string().optional()
|
|
51502
|
+
}), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
|
|
51438
51503
|
deviceId: number(),
|
|
51439
51504
|
key: string(),
|
|
51440
51505
|
value: unknown()
|
|
@@ -54030,7 +54095,7 @@ var TestConnectionStatusEnum = _enum([
|
|
|
54030
54095
|
"inconclusive",
|
|
54031
54096
|
"unsupported"
|
|
54032
54097
|
]);
|
|
54033
|
-
var TestConnectionResultSchema
|
|
54098
|
+
var TestConnectionResultSchema = object({
|
|
54034
54099
|
/** True ONLY for `validated`. Never true for a test that did not run. */
|
|
54035
54100
|
success: boolean(),
|
|
54036
54101
|
error: string().optional(),
|
|
@@ -54051,11 +54116,7 @@ var CreateIntegrationInputSchema = object({
|
|
|
54051
54116
|
info: record(string(), unknown()).optional(),
|
|
54052
54117
|
/** `stableId` of the discovery candidate the form was seeded from (D984). */
|
|
54053
54118
|
originStableId: string().optional(),
|
|
54054
|
-
settings: record(string(), unknown()).optional()
|
|
54055
|
-
/** Keys of `settings` the form's schema declares secret (by type) — kept in
|
|
54056
|
-
* the keychain, not the table (D787). */
|
|
54057
|
-
secretKeys: array(string()).optional(),
|
|
54058
|
-
skipRestart: boolean().optional()
|
|
54119
|
+
settings: record(string(), unknown()).optional()
|
|
54059
54120
|
});
|
|
54060
54121
|
var UpdateIntegrationInputSchema = object({
|
|
54061
54122
|
id: string(),
|
|
@@ -54076,20 +54137,10 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
54076
54137
|
}), method(object({ id: string() }), DeleteIntegrationResultSchema, {
|
|
54077
54138
|
kind: "mutation",
|
|
54078
54139
|
auth: "admin"
|
|
54079
|
-
}), method(object({ id: string() }), record(string(), unknown()), { auth: "admin" }), method(object({
|
|
54080
|
-
id: string(),
|
|
54081
|
-
settings: record(string(), unknown()),
|
|
54082
|
-
/** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
|
|
54083
|
-
secretKeys: array(string()).optional(),
|
|
54084
|
-
/** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
|
|
54085
|
-
skipRestart: boolean().optional()
|
|
54086
|
-
}), object({ success: boolean() }), {
|
|
54087
|
-
kind: "mutation",
|
|
54088
|
-
auth: "admin"
|
|
54089
54140
|
}), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
|
|
54090
54141
|
addonId: string(),
|
|
54091
54142
|
settings: record(string(), unknown())
|
|
54092
|
-
}), TestConnectionResultSchema
|
|
54143
|
+
}), TestConnectionResultSchema, {
|
|
54093
54144
|
kind: "mutation",
|
|
54094
54145
|
auth: "admin"
|
|
54095
54146
|
});
|
|
@@ -54116,6 +54167,14 @@ var ConfigSchemaPassthrough$3 = custom();
|
|
|
54116
54167
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54117
54168
|
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54118
54169
|
/**
|
|
54170
|
+
* What the addon's instances ARE for the Integrations page (planning#416). `devices`: each
|
|
54171
|
+
* instance is an integration that brings devices (the registry owns its on/off switch);
|
|
54172
|
+
* `services`: a connection configured from the addon's own function page, never listed on
|
|
54173
|
+
* Integrations.
|
|
54174
|
+
* It is the same vocabulary as the manifest's `integrationRole`, which stays as an override.
|
|
54175
|
+
*/
|
|
54176
|
+
var IntegrationInstanceRoleSchema = _enum(["devices", "services"]);
|
|
54177
|
+
/**
|
|
54119
54178
|
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
54120
54179
|
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
54121
54180
|
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
@@ -54126,6 +54185,22 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54126
54185
|
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
54127
54186
|
id: string().min(1),
|
|
54128
54187
|
label: string().min(1),
|
|
54188
|
+
/**
|
|
54189
|
+
* The function capability this kind is configured for (`broker`, `smtp-provider`,
|
|
54190
|
+
* `device-provider`, ...). A page section is the kinds whose `functionCap` it names (D1013); a
|
|
54191
|
+
* provider with more than one section must name it on every kind.
|
|
54192
|
+
*/
|
|
54193
|
+
functionCap: string().min(1),
|
|
54194
|
+
/**
|
|
54195
|
+
* For `functionCap: 'broker'`: the registry kind of the broker an instance of this kind is
|
|
54196
|
+
* (`matter`, `home-assistant`, ...). A caller that needs a broker of one kind (a link field)
|
|
54197
|
+
* finds the instances whose kind names it; absent takes the kind `id`.
|
|
54198
|
+
*/
|
|
54199
|
+
brokerKind: string().min(1).optional(),
|
|
54200
|
+
/** What instances of this kind are for the Integrations page; absent takes the provider's `role`. */
|
|
54201
|
+
role: IntegrationInstanceRoleSchema.optional(),
|
|
54202
|
+
/** `testInstance` really probes the remote for this kind; absent takes the provider's `canTest`. */
|
|
54203
|
+
canTest: boolean().optional(),
|
|
54129
54204
|
/** The kind signs in through a server-held interactive session. */
|
|
54130
54205
|
signIn: object({
|
|
54131
54206
|
basePath: string().min(1),
|
|
@@ -54137,6 +54212,17 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54137
54212
|
disconnectPath: string().min(1).optional()
|
|
54138
54213
|
});
|
|
54139
54214
|
var IntegrationInstanceDescriptionSchema = object({
|
|
54215
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
54216
|
+
role: IntegrationInstanceRoleSchema,
|
|
54217
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
54218
|
+
discovery: object({
|
|
54219
|
+
mode: _enum([
|
|
54220
|
+
"none",
|
|
54221
|
+
"lan",
|
|
54222
|
+
"account"
|
|
54223
|
+
]),
|
|
54224
|
+
scopedToInstance: boolean()
|
|
54225
|
+
}).optional(),
|
|
54140
54226
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
54141
54227
|
instanceMode: IntegrationInstanceModeSchema,
|
|
54142
54228
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -54176,10 +54262,11 @@ var IntegrationInstanceSchema = object({
|
|
|
54176
54262
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
54177
54263
|
configured: boolean(),
|
|
54178
54264
|
/**
|
|
54179
|
-
* The instance's own on/off switch (D996)
|
|
54180
|
-
*
|
|
54265
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
54266
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
54267
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
54181
54268
|
*/
|
|
54182
|
-
enabled: boolean(),
|
|
54269
|
+
enabled: boolean().optional(),
|
|
54183
54270
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
54184
54271
|
kind: string().optional(),
|
|
54185
54272
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -54201,7 +54288,11 @@ var integrationInstancesCapability = {
|
|
|
54201
54288
|
methods: {
|
|
54202
54289
|
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
54203
54290
|
/** The form; `id: null` is the create form. */
|
|
54204
|
-
instanceFormSchema: method(object({
|
|
54291
|
+
instanceFormSchema: method(object({
|
|
54292
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
54293
|
+
/** The kind of the create form (`id: null`) of a provider with several. */
|
|
54294
|
+
kind: string().min(1).optional()
|
|
54295
|
+
}), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
54205
54296
|
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54206
54297
|
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54207
54298
|
/**
|
|
@@ -54211,7 +54302,9 @@ var integrationInstancesCapability = {
|
|
|
54211
54302
|
createInstance: method(object({
|
|
54212
54303
|
id: IntegrationInstanceIdSchema.optional(),
|
|
54213
54304
|
name: string().min(1),
|
|
54214
|
-
kind: string().min(1).optional()
|
|
54305
|
+
kind: string().min(1).optional(),
|
|
54306
|
+
/** Form values written with the record (a link picked in the create dialog). */
|
|
54307
|
+
values: record(string(), unknown()).optional()
|
|
54215
54308
|
}), object({ id: string() }), {
|
|
54216
54309
|
kind: "mutation",
|
|
54217
54310
|
auth: "admin"
|
|
@@ -54238,8 +54331,10 @@ var integrationInstancesCapability = {
|
|
|
54238
54331
|
}),
|
|
54239
54332
|
testInstance: optionalMethod(object({
|
|
54240
54333
|
id: IntegrationInstanceIdSchema.nullable(),
|
|
54241
|
-
values: record(string(), unknown())
|
|
54242
|
-
|
|
54334
|
+
values: record(string(), unknown()),
|
|
54335
|
+
/** The kind to test when `id` is null and the provider has several. */
|
|
54336
|
+
kind: string().min(1).optional()
|
|
54337
|
+
}), TestConnectionResultSchema, {
|
|
54243
54338
|
kind: "mutation",
|
|
54244
54339
|
auth: "admin"
|
|
54245
54340
|
})
|
|
@@ -56744,6 +56839,380 @@ var SystemMetricsSchema = object({
|
|
|
56744
56839
|
gpuMemoryPercent: number().optional()
|
|
56745
56840
|
});
|
|
56746
56841
|
method(_void(), SystemResourceSnapshotSchema), method(_void(), SystemResourceSnapshotSchema.nullable()), method(_void(), SystemMetricsSchema), method(object({ dirPath: string() }), DiskSpaceInfoSchema), method(_void(), MetricsGpuInfoSchema.nullable()), method(_void(), number().nullable()), method(object({ pids: array(number()) }), array(PidResourceStatsSchema)), method(_void(), array(AddonInstanceSchema).readonly()), method(object({ addonId: string() }), PidResourceStatsSchema.nullable()), method(_void(), array(NodeProcessSchema).readonly()), method(GetLoadSeriesInputSchema, NodeLoadSeriesSchema);
|
|
56842
|
+
/**
|
|
56843
|
+
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
56844
|
+
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
56845
|
+
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
56846
|
+
*
|
|
56847
|
+
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
56848
|
+
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
56849
|
+
* that with the addon id closed over). It is deliberately NOT in any input
|
|
56850
|
+
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
56851
|
+
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
56852
|
+
* addons go through the owner's capability.
|
|
56853
|
+
*
|
|
56854
|
+
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
56855
|
+
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
56856
|
+
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
56857
|
+
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
56858
|
+
* row's own ref.
|
|
56859
|
+
*
|
|
56860
|
+
* Internal: addons reach it through `ctx.records`, never directly.
|
|
56861
|
+
*/
|
|
56862
|
+
var RecordIdSchema = string().min(1).max(256);
|
|
56863
|
+
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
56864
|
+
var RecordBodySchema = record(string(), unknown());
|
|
56865
|
+
var RecordInputSchema = object({
|
|
56866
|
+
id: RecordIdSchema,
|
|
56867
|
+
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
56868
|
+
deviceId: number().int().optional(),
|
|
56869
|
+
/** The domain clock, ms: observed / created / opened. */
|
|
56870
|
+
at: number().int().optional(),
|
|
56871
|
+
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
56872
|
+
expiresAt: number().int().optional(),
|
|
56873
|
+
data: RecordBodySchema
|
|
56874
|
+
});
|
|
56875
|
+
var RecordViewSchema = object({
|
|
56876
|
+
id: string(),
|
|
56877
|
+
deviceId: number().int().nullable(),
|
|
56878
|
+
at: number().nullable(),
|
|
56879
|
+
expiresAt: number().nullable(),
|
|
56880
|
+
rev: number().int(),
|
|
56881
|
+
updatedAt: number(),
|
|
56882
|
+
data: RecordBodySchema
|
|
56883
|
+
});
|
|
56884
|
+
/** Declared index fields only. Anything else does not compile, by design. */
|
|
56885
|
+
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
56886
|
+
string(),
|
|
56887
|
+
number(),
|
|
56888
|
+
boolean(),
|
|
56889
|
+
object({ in: array(union([
|
|
56890
|
+
string(),
|
|
56891
|
+
number(),
|
|
56892
|
+
boolean()
|
|
56893
|
+
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
56894
|
+
object({
|
|
56895
|
+
gt: union([string(), number()]).optional(),
|
|
56896
|
+
gte: union([string(), number()]).optional(),
|
|
56897
|
+
lt: union([string(), number()]).optional(),
|
|
56898
|
+
lte: union([string(), number()]).optional()
|
|
56899
|
+
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
56900
|
+
]));
|
|
56901
|
+
var KindShape = { kind: RecordKindNameSchema };
|
|
56902
|
+
var TimeWindowShape = {
|
|
56903
|
+
deviceId: number().int().optional(),
|
|
56904
|
+
atFrom: number().optional(),
|
|
56905
|
+
atTo: number().optional()
|
|
56906
|
+
};
|
|
56907
|
+
var RecordQueryInputSchema = object({
|
|
56908
|
+
...KindShape,
|
|
56909
|
+
...TimeWindowShape,
|
|
56910
|
+
where: RecordWhereSchema.optional(),
|
|
56911
|
+
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
56912
|
+
order: _enum(["at", "id"]).optional(),
|
|
56913
|
+
desc: boolean().optional(),
|
|
56914
|
+
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
56915
|
+
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
56916
|
+
cursor: string().min(1).optional()
|
|
56917
|
+
});
|
|
56918
|
+
var RecordQueryResultSchema = object({
|
|
56919
|
+
records: array(RecordViewSchema),
|
|
56920
|
+
nextCursor: string().nullable()
|
|
56921
|
+
});
|
|
56922
|
+
var RecordKindStateSchema = _enum([
|
|
56923
|
+
"created",
|
|
56924
|
+
"unchanged",
|
|
56925
|
+
"updated"
|
|
56926
|
+
]);
|
|
56927
|
+
var DeclareKindResultSchema = object({
|
|
56928
|
+
kindId: number().int(),
|
|
56929
|
+
state: RecordKindStateSchema,
|
|
56930
|
+
createdIndexes: array(string())
|
|
56931
|
+
});
|
|
56932
|
+
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
56933
|
+
var SecretRefsList = array(string());
|
|
56934
|
+
/**
|
|
56935
|
+
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
56936
|
+
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
56937
|
+
*/
|
|
56938
|
+
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
56939
|
+
object({
|
|
56940
|
+
op: literal("put"),
|
|
56941
|
+
kind: RecordKindNameSchema,
|
|
56942
|
+
record: RecordInputSchema,
|
|
56943
|
+
ifRev: number().int().min(0).optional()
|
|
56944
|
+
}),
|
|
56945
|
+
object({
|
|
56946
|
+
op: literal("patch"),
|
|
56947
|
+
kind: RecordKindNameSchema,
|
|
56948
|
+
id: RecordIdSchema,
|
|
56949
|
+
data: RecordBodySchema,
|
|
56950
|
+
at: number().int().nullable().optional(),
|
|
56951
|
+
expiresAt: number().int().nullable().optional(),
|
|
56952
|
+
ifRev: number().int().min(1).optional()
|
|
56953
|
+
}),
|
|
56954
|
+
object({
|
|
56955
|
+
op: literal("delete"),
|
|
56956
|
+
kind: RecordKindNameSchema,
|
|
56957
|
+
id: RecordIdSchema
|
|
56958
|
+
})
|
|
56959
|
+
]);
|
|
56960
|
+
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
56961
|
+
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
56962
|
+
kind: "mutation",
|
|
56963
|
+
auth: "admin"
|
|
56964
|
+
}), method(object({
|
|
56965
|
+
...KindShape,
|
|
56966
|
+
id: RecordIdSchema
|
|
56967
|
+
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
56968
|
+
...KindShape,
|
|
56969
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56970
|
+
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
56971
|
+
...KindShape,
|
|
56972
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56973
|
+
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
56974
|
+
...KindShape,
|
|
56975
|
+
record: RecordInputSchema,
|
|
56976
|
+
/** Compare-and-set. `0` means "the row must not exist". */
|
|
56977
|
+
ifRev: number().int().min(0).optional()
|
|
56978
|
+
}), object({
|
|
56979
|
+
rev: number().int(),
|
|
56980
|
+
releasedRefs: SecretRefsList
|
|
56981
|
+
}), {
|
|
56982
|
+
kind: "mutation",
|
|
56983
|
+
auth: "admin"
|
|
56984
|
+
}), method(object({
|
|
56985
|
+
...KindShape,
|
|
56986
|
+
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
56987
|
+
}), object({
|
|
56988
|
+
count: number().int(),
|
|
56989
|
+
releasedRefs: SecretRefsList
|
|
56990
|
+
}), {
|
|
56991
|
+
kind: "mutation",
|
|
56992
|
+
auth: "admin"
|
|
56993
|
+
}), method(object({
|
|
56994
|
+
...KindShape,
|
|
56995
|
+
id: RecordIdSchema,
|
|
56996
|
+
data: RecordBodySchema,
|
|
56997
|
+
at: number().int().nullable().optional(),
|
|
56998
|
+
expiresAt: number().int().nullable().optional(),
|
|
56999
|
+
ifRev: number().int().min(1).optional()
|
|
57000
|
+
}), object({
|
|
57001
|
+
rev: number().int(),
|
|
57002
|
+
releasedRefs: SecretRefsList
|
|
57003
|
+
}), {
|
|
57004
|
+
kind: "mutation",
|
|
57005
|
+
auth: "admin"
|
|
57006
|
+
}), method(object({
|
|
57007
|
+
...KindShape,
|
|
57008
|
+
id: RecordIdSchema
|
|
57009
|
+
}), object({
|
|
57010
|
+
deleted: boolean(),
|
|
57011
|
+
secretRefs: SecretRefsList
|
|
57012
|
+
}), {
|
|
57013
|
+
kind: "mutation",
|
|
57014
|
+
auth: "admin"
|
|
57015
|
+
}), method(object({
|
|
57016
|
+
...KindShape,
|
|
57017
|
+
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
57018
|
+
}), object({
|
|
57019
|
+
deleted: number().int(),
|
|
57020
|
+
secretRefs: SecretRefsList
|
|
57021
|
+
}), {
|
|
57022
|
+
kind: "mutation",
|
|
57023
|
+
auth: "admin"
|
|
57024
|
+
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
57025
|
+
...KindShape,
|
|
57026
|
+
...TimeWindowShape,
|
|
57027
|
+
where: RecordWhereSchema.optional()
|
|
57028
|
+
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
57029
|
+
...KindShape,
|
|
57030
|
+
deviceId: number().int().optional(),
|
|
57031
|
+
from: number(),
|
|
57032
|
+
to: number(),
|
|
57033
|
+
bucketMs: number().int().positive()
|
|
57034
|
+
}), object({ buckets: array(object({
|
|
57035
|
+
bucketStart: number(),
|
|
57036
|
+
count: number().int()
|
|
57037
|
+
})) }), { auth: "admin" }), method(object({
|
|
57038
|
+
...KindShape,
|
|
57039
|
+
op: _enum([
|
|
57040
|
+
"sum",
|
|
57041
|
+
"min",
|
|
57042
|
+
"max",
|
|
57043
|
+
"count"
|
|
57044
|
+
]),
|
|
57045
|
+
field: RecordFieldPathSchema.optional(),
|
|
57046
|
+
groupBy: RecordFieldPathSchema.optional(),
|
|
57047
|
+
where: RecordWhereSchema.optional()
|
|
57048
|
+
}), object({ rows: array(object({
|
|
57049
|
+
group: union([
|
|
57050
|
+
string(),
|
|
57051
|
+
number(),
|
|
57052
|
+
_null()
|
|
57053
|
+
]),
|
|
57054
|
+
value: number().nullable()
|
|
57055
|
+
})) }), { auth: "admin" }), method(object({
|
|
57056
|
+
...KindShape,
|
|
57057
|
+
deviceId: number().int().optional(),
|
|
57058
|
+
atBefore: number().optional(),
|
|
57059
|
+
where: RecordWhereSchema.optional()
|
|
57060
|
+
}), object({
|
|
57061
|
+
deleted: number().int(),
|
|
57062
|
+
secretRefs: SecretRefsList
|
|
57063
|
+
}), {
|
|
57064
|
+
kind: "mutation",
|
|
57065
|
+
auth: "admin"
|
|
57066
|
+
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
57067
|
+
results: array(object({
|
|
57068
|
+
rev: number().int().nullable(),
|
|
57069
|
+
deleted: boolean()
|
|
57070
|
+
})),
|
|
57071
|
+
releasedRefs: SecretRefsList,
|
|
57072
|
+
secretRefs: SecretRefsList
|
|
57073
|
+
}), {
|
|
57074
|
+
kind: "mutation",
|
|
57075
|
+
auth: "admin"
|
|
57076
|
+
}), method(object({
|
|
57077
|
+
deviceId: number().int(),
|
|
57078
|
+
kinds: array(RecordKindNameSchema).optional()
|
|
57079
|
+
}), object({
|
|
57080
|
+
deleted: record(string(), number().int()),
|
|
57081
|
+
secretRefs: SecretRefsList
|
|
57082
|
+
}), {
|
|
57083
|
+
kind: "mutation",
|
|
57084
|
+
auth: "admin"
|
|
57085
|
+
}), method(object(KindShape), object({ id: number().int() }), {
|
|
57086
|
+
kind: "mutation",
|
|
57087
|
+
auth: "admin"
|
|
57088
|
+
});
|
|
57089
|
+
/**
|
|
57090
|
+
* The wire shapes of the migration API, version 1 (planning#447).
|
|
57091
|
+
*
|
|
57092
|
+
* A migration is code that outlives the server it was written against: an addon at
|
|
57093
|
+
* version 1.4 that is brought to 1.9 in one jump still runs its 1.4 catalogue, against
|
|
57094
|
+
* the 1.9 kernel. The only thing that code may call is {@link MigrationApiV1}; this file
|
|
57095
|
+
* is the contract of every method of it, one Zod pair per method.
|
|
57096
|
+
*
|
|
57097
|
+
* APPEND-ONLY. A released method is never removed and never changes meaning: an input
|
|
57098
|
+
* field is never removed or made required, an enum never narrows, an output field is
|
|
57099
|
+
* never removed. A change that cannot be expressed that way is `migration-api.v2`, next
|
|
57100
|
+
* to this file. `scripts/check-migration-api-append-only.ts` holds the line against
|
|
57101
|
+
* `scripts/migration-api-contract.json`, and every `behaviours` id below must have an
|
|
57102
|
+
* `it('<id>'` in `migration-api.contract.spec.ts`.
|
|
57103
|
+
*/
|
|
57104
|
+
var Rows = record(string(), unknown());
|
|
57105
|
+
var IntegrationId = object({ integrationId: string().min(1) });
|
|
57106
|
+
var RecordId = object({ id: string().min(1) });
|
|
57107
|
+
var RecordRowSchema = object({
|
|
57108
|
+
id: string(),
|
|
57109
|
+
deviceId: number().int().nullable(),
|
|
57110
|
+
at: number().nullable(),
|
|
57111
|
+
expiresAt: number().nullable(),
|
|
57112
|
+
rev: number().int(),
|
|
57113
|
+
updatedAt: number(),
|
|
57114
|
+
data: Rows
|
|
57115
|
+
});
|
|
57116
|
+
var RecordPutShape = {
|
|
57117
|
+
id: string().min(1),
|
|
57118
|
+
deviceId: number().int().optional(),
|
|
57119
|
+
at: number().int().optional(),
|
|
57120
|
+
expiresAt: number().int().optional(),
|
|
57121
|
+
data: Rows
|
|
57122
|
+
};
|
|
57123
|
+
var MigrationIntegrationSchema = object({
|
|
57124
|
+
id: string(),
|
|
57125
|
+
addonId: string(),
|
|
57126
|
+
name: string(),
|
|
57127
|
+
info: Rows,
|
|
57128
|
+
originStableId: string().optional()
|
|
57129
|
+
});
|
|
57130
|
+
var MigrationDeviceSchema = object({
|
|
57131
|
+
id: number().int(),
|
|
57132
|
+
integrationId: string().nullable().optional(),
|
|
57133
|
+
/** The owning device; null or absent for a root. */
|
|
57134
|
+
parentDeviceId: number().int().nullable().optional(),
|
|
57135
|
+
type: string().optional(),
|
|
57136
|
+
name: string().optional(),
|
|
57137
|
+
stableId: string().optional()
|
|
57138
|
+
});
|
|
57139
|
+
/** What the engine says about the migration API it serves: lets an addon notice an older server. */
|
|
57140
|
+
var MigrationApiInfoSchema = object({
|
|
57141
|
+
version: number().int(),
|
|
57142
|
+
methods: array(string())
|
|
57143
|
+
});
|
|
57144
|
+
object({
|
|
57145
|
+
message: string(),
|
|
57146
|
+
meta: Rows.optional(),
|
|
57147
|
+
/** Grouping tags of the line; a line about a device carries `{ deviceId }`. */
|
|
57148
|
+
tags: object({ deviceId: number().int().optional() }).optional()
|
|
57149
|
+
});
|
|
57150
|
+
object({
|
|
57151
|
+
owner: string().min(1),
|
|
57152
|
+
metas: array(MigrationMetaSchema)
|
|
57153
|
+
}), object({
|
|
57154
|
+
bootId: string(),
|
|
57155
|
+
snapshot: _enum([
|
|
57156
|
+
"none",
|
|
57157
|
+
"written",
|
|
57158
|
+
"refused"
|
|
57159
|
+
]),
|
|
57160
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57161
|
+
/** The migration API the engine serves; absent from a server that predates it. */
|
|
57162
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
57163
|
+
}), object({ id: string().min(1) }), object({
|
|
57164
|
+
proceed: boolean(),
|
|
57165
|
+
state: union([literal("pending"), MigrationStatusSchema]),
|
|
57166
|
+
reason: string().nullable()
|
|
57167
|
+
}), object({
|
|
57168
|
+
id: string().min(1),
|
|
57169
|
+
result: MigrationOwnerResultSchema
|
|
57170
|
+
}), object({
|
|
57171
|
+
status: MigrationStatusSchema,
|
|
57172
|
+
error: string().nullable(),
|
|
57173
|
+
rowsChanged: number().int().nonnegative()
|
|
57174
|
+
}), object({
|
|
57175
|
+
rows: number().int().nonnegative(),
|
|
57176
|
+
secrets: number().int().nonnegative()
|
|
57177
|
+
}), IntegrationId.extend({ patch: Rows }), object({ written: boolean() }), IntegrationId.extend({ keys: array(string().min(1)) }), object({
|
|
57178
|
+
present: boolean(),
|
|
57179
|
+
removed: Rows
|
|
57180
|
+
}), RecordRowSchema.nullable(), RecordRowSchema.nullable(), object({
|
|
57181
|
+
...RecordPutShape,
|
|
57182
|
+
ifRev: number().int().optional()
|
|
57183
|
+
}), object({ rev: number().int() }), object({ records: array(object(RecordPutShape)) }), object({ count: number().int().nonnegative() }), RecordId.extend({
|
|
57184
|
+
partial: Rows,
|
|
57185
|
+
ifRev: number().int().optional()
|
|
57186
|
+
}), object({ rev: number().int() }), object({ deleted: boolean() }), object({
|
|
57187
|
+
deviceId: number().int().optional(),
|
|
57188
|
+
atFrom: number().optional(),
|
|
57189
|
+
atTo: number().optional(),
|
|
57190
|
+
where: RecordWhereSchema.optional(),
|
|
57191
|
+
limit: number().int().min(1).optional(),
|
|
57192
|
+
cursor: string().min(1).optional(),
|
|
57193
|
+
resolveSecrets: boolean().optional()
|
|
57194
|
+
}), object({
|
|
57195
|
+
records: array(RecordRowSchema),
|
|
57196
|
+
nextCursor: string().nullable()
|
|
57197
|
+
}), object({
|
|
57198
|
+
deviceId: number().int().optional(),
|
|
57199
|
+
atBefore: number().optional(),
|
|
57200
|
+
where: RecordWhereSchema.optional()
|
|
57201
|
+
}), object({ count: number().int().nonnegative() }), object({}), object({ store: Rows }), object({ patch: Rows }), object({}), object({ keys: array(string().min(1)) }), object({ removed: array(string()) }), object({ refs: array(string()) }), object({ values: record(string(), string().nullable()) }), object({
|
|
57202
|
+
ref: string().min(1),
|
|
57203
|
+
value: string()
|
|
57204
|
+
}), object({}), object({ refs: array(string()) }), object({ deleted: number().int().nonnegative() }), object({}), object({ integrations: array(MigrationIntegrationSchema) }), object({ id: string().min(1) }), MigrationIntegrationSchema.nullable(), object({
|
|
57205
|
+
addonId: string().min(1),
|
|
57206
|
+
integrationId: string().min(1).optional()
|
|
57207
|
+
}), object({ devices: array(MigrationDeviceSchema) }), object({ deviceId: number().int() }), object({
|
|
57208
|
+
id: number().int(),
|
|
57209
|
+
integrationId: string().nullable(),
|
|
57210
|
+
/** Whether the device carries any stored config; its content is not offered. */
|
|
57211
|
+
hasConfig: boolean()
|
|
57212
|
+
}).nullable(), object({
|
|
57213
|
+
deviceId: number().int(),
|
|
57214
|
+
integrationId: string().min(1)
|
|
57215
|
+
}), object({}), object({}), object({ nodeId: string() }), object({}), object({});
|
|
56747
57216
|
method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { auth: "admin" }), method(object({ owner: string().optional() }), object({ pending: array(MigrationPendingEntrySchema) }), { auth: "admin" }), method(object({ id: string().min(1) }), MigrationDryRunResultSchema, { auth: "admin" }), method(object({
|
|
56748
57217
|
id: string().min(1),
|
|
56749
57218
|
error: string().min(1)
|
|
@@ -56773,7 +57242,9 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
|
|
|
56773
57242
|
"written",
|
|
56774
57243
|
"refused"
|
|
56775
57244
|
]),
|
|
56776
|
-
steps: array(MigrationOwnerStepSchema)
|
|
57245
|
+
steps: array(MigrationOwnerStepSchema),
|
|
57246
|
+
/** The migration API this engine serves (additive: a server that predates it omits it). */
|
|
57247
|
+
migrationApi: MigrationApiInfoSchema.optional()
|
|
56777
57248
|
}), {
|
|
56778
57249
|
kind: "mutation",
|
|
56779
57250
|
auth: "admin"
|
|
@@ -56794,6 +57265,27 @@ method(object({}), object({ entries: array(MigrationLedgerEntrySchema) }), { aut
|
|
|
56794
57265
|
}), {
|
|
56795
57266
|
kind: "mutation",
|
|
56796
57267
|
auth: "admin"
|
|
57268
|
+
}), method(object({ integrationId: string().min(1) }), record(string(), unknown()), { auth: "admin" }), method(object({ integrationId: string().min(1) }), object({
|
|
57269
|
+
rows: number().int().nonnegative(),
|
|
57270
|
+
secrets: number().int().nonnegative()
|
|
57271
|
+
}), {
|
|
57272
|
+
kind: "mutation",
|
|
57273
|
+
auth: "admin"
|
|
57274
|
+
}), method(object({
|
|
57275
|
+
integrationId: string().min(1),
|
|
57276
|
+
patch: record(string(), unknown())
|
|
57277
|
+
}), object({ written: boolean() }), {
|
|
57278
|
+
kind: "mutation",
|
|
57279
|
+
auth: "admin"
|
|
57280
|
+
}), method(object({
|
|
57281
|
+
integrationId: string().min(1),
|
|
57282
|
+
keys: array(string().min(1))
|
|
57283
|
+
}), object({
|
|
57284
|
+
present: boolean(),
|
|
57285
|
+
removed: record(string(), unknown())
|
|
57286
|
+
}), {
|
|
57287
|
+
kind: "mutation",
|
|
57288
|
+
auth: "admin"
|
|
56797
57289
|
});
|
|
56798
57290
|
method(object({
|
|
56799
57291
|
sourceUrl: string(),
|
|
@@ -59419,7 +59911,10 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
59419
59911
|
}), method(RelocateMediaInputSchema, object({ jobId: string() }), {
|
|
59420
59912
|
kind: "mutation",
|
|
59421
59913
|
auth: "admin"
|
|
59422
|
-
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(
|
|
59914
|
+
}), method(object({}), UnstampedEventMediaCountSchema, { auth: "admin" }), method(MediaLocationCensusInputSchema, MediaLocationCensusSchema, {
|
|
59915
|
+
kind: "query",
|
|
59916
|
+
auth: "admin"
|
|
59917
|
+
}), method(RelocatableMediaCountInputSchema, RelocatableMediaCountSchema, {
|
|
59423
59918
|
kind: "query",
|
|
59424
59919
|
auth: "admin"
|
|
59425
59920
|
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
@@ -61826,7 +62321,7 @@ var BrokerStatusSchema$1 = _enum([
|
|
|
61826
62321
|
"unreachable",
|
|
61827
62322
|
"tls-error"
|
|
61828
62323
|
]);
|
|
61829
|
-
var BrokerInfoSchema
|
|
62324
|
+
var BrokerInfoSchema = object({
|
|
61830
62325
|
id: string(),
|
|
61831
62326
|
name: string(),
|
|
61832
62327
|
url: string(),
|
|
@@ -61889,7 +62384,7 @@ var StatusSchema = object({
|
|
|
61889
62384
|
brokerCount: number(),
|
|
61890
62385
|
embeddedRunning: boolean()
|
|
61891
62386
|
});
|
|
61892
|
-
method(_void(), array(BrokerInfoSchema
|
|
62387
|
+
method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
|
|
61893
62388
|
kind: "mutation",
|
|
61894
62389
|
auth: "admin"
|
|
61895
62390
|
}), method(IdInputSchema, _void(), {
|
|
@@ -68207,253 +68702,6 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, met
|
|
|
68207
68702
|
auth: "admin"
|
|
68208
68703
|
});
|
|
68209
68704
|
/**
|
|
68210
|
-
* records - an addon's own facts and documents (D881): ledgers, audit rows,
|
|
68211
|
-
* config documents, mirrors. The third storage API next to `ctx.settings`
|
|
68212
|
-
* (operator intent, D870) and device-history (numbers over time, D850).
|
|
68213
|
-
*
|
|
68214
|
-
* Who the OWNER is: `callerAddonId`, stamped by the transport on a call that
|
|
68215
|
-
* crossed a process and stated by a hub in-process caller (`ctx.records` does
|
|
68216
|
-
* that with the addon id closed over). It is deliberately NOT in any input
|
|
68217
|
-
* schema, so a session can never supply it, and a call with no stamp is refused
|
|
68218
|
-
* `records/no-caller`. A kind is readable and writable only by its owner; other
|
|
68219
|
-
* addons go through the owner's capability.
|
|
68220
|
-
*
|
|
68221
|
-
* Every read is three-way: a row, `null`, or the call THROWS. A failed read is
|
|
68222
|
-
* never `[]`. Secret fields are never on the wire: a read shows the
|
|
68223
|
-
* redaction token, `secretRefs` hands the owner's `ctx.secrets` the refs it
|
|
68224
|
-
* resolves itself, and a write may carry only a keychain marker bound to the
|
|
68225
|
-
* row's own ref.
|
|
68226
|
-
*
|
|
68227
|
-
* Internal: addons reach it through `ctx.records`, never directly.
|
|
68228
|
-
*/
|
|
68229
|
-
var RecordIdSchema = string().min(1).max(256);
|
|
68230
|
-
/** A JSON object body. Size is the engine's check (`maxDataBytes`), not a schema's. */
|
|
68231
|
-
var RecordBodySchema = record(string(), unknown());
|
|
68232
|
-
var RecordInputSchema = object({
|
|
68233
|
-
id: RecordIdSchema,
|
|
68234
|
-
/** The device this row belongs to (D865 removal, per-device reads). */
|
|
68235
|
-
deviceId: number().int().optional(),
|
|
68236
|
-
/** The domain clock, ms: observed / created / opened. */
|
|
68237
|
-
at: number().int().optional(),
|
|
68238
|
-
/** Producer-set expiry, ms. Refused on a kind whose retention is an engine TTL. */
|
|
68239
|
-
expiresAt: number().int().optional(),
|
|
68240
|
-
data: RecordBodySchema
|
|
68241
|
-
});
|
|
68242
|
-
var RecordViewSchema = object({
|
|
68243
|
-
id: string(),
|
|
68244
|
-
deviceId: number().int().nullable(),
|
|
68245
|
-
at: number().nullable(),
|
|
68246
|
-
expiresAt: number().nullable(),
|
|
68247
|
-
rev: number().int(),
|
|
68248
|
-
updatedAt: number(),
|
|
68249
|
-
data: RecordBodySchema
|
|
68250
|
-
});
|
|
68251
|
-
/** Declared index fields only. Anything else does not compile, by design. */
|
|
68252
|
-
var RecordWhereSchema = record(RecordFieldPathSchema, union([
|
|
68253
|
-
string(),
|
|
68254
|
-
number(),
|
|
68255
|
-
boolean(),
|
|
68256
|
-
object({ in: array(union([
|
|
68257
|
-
string(),
|
|
68258
|
-
number(),
|
|
68259
|
-
boolean()
|
|
68260
|
-
])).min(1).max(RECORD_BATCH_MAX) }),
|
|
68261
|
-
object({
|
|
68262
|
-
gt: union([string(), number()]).optional(),
|
|
68263
|
-
gte: union([string(), number()]).optional(),
|
|
68264
|
-
lt: union([string(), number()]).optional(),
|
|
68265
|
-
lte: union([string(), number()]).optional()
|
|
68266
|
-
}).refine((r) => Object.keys(r).length > 0, { message: "name at least one bound" })
|
|
68267
|
-
]));
|
|
68268
|
-
var KindShape = { kind: RecordKindNameSchema };
|
|
68269
|
-
var TimeWindowShape = {
|
|
68270
|
-
deviceId: number().int().optional(),
|
|
68271
|
-
atFrom: number().optional(),
|
|
68272
|
-
atTo: number().optional()
|
|
68273
|
-
};
|
|
68274
|
-
var RecordQueryInputSchema = object({
|
|
68275
|
-
...KindShape,
|
|
68276
|
-
...TimeWindowShape,
|
|
68277
|
-
where: RecordWhereSchema.optional(),
|
|
68278
|
-
/** Without `where`: `at` (default for a kind with a clock) or `id`. With `where` the order is the index's. */
|
|
68279
|
-
order: _enum(["at", "id"]).optional(),
|
|
68280
|
-
desc: boolean().optional(),
|
|
68281
|
-
limit: number().int().min(1).max(RECORD_QUERY_MAX).default(100),
|
|
68282
|
-
/** Opaque keyset cursor from a previous answer. There is no offset. */
|
|
68283
|
-
cursor: string().min(1).optional()
|
|
68284
|
-
});
|
|
68285
|
-
var RecordQueryResultSchema = object({
|
|
68286
|
-
records: array(RecordViewSchema),
|
|
68287
|
-
nextCursor: string().nullable()
|
|
68288
|
-
});
|
|
68289
|
-
var RecordKindStateSchema = _enum([
|
|
68290
|
-
"created",
|
|
68291
|
-
"unchanged",
|
|
68292
|
-
"updated"
|
|
68293
|
-
]);
|
|
68294
|
-
var DeclareKindResultSchema = object({
|
|
68295
|
-
kindId: number().int(),
|
|
68296
|
-
state: RecordKindStateSchema,
|
|
68297
|
-
createdIndexes: array(string())
|
|
68298
|
-
});
|
|
68299
|
-
/** Keychain refs a write released (the old marker is gone from the row): the owner deletes the values. */
|
|
68300
|
-
var SecretRefsList = array(string());
|
|
68301
|
-
/**
|
|
68302
|
-
* One op of a `transact`: the single-row writes, each on a kind of the caller.
|
|
68303
|
-
* All ops of one call share ONE database file and ONE `transactFamily`.
|
|
68304
|
-
*/
|
|
68305
|
-
var RecordTransactOpSchema = discriminatedUnion("op", [
|
|
68306
|
-
object({
|
|
68307
|
-
op: literal("put"),
|
|
68308
|
-
kind: RecordKindNameSchema,
|
|
68309
|
-
record: RecordInputSchema,
|
|
68310
|
-
ifRev: number().int().min(0).optional()
|
|
68311
|
-
}),
|
|
68312
|
-
object({
|
|
68313
|
-
op: literal("patch"),
|
|
68314
|
-
kind: RecordKindNameSchema,
|
|
68315
|
-
id: RecordIdSchema,
|
|
68316
|
-
data: RecordBodySchema,
|
|
68317
|
-
at: number().int().nullable().optional(),
|
|
68318
|
-
expiresAt: number().int().nullable().optional(),
|
|
68319
|
-
ifRev: number().int().min(1).optional()
|
|
68320
|
-
}),
|
|
68321
|
-
object({
|
|
68322
|
-
op: literal("delete"),
|
|
68323
|
-
kind: RecordKindNameSchema,
|
|
68324
|
-
id: RecordIdSchema
|
|
68325
|
-
})
|
|
68326
|
-
]);
|
|
68327
|
-
var RecordSecretRefsSchema = record(string(), record(string(), string()));
|
|
68328
|
-
method(object({ kinds: record(RecordKindNameSchema, RecordKindDeclarationSchema) }), object({ kinds: record(string(), DeclareKindResultSchema) }), {
|
|
68329
|
-
kind: "mutation",
|
|
68330
|
-
auth: "admin"
|
|
68331
|
-
}), method(object({
|
|
68332
|
-
...KindShape,
|
|
68333
|
-
id: RecordIdSchema
|
|
68334
|
-
}), RecordViewSchema.nullable(), { auth: "admin" }), method(object({
|
|
68335
|
-
...KindShape,
|
|
68336
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68337
|
-
}), array(RecordViewSchema), { auth: "admin" }), method(object({
|
|
68338
|
-
...KindShape,
|
|
68339
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68340
|
-
}), RecordSecretRefsSchema, { auth: "admin" }), method(object({
|
|
68341
|
-
...KindShape,
|
|
68342
|
-
record: RecordInputSchema,
|
|
68343
|
-
/** Compare-and-set. `0` means "the row must not exist". */
|
|
68344
|
-
ifRev: number().int().min(0).optional()
|
|
68345
|
-
}), object({
|
|
68346
|
-
rev: number().int(),
|
|
68347
|
-
releasedRefs: SecretRefsList
|
|
68348
|
-
}), {
|
|
68349
|
-
kind: "mutation",
|
|
68350
|
-
auth: "admin"
|
|
68351
|
-
}), method(object({
|
|
68352
|
-
...KindShape,
|
|
68353
|
-
records: array(RecordInputSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68354
|
-
}), object({
|
|
68355
|
-
count: number().int(),
|
|
68356
|
-
releasedRefs: SecretRefsList
|
|
68357
|
-
}), {
|
|
68358
|
-
kind: "mutation",
|
|
68359
|
-
auth: "admin"
|
|
68360
|
-
}), method(object({
|
|
68361
|
-
...KindShape,
|
|
68362
|
-
id: RecordIdSchema,
|
|
68363
|
-
data: RecordBodySchema,
|
|
68364
|
-
at: number().int().nullable().optional(),
|
|
68365
|
-
expiresAt: number().int().nullable().optional(),
|
|
68366
|
-
ifRev: number().int().min(1).optional()
|
|
68367
|
-
}), object({
|
|
68368
|
-
rev: number().int(),
|
|
68369
|
-
releasedRefs: SecretRefsList
|
|
68370
|
-
}), {
|
|
68371
|
-
kind: "mutation",
|
|
68372
|
-
auth: "admin"
|
|
68373
|
-
}), method(object({
|
|
68374
|
-
...KindShape,
|
|
68375
|
-
id: RecordIdSchema
|
|
68376
|
-
}), object({
|
|
68377
|
-
deleted: boolean(),
|
|
68378
|
-
secretRefs: SecretRefsList
|
|
68379
|
-
}), {
|
|
68380
|
-
kind: "mutation",
|
|
68381
|
-
auth: "admin"
|
|
68382
|
-
}), method(object({
|
|
68383
|
-
...KindShape,
|
|
68384
|
-
ids: array(RecordIdSchema).min(1).max(RECORD_BATCH_MAX)
|
|
68385
|
-
}), object({
|
|
68386
|
-
deleted: number().int(),
|
|
68387
|
-
secretRefs: SecretRefsList
|
|
68388
|
-
}), {
|
|
68389
|
-
kind: "mutation",
|
|
68390
|
-
auth: "admin"
|
|
68391
|
-
}), method(RecordQueryInputSchema, RecordQueryResultSchema, { auth: "admin" }), method(object({
|
|
68392
|
-
...KindShape,
|
|
68393
|
-
...TimeWindowShape,
|
|
68394
|
-
where: RecordWhereSchema.optional()
|
|
68395
|
-
}), object({ count: number().int() }), { auth: "admin" }), method(object({
|
|
68396
|
-
...KindShape,
|
|
68397
|
-
deviceId: number().int().optional(),
|
|
68398
|
-
from: number(),
|
|
68399
|
-
to: number(),
|
|
68400
|
-
bucketMs: number().int().positive()
|
|
68401
|
-
}), object({ buckets: array(object({
|
|
68402
|
-
bucketStart: number(),
|
|
68403
|
-
count: number().int()
|
|
68404
|
-
})) }), { auth: "admin" }), method(object({
|
|
68405
|
-
...KindShape,
|
|
68406
|
-
op: _enum([
|
|
68407
|
-
"sum",
|
|
68408
|
-
"min",
|
|
68409
|
-
"max",
|
|
68410
|
-
"count"
|
|
68411
|
-
]),
|
|
68412
|
-
field: RecordFieldPathSchema.optional(),
|
|
68413
|
-
groupBy: RecordFieldPathSchema.optional(),
|
|
68414
|
-
where: RecordWhereSchema.optional()
|
|
68415
|
-
}), object({ rows: array(object({
|
|
68416
|
-
group: union([
|
|
68417
|
-
string(),
|
|
68418
|
-
number(),
|
|
68419
|
-
_null()
|
|
68420
|
-
]),
|
|
68421
|
-
value: number().nullable()
|
|
68422
|
-
})) }), { auth: "admin" }), method(object({
|
|
68423
|
-
...KindShape,
|
|
68424
|
-
deviceId: number().int().optional(),
|
|
68425
|
-
atBefore: number().optional(),
|
|
68426
|
-
where: RecordWhereSchema.optional()
|
|
68427
|
-
}), object({
|
|
68428
|
-
deleted: number().int(),
|
|
68429
|
-
secretRefs: SecretRefsList
|
|
68430
|
-
}), {
|
|
68431
|
-
kind: "mutation",
|
|
68432
|
-
auth: "admin"
|
|
68433
|
-
}), method(object({ ops: array(RecordTransactOpSchema).min(1).max(RECORD_BATCH_MAX) }), object({
|
|
68434
|
-
results: array(object({
|
|
68435
|
-
rev: number().int().nullable(),
|
|
68436
|
-
deleted: boolean()
|
|
68437
|
-
})),
|
|
68438
|
-
releasedRefs: SecretRefsList,
|
|
68439
|
-
secretRefs: SecretRefsList
|
|
68440
|
-
}), {
|
|
68441
|
-
kind: "mutation",
|
|
68442
|
-
auth: "admin"
|
|
68443
|
-
}), method(object({
|
|
68444
|
-
deviceId: number().int(),
|
|
68445
|
-
kinds: array(RecordKindNameSchema).optional()
|
|
68446
|
-
}), object({
|
|
68447
|
-
deleted: record(string(), number().int()),
|
|
68448
|
-
secretRefs: SecretRefsList
|
|
68449
|
-
}), {
|
|
68450
|
-
kind: "mutation",
|
|
68451
|
-
auth: "admin"
|
|
68452
|
-
}), method(object(KindShape), object({ id: number().int() }), {
|
|
68453
|
-
kind: "mutation",
|
|
68454
|
-
auth: "admin"
|
|
68455
|
-
});
|
|
68456
|
-
/**
|
|
68457
68705
|
* record-entries - the ONE admin surface for every list of user-created entries
|
|
68458
68706
|
* (D881): notification targets, brokers, LLM profiles, anything an addon
|
|
68459
68707
|
* declares as an ENTRY kind (`entry: { title }` in `defineRecordKinds`).
|
|
@@ -75968,11 +76216,15 @@ discriminatedUnion("kind", [object({
|
|
|
75968
76216
|
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
75969
76217
|
* near F. A device that is not near one level most of the time has no floor.
|
|
75970
76218
|
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
75971
|
-
* 3.
|
|
75972
|
-
*
|
|
76219
|
+
* 3. an episode shorter than `PULSE_MAX_MS` is a PULSE (an anti-crease tumble, a
|
|
76220
|
+
* display waking): it is neither a run nor a pause. Among the remaining
|
|
76221
|
+
* episodes, gaps over `PAUSE_MAX_MS` always separate runs; the rest split in
|
|
76222
|
+
* two by the largest log-scale jump (>= 3x): the short ones are pauses INSIDE
|
|
76223
|
+
* a run (a soak), the long ones separate runs. A stretch under `MIN_RUN_MS`
|
|
76224
|
+
* is no run.
|
|
75973
76225
|
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
75974
|
-
* grid a person would type; stop dwell covers the longest pause
|
|
75975
|
-
* covers the longest standby blip above T.
|
|
76226
|
+
* grid a person would type; stop dwell covers the longest pause and the
|
|
76227
|
+
* kind's floor; start dwell covers the longest pulse and standby blip above T.
|
|
75976
76228
|
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
75977
76229
|
* run must hold exactly one true interval and nothing may flap outside.
|
|
75978
76230
|
*/
|
|
@@ -75983,6 +76235,10 @@ var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
|
75983
76235
|
30 * SECOND_MS;
|
|
75984
76236
|
60 * SECOND_MS;
|
|
75985
76237
|
20 * MINUTE_MS$1;
|
|
76238
|
+
90 * SECOND_MS;
|
|
76239
|
+
30 * MINUTE_MS$1;
|
|
76240
|
+
10 * MINUTE_MS$1;
|
|
76241
|
+
DeviceType.Dryer, 3 * MINUTE_MS$1, DeviceType.Dishwasher, 3 * MINUTE_MS$1, DeviceType.Washer, 10 * MINUTE_MS$1;
|
|
75986
76242
|
10 * SECOND_MS;
|
|
75987
76243
|
10 * SECOND_MS;
|
|
75988
76244
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
@@ -77001,20 +77257,6 @@ Object.freeze({
|
|
|
77001
77257
|
access: "create",
|
|
77002
77258
|
role: "viewer"
|
|
77003
77259
|
},
|
|
77004
|
-
"broker.add": {
|
|
77005
|
-
capName: "broker",
|
|
77006
|
-
capScope: "system",
|
|
77007
|
-
addonId: null,
|
|
77008
|
-
access: "create",
|
|
77009
|
-
role: "admin"
|
|
77010
|
-
},
|
|
77011
|
-
"broker.get": {
|
|
77012
|
-
capName: "broker",
|
|
77013
|
-
capScope: "system",
|
|
77014
|
-
addonId: null,
|
|
77015
|
-
access: "view",
|
|
77016
|
-
role: "admin"
|
|
77017
|
-
},
|
|
77018
77260
|
"broker.getBrokerConfig": {
|
|
77019
77261
|
capName: "broker",
|
|
77020
77262
|
capScope: "system",
|
|
@@ -77022,20 +77264,6 @@ Object.freeze({
|
|
|
77022
77264
|
access: "view",
|
|
77023
77265
|
role: "admin"
|
|
77024
77266
|
},
|
|
77025
|
-
"broker.getSettings": {
|
|
77026
|
-
capName: "broker",
|
|
77027
|
-
capScope: "system",
|
|
77028
|
-
addonId: null,
|
|
77029
|
-
access: "view",
|
|
77030
|
-
role: "admin"
|
|
77031
|
-
},
|
|
77032
|
-
"broker.getSettingsSchema": {
|
|
77033
|
-
capName: "broker",
|
|
77034
|
-
capScope: "system",
|
|
77035
|
-
addonId: null,
|
|
77036
|
-
access: "view",
|
|
77037
|
-
role: "admin"
|
|
77038
|
-
},
|
|
77039
77267
|
"broker.getState": {
|
|
77040
77268
|
capName: "broker",
|
|
77041
77269
|
capScope: "system",
|
|
@@ -77050,20 +77278,6 @@ Object.freeze({
|
|
|
77050
77278
|
access: "view",
|
|
77051
77279
|
role: "admin"
|
|
77052
77280
|
},
|
|
77053
|
-
"broker.list": {
|
|
77054
|
-
capName: "broker",
|
|
77055
|
-
capScope: "system",
|
|
77056
|
-
addonId: null,
|
|
77057
|
-
access: "view",
|
|
77058
|
-
role: "admin"
|
|
77059
|
-
},
|
|
77060
|
-
"broker.listProviders": {
|
|
77061
|
-
capName: "broker",
|
|
77062
|
-
capScope: "system",
|
|
77063
|
-
addonId: null,
|
|
77064
|
-
access: "view",
|
|
77065
|
-
role: "admin"
|
|
77066
|
-
},
|
|
77067
77281
|
"broker.publish": {
|
|
77068
77282
|
capName: "broker",
|
|
77069
77283
|
capScope: "system",
|
|
@@ -77071,20 +77285,6 @@ Object.freeze({
|
|
|
77071
77285
|
access: "create",
|
|
77072
77286
|
role: "admin"
|
|
77073
77287
|
},
|
|
77074
|
-
"broker.remove": {
|
|
77075
|
-
capName: "broker",
|
|
77076
|
-
capScope: "system",
|
|
77077
|
-
addonId: null,
|
|
77078
|
-
access: "delete",
|
|
77079
|
-
role: "admin"
|
|
77080
|
-
},
|
|
77081
|
-
"broker.setSettings": {
|
|
77082
|
-
capName: "broker",
|
|
77083
|
-
capScope: "system",
|
|
77084
|
-
addonId: null,
|
|
77085
|
-
access: "create",
|
|
77086
|
-
role: "admin"
|
|
77087
|
-
},
|
|
77088
77288
|
"broker.subscribe": {
|
|
77089
77289
|
capName: "broker",
|
|
77090
77290
|
capScope: "system",
|
|
@@ -77092,20 +77292,6 @@ Object.freeze({
|
|
|
77092
77292
|
access: "create",
|
|
77093
77293
|
role: "admin"
|
|
77094
77294
|
},
|
|
77095
|
-
"broker.testConnection": {
|
|
77096
|
-
capName: "broker",
|
|
77097
|
-
capScope: "system",
|
|
77098
|
-
addonId: null,
|
|
77099
|
-
access: "create",
|
|
77100
|
-
role: "admin"
|
|
77101
|
-
},
|
|
77102
|
-
"broker.testSettings": {
|
|
77103
|
-
capName: "broker",
|
|
77104
|
-
capScope: "system",
|
|
77105
|
-
addonId: null,
|
|
77106
|
-
access: "create",
|
|
77107
|
-
role: "admin"
|
|
77108
|
-
},
|
|
77109
77295
|
"broker.unsubscribe": {
|
|
77110
77296
|
capName: "broker",
|
|
77111
77297
|
capScope: "system",
|
|
@@ -79738,13 +79924,6 @@ Object.freeze({
|
|
|
79738
79924
|
access: "view",
|
|
79739
79925
|
role: "admin"
|
|
79740
79926
|
},
|
|
79741
|
-
"integrations.getSettings": {
|
|
79742
|
-
capName: "integrations",
|
|
79743
|
-
capScope: "system",
|
|
79744
|
-
addonId: null,
|
|
79745
|
-
access: "view",
|
|
79746
|
-
role: "admin"
|
|
79747
|
-
},
|
|
79748
79927
|
"integrations.list": {
|
|
79749
79928
|
capName: "integrations",
|
|
79750
79929
|
capScope: "system",
|
|
@@ -79752,13 +79931,6 @@ Object.freeze({
|
|
|
79752
79931
|
access: "view",
|
|
79753
79932
|
role: "admin"
|
|
79754
79933
|
},
|
|
79755
|
-
"integrations.setSettings": {
|
|
79756
|
-
capName: "integrations",
|
|
79757
|
-
capScope: "system",
|
|
79758
|
-
addonId: null,
|
|
79759
|
-
access: "create",
|
|
79760
|
-
role: "admin"
|
|
79761
|
-
},
|
|
79762
79934
|
"integrations.testConnection": {
|
|
79763
79935
|
capName: "integrations",
|
|
79764
79936
|
capScope: "system",
|
|
@@ -80753,6 +80925,13 @@ Object.freeze({
|
|
|
80753
80925
|
access: "view",
|
|
80754
80926
|
role: "admin"
|
|
80755
80927
|
},
|
|
80928
|
+
"migrations.clearLegacyIntegrationRows": {
|
|
80929
|
+
capName: "migrations",
|
|
80930
|
+
capScope: "system",
|
|
80931
|
+
addonId: null,
|
|
80932
|
+
access: "delete",
|
|
80933
|
+
role: "admin"
|
|
80934
|
+
},
|
|
80756
80935
|
"migrations.declareOwner": {
|
|
80757
80936
|
capName: "migrations",
|
|
80758
80937
|
capScope: "system",
|
|
@@ -80760,6 +80939,13 @@ Object.freeze({
|
|
|
80760
80939
|
access: "create",
|
|
80761
80940
|
role: "admin"
|
|
80762
80941
|
},
|
|
80942
|
+
"migrations.deleteLegacyIntegrationRowKeys": {
|
|
80943
|
+
capName: "migrations",
|
|
80944
|
+
capScope: "system",
|
|
80945
|
+
addonId: null,
|
|
80946
|
+
access: "delete",
|
|
80947
|
+
role: "admin"
|
|
80948
|
+
},
|
|
80763
80949
|
"migrations.dryRun": {
|
|
80764
80950
|
capName: "migrations",
|
|
80765
80951
|
capScope: "system",
|
|
@@ -80795,6 +80981,20 @@ Object.freeze({
|
|
|
80795
80981
|
access: "create",
|
|
80796
80982
|
role: "admin"
|
|
80797
80983
|
},
|
|
80984
|
+
"migrations.patchLegacyIntegrationRows": {
|
|
80985
|
+
capName: "migrations",
|
|
80986
|
+
capScope: "system",
|
|
80987
|
+
addonId: null,
|
|
80988
|
+
access: "create",
|
|
80989
|
+
role: "admin"
|
|
80990
|
+
},
|
|
80991
|
+
"migrations.readLegacyIntegrationRows": {
|
|
80992
|
+
capName: "migrations",
|
|
80993
|
+
capScope: "system",
|
|
80994
|
+
addonId: null,
|
|
80995
|
+
access: "view",
|
|
80996
|
+
role: "admin"
|
|
80997
|
+
},
|
|
80798
80998
|
"migrations.restorePreMigration": {
|
|
80799
80999
|
capName: "migrations",
|
|
80800
81000
|
capScope: "system",
|
|
@@ -82055,6 +82255,13 @@ Object.freeze({
|
|
|
82055
82255
|
access: "view",
|
|
82056
82256
|
role: "viewer"
|
|
82057
82257
|
},
|
|
82258
|
+
"pipelineAnalytics.getMediaLocationCensus": {
|
|
82259
|
+
capName: "pipeline-analytics",
|
|
82260
|
+
capScope: "device",
|
|
82261
|
+
addonId: null,
|
|
82262
|
+
access: "view",
|
|
82263
|
+
role: "admin"
|
|
82264
|
+
},
|
|
82058
82265
|
"pipelineAnalytics.getMediaReclaimStatus": {
|
|
82059
82266
|
capName: "pipeline-analytics",
|
|
82060
82267
|
capScope: "device",
|
|
@@ -91027,6 +91234,7 @@ object({
|
|
|
91027
91234
|
delay: number().nonnegative().optional(),
|
|
91028
91235
|
commands: record(string(), unknown())
|
|
91029
91236
|
});
|
|
91237
|
+
entryBody({ brokerId: string() });
|
|
91030
91238
|
//#endregion
|
|
91031
91239
|
//#region src/google-types.ts
|
|
91032
91240
|
/** One Google account connection (broker entry). Credentials for the OAuth
|
|
@@ -91112,6 +91320,10 @@ var OAUTH_START_PATH = `/addon/google-services/oauth/start`;
|
|
|
91112
91320
|
var OAUTH_CALLBACK_SUFFIX = `/addon/google-services/oauth/callback`;
|
|
91113
91321
|
//#endregion
|
|
91114
91322
|
//#region src/google-broker-registry.ts
|
|
91323
|
+
/**
|
|
91324
|
+
* Google broker registry — N Google accounts under kind `google`.
|
|
91325
|
+
* No live WS; "connected" means a refresh token exists for the broker id.
|
|
91326
|
+
*/
|
|
91115
91327
|
var GoogleAddSettingsSchema = object({
|
|
91116
91328
|
clientId: string().min(1),
|
|
91117
91329
|
clientSecret: string().min(1).meta({ secret: true }),
|
|
@@ -91219,7 +91431,7 @@ function parseStampedCalendarId(stamped) {
|
|
|
91219
91431
|
}
|
|
91220
91432
|
/**
|
|
91221
91433
|
* In-memory index over broker entries + oauth accounts. The addon owns
|
|
91222
|
-
* persistence; this class only allocates ids and
|
|
91434
|
+
* persistence; this class only allocates ids and tracks the OAuth state.
|
|
91223
91435
|
*/
|
|
91224
91436
|
var GoogleBrokerRegistry = class {
|
|
91225
91437
|
nextIdCounter = 1;
|
|
@@ -91316,28 +91528,6 @@ var GoogleBrokerRegistry = class {
|
|
|
91316
91528
|
this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
|
|
91317
91529
|
this.seedCounterFromEntries(this.entries);
|
|
91318
91530
|
}
|
|
91319
|
-
toBrokerInfo(entry, addonId) {
|
|
91320
|
-
const oauth = this.oauthById.get(entry.id);
|
|
91321
|
-
const connected = this.isConnected(entry.id);
|
|
91322
|
-
const reauth = this.needsReauth(entry.id);
|
|
91323
|
-
return {
|
|
91324
|
-
id: entry.id,
|
|
91325
|
-
addonId,
|
|
91326
|
-
name: entry.name,
|
|
91327
|
-
kind: GOOGLE_BROKER_KIND,
|
|
91328
|
-
status: connected ? "connected" : reauth ? "auth-failed" : "disconnected",
|
|
91329
|
-
info: {
|
|
91330
|
-
accountEmail: oauth?.accountEmail ?? "",
|
|
91331
|
-
hasClient: entry.clientId.length > 0
|
|
91332
|
-
},
|
|
91333
|
-
lastCheckedAt: connected ? Date.now() : null,
|
|
91334
|
-
error: connected ? null : reauth ? "Google session expired — sign in again" : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
|
|
91335
|
-
};
|
|
91336
|
-
}
|
|
91337
|
-
listBrokerInfos(addonId, kind) {
|
|
91338
|
-
if (kind !== void 0 && kind !== "google") return [];
|
|
91339
|
-
return this.entries.map((e) => this.toBrokerInfo(e, addonId));
|
|
91340
|
-
}
|
|
91341
91531
|
size() {
|
|
91342
91532
|
return this.entries.length;
|
|
91343
91533
|
}
|
|
@@ -91443,8 +91633,8 @@ function escapeHtml$1(s) {
|
|
|
91443
91633
|
//#region src/google-broker-settings-schema.ts
|
|
91444
91634
|
/**
|
|
91445
91635
|
* Per-kind broker creation/edit form for kind=`google`.
|
|
91446
|
-
* Account linking (Sign in) is driven by `
|
|
91447
|
-
*
|
|
91636
|
+
* Account linking (Sign in) is driven by the kind's `connectPath`
|
|
91637
|
+
* on the instances page — not a hard-coded Connect link in this form.
|
|
91448
91638
|
*/
|
|
91449
91639
|
function consoleLinksHtml() {
|
|
91450
91640
|
return [
|
|
@@ -92070,12 +92260,15 @@ function asTestResult(r) {
|
|
|
92070
92260
|
function buildGoogleInstances(records, deps) {
|
|
92071
92261
|
return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
|
|
92072
92262
|
description: {
|
|
92263
|
+
role: "services",
|
|
92073
92264
|
instanceMode: "multiple",
|
|
92074
92265
|
kind: "account",
|
|
92075
92266
|
canTest: true,
|
|
92076
92267
|
kinds: [{
|
|
92077
92268
|
id: GOOGLE_BROKER_KIND,
|
|
92078
92269
|
label: GOOGLE_BROKER_KIND_LABEL,
|
|
92270
|
+
functionCap: "broker",
|
|
92271
|
+
brokerKind: GOOGLE_BROKER_KIND,
|
|
92079
92272
|
connectPath: deps.connectPath,
|
|
92080
92273
|
disconnectPath: deps.disconnectPath
|
|
92081
92274
|
}],
|
|
@@ -92500,7 +92693,6 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
92500
92693
|
}
|
|
92501
92694
|
/** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
|
|
92502
92695
|
buildInstances(records) {
|
|
92503
|
-
const brokerProvider = this.buildBrokerProvider();
|
|
92504
92696
|
return buildGoogleInstances(records, {
|
|
92505
92697
|
connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
|
|
92506
92698
|
disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`,
|
|
@@ -92513,131 +92705,19 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
92513
92705
|
this.registry.seedFrom(this.activeBrokers, accounts);
|
|
92514
92706
|
await this.persistOauthAccounts();
|
|
92515
92707
|
},
|
|
92516
|
-
testSaved:
|
|
92517
|
-
testClient:
|
|
92518
|
-
kind: GOOGLE_BROKER_KIND,
|
|
92519
|
-
settings: {
|
|
92520
|
-
clientId,
|
|
92521
|
-
clientSecret
|
|
92522
|
-
}
|
|
92523
|
-
})
|
|
92708
|
+
testSaved: (id) => this.probeSavedAccount(id),
|
|
92709
|
+
testClient: (clientId, clientSecret) => this.probeOauthClient(clientId, clientSecret)
|
|
92524
92710
|
});
|
|
92525
92711
|
}
|
|
92712
|
+
/**
|
|
92713
|
+
* The `broker` cap is the bus only (D1013): the accounts are the `integration-instances`
|
|
92714
|
+
* provider; calendar and mail go through their own caps, so publish and subscribe are refused.
|
|
92715
|
+
*/
|
|
92526
92716
|
buildBrokerProvider() {
|
|
92527
|
-
const ownerAddonId = this.ctx.id;
|
|
92528
|
-
const owns = (id) => this.registry.getEntry(id) !== null;
|
|
92529
92717
|
const notSupported = async () => {
|
|
92530
92718
|
throw new Error("google-services broker: publish/subscribe/getState are not supported — use calendar-source / smtp-provider");
|
|
92531
92719
|
};
|
|
92532
92720
|
return {
|
|
92533
|
-
list: async ({ kind }) => this.registry.listBrokerInfos(ownerAddonId, kind),
|
|
92534
|
-
get: async ({ id }) => {
|
|
92535
|
-
const entry = this.registry.getEntry(id);
|
|
92536
|
-
return entry ? this.registry.toBrokerInfo(entry, ownerAddonId) : null;
|
|
92537
|
-
},
|
|
92538
|
-
listProviders: async () => [{
|
|
92539
|
-
addonId: ownerAddonId,
|
|
92540
|
-
kinds: [{
|
|
92541
|
-
kind: GOOGLE_BROKER_KIND,
|
|
92542
|
-
label: GOOGLE_BROKER_KIND_LABEL,
|
|
92543
|
-
interactiveConnectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
|
|
92544
|
-
interactiveDisconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`
|
|
92545
|
-
}]
|
|
92546
|
-
}],
|
|
92547
|
-
add: async ({ kind, name, settings, id }) => {
|
|
92548
|
-
if (kind !== "google") throw new Error(`google-services: only kind '${GOOGLE_BROKER_KIND}' is handled here (got '${kind}')`);
|
|
92549
|
-
const entry = this.registry.createEntry(name, settings, id !== void 0 ? { id } : {});
|
|
92550
|
-
await this.persistBrokers();
|
|
92551
|
-
return { id: entry.id };
|
|
92552
|
-
},
|
|
92553
|
-
remove: async ({ id }) => {
|
|
92554
|
-
if (!owns(id)) return;
|
|
92555
|
-
this.registry.removeEntry(id);
|
|
92556
|
-
await this.persistBrokers();
|
|
92557
|
-
await this.persistOauthAccounts();
|
|
92558
|
-
},
|
|
92559
|
-
testConnection: async ({ id }) => {
|
|
92560
|
-
if (!owns(id)) return {
|
|
92561
|
-
ok: false,
|
|
92562
|
-
error: "unknown broker"
|
|
92563
|
-
};
|
|
92564
|
-
const entry = this.registry.getEntry(id);
|
|
92565
|
-
if (entry === null) return {
|
|
92566
|
-
ok: false,
|
|
92567
|
-
error: "unknown broker"
|
|
92568
|
-
};
|
|
92569
|
-
if (!entry.clientId || !entry.clientSecret) return {
|
|
92570
|
-
ok: false,
|
|
92571
|
-
error: "Configure Client ID and Client secret first"
|
|
92572
|
-
};
|
|
92573
|
-
let redirectUri;
|
|
92574
|
-
try {
|
|
92575
|
-
redirectUri = await this.resolveRedirectUri();
|
|
92576
|
-
} catch (err) {
|
|
92577
|
-
return {
|
|
92578
|
-
ok: false,
|
|
92579
|
-
error: err instanceof Error ? err.message : String(err)
|
|
92580
|
-
};
|
|
92581
|
-
}
|
|
92582
|
-
if (!this.registry.isConnected(id)) return probeGoogleOauthClient({
|
|
92583
|
-
clientId: entry.clientId,
|
|
92584
|
-
clientSecret: entry.clientSecret,
|
|
92585
|
-
redirectUri
|
|
92586
|
-
});
|
|
92587
|
-
try {
|
|
92588
|
-
const started = Date.now();
|
|
92589
|
-
const token = await this.ensureAccessToken(id, { forceRefresh: true });
|
|
92590
|
-
if (token.trim().length === 0) return {
|
|
92591
|
-
ok: false,
|
|
92592
|
-
error: "No access token after refresh — Sign out and Sign in again"
|
|
92593
|
-
};
|
|
92594
|
-
let res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${token}` } });
|
|
92595
|
-
if (res.status === 401) {
|
|
92596
|
-
const retryToken = await this.ensureAccessToken(id, { forceRefresh: true });
|
|
92597
|
-
res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${retryToken}` } });
|
|
92598
|
-
}
|
|
92599
|
-
if (!res.ok) {
|
|
92600
|
-
const text = await res.text();
|
|
92601
|
-
return {
|
|
92602
|
-
ok: false,
|
|
92603
|
-
error: `Google Calendar probe failed (${res.status}): ${text.slice(0, 200)}`
|
|
92604
|
-
};
|
|
92605
|
-
}
|
|
92606
|
-
const email = await this.fetchAccountEmail(token);
|
|
92607
|
-
if (email.length > 0) {
|
|
92608
|
-
const oauth = this.registry.getOauth(id);
|
|
92609
|
-
if (oauth !== null && oauth.accountEmail !== email) {
|
|
92610
|
-
this.registry.setOauth({
|
|
92611
|
-
...oauth,
|
|
92612
|
-
accountEmail: email
|
|
92613
|
-
});
|
|
92614
|
-
await this.persistOauthAccounts();
|
|
92615
|
-
}
|
|
92616
|
-
}
|
|
92617
|
-
return {
|
|
92618
|
-
ok: true,
|
|
92619
|
-
latencyMs: Date.now() - started
|
|
92620
|
-
};
|
|
92621
|
-
} catch (err) {
|
|
92622
|
-
return {
|
|
92623
|
-
ok: false,
|
|
92624
|
-
error: err instanceof Error ? err.message : String(err)
|
|
92625
|
-
};
|
|
92626
|
-
}
|
|
92627
|
-
},
|
|
92628
|
-
getSettings: async ({ id }) => {
|
|
92629
|
-
const entry = this.registry.getEntry(id);
|
|
92630
|
-
if (entry === null) return null;
|
|
92631
|
-
return {
|
|
92632
|
-
...brokerEntryToSettings(entry),
|
|
92633
|
-
_calendarCatalog: await this.loadStampedCalendarCatalog(id)
|
|
92634
|
-
};
|
|
92635
|
-
},
|
|
92636
|
-
setSettings: async ({ id, settings }) => {
|
|
92637
|
-
if (!owns(id)) return;
|
|
92638
|
-
this.registry.updateEntry(id, settings);
|
|
92639
|
-
await this.persistBrokers();
|
|
92640
|
-
},
|
|
92641
92721
|
getBrokerConfig: async ({ id }) => {
|
|
92642
92722
|
const entry = this.registry.getEntry(id);
|
|
92643
92723
|
if (entry === null) return null;
|
|
@@ -92649,33 +92729,6 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
92649
92729
|
connected: this.registry.isConnected(id)
|
|
92650
92730
|
};
|
|
92651
92731
|
},
|
|
92652
|
-
getSettingsSchema: async ({ kind }) => {
|
|
92653
|
-
if (kind !== "google") return null;
|
|
92654
|
-
return buildGoogleBrokerSettingsSchema(this.publicOrigin);
|
|
92655
|
-
},
|
|
92656
|
-
testSettings: async ({ kind, settings }) => {
|
|
92657
|
-
if (kind !== "google") return {
|
|
92658
|
-
ok: false,
|
|
92659
|
-
error: `unsupported kind: ${kind}`
|
|
92660
|
-
};
|
|
92661
|
-
try {
|
|
92662
|
-
const parsed = object({
|
|
92663
|
-
clientId: string().min(1),
|
|
92664
|
-
clientSecret: string().min(1).meta({ secret: true })
|
|
92665
|
-
}).parse(settings);
|
|
92666
|
-
const redirectUri = await this.resolveRedirectUri();
|
|
92667
|
-
return probeGoogleOauthClient({
|
|
92668
|
-
clientId: parsed.clientId,
|
|
92669
|
-
clientSecret: parsed.clientSecret,
|
|
92670
|
-
redirectUri
|
|
92671
|
-
});
|
|
92672
|
-
} catch (err) {
|
|
92673
|
-
return {
|
|
92674
|
-
ok: false,
|
|
92675
|
-
error: err instanceof Error ? err.message : String(err)
|
|
92676
|
-
};
|
|
92677
|
-
}
|
|
92678
|
-
},
|
|
92679
92732
|
publish: notSupported,
|
|
92680
92733
|
subscribe: notSupported,
|
|
92681
92734
|
unsubscribe: notSupported,
|
|
@@ -92686,6 +92739,95 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
92686
92739
|
})
|
|
92687
92740
|
};
|
|
92688
92741
|
}
|
|
92742
|
+
/** Probe a saved account: the OAuth client, then (when signed in) a Calendar call. */
|
|
92743
|
+
async probeSavedAccount(id) {
|
|
92744
|
+
const entry = this.registry.getEntry(id);
|
|
92745
|
+
if (entry === null) return {
|
|
92746
|
+
ok: false,
|
|
92747
|
+
error: "unknown broker"
|
|
92748
|
+
};
|
|
92749
|
+
if (!entry.clientId || !entry.clientSecret) return {
|
|
92750
|
+
ok: false,
|
|
92751
|
+
error: "Configure Client ID and Client secret first"
|
|
92752
|
+
};
|
|
92753
|
+
let redirectUri;
|
|
92754
|
+
try {
|
|
92755
|
+
redirectUri = await this.resolveRedirectUri();
|
|
92756
|
+
} catch (err) {
|
|
92757
|
+
return {
|
|
92758
|
+
ok: false,
|
|
92759
|
+
error: err instanceof Error ? err.message : String(err)
|
|
92760
|
+
};
|
|
92761
|
+
}
|
|
92762
|
+
if (!this.registry.isConnected(id)) return probeGoogleOauthClient({
|
|
92763
|
+
clientId: entry.clientId,
|
|
92764
|
+
clientSecret: entry.clientSecret,
|
|
92765
|
+
redirectUri
|
|
92766
|
+
});
|
|
92767
|
+
try {
|
|
92768
|
+
const started = Date.now();
|
|
92769
|
+
const token = await this.ensureAccessToken(id, { forceRefresh: true });
|
|
92770
|
+
if (token.trim().length === 0) return {
|
|
92771
|
+
ok: false,
|
|
92772
|
+
error: "No access token after refresh — Sign out and Sign in again"
|
|
92773
|
+
};
|
|
92774
|
+
let res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${token}` } });
|
|
92775
|
+
if (res.status === 401) {
|
|
92776
|
+
const retryToken = await this.ensureAccessToken(id, { forceRefresh: true });
|
|
92777
|
+
res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${retryToken}` } });
|
|
92778
|
+
}
|
|
92779
|
+
if (!res.ok) {
|
|
92780
|
+
const text = await res.text();
|
|
92781
|
+
return {
|
|
92782
|
+
ok: false,
|
|
92783
|
+
error: `Google Calendar probe failed (${res.status}): ${text.slice(0, 200)}`
|
|
92784
|
+
};
|
|
92785
|
+
}
|
|
92786
|
+
const email = await this.fetchAccountEmail(token);
|
|
92787
|
+
if (email.length > 0) {
|
|
92788
|
+
const oauth = this.registry.getOauth(id);
|
|
92789
|
+
if (oauth !== null && oauth.accountEmail !== email) {
|
|
92790
|
+
this.registry.setOauth({
|
|
92791
|
+
...oauth,
|
|
92792
|
+
accountEmail: email
|
|
92793
|
+
});
|
|
92794
|
+
await this.persistOauthAccounts();
|
|
92795
|
+
}
|
|
92796
|
+
}
|
|
92797
|
+
return {
|
|
92798
|
+
ok: true,
|
|
92799
|
+
latencyMs: Date.now() - started
|
|
92800
|
+
};
|
|
92801
|
+
} catch (err) {
|
|
92802
|
+
return {
|
|
92803
|
+
ok: false,
|
|
92804
|
+
error: err instanceof Error ? err.message : String(err)
|
|
92805
|
+
};
|
|
92806
|
+
}
|
|
92807
|
+
}
|
|
92808
|
+
/** Probe an OAuth client that is not saved yet. */
|
|
92809
|
+
async probeOauthClient(clientId, clientSecret) {
|
|
92810
|
+
try {
|
|
92811
|
+
const parsed = object({
|
|
92812
|
+
clientId: string().min(1),
|
|
92813
|
+
clientSecret: string().min(1).meta({ secret: true })
|
|
92814
|
+
}).parse({
|
|
92815
|
+
clientId,
|
|
92816
|
+
clientSecret
|
|
92817
|
+
});
|
|
92818
|
+
const redirectUri = await this.resolveRedirectUri();
|
|
92819
|
+
return probeGoogleOauthClient({
|
|
92820
|
+
clientId: parsed.clientId,
|
|
92821
|
+
clientSecret: parsed.clientSecret,
|
|
92822
|
+
redirectUri
|
|
92823
|
+
});
|
|
92824
|
+
} catch (err) {
|
|
92825
|
+
return {
|
|
92826
|
+
ok: false,
|
|
92827
|
+
error: err instanceof Error ? err.message : String(err)
|
|
92828
|
+
};
|
|
92829
|
+
}
|
|
92830
|
+
}
|
|
92689
92831
|
async handleStart(req, reply) {
|
|
92690
92832
|
const brokerIdRaw = req.query["brokerId"];
|
|
92691
92833
|
const brokerId = typeof brokerIdRaw === "string" ? brokerIdRaw.trim() : "";
|