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