@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 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-BkzQdpzF.mjs
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 key = each ? raw.slice(0, -2) : raw;
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`). A declared
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 (!this.secretKeys.has(key)) patch[key] = value;
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): always present, derived from the record body
54180
- * (`enabled !== false`, so a row written before the field existed is on).
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-BkzQdpzF.mjs
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 key = each ? raw.slice(0, -2) : raw;
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`). A declared
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 (!this.secretKeys.has(key)) patch[key] = value;
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): always present, derived from the record body
54176
- * (`enabled !== false`, so a row written before the field existed is on).
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.37",
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.761"
67
+ "server": ">=1.2.762"
68
68
  }
69
69
  },
70
70
  "files": [