@camstack/addon-google-services 0.2.9 → 0.2.11

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.
@@ -5542,7 +5542,7 @@ var ZodIssueCode = {
5542
5542
  var ZodFirstPartyTypeKind;
5543
5543
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5544
5544
  //#endregion
5545
- //#region ../types/dist/sleep-bZIhUvjd.mjs
5545
+ //#region ../types/dist/sleep-BegKjwUu.mjs
5546
5546
  /**
5547
5547
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5548
5548
  * window to float samples (D455).
@@ -8771,7 +8771,7 @@ function userError(code, params, englishFallback) {
8771
8771
  return new CamstackError(code, params, englishFallback);
8772
8772
  }
8773
8773
  //#endregion
8774
- //#region ../types/dist/err-msg--2RrYiLO.mjs
8774
+ //#region ../types/dist/err-msg-BXKzRBwg.mjs
8775
8775
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
8776
8776
  DeviceType["Camera"] = "camera";
8777
8777
  DeviceType["Hub"] = "hub";
@@ -8863,6 +8863,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
8863
8863
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
8864
8864
  * integrations sharing the same food/desiccant/hopper surface. */
8865
8865
  DeviceType["PetFeeder"] = "pet-feeder";
8866
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
8867
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
8868
+ * Sources: native Bosch Home Connect, reusable by similar integrations. */
8869
+ DeviceType["Dishwasher"] = "dishwasher";
8866
8870
  return DeviceType;
8867
8871
  }({});
8868
8872
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -9110,7 +9114,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
9110
9114
  return DeviceRole;
9111
9115
  }({});
9112
9116
  //#endregion
9113
- //#region ../types/dist/composition-DOwQB_Ua.mjs
9117
+ //#region ../types/dist/composition-CB6PMT9p.mjs
9114
9118
  /**
9115
9119
  * The ffmpeg build every CamStack node downloads, per platform and arch.
9116
9120
  *
@@ -13632,8 +13636,13 @@ var AlertSeveritySchema = _enum([
13632
13636
  "info",
13633
13637
  "success",
13634
13638
  "warning",
13635
- "error"
13639
+ "error",
13640
+ "critical"
13636
13641
  ]);
13642
+ var AlertActionSchema = object({
13643
+ label: string(),
13644
+ href: string().optional()
13645
+ });
13637
13646
  var AlertStatusSchema = _enum([
13638
13647
  "active",
13639
13648
  "in-progress",
@@ -13651,6 +13660,7 @@ var AlertSchema = object({
13651
13660
  severity: AlertSeveritySchema,
13652
13661
  title: string(),
13653
13662
  message: string(),
13663
+ action: AlertActionSchema.optional(),
13654
13664
  status: AlertStatusSchema,
13655
13665
  progress: number().optional(),
13656
13666
  read: boolean(),
@@ -17844,7 +17854,11 @@ DeviceType.Hub, DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number().int
17844
17854
  });
17845
17855
  /**
17846
17856
  * `device-adoption` — generic discovery + adoption surface,
17847
- * system-scoped singleton. The de-HA-ified successor to `ha-discovery`:
17857
+ * system-scoped collection (one provider per integration addon), routed
17858
+ * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
17859
+ * pin model as `connection-test`. The admin UI keeps using
17860
+ * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
17861
+ * successor to `ha-discovery`:
17848
17862
  * keyed by `integrationId` (the addon resolves integrationId → broker
17849
17863
  * internally) rather than a connection-specific `brokerId`, and it
17850
17864
  * REUSES the shared `DiscoveredChildDevice` candidate type from
@@ -53647,7 +53661,7 @@ var BatteryStatusSchema = object({
53647
53661
  */
53648
53662
  binary: boolean().optional()
53649
53663
  });
53650
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, method(object({
53664
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, method(object({
53651
53665
  deviceId: number(),
53652
53666
  /** Bound on the wait. Sensible range 3000–10000ms. */
53653
53667
  timeoutMs: number().int().min(500).max(3e4).default(8e3)
@@ -54533,6 +54547,130 @@ object({
54533
54547
  deviceId: number(),
54534
54548
  status: DeviceStatusSchema
54535
54549
  });
54550
+ /**
54551
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
54552
+ * consumable levels (salt, rinse aid) and common program options.
54553
+ *
54554
+ * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
54555
+ * by any integration that exposes the same program / option surface.
54556
+ *
54557
+ * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
54558
+ * provider refreshes the slice on an interval and eagerly after commands.
54559
+ */
54560
+ var DishwasherStateSchema = _enum([
54561
+ "idle",
54562
+ "running",
54563
+ "paused",
54564
+ "finished",
54565
+ "error",
54566
+ "unavailable"
54567
+ ]);
54568
+ var DishwasherConsumableLevelSchema = _enum([
54569
+ "ok",
54570
+ "low",
54571
+ "empty"
54572
+ ]);
54573
+ object({
54574
+ state: DishwasherStateSchema,
54575
+ /** Active program id/name, or null when none selected / unknown. */
54576
+ program: string().nullable(),
54577
+ /** Program tokens the hardware accepts — drives the UI selector. */
54578
+ availablePrograms: array(string()),
54579
+ /** Minutes remaining on the active cycle, or null when not running. */
54580
+ remainingMinutes: number().int().nonnegative().nullable(),
54581
+ /** Cycle completion 0..100, or null when not applicable. */
54582
+ progressPercent: number().min(0).max(100).nullable(),
54583
+ /** True when the door is reported open. */
54584
+ doorOpen: boolean(),
54585
+ /** True when remote start is allowed (door closed, power on, etc.). */
54586
+ remoteStartAllowed: boolean(),
54587
+ /** Main power / switched-on state. */
54588
+ powerOn: boolean(),
54589
+ saltLevel: DishwasherConsumableLevelSchema.nullable(),
54590
+ rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
54591
+ halfLoad: boolean(),
54592
+ hygienePlus: boolean(),
54593
+ /** Extra dry option when the appliance exposes it. */
54594
+ extraDry: boolean(),
54595
+ /** Intensive zone (lower basket) when the appliance exposes it. */
54596
+ intensiveZone: boolean(),
54597
+ /** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
54598
+ varioSpeedPlus: boolean(),
54599
+ /** Child lock (controls disabled on the appliance). */
54600
+ childLock: boolean(),
54601
+ errorCode: number().nullable(),
54602
+ errorLabel: string().nullable(),
54603
+ lastFetchedAt: number()
54604
+ });
54605
+ DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
54606
+ kind: "mutation",
54607
+ auth: "protected"
54608
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54609
+ kind: "mutation",
54610
+ auth: "protected"
54611
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54612
+ kind: "mutation",
54613
+ auth: "protected"
54614
+ }), method(object({
54615
+ deviceId: number().int().nonnegative(),
54616
+ program: string().min(1)
54617
+ }), _void(), {
54618
+ kind: "mutation",
54619
+ auth: "protected"
54620
+ }), method(object({
54621
+ deviceId: number().int().nonnegative(),
54622
+ on: boolean()
54623
+ }), _void(), {
54624
+ kind: "mutation",
54625
+ auth: "protected"
54626
+ }), method(object({
54627
+ deviceId: number().int().nonnegative(),
54628
+ on: boolean()
54629
+ }), _void(), {
54630
+ kind: "mutation",
54631
+ auth: "protected"
54632
+ }), method(object({
54633
+ deviceId: number().int().nonnegative(),
54634
+ on: boolean()
54635
+ }), _void(), {
54636
+ kind: "mutation",
54637
+ auth: "protected"
54638
+ }), method(object({
54639
+ deviceId: number().int().nonnegative(),
54640
+ on: boolean()
54641
+ }), _void(), {
54642
+ kind: "mutation",
54643
+ auth: "protected"
54644
+ }), method(object({
54645
+ deviceId: number().int().nonnegative(),
54646
+ on: boolean()
54647
+ }), _void(), {
54648
+ kind: "mutation",
54649
+ auth: "protected"
54650
+ }), method(object({
54651
+ deviceId: number().int().nonnegative(),
54652
+ on: boolean()
54653
+ }), _void(), {
54654
+ kind: "mutation",
54655
+ auth: "protected"
54656
+ }), method(object({
54657
+ deviceId: number().int().nonnegative(),
54658
+ on: boolean()
54659
+ }), _void(), {
54660
+ kind: "mutation",
54661
+ auth: "protected"
54662
+ }), method(object({
54663
+ deviceId: number().int().nonnegative(),
54664
+ key: string().min(1),
54665
+ value: union([
54666
+ boolean(),
54667
+ string(),
54668
+ number()
54669
+ ])
54670
+ }), _void(), {
54671
+ kind: "mutation",
54672
+ auth: "protected"
54673
+ });
54536
54674
  object({
54537
54675
  /** Ms epoch of the last press. null = never observed since this provider started. */
54538
54676
  lastPressedAt: number().nullable(),
@@ -55098,6 +55236,13 @@ object({
55098
55236
  lastChangedAt: number()
55099
55237
  });
55100
55238
  DeviceType.Sensor;
55239
+ object({
55240
+ /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
55241
+ position: number().min(0).max(100).nullable(),
55242
+ /** Ms epoch when the slice was last updated (push or poll). */
55243
+ lastFetchedAt: number()
55244
+ });
55245
+ DeviceType.Thermostat;
55101
55246
  object({
55102
55247
  /** Whether the humidifier is currently on. */
55103
55248
  on: boolean(),
@@ -62164,7 +62309,7 @@ var AccessoryKind = {
62164
62309
  PrivacyMask: DeviceRole.PrivacyMask
62165
62310
  };
62166
62311
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
62167
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
62312
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
62168
62313
  new Set(Object.values(DeviceType));
62169
62314
  Object.freeze({
62170
62315
  "accessories.getStatus": {
@@ -64585,6 +64730,84 @@ Object.freeze({
64585
64730
  addonId: null,
64586
64731
  access: "view"
64587
64732
  },
64733
+ "dishwasher.getStatus": {
64734
+ capName: "dishwasher",
64735
+ capScope: "device",
64736
+ addonId: null,
64737
+ access: "view"
64738
+ },
64739
+ "dishwasher.pause": {
64740
+ capName: "dishwasher",
64741
+ capScope: "device",
64742
+ addonId: null,
64743
+ access: "create"
64744
+ },
64745
+ "dishwasher.setChildLock": {
64746
+ capName: "dishwasher",
64747
+ capScope: "device",
64748
+ addonId: null,
64749
+ access: "create"
64750
+ },
64751
+ "dishwasher.setExtraDry": {
64752
+ capName: "dishwasher",
64753
+ capScope: "device",
64754
+ addonId: null,
64755
+ access: "create"
64756
+ },
64757
+ "dishwasher.setHalfLoad": {
64758
+ capName: "dishwasher",
64759
+ capScope: "device",
64760
+ addonId: null,
64761
+ access: "create"
64762
+ },
64763
+ "dishwasher.setHygienePlus": {
64764
+ capName: "dishwasher",
64765
+ capScope: "device",
64766
+ addonId: null,
64767
+ access: "create"
64768
+ },
64769
+ "dishwasher.setIntensiveZone": {
64770
+ capName: "dishwasher",
64771
+ capScope: "device",
64772
+ addonId: null,
64773
+ access: "create"
64774
+ },
64775
+ "dishwasher.setOption": {
64776
+ capName: "dishwasher",
64777
+ capScope: "device",
64778
+ addonId: null,
64779
+ access: "create"
64780
+ },
64781
+ "dishwasher.setPower": {
64782
+ capName: "dishwasher",
64783
+ capScope: "device",
64784
+ addonId: null,
64785
+ access: "create"
64786
+ },
64787
+ "dishwasher.setProgram": {
64788
+ capName: "dishwasher",
64789
+ capScope: "device",
64790
+ addonId: null,
64791
+ access: "create"
64792
+ },
64793
+ "dishwasher.setVarioSpeedPlus": {
64794
+ capName: "dishwasher",
64795
+ capScope: "device",
64796
+ addonId: null,
64797
+ access: "create"
64798
+ },
64799
+ "dishwasher.start": {
64800
+ capName: "dishwasher",
64801
+ capScope: "device",
64802
+ addonId: null,
64803
+ access: "create"
64804
+ },
64805
+ "dishwasher.stop": {
64806
+ capName: "dishwasher",
64807
+ capScope: "device",
64808
+ addonId: null,
64809
+ access: "create"
64810
+ },
64588
64811
  "doorbell.getStatus": {
64589
64812
  capName: "doorbell",
64590
64813
  capScope: "device",
@@ -64795,6 +65018,12 @@ Object.freeze({
64795
65018
  addonId: null,
64796
65019
  access: "view"
64797
65020
  },
65021
+ "heatingValve.getStatus": {
65022
+ capName: "heating-valve",
65023
+ capScope: "device",
65024
+ addonId: null,
65025
+ access: "view"
65026
+ },
64798
65027
  "humidifier.getStatus": {
64799
65028
  capName: "humidifier",
64800
65029
  capScope: "device",
@@ -70382,7 +70611,7 @@ Object.freeze({
70382
70611
  access: "create"
70383
70612
  }
70384
70613
  });
70385
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
70614
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
70386
70615
  DeviceFeature.BatteryOperated;
70387
70616
  Object.freeze({
70388
70617
  "accessories.getStatus": [{
@@ -71206,6 +71435,71 @@ Object.freeze({
71206
71435
  form: "single",
71207
71436
  optional: false
71208
71437
  }],
71438
+ "dishwasher.getStatus": [{
71439
+ name: "deviceId",
71440
+ form: "single",
71441
+ optional: false
71442
+ }],
71443
+ "dishwasher.pause": [{
71444
+ name: "deviceId",
71445
+ form: "single",
71446
+ optional: false
71447
+ }],
71448
+ "dishwasher.setChildLock": [{
71449
+ name: "deviceId",
71450
+ form: "single",
71451
+ optional: false
71452
+ }],
71453
+ "dishwasher.setExtraDry": [{
71454
+ name: "deviceId",
71455
+ form: "single",
71456
+ optional: false
71457
+ }],
71458
+ "dishwasher.setHalfLoad": [{
71459
+ name: "deviceId",
71460
+ form: "single",
71461
+ optional: false
71462
+ }],
71463
+ "dishwasher.setHygienePlus": [{
71464
+ name: "deviceId",
71465
+ form: "single",
71466
+ optional: false
71467
+ }],
71468
+ "dishwasher.setIntensiveZone": [{
71469
+ name: "deviceId",
71470
+ form: "single",
71471
+ optional: false
71472
+ }],
71473
+ "dishwasher.setOption": [{
71474
+ name: "deviceId",
71475
+ form: "single",
71476
+ optional: false
71477
+ }],
71478
+ "dishwasher.setPower": [{
71479
+ name: "deviceId",
71480
+ form: "single",
71481
+ optional: false
71482
+ }],
71483
+ "dishwasher.setProgram": [{
71484
+ name: "deviceId",
71485
+ form: "single",
71486
+ optional: false
71487
+ }],
71488
+ "dishwasher.setVarioSpeedPlus": [{
71489
+ name: "deviceId",
71490
+ form: "single",
71491
+ optional: false
71492
+ }],
71493
+ "dishwasher.start": [{
71494
+ name: "deviceId",
71495
+ form: "single",
71496
+ optional: false
71497
+ }],
71498
+ "dishwasher.stop": [{
71499
+ name: "deviceId",
71500
+ form: "single",
71501
+ optional: false
71502
+ }],
71209
71503
  "doorbell.getStatus": [{
71210
71504
  name: "deviceId",
71211
71505
  form: "single",
@@ -71275,6 +71569,11 @@ Object.freeze({
71275
71569
  form: "single",
71276
71570
  optional: false
71277
71571
  }],
71572
+ "heatingValve.getStatus": [{
71573
+ name: "deviceId",
71574
+ form: "single",
71575
+ optional: false
71576
+ }],
71278
71577
  "humidifier.getStatus": [{
71279
71578
  name: "deviceId",
71280
71579
  form: "single",
@@ -74970,6 +75269,40 @@ function createGmailSmtpProvider(deps) {
74970
75269
  };
74971
75270
  }
74972
75271
  //#endregion
75272
+ //#region src/google-broker-reconnect-alert.ts
75273
+ function googleBrokerReconnectAlertId(brokerId) {
75274
+ return `google-broker:${brokerId}`;
75275
+ }
75276
+ function isInvalidGrantRefreshError(err) {
75277
+ return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
75278
+ }
75279
+ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75280
+ const pushLine = `${brokerName}: Google session expired — sign in again`;
75281
+ return {
75282
+ id: googleBrokerReconnectAlertId(brokerId),
75283
+ category: "google.broker-reconnect",
75284
+ severity: "critical",
75285
+ title: "Google needs a new sign-in",
75286
+ message: `${brokerName} needs you to sign in to Google again.`,
75287
+ action: {
75288
+ label: "Sign in again",
75289
+ href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`
75290
+ },
75291
+ status: "active",
75292
+ read: false,
75293
+ createdAt: Date.now(),
75294
+ updatedAt: Date.now(),
75295
+ source: {
75296
+ type: "google-broker",
75297
+ id: brokerId
75298
+ },
75299
+ metadata: {
75300
+ episodeKey: "reconnect",
75301
+ pushLine
75302
+ }
75303
+ };
75304
+ }
75305
+ //#endregion
74973
75306
  //#region src/google-oauth.ts
74974
75307
  /**
74975
75308
  * Google OAuth2 authorization-code + refresh for Calendar API.
@@ -75585,6 +75918,7 @@ var GoogleServicesAddon = class extends BaseAddon {
75585
75918
  };
75586
75919
  this.registry.setOauth(next);
75587
75920
  await this.persistOauthAccounts();
75921
+ if (next.refreshToken.length > 0) this.resolveGoogleBrokerReconnectAlert(brokerId);
75588
75922
  if (next.accountEmail && (entry.name === "Google account" || entry.name.startsWith("google_"))) {
75589
75923
  const renamed = {
75590
75924
  ...entry,
@@ -75717,6 +76051,39 @@ var GoogleServicesAddon = class extends BaseAddon {
75717
76051
  }
75718
76052
  return out.toSorted((a, b) => a.label.localeCompare(b.label));
75719
76053
  }
76054
+ async emitGoogleBrokerReconnectAlert(brokerId, brokerName) {
76055
+ const emit = this.ctx.api.alerts?.emit;
76056
+ if (emit === void 0) {
76057
+ this.ctx.logger.warn("google broker reconnect alert skipped — alerts capability missing", { meta: { brokerId } });
76058
+ return;
76059
+ }
76060
+ try {
76061
+ await emit.mutate(buildGoogleBrokerReconnectAlert(brokerId, brokerName));
76062
+ } catch (err) {
76063
+ this.ctx.logger.warn("google broker reconnect alert emit failed", { meta: {
76064
+ brokerId,
76065
+ error: err instanceof Error ? err.message : String(err)
76066
+ } });
76067
+ }
76068
+ }
76069
+ async resolveGoogleBrokerReconnectAlert(brokerId) {
76070
+ const update = this.ctx.api.alerts?.update;
76071
+ if (update === void 0) return;
76072
+ try {
76073
+ await update.mutate({
76074
+ alertId: googleBrokerReconnectAlertId(brokerId),
76075
+ patch: {
76076
+ status: "completed",
76077
+ read: true
76078
+ }
76079
+ });
76080
+ } catch (err) {
76081
+ this.ctx.logger.warn("google broker reconnect alert resolve failed", { meta: {
76082
+ brokerId,
76083
+ error: err instanceof Error ? err.message : String(err)
76084
+ } });
76085
+ }
76086
+ }
75720
76087
  async ensureAccessToken(brokerId, opts = {}) {
75721
76088
  const entry = this.registry.getEntry(brokerId);
75722
76089
  const oauth = this.registry.getOauth(brokerId);
@@ -75729,10 +76096,16 @@ var GoogleServicesAddon = class extends BaseAddon {
75729
76096
  accountEmail: oauth.accountEmail
75730
76097
  };
75731
76098
  if (!opts.forceRefresh && !needsRefresh(state)) return state.accessToken;
75732
- const tokens = await refreshAccessToken({
75733
- clientId: entry.clientId.trim(),
75734
- clientSecret: entry.clientSecret.trim()
75735
- }, state.refreshToken);
76099
+ let tokens;
76100
+ try {
76101
+ tokens = await refreshAccessToken({
76102
+ clientId: entry.clientId.trim(),
76103
+ clientSecret: entry.clientSecret.trim()
76104
+ }, state.refreshToken);
76105
+ } catch (err) {
76106
+ if (isInvalidGrantRefreshError(err)) this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76107
+ throw err;
76108
+ }
75736
76109
  if (typeof tokens.access_token !== "string" || tokens.access_token.trim().length === 0) throw new Error("Google token refresh returned an empty access_token — Sign out and Sign in again");
75737
76110
  const nextState = applyTokenResponse(state, tokens);
75738
76111
  this.registry.setOauth({
@@ -5538,7 +5538,7 @@ var ZodIssueCode = {
5538
5538
  var ZodFirstPartyTypeKind;
5539
5539
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5540
5540
  //#endregion
5541
- //#region ../types/dist/sleep-bZIhUvjd.mjs
5541
+ //#region ../types/dist/sleep-BegKjwUu.mjs
5542
5542
  /**
5543
5543
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5544
5544
  * window to float samples (D455).
@@ -8767,7 +8767,7 @@ function userError(code, params, englishFallback) {
8767
8767
  return new CamstackError(code, params, englishFallback);
8768
8768
  }
8769
8769
  //#endregion
8770
- //#region ../types/dist/err-msg--2RrYiLO.mjs
8770
+ //#region ../types/dist/err-msg-BXKzRBwg.mjs
8771
8771
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
8772
8772
  DeviceType["Camera"] = "camera";
8773
8773
  DeviceType["Hub"] = "hub";
@@ -8859,6 +8859,10 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
8859
8859
  * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
8860
8860
  * integrations sharing the same food/desiccant/hopper surface. */
8861
8861
  DeviceType["PetFeeder"] = "pet-feeder";
8862
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
8863
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
8864
+ * Sources: native Bosch Home Connect, reusable by similar integrations. */
8865
+ DeviceType["Dishwasher"] = "dishwasher";
8862
8866
  return DeviceType;
8863
8867
  }({});
8864
8868
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -9106,7 +9110,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
9106
9110
  return DeviceRole;
9107
9111
  }({});
9108
9112
  //#endregion
9109
- //#region ../types/dist/composition-DOwQB_Ua.mjs
9113
+ //#region ../types/dist/composition-CB6PMT9p.mjs
9110
9114
  /**
9111
9115
  * The ffmpeg build every CamStack node downloads, per platform and arch.
9112
9116
  *
@@ -13628,8 +13632,13 @@ var AlertSeveritySchema = _enum([
13628
13632
  "info",
13629
13633
  "success",
13630
13634
  "warning",
13631
- "error"
13635
+ "error",
13636
+ "critical"
13632
13637
  ]);
13638
+ var AlertActionSchema = object({
13639
+ label: string(),
13640
+ href: string().optional()
13641
+ });
13633
13642
  var AlertStatusSchema = _enum([
13634
13643
  "active",
13635
13644
  "in-progress",
@@ -13647,6 +13656,7 @@ var AlertSchema = object({
13647
13656
  severity: AlertSeveritySchema,
13648
13657
  title: string(),
13649
13658
  message: string(),
13659
+ action: AlertActionSchema.optional(),
13650
13660
  status: AlertStatusSchema,
13651
13661
  progress: number().optional(),
13652
13662
  read: boolean(),
@@ -17840,7 +17850,11 @@ DeviceType.Hub, DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number().int
17840
17850
  });
17841
17851
  /**
17842
17852
  * `device-adoption` — generic discovery + adoption surface,
17843
- * system-scoped singleton. The de-HA-ified successor to `ha-discovery`:
17853
+ * system-scoped collection (one provider per integration addon), routed
17854
+ * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
17855
+ * pin model as `connection-test`. The admin UI keeps using
17856
+ * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
17857
+ * successor to `ha-discovery`:
17844
17858
  * keyed by `integrationId` (the addon resolves integrationId → broker
17845
17859
  * internally) rather than a connection-specific `brokerId`, and it
17846
17860
  * REUSES the shared `DiscoveredChildDevice` candidate type from
@@ -53643,7 +53657,7 @@ var BatteryStatusSchema = object({
53643
53657
  */
53644
53658
  binary: boolean().optional()
53645
53659
  });
53646
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, method(object({
53660
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, method(object({
53647
53661
  deviceId: number(),
53648
53662
  /** Bound on the wait. Sensible range 3000–10000ms. */
53649
53663
  timeoutMs: number().int().min(500).max(3e4).default(8e3)
@@ -54529,6 +54543,130 @@ object({
54529
54543
  deviceId: number(),
54530
54544
  status: DeviceStatusSchema
54531
54545
  });
54546
+ /**
54547
+ * Smart-dishwasher cap — program lifecycle, door / remote-start gates,
54548
+ * consumable levels (salt, rinse aid) and common program options.
54549
+ *
54550
+ * Sources: native Bosch Home Connect (`dishwasher` device type). Reusable
54551
+ * by any integration that exposes the same program / option surface.
54552
+ *
54553
+ * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
54554
+ * provider refreshes the slice on an interval and eagerly after commands.
54555
+ */
54556
+ var DishwasherStateSchema = _enum([
54557
+ "idle",
54558
+ "running",
54559
+ "paused",
54560
+ "finished",
54561
+ "error",
54562
+ "unavailable"
54563
+ ]);
54564
+ var DishwasherConsumableLevelSchema = _enum([
54565
+ "ok",
54566
+ "low",
54567
+ "empty"
54568
+ ]);
54569
+ object({
54570
+ state: DishwasherStateSchema,
54571
+ /** Active program id/name, or null when none selected / unknown. */
54572
+ program: string().nullable(),
54573
+ /** Program tokens the hardware accepts — drives the UI selector. */
54574
+ availablePrograms: array(string()),
54575
+ /** Minutes remaining on the active cycle, or null when not running. */
54576
+ remainingMinutes: number().int().nonnegative().nullable(),
54577
+ /** Cycle completion 0..100, or null when not applicable. */
54578
+ progressPercent: number().min(0).max(100).nullable(),
54579
+ /** True when the door is reported open. */
54580
+ doorOpen: boolean(),
54581
+ /** True when remote start is allowed (door closed, power on, etc.). */
54582
+ remoteStartAllowed: boolean(),
54583
+ /** Main power / switched-on state. */
54584
+ powerOn: boolean(),
54585
+ saltLevel: DishwasherConsumableLevelSchema.nullable(),
54586
+ rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
54587
+ halfLoad: boolean(),
54588
+ hygienePlus: boolean(),
54589
+ /** Extra dry option when the appliance exposes it. */
54590
+ extraDry: boolean(),
54591
+ /** Intensive zone (lower basket) when the appliance exposes it. */
54592
+ intensiveZone: boolean(),
54593
+ /** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
54594
+ varioSpeedPlus: boolean(),
54595
+ /** Child lock (controls disabled on the appliance). */
54596
+ childLock: boolean(),
54597
+ errorCode: number().nullable(),
54598
+ errorLabel: string().nullable(),
54599
+ lastFetchedAt: number()
54600
+ });
54601
+ DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
54602
+ kind: "mutation",
54603
+ auth: "protected"
54604
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54605
+ kind: "mutation",
54606
+ auth: "protected"
54607
+ }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54608
+ kind: "mutation",
54609
+ auth: "protected"
54610
+ }), method(object({
54611
+ deviceId: number().int().nonnegative(),
54612
+ program: string().min(1)
54613
+ }), _void(), {
54614
+ kind: "mutation",
54615
+ auth: "protected"
54616
+ }), method(object({
54617
+ deviceId: number().int().nonnegative(),
54618
+ on: boolean()
54619
+ }), _void(), {
54620
+ kind: "mutation",
54621
+ auth: "protected"
54622
+ }), method(object({
54623
+ deviceId: number().int().nonnegative(),
54624
+ on: boolean()
54625
+ }), _void(), {
54626
+ kind: "mutation",
54627
+ auth: "protected"
54628
+ }), method(object({
54629
+ deviceId: number().int().nonnegative(),
54630
+ on: boolean()
54631
+ }), _void(), {
54632
+ kind: "mutation",
54633
+ auth: "protected"
54634
+ }), method(object({
54635
+ deviceId: number().int().nonnegative(),
54636
+ on: boolean()
54637
+ }), _void(), {
54638
+ kind: "mutation",
54639
+ auth: "protected"
54640
+ }), method(object({
54641
+ deviceId: number().int().nonnegative(),
54642
+ on: boolean()
54643
+ }), _void(), {
54644
+ kind: "mutation",
54645
+ auth: "protected"
54646
+ }), method(object({
54647
+ deviceId: number().int().nonnegative(),
54648
+ on: boolean()
54649
+ }), _void(), {
54650
+ kind: "mutation",
54651
+ auth: "protected"
54652
+ }), method(object({
54653
+ deviceId: number().int().nonnegative(),
54654
+ on: boolean()
54655
+ }), _void(), {
54656
+ kind: "mutation",
54657
+ auth: "protected"
54658
+ }), method(object({
54659
+ deviceId: number().int().nonnegative(),
54660
+ key: string().min(1),
54661
+ value: union([
54662
+ boolean(),
54663
+ string(),
54664
+ number()
54665
+ ])
54666
+ }), _void(), {
54667
+ kind: "mutation",
54668
+ auth: "protected"
54669
+ });
54532
54670
  object({
54533
54671
  /** Ms epoch of the last press. null = never observed since this provider started. */
54534
54672
  lastPressedAt: number().nullable(),
@@ -55094,6 +55232,13 @@ object({
55094
55232
  lastChangedAt: number()
55095
55233
  });
55096
55234
  DeviceType.Sensor;
55235
+ object({
55236
+ /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
55237
+ position: number().min(0).max(100).nullable(),
55238
+ /** Ms epoch when the slice was last updated (push or poll). */
55239
+ lastFetchedAt: number()
55240
+ });
55241
+ DeviceType.Thermostat;
55097
55242
  object({
55098
55243
  /** Whether the humidifier is currently on. */
55099
55244
  on: boolean(),
@@ -62160,7 +62305,7 @@ var AccessoryKind = {
62160
62305
  PrivacyMask: DeviceRole.PrivacyMask
62161
62306
  };
62162
62307
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
62163
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
62308
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
62164
62309
  new Set(Object.values(DeviceType));
62165
62310
  Object.freeze({
62166
62311
  "accessories.getStatus": {
@@ -64581,6 +64726,84 @@ Object.freeze({
64581
64726
  addonId: null,
64582
64727
  access: "view"
64583
64728
  },
64729
+ "dishwasher.getStatus": {
64730
+ capName: "dishwasher",
64731
+ capScope: "device",
64732
+ addonId: null,
64733
+ access: "view"
64734
+ },
64735
+ "dishwasher.pause": {
64736
+ capName: "dishwasher",
64737
+ capScope: "device",
64738
+ addonId: null,
64739
+ access: "create"
64740
+ },
64741
+ "dishwasher.setChildLock": {
64742
+ capName: "dishwasher",
64743
+ capScope: "device",
64744
+ addonId: null,
64745
+ access: "create"
64746
+ },
64747
+ "dishwasher.setExtraDry": {
64748
+ capName: "dishwasher",
64749
+ capScope: "device",
64750
+ addonId: null,
64751
+ access: "create"
64752
+ },
64753
+ "dishwasher.setHalfLoad": {
64754
+ capName: "dishwasher",
64755
+ capScope: "device",
64756
+ addonId: null,
64757
+ access: "create"
64758
+ },
64759
+ "dishwasher.setHygienePlus": {
64760
+ capName: "dishwasher",
64761
+ capScope: "device",
64762
+ addonId: null,
64763
+ access: "create"
64764
+ },
64765
+ "dishwasher.setIntensiveZone": {
64766
+ capName: "dishwasher",
64767
+ capScope: "device",
64768
+ addonId: null,
64769
+ access: "create"
64770
+ },
64771
+ "dishwasher.setOption": {
64772
+ capName: "dishwasher",
64773
+ capScope: "device",
64774
+ addonId: null,
64775
+ access: "create"
64776
+ },
64777
+ "dishwasher.setPower": {
64778
+ capName: "dishwasher",
64779
+ capScope: "device",
64780
+ addonId: null,
64781
+ access: "create"
64782
+ },
64783
+ "dishwasher.setProgram": {
64784
+ capName: "dishwasher",
64785
+ capScope: "device",
64786
+ addonId: null,
64787
+ access: "create"
64788
+ },
64789
+ "dishwasher.setVarioSpeedPlus": {
64790
+ capName: "dishwasher",
64791
+ capScope: "device",
64792
+ addonId: null,
64793
+ access: "create"
64794
+ },
64795
+ "dishwasher.start": {
64796
+ capName: "dishwasher",
64797
+ capScope: "device",
64798
+ addonId: null,
64799
+ access: "create"
64800
+ },
64801
+ "dishwasher.stop": {
64802
+ capName: "dishwasher",
64803
+ capScope: "device",
64804
+ addonId: null,
64805
+ access: "create"
64806
+ },
64584
64807
  "doorbell.getStatus": {
64585
64808
  capName: "doorbell",
64586
64809
  capScope: "device",
@@ -64791,6 +65014,12 @@ Object.freeze({
64791
65014
  addonId: null,
64792
65015
  access: "view"
64793
65016
  },
65017
+ "heatingValve.getStatus": {
65018
+ capName: "heating-valve",
65019
+ capScope: "device",
65020
+ addonId: null,
65021
+ access: "view"
65022
+ },
64794
65023
  "humidifier.getStatus": {
64795
65024
  capName: "humidifier",
64796
65025
  capScope: "device",
@@ -70378,7 +70607,7 @@ Object.freeze({
70378
70607
  access: "create"
70379
70608
  }
70380
70609
  });
70381
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
70610
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
70382
70611
  DeviceFeature.BatteryOperated;
70383
70612
  Object.freeze({
70384
70613
  "accessories.getStatus": [{
@@ -71202,6 +71431,71 @@ Object.freeze({
71202
71431
  form: "single",
71203
71432
  optional: false
71204
71433
  }],
71434
+ "dishwasher.getStatus": [{
71435
+ name: "deviceId",
71436
+ form: "single",
71437
+ optional: false
71438
+ }],
71439
+ "dishwasher.pause": [{
71440
+ name: "deviceId",
71441
+ form: "single",
71442
+ optional: false
71443
+ }],
71444
+ "dishwasher.setChildLock": [{
71445
+ name: "deviceId",
71446
+ form: "single",
71447
+ optional: false
71448
+ }],
71449
+ "dishwasher.setExtraDry": [{
71450
+ name: "deviceId",
71451
+ form: "single",
71452
+ optional: false
71453
+ }],
71454
+ "dishwasher.setHalfLoad": [{
71455
+ name: "deviceId",
71456
+ form: "single",
71457
+ optional: false
71458
+ }],
71459
+ "dishwasher.setHygienePlus": [{
71460
+ name: "deviceId",
71461
+ form: "single",
71462
+ optional: false
71463
+ }],
71464
+ "dishwasher.setIntensiveZone": [{
71465
+ name: "deviceId",
71466
+ form: "single",
71467
+ optional: false
71468
+ }],
71469
+ "dishwasher.setOption": [{
71470
+ name: "deviceId",
71471
+ form: "single",
71472
+ optional: false
71473
+ }],
71474
+ "dishwasher.setPower": [{
71475
+ name: "deviceId",
71476
+ form: "single",
71477
+ optional: false
71478
+ }],
71479
+ "dishwasher.setProgram": [{
71480
+ name: "deviceId",
71481
+ form: "single",
71482
+ optional: false
71483
+ }],
71484
+ "dishwasher.setVarioSpeedPlus": [{
71485
+ name: "deviceId",
71486
+ form: "single",
71487
+ optional: false
71488
+ }],
71489
+ "dishwasher.start": [{
71490
+ name: "deviceId",
71491
+ form: "single",
71492
+ optional: false
71493
+ }],
71494
+ "dishwasher.stop": [{
71495
+ name: "deviceId",
71496
+ form: "single",
71497
+ optional: false
71498
+ }],
71205
71499
  "doorbell.getStatus": [{
71206
71500
  name: "deviceId",
71207
71501
  form: "single",
@@ -71271,6 +71565,11 @@ Object.freeze({
71271
71565
  form: "single",
71272
71566
  optional: false
71273
71567
  }],
71568
+ "heatingValve.getStatus": [{
71569
+ name: "deviceId",
71570
+ form: "single",
71571
+ optional: false
71572
+ }],
71274
71573
  "humidifier.getStatus": [{
71275
71574
  name: "deviceId",
71276
71575
  form: "single",
@@ -74966,6 +75265,40 @@ function createGmailSmtpProvider(deps) {
74966
75265
  };
74967
75266
  }
74968
75267
  //#endregion
75268
+ //#region src/google-broker-reconnect-alert.ts
75269
+ function googleBrokerReconnectAlertId(brokerId) {
75270
+ return `google-broker:${brokerId}`;
75271
+ }
75272
+ function isInvalidGrantRefreshError(err) {
75273
+ return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
75274
+ }
75275
+ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75276
+ const pushLine = `${brokerName}: Google session expired — sign in again`;
75277
+ return {
75278
+ id: googleBrokerReconnectAlertId(brokerId),
75279
+ category: "google.broker-reconnect",
75280
+ severity: "critical",
75281
+ title: "Google needs a new sign-in",
75282
+ message: `${brokerName} needs you to sign in to Google again.`,
75283
+ action: {
75284
+ label: "Sign in again",
75285
+ href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`
75286
+ },
75287
+ status: "active",
75288
+ read: false,
75289
+ createdAt: Date.now(),
75290
+ updatedAt: Date.now(),
75291
+ source: {
75292
+ type: "google-broker",
75293
+ id: brokerId
75294
+ },
75295
+ metadata: {
75296
+ episodeKey: "reconnect",
75297
+ pushLine
75298
+ }
75299
+ };
75300
+ }
75301
+ //#endregion
74969
75302
  //#region src/google-oauth.ts
74970
75303
  /**
74971
75304
  * Google OAuth2 authorization-code + refresh for Calendar API.
@@ -75581,6 +75914,7 @@ var GoogleServicesAddon = class extends BaseAddon {
75581
75914
  };
75582
75915
  this.registry.setOauth(next);
75583
75916
  await this.persistOauthAccounts();
75917
+ if (next.refreshToken.length > 0) this.resolveGoogleBrokerReconnectAlert(brokerId);
75584
75918
  if (next.accountEmail && (entry.name === "Google account" || entry.name.startsWith("google_"))) {
75585
75919
  const renamed = {
75586
75920
  ...entry,
@@ -75713,6 +76047,39 @@ var GoogleServicesAddon = class extends BaseAddon {
75713
76047
  }
75714
76048
  return out.toSorted((a, b) => a.label.localeCompare(b.label));
75715
76049
  }
76050
+ async emitGoogleBrokerReconnectAlert(brokerId, brokerName) {
76051
+ const emit = this.ctx.api.alerts?.emit;
76052
+ if (emit === void 0) {
76053
+ this.ctx.logger.warn("google broker reconnect alert skipped — alerts capability missing", { meta: { brokerId } });
76054
+ return;
76055
+ }
76056
+ try {
76057
+ await emit.mutate(buildGoogleBrokerReconnectAlert(brokerId, brokerName));
76058
+ } catch (err) {
76059
+ this.ctx.logger.warn("google broker reconnect alert emit failed", { meta: {
76060
+ brokerId,
76061
+ error: err instanceof Error ? err.message : String(err)
76062
+ } });
76063
+ }
76064
+ }
76065
+ async resolveGoogleBrokerReconnectAlert(brokerId) {
76066
+ const update = this.ctx.api.alerts?.update;
76067
+ if (update === void 0) return;
76068
+ try {
76069
+ await update.mutate({
76070
+ alertId: googleBrokerReconnectAlertId(brokerId),
76071
+ patch: {
76072
+ status: "completed",
76073
+ read: true
76074
+ }
76075
+ });
76076
+ } catch (err) {
76077
+ this.ctx.logger.warn("google broker reconnect alert resolve failed", { meta: {
76078
+ brokerId,
76079
+ error: err instanceof Error ? err.message : String(err)
76080
+ } });
76081
+ }
76082
+ }
75716
76083
  async ensureAccessToken(brokerId, opts = {}) {
75717
76084
  const entry = this.registry.getEntry(brokerId);
75718
76085
  const oauth = this.registry.getOauth(brokerId);
@@ -75725,10 +76092,16 @@ var GoogleServicesAddon = class extends BaseAddon {
75725
76092
  accountEmail: oauth.accountEmail
75726
76093
  };
75727
76094
  if (!opts.forceRefresh && !needsRefresh(state)) return state.accessToken;
75728
- const tokens = await refreshAccessToken({
75729
- clientId: entry.clientId.trim(),
75730
- clientSecret: entry.clientSecret.trim()
75731
- }, state.refreshToken);
76095
+ let tokens;
76096
+ try {
76097
+ tokens = await refreshAccessToken({
76098
+ clientId: entry.clientId.trim(),
76099
+ clientSecret: entry.clientSecret.trim()
76100
+ }, state.refreshToken);
76101
+ } catch (err) {
76102
+ if (isInvalidGrantRefreshError(err)) this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76103
+ throw err;
76104
+ }
75732
76105
  if (typeof tokens.access_token !== "string" || tokens.access_token.trim().length === 0) throw new Error("Google token refresh returned an empty access_token — Sign out and Sign in again");
75733
76106
  const nextState = applyTokenResponse(state, tokens);
75734
76107
  this.registry.setOauth({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-google-services",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Google Services — Google brokers (OAuth2): Calendar as calendar-source, Gmail send as smtp-provider.",
5
5
  "keywords": [
6
6
  "camstack",