@camstack/addon-google-services 0.2.37 → 0.2.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/google-services.addon.js +103 -14
- package/dist/google-services.addon.mjs +103 -14
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.38] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The integration states its role (devices, services or both) to the Integrations page
|
|
15
|
+
|
|
10
16
|
## [0.2.37] - 2026-11-04
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -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-BaWhXZFm.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).
|
|
@@ -8821,6 +8821,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8821
8821
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8822
8822
|
* (`auth.accessToken`, `headers[].value`).
|
|
8823
8823
|
*
|
|
8824
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8825
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8826
|
+
* key releases exactly that key's secret.
|
|
8827
|
+
*
|
|
8824
8828
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8825
8829
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8826
8830
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8833,11 +8837,13 @@ function parseSecretPath(path) {
|
|
|
8833
8837
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8834
8838
|
return path.split(".").map((raw) => {
|
|
8835
8839
|
const each = raw.endsWith("[]");
|
|
8836
|
-
const
|
|
8840
|
+
const map = raw.endsWith("{}");
|
|
8841
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8837
8842
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8838
8843
|
return {
|
|
8839
8844
|
key,
|
|
8840
|
-
each
|
|
8845
|
+
each,
|
|
8846
|
+
map
|
|
8841
8847
|
};
|
|
8842
8848
|
});
|
|
8843
8849
|
}
|
|
@@ -8889,6 +8895,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8889
8895
|
[segment.key]: mapped
|
|
8890
8896
|
};
|
|
8891
8897
|
}
|
|
8898
|
+
if (segment.map) {
|
|
8899
|
+
if (!isRecord$4(child)) return node;
|
|
8900
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8901
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8902
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8903
|
+
return [mapKey, visit({
|
|
8904
|
+
declared,
|
|
8905
|
+
concrete,
|
|
8906
|
+
value
|
|
8907
|
+
})];
|
|
8908
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8909
|
+
return {
|
|
8910
|
+
...node,
|
|
8911
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8912
|
+
};
|
|
8913
|
+
}
|
|
8892
8914
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8893
8915
|
if (last) {
|
|
8894
8916
|
const replacement = visit({
|
|
@@ -9030,7 +9052,8 @@ function isZodSecret(schema) {
|
|
|
9030
9052
|
}
|
|
9031
9053
|
/**
|
|
9032
9054
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9033
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9055
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9056
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9034
9057
|
* object or array is reported as one path and not descended into — everything
|
|
9035
9058
|
* under it is secret.
|
|
9036
9059
|
*/
|
|
@@ -9041,6 +9064,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9041
9064
|
const def = defOf(layer);
|
|
9042
9065
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9043
9066
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9067
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9044
9068
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9045
9069
|
else if (def.type === "intersection") {
|
|
9046
9070
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13365,11 +13389,49 @@ function buildCapturingReply(envelope) {
|
|
|
13365
13389
|
};
|
|
13366
13390
|
return wrapper;
|
|
13367
13391
|
}
|
|
13392
|
+
/**
|
|
13393
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13394
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13395
|
+
*
|
|
13396
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13397
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13398
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13399
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13400
|
+
*
|
|
13401
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13402
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13403
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13404
|
+
*/
|
|
13368
13405
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13369
13406
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13370
13407
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13371
13408
|
function topLevelSecretKeys(paths) {
|
|
13372
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13409
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13410
|
+
}
|
|
13411
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13412
|
+
function secretMapKeys(paths) {
|
|
13413
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13414
|
+
}
|
|
13415
|
+
/**
|
|
13416
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13417
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13418
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13419
|
+
*/
|
|
13420
|
+
function secretMapPatch(value) {
|
|
13421
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13422
|
+
const out = {};
|
|
13423
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13424
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13425
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13426
|
+
}
|
|
13427
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13428
|
+
function declaresEnabled(kind) {
|
|
13429
|
+
const json = toJSONSchema(kind.schema, {
|
|
13430
|
+
unrepresentable: "any",
|
|
13431
|
+
io: "input"
|
|
13432
|
+
});
|
|
13433
|
+
const properties = Reflect.get(json, "properties");
|
|
13434
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13373
13435
|
}
|
|
13374
13436
|
var RecordIntegrationInstances = class {
|
|
13375
13437
|
records;
|
|
@@ -13377,14 +13439,20 @@ var RecordIntegrationInstances = class {
|
|
|
13377
13439
|
hooks;
|
|
13378
13440
|
handle;
|
|
13379
13441
|
secretKeys;
|
|
13442
|
+
secretMaps;
|
|
13380
13443
|
nameField;
|
|
13444
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13445
|
+
registryOwnsEnabled;
|
|
13381
13446
|
constructor(records, kind, hooks) {
|
|
13382
13447
|
this.records = records;
|
|
13383
13448
|
this.kind = kind;
|
|
13384
13449
|
this.hooks = hooks;
|
|
13385
13450
|
this.handle = records.kind(kind);
|
|
13386
13451
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13452
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13387
13453
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13454
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13455
|
+
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
13456
|
}
|
|
13389
13457
|
/**
|
|
13390
13458
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13435,7 +13503,7 @@ var RecordIntegrationInstances = class {
|
|
|
13435
13503
|
id: record.id,
|
|
13436
13504
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13437
13505
|
configured: this.hooks.configured(record.data),
|
|
13438
|
-
enabled: record.data.enabled !== false,
|
|
13506
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13439
13507
|
formValues: shown,
|
|
13440
13508
|
...kind !== void 0 ? { kind } : {},
|
|
13441
13509
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13465,32 +13533,36 @@ var RecordIntegrationInstances = class {
|
|
|
13465
13533
|
id: minted,
|
|
13466
13534
|
data: this.hooks.initialBody(name, kind)
|
|
13467
13535
|
}, { ifRev: 0 });
|
|
13468
|
-
await this.hooks.onChanged?.(minted);
|
|
13536
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13469
13537
|
return { id: minted };
|
|
13470
13538
|
};
|
|
13471
13539
|
updateInstance = async ({ id, name, values }) => {
|
|
13472
13540
|
const patch = {};
|
|
13473
13541
|
for (const [key, value] of Object.entries(values)) {
|
|
13474
13542
|
if (value === void 0) continue;
|
|
13475
|
-
if (
|
|
13543
|
+
if (this.secretMaps.has(key)) {
|
|
13544
|
+
const entries = secretMapPatch(value);
|
|
13545
|
+
if (entries !== null) patch[key] = entries;
|
|
13546
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13476
13547
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13477
13548
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13478
13549
|
}
|
|
13479
13550
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13480
13551
|
await this.handle.patch(id, patch);
|
|
13481
|
-
await this.hooks.onChanged?.(id);
|
|
13552
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13482
13553
|
return { ok: true };
|
|
13483
13554
|
};
|
|
13484
13555
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13485
13556
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13557
|
+
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
13558
|
await this.handle.patch(id, { enabled });
|
|
13487
|
-
await this.hooks.onChanged?.(id);
|
|
13559
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13488
13560
|
return { ok: true };
|
|
13489
13561
|
};
|
|
13490
13562
|
/** The row and its keychain refs go together (the records door). */
|
|
13491
13563
|
deleteInstance = async ({ id }) => {
|
|
13492
13564
|
await this.handle.delete(id);
|
|
13493
|
-
await this.hooks.onChanged?.(id);
|
|
13565
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13494
13566
|
return { ok: true };
|
|
13495
13567
|
};
|
|
13496
13568
|
testInstance = async (input) => {
|
|
@@ -54137,6 +54209,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54137
54209
|
disconnectPath: string().min(1).optional()
|
|
54138
54210
|
});
|
|
54139
54211
|
var IntegrationInstanceDescriptionSchema = object({
|
|
54212
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
54213
|
+
role: _enum([
|
|
54214
|
+
"devices",
|
|
54215
|
+
"services",
|
|
54216
|
+
"both"
|
|
54217
|
+
]),
|
|
54218
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
54219
|
+
discovery: object({
|
|
54220
|
+
mode: _enum([
|
|
54221
|
+
"none",
|
|
54222
|
+
"lan",
|
|
54223
|
+
"account"
|
|
54224
|
+
]),
|
|
54225
|
+
scopedToInstance: boolean()
|
|
54226
|
+
}).optional(),
|
|
54140
54227
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
54141
54228
|
instanceMode: IntegrationInstanceModeSchema,
|
|
54142
54229
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -54176,10 +54263,11 @@ var IntegrationInstanceSchema = object({
|
|
|
54176
54263
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
54177
54264
|
configured: boolean(),
|
|
54178
54265
|
/**
|
|
54179
|
-
* The instance's own on/off switch (D996)
|
|
54180
|
-
*
|
|
54266
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
54267
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
54268
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
54181
54269
|
*/
|
|
54182
|
-
enabled: boolean(),
|
|
54270
|
+
enabled: boolean().optional(),
|
|
54183
54271
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
54184
54272
|
kind: string().optional(),
|
|
54185
54273
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -92070,6 +92158,7 @@ function asTestResult(r) {
|
|
|
92070
92158
|
function buildGoogleInstances(records, deps) {
|
|
92071
92159
|
return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
|
|
92072
92160
|
description: {
|
|
92161
|
+
role: "services",
|
|
92073
92162
|
instanceMode: "multiple",
|
|
92074
92163
|
kind: "account",
|
|
92075
92164
|
canTest: true,
|
|
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
|
|
|
5981
5981
|
var ZodFirstPartyTypeKind;
|
|
5982
5982
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5983
5983
|
//#endregion
|
|
5984
|
-
//#region ../types/dist/sleep-
|
|
5984
|
+
//#region ../types/dist/sleep-BaWhXZFm.mjs
|
|
5985
5985
|
/**
|
|
5986
5986
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5987
5987
|
* window to float samples (D455).
|
|
@@ -8817,6 +8817,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8817
8817
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8818
8818
|
* (`auth.accessToken`, `headers[].value`).
|
|
8819
8819
|
*
|
|
8820
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8821
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8822
|
+
* key releases exactly that key's secret.
|
|
8823
|
+
*
|
|
8820
8824
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8821
8825
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8822
8826
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8829,11 +8833,13 @@ function parseSecretPath(path) {
|
|
|
8829
8833
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8830
8834
|
return path.split(".").map((raw) => {
|
|
8831
8835
|
const each = raw.endsWith("[]");
|
|
8832
|
-
const
|
|
8836
|
+
const map = raw.endsWith("{}");
|
|
8837
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8833
8838
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8834
8839
|
return {
|
|
8835
8840
|
key,
|
|
8836
|
-
each
|
|
8841
|
+
each,
|
|
8842
|
+
map
|
|
8837
8843
|
};
|
|
8838
8844
|
});
|
|
8839
8845
|
}
|
|
@@ -8885,6 +8891,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8885
8891
|
[segment.key]: mapped
|
|
8886
8892
|
};
|
|
8887
8893
|
}
|
|
8894
|
+
if (segment.map) {
|
|
8895
|
+
if (!isRecord$4(child)) return node;
|
|
8896
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8897
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8898
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8899
|
+
return [mapKey, visit({
|
|
8900
|
+
declared,
|
|
8901
|
+
concrete,
|
|
8902
|
+
value
|
|
8903
|
+
})];
|
|
8904
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8905
|
+
return {
|
|
8906
|
+
...node,
|
|
8907
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8908
|
+
};
|
|
8909
|
+
}
|
|
8888
8910
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8889
8911
|
if (last) {
|
|
8890
8912
|
const replacement = visit({
|
|
@@ -9026,7 +9048,8 @@ function isZodSecret(schema) {
|
|
|
9026
9048
|
}
|
|
9027
9049
|
/**
|
|
9028
9050
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9029
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9051
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9052
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9030
9053
|
* object or array is reported as one path and not descended into — everything
|
|
9031
9054
|
* under it is secret.
|
|
9032
9055
|
*/
|
|
@@ -9037,6 +9060,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9037
9060
|
const def = defOf(layer);
|
|
9038
9061
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9039
9062
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9063
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9040
9064
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9041
9065
|
else if (def.type === "intersection") {
|
|
9042
9066
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13361,11 +13385,49 @@ function buildCapturingReply(envelope) {
|
|
|
13361
13385
|
};
|
|
13362
13386
|
return wrapper;
|
|
13363
13387
|
}
|
|
13388
|
+
/**
|
|
13389
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13390
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13391
|
+
*
|
|
13392
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13393
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13394
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13395
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13396
|
+
*
|
|
13397
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13398
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13399
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13400
|
+
*/
|
|
13364
13401
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13365
13402
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13366
13403
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13367
13404
|
function topLevelSecretKeys(paths) {
|
|
13368
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13405
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13406
|
+
}
|
|
13407
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13408
|
+
function secretMapKeys(paths) {
|
|
13409
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13410
|
+
}
|
|
13411
|
+
/**
|
|
13412
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13413
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13414
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13415
|
+
*/
|
|
13416
|
+
function secretMapPatch(value) {
|
|
13417
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13418
|
+
const out = {};
|
|
13419
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13420
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13421
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13422
|
+
}
|
|
13423
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13424
|
+
function declaresEnabled(kind) {
|
|
13425
|
+
const json = toJSONSchema(kind.schema, {
|
|
13426
|
+
unrepresentable: "any",
|
|
13427
|
+
io: "input"
|
|
13428
|
+
});
|
|
13429
|
+
const properties = Reflect.get(json, "properties");
|
|
13430
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13369
13431
|
}
|
|
13370
13432
|
var RecordIntegrationInstances = class {
|
|
13371
13433
|
records;
|
|
@@ -13373,14 +13435,20 @@ var RecordIntegrationInstances = class {
|
|
|
13373
13435
|
hooks;
|
|
13374
13436
|
handle;
|
|
13375
13437
|
secretKeys;
|
|
13438
|
+
secretMaps;
|
|
13376
13439
|
nameField;
|
|
13440
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13441
|
+
registryOwnsEnabled;
|
|
13377
13442
|
constructor(records, kind, hooks) {
|
|
13378
13443
|
this.records = records;
|
|
13379
13444
|
this.kind = kind;
|
|
13380
13445
|
this.hooks = hooks;
|
|
13381
13446
|
this.handle = records.kind(kind);
|
|
13382
13447
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13448
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13383
13449
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13450
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13451
|
+
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"`);
|
|
13384
13452
|
}
|
|
13385
13453
|
/**
|
|
13386
13454
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13431,7 +13499,7 @@ var RecordIntegrationInstances = class {
|
|
|
13431
13499
|
id: record.id,
|
|
13432
13500
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13433
13501
|
configured: this.hooks.configured(record.data),
|
|
13434
|
-
enabled: record.data.enabled !== false,
|
|
13502
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13435
13503
|
formValues: shown,
|
|
13436
13504
|
...kind !== void 0 ? { kind } : {},
|
|
13437
13505
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13461,32 +13529,36 @@ var RecordIntegrationInstances = class {
|
|
|
13461
13529
|
id: minted,
|
|
13462
13530
|
data: this.hooks.initialBody(name, kind)
|
|
13463
13531
|
}, { ifRev: 0 });
|
|
13464
|
-
await this.hooks.onChanged?.(minted);
|
|
13532
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13465
13533
|
return { id: minted };
|
|
13466
13534
|
};
|
|
13467
13535
|
updateInstance = async ({ id, name, values }) => {
|
|
13468
13536
|
const patch = {};
|
|
13469
13537
|
for (const [key, value] of Object.entries(values)) {
|
|
13470
13538
|
if (value === void 0) continue;
|
|
13471
|
-
if (
|
|
13539
|
+
if (this.secretMaps.has(key)) {
|
|
13540
|
+
const entries = secretMapPatch(value);
|
|
13541
|
+
if (entries !== null) patch[key] = entries;
|
|
13542
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13472
13543
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13473
13544
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13474
13545
|
}
|
|
13475
13546
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13476
13547
|
await this.handle.patch(id, patch);
|
|
13477
|
-
await this.hooks.onChanged?.(id);
|
|
13548
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13478
13549
|
return { ok: true };
|
|
13479
13550
|
};
|
|
13480
13551
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13481
13552
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13553
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13482
13554
|
await this.handle.patch(id, { enabled });
|
|
13483
|
-
await this.hooks.onChanged?.(id);
|
|
13555
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13484
13556
|
return { ok: true };
|
|
13485
13557
|
};
|
|
13486
13558
|
/** The row and its keychain refs go together (the records door). */
|
|
13487
13559
|
deleteInstance = async ({ id }) => {
|
|
13488
13560
|
await this.handle.delete(id);
|
|
13489
|
-
await this.hooks.onChanged?.(id);
|
|
13561
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13490
13562
|
return { ok: true };
|
|
13491
13563
|
};
|
|
13492
13564
|
testInstance = async (input) => {
|
|
@@ -54133,6 +54205,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
54133
54205
|
disconnectPath: string().min(1).optional()
|
|
54134
54206
|
});
|
|
54135
54207
|
var IntegrationInstanceDescriptionSchema = object({
|
|
54208
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
54209
|
+
role: _enum([
|
|
54210
|
+
"devices",
|
|
54211
|
+
"services",
|
|
54212
|
+
"both"
|
|
54213
|
+
]),
|
|
54214
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
54215
|
+
discovery: object({
|
|
54216
|
+
mode: _enum([
|
|
54217
|
+
"none",
|
|
54218
|
+
"lan",
|
|
54219
|
+
"account"
|
|
54220
|
+
]),
|
|
54221
|
+
scopedToInstance: boolean()
|
|
54222
|
+
}).optional(),
|
|
54136
54223
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
54137
54224
|
instanceMode: IntegrationInstanceModeSchema,
|
|
54138
54225
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -54172,10 +54259,11 @@ var IntegrationInstanceSchema = object({
|
|
|
54172
54259
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
54173
54260
|
configured: boolean(),
|
|
54174
54261
|
/**
|
|
54175
|
-
* The instance's own on/off switch (D996)
|
|
54176
|
-
*
|
|
54262
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
54263
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
54264
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
54177
54265
|
*/
|
|
54178
|
-
enabled: boolean(),
|
|
54266
|
+
enabled: boolean().optional(),
|
|
54179
54267
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
54180
54268
|
kind: string().optional(),
|
|
54181
54269
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -92066,6 +92154,7 @@ function asTestResult(r) {
|
|
|
92066
92154
|
function buildGoogleInstances(records, deps) {
|
|
92067
92155
|
return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
|
|
92068
92156
|
description: {
|
|
92157
|
+
role: "services",
|
|
92069
92158
|
instanceMode: "multiple",
|
|
92070
92159
|
kind: "account",
|
|
92071
92160
|
canTest: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-google-services",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
4
4
|
"description": "Google — Google brokers (OAuth2): Calendar as calendar-source, Gmail send as smtp-provider.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
}
|
|
65
65
|
],
|
|
66
66
|
"requires": {
|
|
67
|
-
"server": ">=1.2.
|
|
67
|
+
"server": ">=1.2.762"
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
70
|
"files": [
|