@camstack/addon-google-services 0.2.39 → 0.2.40

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.
@@ -13521,6 +13521,39 @@ function buildCapturingReply(envelope) {
13521
13521
  return wrapper;
13522
13522
  }
13523
13523
  /**
13524
+ * The link primitive's declaration half (D1013 rule 5): what a link field names and how a kind
13525
+ * records it. Split from `instance-link.ts` so `RecordIntegrationInstances` can read the links of
13526
+ * its kind without importing the builders that import it.
13527
+ */
13528
+ /** The body field of a link: the id of the instance it names; the target rides in the field meta. */
13529
+ function linkField(target) {
13530
+ return string().meta({ link: target });
13531
+ }
13532
+ function linkTargetOf(meta) {
13533
+ if (typeof meta !== "object" || meta === null) return void 0;
13534
+ const link = Reflect.get(meta, "link");
13535
+ const parsed = LinkTargetSchema.safeParse(link);
13536
+ return parsed.success ? parsed.data : void 0;
13537
+ }
13538
+ var LinkTargetSchema = object({
13539
+ cap: string().min(1),
13540
+ kind: string().min(1).optional()
13541
+ });
13542
+ /** The links a kind declares, read from the meta of its body fields. */
13543
+ function linksOfKind(kind) {
13544
+ const schema = kind.schema;
13545
+ if (!(schema instanceof ZodObject)) return [];
13546
+ const links = [];
13547
+ for (const [field, fieldSchema] of Object.entries(schema.shape)) {
13548
+ const to = linkTargetOf(globalRegistry.get(fieldSchema));
13549
+ if (to !== void 0) links.push({
13550
+ field,
13551
+ to
13552
+ });
13553
+ }
13554
+ return links;
13555
+ }
13556
+ /**
13524
13557
  * `RecordIntegrationInstances` - the `integration-instances` provider over ONE
13525
13558
  * entry kind, so an addon that holds a connection serves it in ~30 lines.
13526
13559
  *
@@ -13641,9 +13674,18 @@ var RecordIntegrationInstances = class {
13641
13674
  ...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
13642
13675
  };
13643
13676
  }
13644
- describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
13645
- ...this.hooks.description,
13646
- mintsId: true
13677
+ describe = async () => {
13678
+ const declared = linksOfKind(this.kind);
13679
+ const links = this.hooks.description.links !== void 0 || declared.length === 0 ? {} : { links: declared.map((link) => ({
13680
+ kind: this.kind.name,
13681
+ field: link.field,
13682
+ to: link.to
13683
+ })) };
13684
+ return {
13685
+ ...this.hooks.description,
13686
+ ...links,
13687
+ ...this.hooks.mintId === void 0 ? {} : { mintsId: true }
13688
+ };
13647
13689
  };
13648
13690
  instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
13649
13691
  listInstances = async () => {
@@ -13726,6 +13768,80 @@ var RecordIntegrationInstances = class {
13726
13768
  };
13727
13769
  }
13728
13770
  };
13771
+ /** No part holds the id a call named. */
13772
+ var UnknownInstanceError = class extends Error {
13773
+ instanceId;
13774
+ constructor(instanceId) {
13775
+ super(`Unknown instance '${instanceId}'`);
13776
+ this.instanceId = instanceId;
13777
+ this.name = "UnknownInstanceError";
13778
+ }
13779
+ };
13780
+ /** A create or a form named no kind, or a kind no part serves. */
13781
+ var UnknownInstanceKindError = class extends Error {
13782
+ kindId;
13783
+ constructor(kindId) {
13784
+ super(kindId === void 0 ? "This provider has several kinds: name one" : `Unknown instance kind '${kindId}'`);
13785
+ this.kindId = kindId;
13786
+ this.name = "UnknownInstanceKindError";
13787
+ }
13788
+ };
13789
+ function composeInstanceKinds(parts, base) {
13790
+ const byKind = (kindId) => {
13791
+ if (kindId === void 0) {
13792
+ const only = parts.length === 1 ? parts[0] : void 0;
13793
+ if (only === void 0) throw new UnknownInstanceKindError(void 0);
13794
+ return only;
13795
+ }
13796
+ const part = parts.find((candidate) => candidate.kind.id === kindId);
13797
+ if (part === void 0) throw new UnknownInstanceKindError(kindId);
13798
+ return part;
13799
+ };
13800
+ const holderOf = async (id) => {
13801
+ for (const part of parts) if (await part.provider.getInstance({ id }) !== null) return part;
13802
+ throw new UnknownInstanceError(id);
13803
+ };
13804
+ const tagged = (part, instance) => ({
13805
+ ...instance,
13806
+ kind: part.kind.id
13807
+ });
13808
+ return {
13809
+ describe: async () => {
13810
+ const links = (await Promise.all(parts.map(async (part) => (await part.provider.describe()).links ?? []))).flat();
13811
+ return {
13812
+ ...base,
13813
+ kinds: parts.map((part) => part.kind),
13814
+ ...links.length > 0 ? { links } : {}
13815
+ };
13816
+ },
13817
+ instanceFormSchema: async ({ id, kind }) => {
13818
+ return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
13819
+ },
13820
+ listInstances: async () => {
13821
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
13822
+ },
13823
+ getInstance: async ({ id }) => {
13824
+ for (const part of parts) {
13825
+ const found = await part.provider.getInstance({ id });
13826
+ if (found !== null) return tagged(part, found);
13827
+ }
13828
+ return null;
13829
+ },
13830
+ createInstance: async (input) => byKind(input.kind).provider.createInstance(input),
13831
+ updateInstance: async (input) => (await holderOf(input.id)).provider.updateInstance(input),
13832
+ setInstanceEnabled: async (input) => (await holderOf(input.id)).provider.setInstanceEnabled(input),
13833
+ deleteInstance: async (input) => (await holderOf(input.id)).provider.deleteInstance(input),
13834
+ testInstance: async (input) => {
13835
+ const test = (input.id === null ? byKind(input.kind) : await holderOf(input.id)).provider.testInstance;
13836
+ if (test === void 0) return {
13837
+ success: false,
13838
+ status: "inconclusive",
13839
+ error: "This kind cannot be tested"
13840
+ };
13841
+ return test(input);
13842
+ }
13843
+ };
13844
+ }
13729
13845
  var EncodeProfileSchema = object({
13730
13846
  video: object({
13731
13847
  codec: _enum([
@@ -54211,18 +54327,38 @@ var IntegrationInstanceKindSchema = object({
54211
54327
  /** Relative hub path to disconnect that account. */
54212
54328
  disconnectPath: string().min(1).optional()
54213
54329
  });
54330
+ /**
54331
+ * How devices of an instance are found. `none`: the operator adds them by hand; `lan`: the addon
54332
+ * scans the network before any instance exists (the wizard offers the candidates); `account`: the
54333
+ * devices appear after the instance signs in. `scopedToInstance`: a discovery run belongs to ONE
54334
+ * instance and its candidates are claimed by it.
54335
+ */
54336
+ var IntegrationInstanceDiscoverySchema = object({
54337
+ mode: _enum([
54338
+ "none",
54339
+ "lan",
54340
+ "account"
54341
+ ]),
54342
+ scopedToInstance: boolean()
54343
+ });
54344
+ /**
54345
+ * One link a kind of this provider declares (D1013 rule 5): the body `field` of an instance of
54346
+ * `kind` holds the id of an instance of the function cap `to.cap` (of kind `to.kind`, when set).
54347
+ * The hub indexes these so the target's delete is refused while a link names it.
54348
+ */
54349
+ var IntegrationInstanceLinkSchema = object({
54350
+ kind: string().min(1),
54351
+ field: string().min(1),
54352
+ to: object({
54353
+ cap: string().min(1),
54354
+ kind: string().min(1).optional()
54355
+ })
54356
+ });
54214
54357
  var IntegrationInstanceDescriptionSchema = object({
54215
54358
  /** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
54216
54359
  role: IntegrationInstanceRoleSchema,
54217
54360
  /** 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(),
54361
+ discovery: IntegrationInstanceDiscoverySchema.optional(),
54226
54362
  /** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
54227
54363
  instanceMode: IntegrationInstanceModeSchema,
54228
54364
  /** What the instance is, for the UI (`destination`, `relay`, `account`). */
@@ -54243,7 +54379,9 @@ var IntegrationInstanceDescriptionSchema = object({
54243
54379
  * `createInstance` without an `id` works: the provider mints it. Absent means the caller must
54244
54380
  * send an id (the UI then mints an opaque one).
54245
54381
  */
54246
- mintsId: boolean().optional()
54382
+ mintsId: boolean().optional(),
54383
+ /** The links to other instances the provider's kinds declare; absent means none. */
54384
+ links: array(IntegrationInstanceLinkSchema).optional()
54247
54385
  });
54248
54386
  var IntegrationInstanceStatusSchema = object({
54249
54387
  state: _enum([
@@ -91234,7 +91372,165 @@ object({
91234
91372
  delay: number().nonnegative().optional(),
91235
91373
  commands: record(string(), unknown())
91236
91374
  });
91375
+ /** The `instance-select` field of a link, keyed like the body field it fills. */
91376
+ function linkFieldDescriptor(key, target, labels) {
91377
+ return {
91378
+ type: "instance-select",
91379
+ key,
91380
+ label: labels.label,
91381
+ ...labels.description !== void 0 ? { description: labels.description } : {},
91382
+ required: labels.required ?? true,
91383
+ ...labels.noneLabel !== void 0 ? { noneLabel: labels.noneLabel } : {},
91384
+ functionCap: target.cap,
91385
+ ...target.kind !== void 0 ? { kind: target.kind } : {}
91386
+ };
91387
+ }
91388
+ /**
91389
+ * Declare the record kind of a service that links to other instances. The body is
91390
+ * `{ name, ...links, ...fields }`; the links are recorded in the kind (read back by
91391
+ * {@link linksOfKind}) so the hub can index who points at what.
91392
+ */
91393
+ function declareLinkKind(spec) {
91394
+ const linkShape = {};
91395
+ for (const [field, target] of Object.entries(spec.links)) linkShape[field] = linkField(target);
91396
+ const typed = entryBody({
91397
+ ...linkShape,
91398
+ ...spec.fields
91399
+ });
91400
+ return defineEntryKind(spec.name, {
91401
+ title: spec.title,
91402
+ schema: typed,
91403
+ writeTrigger: spec.writeTrigger
91404
+ });
91405
+ }
91406
+ /** The id a record holds in a link field; undefined while it names none. */
91407
+ function linkedId(body, field) {
91408
+ const value = body[field];
91409
+ return typeof value === "string" && value !== "" ? value : void 0;
91410
+ }
91411
+ /** The `integration-instances` provider of a service whose records link to other instances. */
91412
+ function buildLinkedInstances(records, spec) {
91413
+ const links = linksOfKind(spec.kind);
91414
+ const kindId = spec.instanceKind ?? spec.kind.name;
91415
+ const instances = new RecordIntegrationInstances(records, spec.kind, {
91416
+ description: {
91417
+ ...spec.description,
91418
+ links: links.map((link) => ({
91419
+ kind: kindId,
91420
+ field: link.field,
91421
+ to: link.to
91422
+ }))
91423
+ },
91424
+ formSchema: async (id) => {
91425
+ const first = links[0];
91426
+ const chosen = id === null || first === void 0 || spec.dynamicSections === void 0 ? void 0 : linkedId(await instances.getResolved(id) ?? {}, first.field);
91427
+ const dynamic = chosen !== void 0 && spec.dynamicSections !== void 0 ? await spec.dynamicSections(chosen) : [];
91428
+ return { sections: [
91429
+ {
91430
+ id: "links",
91431
+ title: "Linked instances",
91432
+ columns: 1,
91433
+ fields: links.map((link) => linkFieldDescriptor(link.field, link.to, spec.labels?.[link.field] ?? { label: link.field }))
91434
+ },
91435
+ ...spec.linkSections ?? [],
91436
+ ...dynamic
91437
+ ] };
91438
+ },
91439
+ initialBody: (name) => spec.kind.schema.parse({
91440
+ name,
91441
+ ...Object.fromEntries(links.map((link) => [link.field, ""]))
91442
+ }),
91443
+ configured: (body) => links.every((link) => linkedId(body, link.field) !== void 0) && (spec.alsoConfigured?.(body) ?? true),
91444
+ ...spec.mintId !== void 0 ? { mintId: spec.mintId } : {},
91445
+ ...spec.onChanged !== void 0 ? { onChanged: spec.onChanged } : {}
91446
+ });
91447
+ return instances;
91448
+ }
91237
91449
  entryBody({ brokerId: string() });
91450
+ /**
91451
+ * The calendars an account imports leave the account (D1013). An account record used to carry a
91452
+ * `calendarIds` array; each calendar is now a record of its own that LINKS to the account
91453
+ * (`calendars`, one per account and calendar). For every account row with an array:
91454
+ *
91455
+ * 1. each entry becomes a calendar record under a deterministic id (an existing one is kept);
91456
+ * 2. each record is read back and must name this account and calendar;
91457
+ * 3. only then is the array cleared (a patch: nothing else on the account is touched).
91458
+ *
91459
+ * An entry that names another account, or is not a calendar id at all, selected nothing before
91460
+ * (the reader only matched its own account's ids): it is counted, named in a warning and goes with
91461
+ * the array. An array that is not a list of strings is left where it is. A failure on one account
91462
+ * does not stop the others; the run then throws, and the accounts that failed keep their array.
91463
+ * Accounts are never created or deleted. Idempotent: with the arrays cleared nothing is left to do.
91464
+ */
91465
+ /** The id of the calendar record of one (account, calendar): the same pair is always the same row. */
91466
+ function calendarRecordId(accountId, calendarId) {
91467
+ return `cal_${(0, node_crypto.createHash)("sha1").update(`${accountId}\0${calendarId}`).digest("hex").slice(0, 16)}`;
91468
+ }
91469
+ function entriesOf(value) {
91470
+ if (!Array.isArray(value)) return null;
91471
+ const out = [];
91472
+ for (const item of value) {
91473
+ if (typeof item !== "string") return null;
91474
+ out.push(item);
91475
+ }
91476
+ return out;
91477
+ }
91478
+ async function moveCalendarIdsToRecords(ports, spec) {
91479
+ let created = 0;
91480
+ let cleared = 0;
91481
+ let dropped = 0;
91482
+ const failed = [];
91483
+ for (const account of await ports.listAccounts()) {
91484
+ if (account.calendarIds === void 0 || account.calendarIds === null) continue;
91485
+ const entries = entriesOf(account.calendarIds);
91486
+ if (entries === null) {
91487
+ ports.warn(`account '${account.id}': calendarIds is not a list of ids and stays as it is`);
91488
+ failed.push(account.id);
91489
+ continue;
91490
+ }
91491
+ try {
91492
+ const own = /* @__PURE__ */ new Set();
91493
+ const inert = [];
91494
+ for (const entry of entries) {
91495
+ const calendarId = spec.calendarIdOf(account.id, entry);
91496
+ if (calendarId === null || calendarId === "") inert.push(entry);
91497
+ else own.add(calendarId);
91498
+ }
91499
+ for (const calendarId of own) {
91500
+ const recordId = calendarRecordId(account.id, calendarId);
91501
+ const body = {
91502
+ name: calendarId,
91503
+ accountId: account.id,
91504
+ calendarId
91505
+ };
91506
+ if (await ports.readCalendar(recordId) === null) {
91507
+ await ports.putCalendar(recordId, body);
91508
+ created += 1;
91509
+ }
91510
+ const back = await ports.readCalendar(recordId);
91511
+ if (back?.accountId !== account.id || back.calendarId !== calendarId) throw new Error(`account '${account.id}': the calendar record '${recordId}' did not read back; the array stays`);
91512
+ }
91513
+ if (inert.length > 0) {
91514
+ ports.warn(`account '${account.id}': ${inert.length} entr${inert.length === 1 ? "y" : "ies"} of calendarIds named another account or no calendar and selected nothing: ${inert.join(", ")}`);
91515
+ dropped += inert.length;
91516
+ }
91517
+ await ports.clearCalendarIds(account.id);
91518
+ cleared += 1;
91519
+ } catch (error) {
91520
+ ports.warn(error instanceof Error ? error.message : String(error));
91521
+ failed.push(account.id);
91522
+ }
91523
+ }
91524
+ if (failed.length > 0) throw new Error(`calendarIds kept on ${failed.length} account(s) (${failed.join(", ")}); ${created} calendar record(s) written, ${cleared} account(s) cleared`);
91525
+ return {
91526
+ outcome: created === 0 && cleared === 0 ? "already-done" : "done",
91527
+ summary: {
91528
+ created,
91529
+ cleared,
91530
+ dropped
91531
+ }
91532
+ };
91533
+ }
91238
91534
  //#endregion
91239
91535
  //#region src/google-types.ts
91240
91536
  /** One Google account connection (broker entry). Credentials for the OAuth
@@ -91245,28 +91541,16 @@ var GoogleBrokerEntrySchema = object({
91245
91541
  clientId: string().default(""),
91246
91542
  clientSecret: string().default("").meta({ secret: true }),
91247
91543
  /**
91248
- * @deprecated Per-broker filter; import selection lives on addon
91249
- * `calendarIds` (Connectivity → Calendars). Kept for migrate only.
91544
+ * @deprecated Per-broker filter; the calendars an account imports are `calendars` records
91545
+ * (Connectivity → Calendars). Kept for migrate only.
91250
91546
  */
91251
91547
  calendarFilter: string().default(""),
91252
91548
  /** Default From for Gmail send — usually the connected account email. */
91253
91549
  fromAddress: string().default(""),
91254
- fromName: string().default(""),
91255
- /**
91256
- * The stamped calendar ids (`<brokerId>:<googleCalendarId>`) this broker imports. `undefined`
91257
- * means "not moved here yet": the reader falls back to the shared `calendarIds` setting of
91258
- * the addon until the migration copied it. Empty = import none.
91259
- */
91260
- calendarIds: array(string()).optional()
91550
+ fromName: string().default("")
91261
91551
  });
91262
91552
  var GoogleServicesConfigSchema = object({
91263
91553
  brokers: array(GoogleBrokerEntrySchema).default([]),
91264
- /**
91265
- * @deprecated The shared set (`brokerId:googleCalendarId`). Each broker owns its own
91266
- * `calendarIds` now; this key is read only while a broker has none, and the migration
91267
- * `google-services/2026-11-03-drop-shared-calendar-ids` deletes it.
91268
- */
91269
- calendarIds: array(string()).default([]),
91270
91554
  /** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
91271
91555
  clientId: string().optional(),
91272
91556
  /** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
@@ -91327,34 +91611,25 @@ var OAUTH_CALLBACK_SUFFIX = `/addon/google-services/oauth/callback`;
91327
91611
  var GoogleAddSettingsSchema = object({
91328
91612
  clientId: string().min(1),
91329
91613
  clientSecret: string().min(1).meta({ secret: true }),
91330
- /** Selected Google calendar ids; empty = import all. */
91331
- calendarIds: array(string()).optional(),
91332
- /** @deprecated Prefer calendarIds — still accepted on save. */
91614
+ /** @deprecated Calendars are `calendars` records now — still accepted on save. */
91333
91615
  calendarFilter: string().optional(),
91334
91616
  fromAddress: string().optional(),
91335
91617
  fromName: string().optional()
91336
91618
  });
91337
- /** Persist UI calendarIds (or legacy filter string) as comma-separated filter. */
91338
- function calendarIdsToFilter(settings) {
91339
- if (settings.calendarIds !== void 0) return settings.calendarIds.map((s) => s.trim()).filter((s) => s.length > 0).join(",");
91340
- return settings.calendarFilter ?? "";
91341
- }
91342
91619
  function settingsToBrokerFields(settings) {
91343
91620
  const parsed = GoogleAddSettingsSchema.parse(settings);
91344
91621
  return {
91345
91622
  clientId: parsed.clientId,
91346
91623
  clientSecret: parsed.clientSecret,
91347
- calendarFilter: calendarIdsToFilter(parsed),
91624
+ calendarFilter: parsed.calendarFilter ?? "",
91348
91625
  fromAddress: parsed.fromAddress ?? "",
91349
- fromName: parsed.fromName ?? "",
91350
- ...parsed.calendarIds !== void 0 ? { calendarIds: [...parsed.calendarIds] } : {}
91626
+ fromName: parsed.fromName ?? ""
91351
91627
  };
91352
91628
  }
91353
91629
  function brokerEntryToSettings(entry) {
91354
91630
  return {
91355
91631
  clientId: entry.clientId,
91356
91632
  clientSecret: entry.clientSecret,
91357
- calendarIds: [...entry.calendarIds ?? parseCalendarFilter(entry.calendarFilter)],
91358
91633
  fromAddress: entry.fromAddress,
91359
91634
  fromName: entry.fromName
91360
91635
  };
@@ -91405,18 +91680,6 @@ function migrateLegacyToBrokers(rawConfig, legacyOauth) {
91405
91680
  didMigrate: true
91406
91681
  };
91407
91682
  }
91408
- /**
91409
- * The stamped calendar ids one broker imports: its own set, or — while the migration has not
91410
- * reached it — the shared setting older versions wrote, so the move never changes what is imported.
91411
- */
91412
- function calendarIdsOfEntry(entry, shared) {
91413
- return new Set(entry?.calendarIds ?? shared);
91414
- }
91415
- function parseCalendarFilter(raw) {
91416
- const trimmed = raw.trim();
91417
- if (!trimmed) return /* @__PURE__ */ new Set();
91418
- return new Set(trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0));
91419
- }
91420
91683
  /** Local calendar id stamped as `brokerId:googleCalendarId`. */
91421
91684
  function stampCalendarId(brokerId, googleCalendarId) {
91422
91685
  return `${brokerId}:${googleCalendarId}`;
@@ -91498,7 +91761,6 @@ var GoogleBrokerRegistry = class {
91498
91761
  const entry = GoogleBrokerEntrySchema.parse({
91499
91762
  id,
91500
91763
  name,
91501
- calendarIds: [],
91502
91764
  ...fields
91503
91765
  });
91504
91766
  this.entries = [...this.entries, entry];
@@ -91512,7 +91774,6 @@ var GoogleBrokerRegistry = class {
91512
91774
  const next = GoogleBrokerEntrySchema.parse({
91513
91775
  id: prev.id,
91514
91776
  name: prev.name,
91515
- ...prev.calendarIds !== void 0 ? { calendarIds: prev.calendarIds } : {},
91516
91777
  ...fields
91517
91778
  });
91518
91779
  this.entries = this.entries.map((e, i) => i === idx ? next : e);
@@ -91534,11 +91795,8 @@ var GoogleBrokerRegistry = class {
91534
91795
  connectedSize() {
91535
91796
  return this.entries.filter((e) => this.isConnected(e.id)).length;
91536
91797
  }
91537
- toConfig(calendarIds = []) {
91538
- return {
91539
- brokers: [...this.entries],
91540
- calendarIds: [...calendarIds]
91541
- };
91798
+ toConfig() {
91799
+ return { brokers: [...this.entries] };
91542
91800
  }
91543
91801
  seedCounterFromEntries(entries) {
91544
91802
  let maxSeen = 0;
@@ -91696,24 +91954,6 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
91696
91954
  showToggle: true
91697
91955
  }]
91698
91956
  },
91699
- {
91700
- id: "google-calendars-import",
91701
- title: "Calendars to import",
91702
- titleKey: "google-services:googleBrokerSettings.s_google-calendars-import.title",
91703
- description: "Nothing selected = no calendars imported from this account. Options appear once the account is signed in.",
91704
- descriptionKey: "google-services:googleBrokerSettings.s_google-calendars-import.description",
91705
- columns: 1,
91706
- fields: [{
91707
- type: "multiselect",
91708
- key: "calendarIds",
91709
- label: "Calendars",
91710
- labelKey: "google-services:googleBrokerSettings.s_google-calendars-import.calendarIds.label",
91711
- options: [],
91712
- optionsFrom: "_calendarCatalog",
91713
- description: "The calendars of this Google account that CamStack imports.",
91714
- descriptionKey: "google-services:googleBrokerSettings.s_google-calendars-import.calendarIds.description"
91715
- }]
91716
- },
91717
91957
  {
91718
91958
  id: "gmail-send",
91719
91959
  title: "Gmail send",
@@ -91753,7 +91993,7 @@ function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
91753
91993
  format: "html",
91754
91994
  content: [
91755
91995
  "<p>Link a Google account under Connectivity → <strong>Brokers</strong> ",
91756
- "(Client ID / secret + Sign in). Pick the calendars to import on each broker. ",
91996
+ "(Client ID / secret + Sign in), then add a calendar under Connectivity → <strong>Calendars</strong>. ",
91757
91997
  `OAuth redirect URI: <code>${redirect}</code>.</p>`
91758
91998
  ].join("")
91759
91999
  }]
@@ -92233,11 +92473,76 @@ var googleBrokersEntryKind = defineEntryKind("brokers", {
92233
92473
  clientSecret: GoogleBrokerEntrySchema.shape.clientSecret,
92234
92474
  calendarFilter: GoogleBrokerEntrySchema.shape.calendarFilter,
92235
92475
  fromAddress: GoogleBrokerEntrySchema.shape.fromAddress,
92236
- fromName: GoogleBrokerEntrySchema.shape.fromName,
92237
- calendarIds: GoogleBrokerEntrySchema.shape.calendarIds
92476
+ fromName: GoogleBrokerEntrySchema.shape.fromName
92238
92477
  }),
92239
92478
  writeTrigger: "an operator adds or edits a Google OAuth broker"
92240
92479
  });
92480
+ /**
92481
+ * One calendar an account imports (D1013): a record that LINKS to the account it belongs to and
92482
+ * names the Google calendar by its own id (`primary`, `name@group.calendar.google.com`). The id the
92483
+ * calendar answers to elsewhere stays `<accountId>:<calendarId>` (`stampCalendarId`).
92484
+ */
92485
+ var googleCalendarsEntryKind = declareLinkKind({
92486
+ name: "calendars",
92487
+ title: "Google calendars",
92488
+ writeTrigger: "an operator adds or removes a calendar an account imports",
92489
+ links: { accountId: {
92490
+ cap: "broker",
92491
+ kind: GOOGLE_BROKER_KIND
92492
+ } },
92493
+ fields: { calendarId: string().default("") }
92494
+ });
92495
+ //#endregion
92496
+ //#region src/google-calendar-instances.ts
92497
+ /**
92498
+ * The calendars a Google account imports as `integration-instances` (D1013): one `calendars`
92499
+ * record per (account, calendar), linking to its account through the link primitive. The calendar
92500
+ * source reads the records; nothing is stored on the account.
92501
+ */
92502
+ var GOOGLE_CALENDAR_INSTANCE_KIND = "calendar";
92503
+ /** The stamped ids each account imports, from the calendar records. */
92504
+ function selectedStampedIds(rows) {
92505
+ const out = /* @__PURE__ */ new Map();
92506
+ for (const row of rows) {
92507
+ const { accountId, calendarId } = row.body;
92508
+ if (!row.enabled || accountId === "" || calendarId === "") continue;
92509
+ const set = out.get(accountId) ?? /* @__PURE__ */ new Set();
92510
+ set.add(stampCalendarId(accountId, calendarId));
92511
+ out.set(accountId, set);
92512
+ }
92513
+ return out;
92514
+ }
92515
+ function buildGoogleCalendarInstances(records, deps) {
92516
+ return buildLinkedInstances(records, {
92517
+ kind: googleCalendarsEntryKind,
92518
+ instanceKind: GOOGLE_CALENDAR_INSTANCE_KIND,
92519
+ description: {
92520
+ role: "services",
92521
+ instanceMode: "multiple",
92522
+ kind: GOOGLE_CALENDAR_INSTANCE_KIND,
92523
+ canTest: false
92524
+ },
92525
+ labels: { accountId: {
92526
+ label: "Google account",
92527
+ description: "The account this calendar belongs to."
92528
+ } },
92529
+ alsoConfigured: (body) => body.calendarId !== "",
92530
+ dynamicSections: async (accountId) => [{
92531
+ id: "google-calendar-pick",
92532
+ title: "Calendar",
92533
+ columns: 1,
92534
+ fields: [{
92535
+ type: "select",
92536
+ key: "calendarId",
92537
+ label: "Calendar",
92538
+ options: [...await deps.catalog(accountId)],
92539
+ description: "The calendars of that account. Sign the account in to see them."
92540
+ }]
92541
+ }],
92542
+ mintId: () => `cal_${(0, node_crypto.randomBytes)(6).toString("hex")}`,
92543
+ onChanged: deps.onChanged
92544
+ });
92545
+ }
92241
92546
  //#endregion
92242
92547
  //#region src/google-instances.ts
92243
92548
  /**
@@ -92257,6 +92562,17 @@ function asTestResult(r) {
92257
92562
  error: r.error ?? "Google rejected the check"
92258
92563
  };
92259
92564
  }
92565
+ /** The `account` kind of the composed provider: where the OAuth sign-in starts and ends. */
92566
+ function googleAccountKind(deps) {
92567
+ return {
92568
+ id: GOOGLE_BROKER_KIND,
92569
+ label: GOOGLE_BROKER_KIND_LABEL,
92570
+ functionCap: "broker",
92571
+ brokerKind: GOOGLE_BROKER_KIND,
92572
+ connectPath: deps.connectPath,
92573
+ disconnectPath: deps.disconnectPath
92574
+ };
92575
+ }
92260
92576
  function buildGoogleInstances(records, deps) {
92261
92577
  return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
92262
92578
  description: {
@@ -92264,31 +92580,16 @@ function buildGoogleInstances(records, deps) {
92264
92580
  instanceMode: "multiple",
92265
92581
  kind: "account",
92266
92582
  canTest: true,
92267
- kinds: [{
92268
- id: GOOGLE_BROKER_KIND,
92269
- label: GOOGLE_BROKER_KIND_LABEL,
92270
- functionCap: "broker",
92271
- brokerKind: GOOGLE_BROKER_KIND,
92272
- connectPath: deps.connectPath,
92273
- disconnectPath: deps.disconnectPath
92274
- }],
92275
92583
  allowRestoreId: true
92276
92584
  },
92277
- formSchema: () => {
92278
- const full = buildGoogleBrokerSettingsSchema(deps.publicOrigin());
92279
- return {
92280
- ...full,
92281
- sections: full.sections.filter((s) => s.id !== "google-calendars-import")
92282
- };
92283
- },
92585
+ formSchema: () => buildGoogleBrokerSettingsSchema(deps.publicOrigin()),
92284
92586
  initialBody: (name) => ({
92285
92587
  name,
92286
92588
  clientId: "",
92287
92589
  clientSecret: "",
92288
92590
  calendarFilter: "",
92289
92591
  fromAddress: "",
92290
- fromName: "",
92291
- calendarIds: []
92592
+ fromName: ""
92292
92593
  }),
92293
92594
  mintId: deps.mintId,
92294
92595
  kindOf: () => GOOGLE_BROKER_KIND,
@@ -92407,42 +92708,66 @@ async function planDropSharedCalendarIds(ports) {
92407
92708
  } })
92408
92709
  }] };
92409
92710
  }
92410
- //#endregion
92411
- //#region src/migrations/index.ts
92412
92711
  /** The `google-services` catalogue (D987): offered to the engine when the addon initialises. */
92413
- var ADDON_MIGRATIONS = [{
92414
- meta: {
92415
- id: "google-services/2026-11-03-calendar-ids-to-brokers",
92416
- owner: "google-services",
92417
- level: "addon-state",
92418
- seq: 1,
92419
- dependsOn: [],
92420
- store: "settings",
92421
- scope: "cluster",
92422
- onFailure: "tolerate",
92423
- bodyKind: "procedure",
92424
- description: "Copy the shared calendar set to every Google broker record that has none of its own",
92425
- fixtureMark: "the client secret marker of a broker record the migration does not know"
92426
- },
92427
- procedure: copyCalendarIdsToBrokers
92428
- }, {
92429
- meta: {
92430
- id: "google-services/2026-11-03-drop-shared-calendar-ids",
92431
- owner: "google-services",
92432
- level: "addon-state",
92433
- seq: 2,
92434
- dependsOn: ["google-services/2026-11-03-calendar-ids-to-brokers"],
92435
- store: "settings",
92436
- scope: "cluster",
92437
- onFailure: "tolerate",
92438
- bodyKind: "plan",
92439
- description: "Delete the shared calendar set once every broker shows its own",
92440
- fixtureMark: "the shared set parked when a broker holds a different one"
92441
- },
92442
- plan: planDropSharedCalendarIds
92443
- }];
92712
+ var ADDON_MIGRATIONS = [
92713
+ {
92714
+ meta: {
92715
+ id: "google-services/2026-11-03-calendar-ids-to-brokers",
92716
+ owner: "google-services",
92717
+ level: "addon-state",
92718
+ seq: 1,
92719
+ dependsOn: [],
92720
+ store: "settings",
92721
+ scope: "cluster",
92722
+ onFailure: "tolerate",
92723
+ bodyKind: "procedure",
92724
+ description: "Copy the shared calendar set to every Google broker record that has none of its own",
92725
+ fixtureMark: "the client secret marker of a broker record the migration does not know"
92726
+ },
92727
+ procedure: copyCalendarIdsToBrokers
92728
+ },
92729
+ {
92730
+ meta: {
92731
+ id: "google-services/2026-11-03-drop-shared-calendar-ids",
92732
+ owner: "google-services",
92733
+ level: "addon-state",
92734
+ seq: 2,
92735
+ dependsOn: ["google-services/2026-11-03-calendar-ids-to-brokers"],
92736
+ store: "settings",
92737
+ scope: "cluster",
92738
+ onFailure: "tolerate",
92739
+ bodyKind: "plan",
92740
+ description: "Delete the shared calendar set once every broker shows its own",
92741
+ fixtureMark: "the shared set parked when a broker holds a different one"
92742
+ },
92743
+ plan: planDropSharedCalendarIds
92744
+ },
92745
+ {
92746
+ meta: {
92747
+ id: "google-services/2026-11-07-calendar-ids-to-calendar-records",
92748
+ owner: "google-services",
92749
+ level: "addon-state",
92750
+ seq: 3,
92751
+ dependsOn: ["google-services/2026-11-03-drop-shared-calendar-ids"],
92752
+ store: "settings",
92753
+ scope: "cluster",
92754
+ onFailure: "tolerate",
92755
+ bodyKind: "procedure",
92756
+ description: "Write one calendar record per calendar an account imports (linked to the account), read each back, then clear the calendar array of the account row",
92757
+ fixtureMark: "the client secret marker of the account that keeps its calendars"
92758
+ },
92759
+ procedure: (ports) => moveCalendarIdsToRecords(ports, { calendarIdOf: (accountId, entry) => {
92760
+ const parsed = parseStampedCalendarId(entry);
92761
+ return parsed?.brokerId === accountId ? parsed.googleCalendarId : null;
92762
+ } })
92763
+ }
92764
+ ];
92444
92765
  //#endregion
92445
92766
  //#region src/migrations/ports.ts
92767
+ function listOf(value) {
92768
+ if (!Array.isArray(value)) return void 0;
92769
+ return value.filter((item) => typeof item === "string");
92770
+ }
92446
92771
  /** The migration's ports over the `brokers` record kind and the addon's raw settings read. */
92447
92772
  function calendarIdsPortsOver(kind, readStored) {
92448
92773
  return {
@@ -92450,18 +92775,43 @@ function calendarIdsPortsOver(kind, readStored) {
92450
92775
  listBrokers: async () => {
92451
92776
  return (await kind.query({ limit: 1e3 })).records.map((record) => ({
92452
92777
  id: record.id,
92453
- calendarIds: record.data.calendarIds
92778
+ calendarIds: listOf(record.data["calendarIds"])
92454
92779
  }));
92455
92780
  },
92456
92781
  setBrokerCalendarIds: async (id, ids) => {
92457
92782
  const row = await kind.get(id);
92458
92783
  if (row === null) return false;
92459
- if (row.data.calendarIds !== void 0) return true;
92784
+ if (row.data["calendarIds"] !== void 0) return true;
92460
92785
  await kind.patch(id, { calendarIds: [...ids] });
92461
92786
  return true;
92462
92787
  }
92463
92788
  };
92464
92789
  }
92790
+ /** The ports of the calendars move: the raw `brokers` rows and the `calendars` records. */
92791
+ function calendarRecordPortsOver(deps) {
92792
+ return {
92793
+ warn: deps.warn,
92794
+ listAccounts: async () => {
92795
+ return (await deps.brokers.query({ limit: 1e3 })).records.map((record) => ({
92796
+ id: record.id,
92797
+ calendarIds: record.data["calendarIds"]
92798
+ }));
92799
+ },
92800
+ readCalendar: async (recordId) => {
92801
+ const row = await deps.calendars.get(recordId);
92802
+ return row === null ? null : row.data;
92803
+ },
92804
+ putCalendar: async (recordId, body) => {
92805
+ await deps.calendars.put({
92806
+ id: recordId,
92807
+ data: body
92808
+ }, { ifRev: 0 });
92809
+ },
92810
+ clearCalendarIds: async (accountId) => {
92811
+ await deps.brokers.patch(accountId, { calendarIds: null });
92812
+ }
92813
+ };
92814
+ }
92465
92815
  //#endregion
92466
92816
  //#region src/google-broker-store.ts
92467
92817
  function rowToEntry(id, data) {
@@ -92500,15 +92850,14 @@ var GoogleBrokerStore = class GoogleBrokerStore {
92500
92850
  const nextIds = new Set(entries.map((e) => e.id));
92501
92851
  for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
92502
92852
  for (const entry of entries) {
92503
- const { id, name, clientId, clientSecret, calendarFilter, fromAddress, fromName, calendarIds } = entry;
92853
+ const { id, name, clientId, clientSecret, calendarFilter, fromAddress, fromName } = entry;
92504
92854
  const body = {
92505
92855
  name,
92506
92856
  clientId,
92507
92857
  clientSecret,
92508
92858
  calendarFilter,
92509
92859
  fromAddress,
92510
- fromName,
92511
- ...calendarIds !== void 0 ? { calendarIds } : {}
92860
+ fromName
92512
92861
  };
92513
92862
  const parsed = googleBrokersEntryKind.schema.safeParse(body);
92514
92863
  if (!parsed.success) throw new Error(`invalid Google broker '${id}': ${parsed.error.message}`);
@@ -92554,6 +92903,9 @@ var GoogleServicesAddon = class extends BaseAddon {
92554
92903
  registry = new GoogleBrokerRegistry();
92555
92904
  brokerStore = null;
92556
92905
  instances = null;
92906
+ calendarInstances = null;
92907
+ /** The stamped calendar ids each account imports, from the `calendars` records. */
92908
+ calendarSelection = /* @__PURE__ */ new Map();
92557
92909
  activeBrokers = [];
92558
92910
  /** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
92559
92911
  pendingStates = /* @__PURE__ */ new Map();
@@ -92599,15 +92951,23 @@ var GoogleServicesAddon = class extends BaseAddon {
92599
92951
  }
92600
92952
  const records = this.ctx.records;
92601
92953
  if (records === void 0) throw new Error("google-services: ctx.records is required (hub-only addon)");
92602
- await records.declare({ brokers: googleBrokersEntryKind });
92954
+ await records.declare({
92955
+ brokers: googleBrokersEntryKind,
92956
+ calendars: googleCalendarsEntryKind
92957
+ });
92603
92958
  this.brokerStore = GoogleBrokerStore.of(records);
92604
92959
  this.instances = this.buildInstances(records);
92960
+ this.calendarInstances = buildGoogleCalendarInstances(records, {
92961
+ catalog: (accountId) => this.loadCalendarCatalog(accountId),
92962
+ onChanged: () => this.refreshCalendarSelection()
92963
+ });
92605
92964
  this.activeBrokers = await this.loadPersistedBrokers();
92606
92965
  if ((await this.brokerStore.listResolved()).length === 0 && this.config.brokers.length > 0) {
92607
92966
  await this.brokerStore.replaceAll(this.config.brokers);
92608
92967
  this.activeBrokers = await this.loadPersistedBrokers();
92609
92968
  }
92610
92969
  await this.migrateCalendarIds(records);
92970
+ await this.refreshCalendarSelection();
92611
92971
  this.registry.seedFrom(this.activeBrokers, oauthAccounts.accounts);
92612
92972
  await this.refreshPublicOrigin();
92613
92973
  const calendarProvider = createGoogleCalendarSource({
@@ -92617,7 +92977,7 @@ var GoogleServicesAddon = class extends BaseAddon {
92617
92977
  connected: this.registry.isConnected(entry.id),
92618
92978
  api: createGoogleCalendarApi(() => this.ensureAccessToken(entry.id))
92619
92979
  })),
92620
- selectedCalendarIds: (brokerId) => this.calendarIdsOf(brokerId)
92980
+ selectedCalendarIds: (brokerId) => this.calendarSelection.get(brokerId) ?? /* @__PURE__ */ new Set()
92621
92981
  });
92622
92982
  const smtpProvider = createGmailSmtpProvider({ accounts: () => this.registry.listEntries().map((entry) => {
92623
92983
  const oauth = this.registry.getOauth(entry.id);
@@ -92666,7 +93026,7 @@ var GoogleServicesAddon = class extends BaseAddon {
92666
93026
  },
92667
93027
  {
92668
93028
  capability: integrationInstancesCapability,
92669
- provider: this.requireInstances().toProvider()
93029
+ provider: this.buildInstancesProvider()
92670
93030
  },
92671
93031
  {
92672
93032
  capability: calendarSourceCapability,
@@ -92691,6 +93051,41 @@ var GoogleServicesAddon = class extends BaseAddon {
92691
93051
  if (this.instances === null) throw new Error("google-services: instances used before initialize");
92692
93052
  return this.instances;
92693
93053
  }
93054
+ /** One provider over the two record kinds: the Google accounts and the calendars they import. */
93055
+ buildInstancesProvider() {
93056
+ const calendars = this.calendarInstances;
93057
+ if (calendars === null) throw new Error("google-services: instances used before initialize");
93058
+ return composeInstanceKinds([{
93059
+ kind: {
93060
+ ...googleAccountKind({
93061
+ connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
93062
+ disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`
93063
+ }),
93064
+ functionCap: "broker"
93065
+ },
93066
+ provider: this.requireInstances().toProvider()
93067
+ }, {
93068
+ kind: {
93069
+ id: GOOGLE_CALENDAR_INSTANCE_KIND,
93070
+ label: "Google calendar",
93071
+ functionCap: "calendar-source",
93072
+ role: "services"
93073
+ },
93074
+ provider: calendars.toProvider()
93075
+ }], {
93076
+ role: "services",
93077
+ instanceMode: "multiple",
93078
+ kind: "account",
93079
+ canTest: true,
93080
+ allowRestoreId: true
93081
+ });
93082
+ }
93083
+ /** Re-read which calendars each account imports from the records. */
93084
+ async refreshCalendarSelection() {
93085
+ const calendars = this.calendarInstances;
93086
+ if (calendars === null) return;
93087
+ this.calendarSelection = selectedStampedIds(await calendars.listResolved());
93088
+ }
92694
93089
  /** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
92695
93090
  buildInstances(records) {
92696
93091
  return buildGoogleInstances(records, {
@@ -92724,7 +93119,6 @@ var GoogleServicesAddon = class extends BaseAddon {
92724
93119
  const oauth = this.registry.getOauth(id);
92725
93120
  return {
92726
93121
  ...brokerEntryToSettings(entry),
92727
- _calendarCatalog: await this.loadStampedCalendarCatalog(id),
92728
93122
  accountEmail: oauth?.accountEmail ?? "",
92729
93123
  connected: this.registry.isConnected(id)
92730
93124
  };
@@ -92993,11 +93387,25 @@ var GoogleServicesAddon = class extends BaseAddon {
92993
93387
  for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
92994
93388
  }
92995
93389
  /**
92996
- * The shared calendar set becomes each broker's own (D985, D987). Brokers are reloaded once the
92997
- * copy landed, so the registry is seeded with the sets the migration wrote.
93390
+ * The shared calendar set becomes each broker's own (D985, D987), then each broker's set becomes
93391
+ * `calendars` records linked to the account (D1013). Brokers are reloaded once a migration
93392
+ * landed, so the registry is seeded with what it wrote.
92998
93393
  */
92999
93394
  async migrateCalendarIds(records) {
93000
- if ((await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, calendarIdsPortsOver(records.kind(googleBrokersEntryKind), (key) => this.readStoredGlobalKey(key))))).applied.length > 0) this.activeBrokers = await this.loadPersistedBrokers();
93395
+ const brokers = records.kind({
93396
+ name: googleBrokersEntryKind.name,
93397
+ declaration: googleBrokersEntryKind.declaration,
93398
+ schema: record(string(), unknown())
93399
+ });
93400
+ const ports = {
93401
+ ...calendarIdsPortsOver(brokers, (key) => this.readStoredGlobalKey(key)),
93402
+ ...calendarRecordPortsOver({
93403
+ brokers,
93404
+ calendars: records.kind(googleCalendarsEntryKind),
93405
+ warn: (message) => this.ctx.logger.warn(message)
93406
+ })
93407
+ };
93408
+ if ((await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, ports))).applied.length > 0) this.activeBrokers = await this.loadPersistedBrokers();
93001
93409
  }
93002
93410
  async loadPersistedBrokers() {
93003
93411
  const store = this.brokerStore;
@@ -93021,30 +93429,24 @@ var GoogleServicesAddon = class extends BaseAddon {
93021
93429
  if (!this.oauthAccountsHandle) return;
93022
93430
  await this.oauthAccountsHandle.set({ accounts: [...this.registry.listOauthAccounts()] });
93023
93431
  }
93024
- /** Stamped catalog for Connectivity → Calendars (`optionsFrom`). */
93025
- async loadStampedCalendarCatalog(onlyBrokerId) {
93026
- const out = [];
93027
- for (const entry of this.registry.listEntries()) {
93028
- if (onlyBrokerId !== void 0 && entry.id !== onlyBrokerId) continue;
93029
- if (!this.registry.isConnected(entry.id)) continue;
93030
- try {
93031
- const list = await createGoogleCalendarApi(() => this.ensureAccessToken(entry.id)).listCalendars();
93032
- const accountLabel = this.registry.getOauth(entry.id)?.accountEmail || entry.name;
93033
- for (const c of list) {
93034
- const summary = c.summary.trim().length > 0 ? c.summary : c.id;
93035
- out.push({
93036
- value: stampCalendarId(entry.id, c.id),
93037
- label: `${summary} (${accountLabel})`
93038
- });
93039
- }
93040
- } catch (err) {
93041
- this.ctx.logger.warn("google calendar catalog failed", { meta: {
93042
- brokerId: entry.id,
93043
- error: err instanceof Error ? err.message : String(err)
93044
- } });
93045
- }
93432
+ /** The calendars one signed-in account lists, by their own id (the picker of a calendar record). */
93433
+ async loadCalendarCatalog(accountId) {
93434
+ const entry = this.registry.getEntry(accountId);
93435
+ if (entry === null || !this.registry.isConnected(entry.id)) return [];
93436
+ try {
93437
+ const api = createGoogleCalendarApi(() => this.ensureAccessToken(entry.id));
93438
+ const accountLabel = this.registry.getOauth(entry.id)?.accountEmail || entry.name;
93439
+ return (await api.listCalendars()).map((c) => ({
93440
+ value: c.id,
93441
+ label: `${c.summary.trim().length > 0 ? c.summary : c.id} (${accountLabel})`
93442
+ })).toSorted((a, b) => a.label.localeCompare(b.label));
93443
+ } catch (err) {
93444
+ this.ctx.logger.warn("google calendar catalog failed", { meta: {
93445
+ brokerId: entry.id,
93446
+ error: err instanceof Error ? err.message : String(err)
93447
+ } });
93448
+ return [];
93046
93449
  }
93047
- return out.toSorted((a, b) => a.label.localeCompare(b.label));
93048
93450
  }
93049
93451
  async emitGoogleBrokerReconnectAlert(brokerId, brokerName) {
93050
93452
  const emit = this.ctx.api.alerts?.emit;
@@ -93167,14 +93569,6 @@ var GoogleServicesAddon = class extends BaseAddon {
93167
93569
  await this.refreshPublicOrigin();
93168
93570
  return super.getGlobalSettings(overlay, cap, nodeId);
93169
93571
  }
93170
- /**
93171
- * The stamped calendar ids ONE broker imports. A broker the migration has not reached yet
93172
- * (no `calendarIds` of its own) reads the shared setting older versions wrote, so the move
93173
- * never changes what is imported mid-way.
93174
- */
93175
- calendarIdsOf(brokerId) {
93176
- return calendarIdsOfEntry(this.registry.getEntry(brokerId), this.config.calendarIds ?? []);
93177
- }
93178
93572
  };
93179
93573
  function asSettingsCap(cap) {
93180
93574
  if (cap === "calendar-source" || cap === "smtp-provider") return cap;