@camstack/addon-google-services 0.2.10 → 0.2.12

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,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- //#region ../types/dist/event-category-BvOFi6PY.mjs
2
+ //#region ../types/dist/event-category-DOl51-Ij.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -78,9 +78,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
78
78
  /**
79
79
  * The node's one-shot runner boot check (D877): planned runners against the
80
80
  * process service, `degraded` when any is failed, refused or missing. The
81
- * alert centre turns it into one alert and a direct critical push.
81
+ * alert centre turns it into one critical alert.
82
82
  */
83
83
  EventCategory["SystemRunnerCheck"] = "system.runner-check";
84
+ /**
85
+ * An alert EPISODE opened or closed, emitted by the alert centre for alerts
86
+ * of severity `critical` or `error` (`AlertEpisodePayload`). The alert
87
+ * centre sends no notification of its own: Notification Center turns this
88
+ * into the `alert-critical` / `alert-error` / `alert-critical-resolved`
89
+ * system-event kinds, and reconciles from the alerts store because a bus
90
+ * event may be dropped (D8, D11).
91
+ */
92
+ EventCategory["AlertEpisode"] = "alert.episode";
84
93
  EventCategory["AddonError"] = "addon.error";
85
94
  EventCategory["AddonPageReady"] = "addon.page-ready";
86
95
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -5538,7 +5547,7 @@ var ZodIssueCode = {
5538
5547
  var ZodFirstPartyTypeKind;
5539
5548
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5540
5549
  //#endregion
5541
- //#region ../types/dist/sleep-BOjuyoPC.mjs
5550
+ //#region ../types/dist/sleep-BxjU_fJM.mjs
5542
5551
  /**
5543
5552
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5544
5553
  * window to float samples (D455).
@@ -13637,7 +13646,14 @@ var AlertSeveritySchema = _enum([
13637
13646
  ]);
13638
13647
  var AlertActionSchema = object({
13639
13648
  label: string(),
13640
- href: string().optional()
13649
+ href: string().optional(),
13650
+ /**
13651
+ * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
13652
+ * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
13653
+ * start) — the browser must leave the SPA with a full navigation, because
13654
+ * the SPA has no such route and would bounce to the home page.
13655
+ */
13656
+ navigation: _enum(["app", "server"]).optional()
13641
13657
  });
13642
13658
  var AlertStatusSchema = _enum([
13643
13659
  "active",
@@ -17850,7 +17866,11 @@ DeviceType.Hub, DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number().int
17850
17866
  });
17851
17867
  /**
17852
17868
  * `device-adoption` — generic discovery + adoption surface,
17853
- * system-scoped singleton. The de-HA-ified successor to `ha-discovery`:
17869
+ * system-scoped collection (one provider per integration addon), routed
17870
+ * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
17871
+ * pin model as `connection-test`. The admin UI keeps using
17872
+ * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
17873
+ * successor to `ha-discovery`:
17854
17874
  * keyed by `integrationId` (the addon resolves integrationId → broker
17855
17875
  * internally) rather than a connection-specific `brokerId`, and it
17856
17876
  * REUSES the shared `DiscoveredChildDevice` candidate type from
@@ -42058,6 +42078,9 @@ var NcSystemEventKindSchema = _enum([
42058
42078
  "server-updated",
42059
42079
  "export-completed",
42060
42080
  "backup-completed",
42081
+ "alert-critical",
42082
+ "alert-error",
42083
+ "alert-critical-resolved",
42061
42084
  "camera-online",
42062
42085
  "camera-offline",
42063
42086
  "camera-disabled",
@@ -51103,7 +51126,10 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
51103
51126
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
51104
51127
  kind: "mutation",
51105
51128
  auth: "admin"
51106
- }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
51129
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({ locationIds: array(string()).readonly() }), object({
51130
+ clear: boolean(),
51131
+ repointLocationId: string().optional()
51132
+ }), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
51107
51133
  kind: "mutation",
51108
51134
  auth: "admin"
51109
51135
  }), method(StorageCleanupInputSchema, object({ jobId: string() }), {
@@ -54548,6 +54574,11 @@ object({
54548
54574
  *
54549
54575
  * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
54550
54576
  * provider refreshes the slice on an interval and eagerly after commands.
54577
+ *
54578
+ * UNKNOWN IS NOT OFF. Every field the appliance reports separately is
54579
+ * nullable: `null` means "not known right now" (the appliance has not said,
54580
+ * the link is down, or the description does not carry it) and is never
54581
+ * rendered or exported as `false` / `0`. `availability` says why.
54551
54582
  */
54552
54583
  var DishwasherStateSchema = _enum([
54553
54584
  "idle",
@@ -54557,80 +54588,227 @@ var DishwasherStateSchema = _enum([
54557
54588
  "error",
54558
54589
  "unavailable"
54559
54590
  ]);
54591
+ /**
54592
+ * The appliance's own operation state, uncollapsed. `state` above is the coarse
54593
+ * view for compact surfaces; this is what the appliance said.
54594
+ */
54595
+ var DishwasherOperationStateSchema = _enum([
54596
+ "Inactive",
54597
+ "Ready",
54598
+ "DelayedStart",
54599
+ "Run",
54600
+ "Pause",
54601
+ "ActionRequired",
54602
+ "Finished",
54603
+ "Error",
54604
+ "Aborting"
54605
+ ]);
54606
+ /**
54607
+ * Why fields may be `null`:
54608
+ * - `available` — the link is up and the description is loaded;
54609
+ * - `offline` — the local link is down (values are unknown, not last-known);
54610
+ * - `description-missing` — the link is up but the appliance's description could not
54611
+ * be loaded, so its values cannot be decoded.
54612
+ */
54613
+ var DishwasherAvailabilitySchema = _enum([
54614
+ "available",
54615
+ "offline",
54616
+ "description-missing"
54617
+ ]);
54560
54618
  var DishwasherConsumableLevelSchema = _enum([
54561
54619
  "ok",
54562
54620
  "low",
54563
54621
  "empty"
54564
54622
  ]);
54565
54623
  object({
54624
+ availability: DishwasherAvailabilitySchema,
54625
+ /** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
54566
54626
  state: DishwasherStateSchema,
54567
- /** Active program id/name, or null when none selected / unknown. */
54627
+ /** The appliance's operation state as reported; null when not known. */
54628
+ operationState: DishwasherOperationStateSchema.nullable(),
54629
+ /** Feature key of the ACTIVE (running) program, or null when none runs / not known. */
54568
54630
  program: string().nullable(),
54569
- /** Program tokens the hardware accepts — drives the UI selector. */
54631
+ /** Feature key of the SELECTED program (what Start would run), or null when none / not known. */
54632
+ selectedProgram: string().nullable(),
54633
+ /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
54570
54634
  availablePrograms: array(string()),
54571
- /** Minutes remaining on the active cycle, or null when not running. */
54572
- remainingMinutes: number().int().nonnegative().nullable(),
54635
+ /** Seconds remaining on the active cycle, or null when not running / not known. */
54636
+ remainingSeconds: number().int().nonnegative().nullable(),
54637
+ /** Seconds the delayed start is set to (time until the cycle starts), or null. */
54638
+ delayedStartSeconds: number().int().nonnegative().nullable(),
54573
54639
  /** Cycle completion 0..100, or null when not applicable. */
54574
54640
  progressPercent: number().min(0).max(100).nullable(),
54575
- /** True when the door is reported open. */
54576
- doorOpen: boolean(),
54577
- /** True when remote start is allowed (door closed, power on, etc.). */
54578
- remoteStartAllowed: boolean(),
54579
- /** Main power / switched-on state. */
54580
- powerOn: boolean(),
54641
+ /** True when the door is reported open; null when the door state is not known. */
54642
+ doorOpen: boolean().nullable(),
54643
+ /** True when the door is latched (locked) during a cycle; null when not reported. */
54644
+ doorLocked: boolean().nullable(),
54645
+ /** True when remote start is allowed (door closed, power on, etc.); null when not known. */
54646
+ remoteStartAllowed: boolean().nullable(),
54647
+ /** True when the appliance accepts remote control now; null when not known. */
54648
+ remoteControlActive: boolean().nullable(),
54649
+ /** True while somebody operates the appliance at its panel; null when not reported. */
54650
+ localControlActive: boolean().nullable(),
54651
+ /** Main power / switched-on state; null when not known. */
54652
+ powerOn: boolean().nullable(),
54653
+ /** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
54654
+ powerState: string().nullable(),
54581
54655
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
54582
54656
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
54583
- halfLoad: boolean(),
54584
- hygienePlus: boolean(),
54657
+ /** Option states: null when the appliance does not expose the option or has not said. */
54658
+ halfLoad: boolean().nullable(),
54659
+ hygienePlus: boolean().nullable(),
54660
+ /** Extra dry option when the appliance exposes it. */
54661
+ extraDry: boolean().nullable(),
54662
+ /** Intensive zone (lower basket) when the appliance exposes it. */
54663
+ intensiveZone: boolean().nullable(),
54664
+ /** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
54665
+ varioSpeedPlus: boolean().nullable(),
54666
+ /** Child lock (controls disabled on the appliance); null when not known. */
54667
+ childLock: boolean().nullable(),
54668
+ /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
54669
+ activeEvents: array(string()),
54670
+ /** Numeric error code when the appliance reports one; null otherwise. */
54585
54671
  errorCode: number().nullable(),
54672
+ /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
54586
54673
  errorLabel: string().nullable(),
54674
+ /** Ms epoch of the last value received from the appliance; 0 = never. */
54587
54675
  lastFetchedAt: number()
54588
54676
  });
54589
- DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
54677
+ var DeviceIdSchema = number().int().nonnegative();
54678
+ /** A value a program option or setting can take. */
54679
+ var DishwasherValueSchema = union([
54680
+ boolean(),
54681
+ string(),
54682
+ number()
54683
+ ]);
54684
+ var DishwasherAccessSchema = _enum([
54685
+ "none",
54686
+ "read",
54687
+ "readWrite",
54688
+ "writeOnly",
54689
+ "readStatic"
54690
+ ]);
54691
+ /**
54692
+ * One option or setting the appliance declares, with the constraints its description
54693
+ * gives. A `readWrite` descriptor that is `available` is what `setOption` accepts.
54694
+ */
54695
+ var DishwasherOptionDescriptorSchema = object({
54696
+ /** Feature key (`Dishcare.Dishwasher.Option.ExtraDry`). */
54697
+ key: string(),
54698
+ /** Last segment of the key, for display. */
54699
+ name: string(),
54700
+ type: _enum([
54701
+ "boolean",
54702
+ "number",
54703
+ "enum",
54704
+ "string"
54705
+ ]),
54706
+ access: DishwasherAccessSchema,
54707
+ /** The appliance reports this entity as currently available. */
54708
+ available: boolean(),
54709
+ /** Unit the description declares for a number; null when it declares none. */
54710
+ unit: string().nullable(),
54711
+ min: number().nullable(),
54712
+ max: number().nullable(),
54713
+ step: number().nullable(),
54714
+ /** Allowed members of an enum; null for other types. */
54715
+ enumValues: array(string()).nullable(),
54716
+ defaultValue: DishwasherValueSchema.nullable()
54717
+ });
54718
+ var DishwasherProgramSchema = object({
54719
+ /** Feature key (`Dishcare.Dishwasher.Program.Eco50`) — what `setProgram` / `start` take. */
54720
+ key: string(),
54721
+ name: string(),
54722
+ /** The appliance currently offers this program. */
54723
+ available: boolean(),
54724
+ /** The options this program lists, with the program-level access / availability / default. */
54725
+ options: array(DishwasherOptionDescriptorSchema)
54726
+ });
54727
+ var DishwasherOptionsSchema = object({
54728
+ availability: DishwasherAvailabilitySchema,
54729
+ /** What the appliance can be told to do, from the commands its description declares. */
54730
+ supportsStart: boolean(),
54731
+ supportsPause: boolean(),
54732
+ supportsResume: boolean(),
54733
+ supportsStop: boolean(),
54734
+ supportsPower: boolean(),
54735
+ /** Every program the description lists, with its options. */
54736
+ programs: array(DishwasherProgramSchema),
54737
+ /** Every option and setting entity the description lists (programs reference these by key). */
54738
+ options: array(DishwasherOptionDescriptorSchema)
54739
+ });
54740
+ DeviceType.Dishwasher, method(object({
54741
+ deviceId: DeviceIdSchema,
54742
+ program: string().min(1).optional(),
54743
+ options: record(string(), DishwasherValueSchema).optional()
54744
+ }), _void(), {
54590
54745
  kind: "mutation",
54591
54746
  auth: "protected"
54592
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54747
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
54593
54748
  kind: "mutation",
54594
54749
  auth: "protected"
54595
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54750
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
54751
+ kind: "mutation",
54752
+ auth: "protected"
54753
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
54596
54754
  kind: "mutation",
54597
54755
  auth: "protected"
54598
54756
  }), method(object({
54599
- deviceId: number().int().nonnegative(),
54757
+ deviceId: DeviceIdSchema,
54600
54758
  program: string().min(1)
54601
54759
  }), _void(), {
54602
54760
  kind: "mutation",
54603
54761
  auth: "protected"
54604
54762
  }), method(object({
54605
- deviceId: number().int().nonnegative(),
54763
+ deviceId: DeviceIdSchema,
54606
54764
  on: boolean()
54607
54765
  }), _void(), {
54608
54766
  kind: "mutation",
54609
54767
  auth: "protected"
54610
54768
  }), method(object({
54611
- deviceId: number().int().nonnegative(),
54769
+ deviceId: DeviceIdSchema,
54612
54770
  on: boolean()
54613
54771
  }), _void(), {
54614
54772
  kind: "mutation",
54615
54773
  auth: "protected"
54616
54774
  }), method(object({
54617
- deviceId: number().int().nonnegative(),
54775
+ deviceId: DeviceIdSchema,
54618
54776
  on: boolean()
54619
54777
  }), _void(), {
54620
54778
  kind: "mutation",
54621
54779
  auth: "protected"
54622
54780
  }), method(object({
54623
- deviceId: number().int().nonnegative(),
54781
+ deviceId: DeviceIdSchema,
54782
+ on: boolean()
54783
+ }), _void(), {
54784
+ kind: "mutation",
54785
+ auth: "protected"
54786
+ }), method(object({
54787
+ deviceId: DeviceIdSchema,
54788
+ on: boolean()
54789
+ }), _void(), {
54790
+ kind: "mutation",
54791
+ auth: "protected"
54792
+ }), method(object({
54793
+ deviceId: DeviceIdSchema,
54794
+ on: boolean()
54795
+ }), _void(), {
54796
+ kind: "mutation",
54797
+ auth: "protected"
54798
+ }), method(object({
54799
+ deviceId: DeviceIdSchema,
54800
+ on: boolean()
54801
+ }), _void(), {
54802
+ kind: "mutation",
54803
+ auth: "protected"
54804
+ }), method(object({
54805
+ deviceId: DeviceIdSchema,
54624
54806
  key: string().min(1),
54625
- value: union([
54626
- boolean(),
54627
- string(),
54628
- number()
54629
- ])
54807
+ value: DishwasherValueSchema
54630
54808
  }), _void(), {
54631
54809
  kind: "mutation",
54632
54810
  auth: "protected"
54633
- });
54811
+ }), method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" });
54634
54812
  object({
54635
54813
  /** Ms epoch of the last press. null = never observed since this provider started. */
54636
54814
  lastPressedAt: number().nullable(),
@@ -64690,6 +64868,12 @@ Object.freeze({
64690
64868
  addonId: null,
64691
64869
  access: "view"
64692
64870
  },
64871
+ "dishwasher.getOptions": {
64872
+ capName: "dishwasher",
64873
+ capScope: "device",
64874
+ addonId: null,
64875
+ access: "view"
64876
+ },
64693
64877
  "dishwasher.getStatus": {
64694
64878
  capName: "dishwasher",
64695
64879
  capScope: "device",
@@ -64702,6 +64886,24 @@ Object.freeze({
64702
64886
  addonId: null,
64703
64887
  access: "create"
64704
64888
  },
64889
+ "dishwasher.resume": {
64890
+ capName: "dishwasher",
64891
+ capScope: "device",
64892
+ addonId: null,
64893
+ access: "create"
64894
+ },
64895
+ "dishwasher.setChildLock": {
64896
+ capName: "dishwasher",
64897
+ capScope: "device",
64898
+ addonId: null,
64899
+ access: "create"
64900
+ },
64901
+ "dishwasher.setExtraDry": {
64902
+ capName: "dishwasher",
64903
+ capScope: "device",
64904
+ addonId: null,
64905
+ access: "create"
64906
+ },
64705
64907
  "dishwasher.setHalfLoad": {
64706
64908
  capName: "dishwasher",
64707
64909
  capScope: "device",
@@ -64714,6 +64916,12 @@ Object.freeze({
64714
64916
  addonId: null,
64715
64917
  access: "create"
64716
64918
  },
64919
+ "dishwasher.setIntensiveZone": {
64920
+ capName: "dishwasher",
64921
+ capScope: "device",
64922
+ addonId: null,
64923
+ access: "create"
64924
+ },
64717
64925
  "dishwasher.setOption": {
64718
64926
  capName: "dishwasher",
64719
64927
  capScope: "device",
@@ -64732,6 +64940,12 @@ Object.freeze({
64732
64940
  addonId: null,
64733
64941
  access: "create"
64734
64942
  },
64943
+ "dishwasher.setVarioSpeedPlus": {
64944
+ capName: "dishwasher",
64945
+ capScope: "device",
64946
+ addonId: null,
64947
+ access: "create"
64948
+ },
64735
64949
  "dishwasher.start": {
64736
64950
  capName: "dishwasher",
64737
64951
  capScope: "device",
@@ -69178,6 +69392,12 @@ Object.freeze({
69178
69392
  addonId: null,
69179
69393
  access: "view"
69180
69394
  },
69395
+ "storageMigration.moverClearance": {
69396
+ capName: "storage-migration",
69397
+ capScope: "system",
69398
+ addonId: null,
69399
+ access: "view"
69400
+ },
69181
69401
  "storageMigration.movers": {
69182
69402
  capName: "storage-migration",
69183
69403
  capScope: "system",
@@ -71371,6 +71591,11 @@ Object.freeze({
71371
71591
  form: "single",
71372
71592
  optional: false
71373
71593
  }],
71594
+ "dishwasher.getOptions": [{
71595
+ name: "deviceId",
71596
+ form: "single",
71597
+ optional: false
71598
+ }],
71374
71599
  "dishwasher.getStatus": [{
71375
71600
  name: "deviceId",
71376
71601
  form: "single",
@@ -71381,6 +71606,21 @@ Object.freeze({
71381
71606
  form: "single",
71382
71607
  optional: false
71383
71608
  }],
71609
+ "dishwasher.resume": [{
71610
+ name: "deviceId",
71611
+ form: "single",
71612
+ optional: false
71613
+ }],
71614
+ "dishwasher.setChildLock": [{
71615
+ name: "deviceId",
71616
+ form: "single",
71617
+ optional: false
71618
+ }],
71619
+ "dishwasher.setExtraDry": [{
71620
+ name: "deviceId",
71621
+ form: "single",
71622
+ optional: false
71623
+ }],
71384
71624
  "dishwasher.setHalfLoad": [{
71385
71625
  name: "deviceId",
71386
71626
  form: "single",
@@ -71391,6 +71631,11 @@ Object.freeze({
71391
71631
  form: "single",
71392
71632
  optional: false
71393
71633
  }],
71634
+ "dishwasher.setIntensiveZone": [{
71635
+ name: "deviceId",
71636
+ form: "single",
71637
+ optional: false
71638
+ }],
71394
71639
  "dishwasher.setOption": [{
71395
71640
  name: "deviceId",
71396
71641
  form: "single",
@@ -71406,6 +71651,11 @@ Object.freeze({
71406
71651
  form: "single",
71407
71652
  optional: false
71408
71653
  }],
71654
+ "dishwasher.setVarioSpeedPlus": [{
71655
+ name: "deviceId",
71656
+ form: "single",
71657
+ optional: false
71658
+ }],
71409
71659
  "dishwasher.start": [{
71410
71660
  name: "deviceId",
71411
71661
  form: "single",
@@ -73447,6 +73697,9 @@ new Map(Object.entries({
73447
73697
  "wrapper-update-available": "system",
73448
73698
  "backup-completed": "system",
73449
73699
  "export-completed": "system",
73700
+ "alert-critical": "system",
73701
+ "alert-error": "system",
73702
+ "alert-critical-resolved": "system",
73450
73703
  "storage-location-low": "system",
73451
73704
  "storage-location-critical": "system",
73452
73705
  "storage-location-ok": "system",
@@ -73571,6 +73824,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
73571
73824
  "storage-location-ok"
73572
73825
  ]
73573
73826
  },
73827
+ {
73828
+ id: "alerts",
73829
+ family: "system",
73830
+ label: "Alerts",
73831
+ kinds: [
73832
+ "alert-critical",
73833
+ "alert-error",
73834
+ "alert-critical-resolved"
73835
+ ]
73836
+ },
73574
73837
  {
73575
73838
  id: "alarm",
73576
73839
  family: "alarm",
@@ -73615,6 +73878,21 @@ var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.ent
73615
73878
  return [[descriptor.kind, field]];
73616
73879
  })));
73617
73880
  Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
73881
+ object({
73882
+ phase: _enum(["raised", "resolved"]),
73883
+ episodeId: string().min(1),
73884
+ alertId: string().min(1),
73885
+ severity: AlertSeveritySchema,
73886
+ category: string(),
73887
+ title: string(),
73888
+ message: string(),
73889
+ sourceType: string().optional(),
73890
+ sourceId: string().optional(),
73891
+ nodeId: string().optional(),
73892
+ /** The alert's own action link (where the operator acts on it), when it has one. */
73893
+ actionUrl: string().optional(),
73894
+ at: number()
73895
+ });
73618
73896
  var RULE_BODY_ONLY = ["body"];
73619
73897
  var RULE_TEXT = ["title", "body"];
73620
73898
  var BATTERY = ["device-battery-low", "device-battery-normal"];
@@ -73624,6 +73902,11 @@ var TANK = [
73624
73902
  "device-tank-full",
73625
73903
  "device-tank-normal"
73626
73904
  ];
73905
+ var ALERT_EPISODE = [
73906
+ "alert-critical",
73907
+ "alert-error",
73908
+ "alert-critical-resolved"
73909
+ ];
73627
73910
  var UPDATES = [
73628
73911
  "addon-update-available",
73629
73912
  "server-update-available",
@@ -73816,14 +74099,14 @@ var STATE_SUMMARY_VARS = [
73816
74099
  ([...RULE_TEXT], [...CONSUMABLE]),
73817
74100
  ([...RULE_TEXT], [...TANK]),
73818
74101
  [...RULE_TEXT],
73819
- ([...RULE_TEXT], [...UPDATES]),
73820
- ([...RULE_TEXT], [...UPDATES]),
73821
- [...RULE_TEXT],
74102
+ ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
74103
+ ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
73822
74104
  [...RULE_TEXT],
73823
74105
  [...RULE_TEXT],
73824
74106
  [...RULE_TEXT],
73825
74107
  [...RULE_TEXT],
73826
74108
  [...RULE_TEXT],
74109
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
73827
74110
  [...RULE_TEXT],
73828
74111
  [...RULE_TEXT],
73829
74112
  [...RULE_TEXT],
@@ -73841,7 +74124,11 @@ var STATE_SUMMARY_VARS = [
73841
74124
  [...RULE_TEXT],
73842
74125
  [...RULE_TEXT],
73843
74126
  [...RULE_TEXT],
73844
- [...RULE_TEXT],
74127
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74128
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74129
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74130
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74131
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
73845
74132
  [...RULE_TEXT],
73846
74133
  [...RULE_TEXT],
73847
74134
  [...RULE_TEXT],
@@ -74379,6 +74666,37 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
74379
74666
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
74380
74667
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
74381
74668
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
74669
+ /**
74670
+ * Where an OAuth round-trip ends: the admin-ui completion route
74671
+ * (`/sso-complete`, the page every popup sign-in already lands on), with the
74672
+ * OUTCOME in the URL fragment.
74673
+ *
74674
+ * The outcome is a status plus a reason CODE from a closed list. It never
74675
+ * carries a token, a code, a client secret or a free-form error message: the
74676
+ * page words each code itself, and an unknown code reads as `unknown` rather
74677
+ * than being echoed. Fragment only, so nothing here reaches a server log.
74678
+ *
74679
+ * Pure and browser-safe.
74680
+ */
74681
+ /** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
74682
+ var OAUTH_LANDING_PATH = "/sso-complete";
74683
+ /**
74684
+ * The landing URL for `outcome`. `origin` is the hub address the flow already
74685
+ * runs on (from the `local-network` palette role, never a literal); `null`
74686
+ * gives a hub-relative URL.
74687
+ */
74688
+ function oauthLandingUrl(origin, outcome) {
74689
+ const params = new URLSearchParams({
74690
+ return: "oauth",
74691
+ status: outcome.status
74692
+ });
74693
+ if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
74694
+ if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
74695
+ if (outcome.integration !== void 0) params.set("integration", outcome.integration);
74696
+ if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
74697
+ if (outcome.action === "disconnect") params.set("action", "disconnect");
74698
+ return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
74699
+ }
74382
74700
  var MB = 1024 * 1024;
74383
74701
  1024 * MB, 3072 * MB;
74384
74702
  //#endregion
@@ -74426,13 +74744,22 @@ var DEFAULT_OAUTH_STATE = {
74426
74744
  accessExpiresAt: 0,
74427
74745
  accountEmail: ""
74428
74746
  };
74429
- var GoogleOauthAccountsStateSchema = object({ accounts: array(object({
74747
+ /** Per-broker OAuth tokens (durable state `oauthAccounts`). */
74748
+ var GoogleOauthAccountSchema = object({
74430
74749
  id: string().min(1),
74431
74750
  refreshToken: string().default("").meta({ secret: true }),
74432
74751
  accessToken: string().default("").meta({ secret: true }),
74433
74752
  accessExpiresAt: number().default(0),
74434
- accountEmail: string().default("")
74435
- })).default([]) });
74753
+ accountEmail: string().default(""),
74754
+ /**
74755
+ * Google refused the refresh token (`invalid_grant`): it is dead and only a
74756
+ * new sign-in replaces it. Set where the refusal is seen, cleared by the
74757
+ * OAuth callback that stores a fresh token — so the status, the Sign-in
74758
+ * button and the alert all read the same fact.
74759
+ */
74760
+ needsReauth: boolean().default(false)
74761
+ });
74762
+ var GoogleOauthAccountsStateSchema = object({ accounts: array(GoogleOauthAccountSchema).default([]) });
74436
74763
  var GOOGLE_BROKER_KIND = "google";
74437
74764
  var GOOGLE_BROKER_KIND_LABEL = "Google";
74438
74765
  var GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
@@ -74521,7 +74848,8 @@ function migrateLegacyToBrokers(rawConfig, legacyOauth) {
74521
74848
  refreshToken: legacyOauth.refreshToken,
74522
74849
  accessToken: legacyOauth.accessToken,
74523
74850
  accessExpiresAt: legacyOauth.accessExpiresAt,
74524
- accountEmail: legacyOauth.accountEmail
74851
+ accountEmail: legacyOauth.accountEmail,
74852
+ needsReauth: false
74525
74853
  }] : [];
74526
74854
  return {
74527
74855
  brokers: [entry],
@@ -74556,7 +74884,10 @@ var GoogleBrokerRegistry = class {
74556
74884
  oauthById = /* @__PURE__ */ new Map();
74557
74885
  seedFrom(entries, accounts) {
74558
74886
  this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
74559
- this.oauthById = new Map(accounts.map((a) => [a.id, { ...a }]));
74887
+ this.oauthById = new Map(accounts.map((a) => {
74888
+ const account = GoogleOauthAccountSchema.parse(a);
74889
+ return [account.id, account];
74890
+ }));
74560
74891
  this.seedCounterFromEntries(this.entries);
74561
74892
  }
74562
74893
  listEntries() {
@@ -74572,14 +74903,28 @@ var GoogleBrokerRegistry = class {
74572
74903
  return [...this.oauthById.values()];
74573
74904
  }
74574
74905
  setOauth(account) {
74575
- this.oauthById.set(account.id, { ...account });
74906
+ const parsed = GoogleOauthAccountSchema.parse(account);
74907
+ this.oauthById.set(parsed.id, parsed);
74576
74908
  }
74577
74909
  removeOauth(id) {
74578
74910
  this.oauthById.delete(id);
74579
74911
  }
74580
74912
  isConnected(id) {
74581
74913
  const o = this.oauthById.get(id);
74582
- return o !== void 0 && o.refreshToken.length > 0;
74914
+ return o !== void 0 && o.refreshToken.length > 0 && !o.needsReauth;
74915
+ }
74916
+ /** A refresh token Google refused: kept (a sign-in replaces it) but not usable. */
74917
+ needsReauth(id) {
74918
+ return this.oauthById.get(id)?.needsReauth === true;
74919
+ }
74920
+ markNeedsReauth(id) {
74921
+ const o = this.oauthById.get(id);
74922
+ if (o === void 0 || o.needsReauth) return false;
74923
+ this.oauthById.set(id, {
74924
+ ...o,
74925
+ needsReauth: true
74926
+ });
74927
+ return true;
74583
74928
  }
74584
74929
  allocateId() {
74585
74930
  const id = `google_${String(this.nextIdCounter).padStart(3, "0")}`;
@@ -74629,18 +74974,19 @@ var GoogleBrokerRegistry = class {
74629
74974
  toBrokerInfo(entry, addonId) {
74630
74975
  const oauth = this.oauthById.get(entry.id);
74631
74976
  const connected = this.isConnected(entry.id);
74977
+ const reauth = this.needsReauth(entry.id);
74632
74978
  return {
74633
74979
  id: entry.id,
74634
74980
  addonId,
74635
74981
  name: entry.name,
74636
74982
  kind: GOOGLE_BROKER_KIND,
74637
- status: connected ? "connected" : "disconnected",
74983
+ status: connected ? "connected" : reauth ? "auth-failed" : "disconnected",
74638
74984
  info: {
74639
74985
  accountEmail: oauth?.accountEmail ?? "",
74640
74986
  hasClient: entry.clientId.length > 0
74641
74987
  },
74642
74988
  lastCheckedAt: connected ? Date.now() : null,
74643
- error: connected ? null : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
74989
+ error: connected ? null : reauth ? "Google session expired — sign in again" : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
74644
74990
  };
74645
74991
  }
74646
74992
  listBrokerInfos(addonId, kind) {
@@ -74692,7 +75038,7 @@ function consoleLinksHtml$1() {
74692
75038
  ].join("");
74693
75039
  }
74694
75040
  function introHtml(cap, publicOrigin) {
74695
- const redirect = escapeHtml$2(redirectUriDisplay(publicOrigin));
75041
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
74696
75042
  const console = consoleLinksHtml$1();
74697
75043
  if (cap === "smtp-provider") return [
74698
75044
  "<ol>",
@@ -74725,7 +75071,7 @@ function introHtml(cap, publicOrigin) {
74725
75071
  ].join("");
74726
75072
  }
74727
75073
  function connectHtml(cap, publicOrigin) {
74728
- const redirect = escapeHtml$2(redirectUriDisplay(publicOrigin));
75074
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
74729
75075
  if (cap === "smtp-provider") return [
74730
75076
  `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
74731
75077
  "Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
@@ -74745,7 +75091,7 @@ function connectHtml(cap, publicOrigin) {
74745
75091
  `Console: ${consoleLinksHtml$1()}.`
74746
75092
  ].join("");
74747
75093
  }
74748
- function escapeHtml$2(s) {
75094
+ function escapeHtml$1(s) {
74749
75095
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
74750
75096
  }
74751
75097
  //#endregion
@@ -74764,7 +75110,7 @@ function consoleLinksHtml() {
74764
75110
  }
74765
75111
  /** Setup steps shown on Add / Edit — includes Audience / Test users. */
74766
75112
  function googleBrokerSetupHtml(publicOrigin) {
74767
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
75113
+ const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
74768
75114
  return [
74769
75115
  "<ol>",
74770
75116
  `<li>Open Google Cloud Console (${consoleLinksHtml()}). Create a <strong>new project</strong> (or pick an existing one) from the project picker at the top.</li>`,
@@ -74840,7 +75186,7 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
74840
75186
  }
74841
75187
  /** Connectivity Calendars / Email pages — calendar import picker lives here (not on broker Add). */
74842
75188
  function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
74843
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
75189
+ const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
74844
75190
  if (cap === "calendar-source") return { sections: [{
74845
75191
  id: "setup-intro",
74846
75192
  title: "Google brokers",
@@ -74898,7 +75244,7 @@ function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
74898
75244
  }]
74899
75245
  }] };
74900
75246
  }
74901
- function escapeHtml$1(s) {
75247
+ function escapeHtml(s) {
74902
75248
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
74903
75249
  }
74904
75250
  //#endregion
@@ -75193,7 +75539,6 @@ function isInvalidGrantRefreshError(err) {
75193
75539
  return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
75194
75540
  }
75195
75541
  function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75196
- const pushLine = `${brokerName}: Google session expired — sign in again`;
75197
75542
  return {
75198
75543
  id: googleBrokerReconnectAlertId(brokerId),
75199
75544
  category: "google.broker-reconnect",
@@ -75202,7 +75547,8 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75202
75547
  message: `${brokerName} needs you to sign in to Google again.`,
75203
75548
  action: {
75204
75549
  label: "Sign in again",
75205
- href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`
75550
+ href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`,
75551
+ navigation: "server"
75206
75552
  },
75207
75553
  status: "active",
75208
75554
  read: false,
@@ -75212,10 +75558,7 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75212
75558
  type: "google-broker",
75213
75559
  id: brokerId
75214
75560
  },
75215
- metadata: {
75216
- episodeKey: "reconnect",
75217
- pushLine
75218
- }
75561
+ metadata: { episodeKey: "reconnect" }
75219
75562
  };
75220
75563
  }
75221
75564
  //#endregion
@@ -75424,6 +75767,14 @@ var GoogleBrokerStore = class GoogleBrokerStore {
75424
75767
  * One OAuth account = one broker entry; feature caps aggregate connected brokers.
75425
75768
  */
75426
75769
  var ADDON_ID = "google-services";
75770
+ /** A callback failure the landing page can name by code. */
75771
+ var GoogleCallbackError = class extends Error {
75772
+ reason;
75773
+ constructor(reason, message) {
75774
+ super(message);
75775
+ this.reason = reason;
75776
+ }
75777
+ };
75427
75778
  var CALLBACK_PATH = "/oauth/callback";
75428
75779
  var START_PATH = "/oauth/start";
75429
75780
  var DISCONNECT_PATH = "/oauth/disconnect";
@@ -75444,7 +75795,7 @@ var GoogleServicesAddon = class extends BaseAddon {
75444
75795
  registry = new GoogleBrokerRegistry();
75445
75796
  brokerStore = null;
75446
75797
  activeBrokers = [];
75447
- /** CSRF state → { brokerId, popup, issuedAt } — popup mirrors OIDC D794. */
75798
+ /** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
75448
75799
  pendingStates = /* @__PURE__ */ new Map();
75449
75800
  publicOrigin = null;
75450
75801
  constructor() {
@@ -75480,7 +75831,8 @@ var GoogleServicesAddon = class extends BaseAddon {
75480
75831
  refreshToken: legacyOauth.refreshToken,
75481
75832
  accessToken: legacyOauth.accessToken,
75482
75833
  accessExpiresAt: legacyOauth.accessExpiresAt,
75483
- accountEmail: legacyOauth.accountEmail
75834
+ accountEmail: legacyOauth.accountEmail,
75835
+ needsReauth: false
75484
75836
  }] };
75485
75837
  await this.oauthAccountsHandle.set(oauthAccounts);
75486
75838
  await legacyHandle.set({ ...DEFAULT_OAUTH_STATE });
@@ -75751,12 +76103,9 @@ var GoogleServicesAddon = class extends BaseAddon {
75751
76103
  reply.status(500).send({ error: err instanceof Error ? err.message : String(err) });
75752
76104
  return;
75753
76105
  }
75754
- const popupRaw = req.query["popup"];
75755
- const popup = popupRaw === "1" || popupRaw === "true";
75756
76106
  const state = randomBytes(16).toString("hex");
75757
76107
  this.pendingStates.set(state, {
75758
76108
  brokerId,
75759
- popup,
75760
76109
  issuedAt: Date.now()
75761
76110
  });
75762
76111
  this.prunePendingStates();
@@ -75768,42 +76117,39 @@ var GoogleServicesAddon = class extends BaseAddon {
75768
76117
  reply.redirect(url);
75769
76118
  }
75770
76119
  async handleCallback(req, reply) {
76120
+ const state = typeof req.query["state"] === "string" ? req.query["state"] : "";
75771
76121
  const err = req.query["error"];
75772
76122
  if (err) {
75773
- const statePeek = typeof req.query["state"] === "string" ? req.query["state"] : "";
75774
- const pendingPeek = statePeek ? this.pendingStates.get(statePeek) : void 0;
75775
- if (pendingPeek?.popup) {
75776
- this.pendingStates.delete(statePeek);
75777
- this.popupComplete(reply, {
75778
- ok: false,
75779
- brokerId: pendingPeek.brokerId,
75780
- error: String(err)
75781
- });
75782
- return;
75783
- }
75784
- this.html(reply, 400, `Google OAuth error: ${err}`);
76123
+ const pendingPeek = state ? this.pendingStates.get(state) : void 0;
76124
+ if (pendingPeek !== void 0) this.pendingStates.delete(state);
76125
+ await this.landOnAdminUi(reply, {
76126
+ status: "error",
76127
+ addonId: ADDON_ID,
76128
+ ...pendingPeek !== void 0 ? { brokerId: pendingPeek.brokerId } : {},
76129
+ reason: err === "access_denied" ? "access_denied" : "invalid_request"
76130
+ });
75785
76131
  return;
75786
76132
  }
75787
76133
  const code = req.query["code"];
75788
- const state = req.query["state"];
75789
- const pending = typeof state === "string" ? this.pendingStates.get(state) : void 0;
76134
+ const pending = state ? this.pendingStates.get(state) : void 0;
75790
76135
  if (!code || !state || pending === void 0) {
75791
- this.html(reply, 400, "Invalid or expired OAuth state.");
76136
+ await this.landOnAdminUi(reply, {
76137
+ status: "error",
76138
+ addonId: ADDON_ID,
76139
+ reason: "invalid_state"
76140
+ });
75792
76141
  return;
75793
76142
  }
75794
76143
  this.pendingStates.delete(state);
75795
- const { brokerId, popup } = pending;
76144
+ const { brokerId } = pending;
75796
76145
  const entry = this.registry.getEntry(brokerId);
75797
76146
  if (entry === null) {
75798
- if (popup) {
75799
- this.popupComplete(reply, {
75800
- ok: false,
75801
- brokerId,
75802
- error: `Unknown Google broker: ${brokerId}`
75803
- });
75804
- return;
75805
- }
75806
- this.html(reply, 404, `Unknown Google broker: ${brokerId}`);
76147
+ await this.landOnAdminUi(reply, {
76148
+ status: "error",
76149
+ addonId: ADDON_ID,
76150
+ brokerId,
76151
+ reason: "unknown_broker"
76152
+ });
75807
76153
  return;
75808
76154
  }
75809
76155
  try {
@@ -75815,11 +76161,12 @@ var GoogleServicesAddon = class extends BaseAddon {
75815
76161
  }, code);
75816
76162
  const prev = this.registry.getOauth(brokerId) ?? {
75817
76163
  id: brokerId,
75818
- ...DEFAULT_OAUTH_STATE
76164
+ ...DEFAULT_OAUTH_STATE,
76165
+ needsReauth: false
75819
76166
  };
75820
- if (!tokens.refresh_token && !prev.refreshToken) throw new Error("Google did not return a refresh_token. Revoke CamStack access in Google Account → Security and retry with prompt=consent.");
76167
+ if (!tokens.refresh_token && (!prev.refreshToken || prev.needsReauth)) throw new GoogleCallbackError("no_refresh_token", "Google did not return a refresh_token. Revoke CamStack access in Google Account → Security and retry with prompt=consent.");
75821
76168
  const applied = applyTokenResponse({
75822
- refreshToken: prev.refreshToken,
76169
+ refreshToken: prev.needsReauth && tokens.refresh_token ? "" : prev.refreshToken,
75823
76170
  accessToken: prev.accessToken,
75824
76171
  accessExpiresAt: prev.accessExpiresAt,
75825
76172
  accountEmail: prev.accountEmail
@@ -75830,7 +76177,8 @@ var GoogleServicesAddon = class extends BaseAddon {
75830
76177
  refreshToken: applied.refreshToken,
75831
76178
  accessToken: applied.accessToken,
75832
76179
  accessExpiresAt: applied.accessExpiresAt,
75833
- accountEmail: email || applied.accountEmail
76180
+ accountEmail: email || applied.accountEmail,
76181
+ needsReauth: false
75834
76182
  };
75835
76183
  this.registry.setOauth(next);
75836
76184
  await this.persistOauthAccounts();
@@ -75843,65 +76191,44 @@ var GoogleServicesAddon = class extends BaseAddon {
75843
76191
  this.registry.replaceEntries(this.registry.listEntries().map((e) => e.id === brokerId ? renamed : e));
75844
76192
  await this.persistBrokers();
75845
76193
  }
75846
- this.ctx.logger.info("google oauth connected", { meta: {
75847
- brokerId,
75848
- hasRefresh: next.refreshToken.length > 0,
75849
- accountEmail: next.accountEmail || void 0
75850
- } });
75851
- if (popup) {
75852
- this.popupComplete(reply, {
75853
- ok: true,
76194
+ this.ctx.logger.info("google oauth connected", {
76195
+ tags: { brokerId },
76196
+ meta: {
75854
76197
  brokerId,
75855
- message: "Google account connected. This window should close."
75856
- });
75857
- return;
75858
- }
75859
- this.html(reply, 200, `Google account connected for ${brokerId} (Calendar + Gmail send). You can close this window.`);
76198
+ hasRefresh: next.refreshToken.length > 0,
76199
+ accountEmail: next.accountEmail || void 0
76200
+ }
76201
+ });
76202
+ await this.landOnAdminUi(reply, {
76203
+ status: "success",
76204
+ addonId: ADDON_ID,
76205
+ brokerId
76206
+ });
75860
76207
  } catch (e) {
75861
76208
  const message = e instanceof Error ? e.message : String(e);
75862
76209
  this.ctx.logger.warn("google oauth callback failed", { meta: {
75863
76210
  brokerId,
75864
76211
  error: message
75865
76212
  } });
75866
- if (popup) {
75867
- this.popupComplete(reply, {
75868
- ok: false,
75869
- brokerId,
75870
- error: message
75871
- });
75872
- return;
75873
- }
75874
- this.html(reply, 500, `OAuth failed: ${message}`);
76213
+ await this.landOnAdminUi(reply, {
76214
+ status: "error",
76215
+ addonId: ADDON_ID,
76216
+ brokerId,
76217
+ reason: e instanceof GoogleCallbackError ? e.reason : "exchange_failed"
76218
+ });
75875
76219
  }
75876
76220
  }
75877
76221
  /**
75878
- * Popup completion page (OIDC D794 pattern): postMessage to opener at the
75879
- * hub origin, then close. Google never redirects back to admin-ui — this
75880
- * page is the only return surface.
76222
+ * Every OAuth completion ends in CamStack: a redirect to the admin-ui
76223
+ * completion page (`/sso-complete`) on the hub's public address (the
76224
+ * `local-network` palette role), carrying a status and a reason CODE — never
76225
+ * a token, a code or an error message. The page hands a popup's outcome to
76226
+ * its opener and closes it, or, in a full-page flow, shows the result and
76227
+ * returns to Connectivity.
75881
76228
  */
75882
- popupComplete(reply, outcome) {
75883
- const payload = JSON.stringify({
75884
- type: "camstack:broker-oauth",
75885
- ok: outcome.ok,
75886
- brokerId: outcome.brokerId,
75887
- ...outcome.error !== void 0 ? { error: outcome.error } : {}
75888
- });
75889
- const body = outcome.ok ? outcome.message ?? "Done. This window should close." : `OAuth failed: ${outcome.error ?? "unknown"}`;
75890
- reply.header("content-type", "text/html; charset=utf-8");
75891
- reply.status(outcome.ok ? 200 : 400).send(`<!doctype html><html><head><title>Google</title></head><body>
75892
- <p>${escapeHtml(body)}</p>
75893
- <script>
75894
- (function () {
75895
- var msg = ${payload};
75896
- try {
75897
- if (window.opener) {
75898
- window.opener.postMessage(msg, window.location.origin);
75899
- }
75900
- } catch (e) {}
75901
- setTimeout(function () { window.close(); }, 200);
75902
- })();
75903
- <\/script>
75904
- </body></html>`);
76229
+ async landOnAdminUi(reply, outcome) {
76230
+ await this.refreshPublicOrigin();
76231
+ reply.redirect(oauthLandingUrl(this.publicOrigin, outcome));
75905
76232
  }
75906
76233
  async fetchAccountEmail(accessToken) {
75907
76234
  try {
@@ -75913,10 +76240,6 @@ var GoogleServicesAddon = class extends BaseAddon {
75913
76240
  return "";
75914
76241
  }
75915
76242
  }
75916
- html(reply, code, body) {
75917
- reply.header("content-type", "text/html; charset=utf-8");
75918
- reply.status(code).send(`<!doctype html><html><body><p>${escapeHtml(body)}</p></body></html>`);
75919
- }
75920
76243
  prunePendingStates() {
75921
76244
  const cutoff = Date.now() - 10 * 6e4;
75922
76245
  for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
@@ -76004,6 +76327,7 @@ var GoogleServicesAddon = class extends BaseAddon {
76004
76327
  const entry = this.registry.getEntry(brokerId);
76005
76328
  const oauth = this.registry.getOauth(brokerId);
76006
76329
  if (entry === null || oauth === null || !oauth.refreshToken.trim()) throw new Error(`Google broker ${brokerId} is not connected — use Sign in under Connectivity → Brokers`);
76330
+ if (oauth.needsReauth) throw new Error(`Google broker ${brokerId} needs a new sign-in — use Sign in under Connectivity → Brokers`);
76007
76331
  if (!entry.clientId.trim() || !entry.clientSecret.trim()) throw userError("google.client-credentials-required", void 0, "Configure Client ID and Client secret before refreshing the Google token");
76008
76332
  const state = {
76009
76333
  refreshToken: oauth.refreshToken.trim(),
@@ -76019,7 +76343,10 @@ var GoogleServicesAddon = class extends BaseAddon {
76019
76343
  clientSecret: entry.clientSecret.trim()
76020
76344
  }, state.refreshToken);
76021
76345
  } catch (err) {
76022
- if (isInvalidGrantRefreshError(err)) this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76346
+ if (isInvalidGrantRefreshError(err)) {
76347
+ if (this.registry.markNeedsReauth(brokerId)) await this.persistOauthAccounts();
76348
+ this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76349
+ }
76023
76350
  throw err;
76024
76351
  }
76025
76352
  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");
@@ -76029,7 +76356,8 @@ var GoogleServicesAddon = class extends BaseAddon {
76029
76356
  refreshToken: nextState.refreshToken,
76030
76357
  accessToken: nextState.accessToken,
76031
76358
  accessExpiresAt: nextState.accessExpiresAt,
76032
- accountEmail: nextState.accountEmail
76359
+ accountEmail: nextState.accountEmail,
76360
+ needsReauth: false
76033
76361
  });
76034
76362
  await this.persistOauthAccounts();
76035
76363
  return nextState.accessToken;
@@ -76045,20 +76373,18 @@ var GoogleServicesAddon = class extends BaseAddon {
76045
76373
  reply.status(404).send({ error: `Unknown Google broker: ${brokerId}` });
76046
76374
  return;
76047
76375
  }
76048
- const popupRaw = req.query["popup"];
76049
- const popup = popupRaw === "1" || popupRaw === "true";
76050
76376
  this.registry.removeOauth(brokerId);
76051
76377
  await this.persistOauthAccounts();
76052
- this.ctx.logger.info("google oauth disconnected", { meta: { brokerId } });
76053
- if (popup) {
76054
- this.popupComplete(reply, {
76055
- ok: true,
76056
- brokerId,
76057
- message: "Google account disconnected. This window should close."
76058
- });
76059
- return;
76060
- }
76061
- this.html(reply, 200, `Google account disconnected for ${brokerId}. You can close this window.`);
76378
+ this.ctx.logger.info("google oauth disconnected", {
76379
+ tags: { brokerId },
76380
+ meta: { brokerId }
76381
+ });
76382
+ await this.landOnAdminUi(reply, {
76383
+ status: "success",
76384
+ addonId: ADDON_ID,
76385
+ brokerId,
76386
+ action: "disconnect"
76387
+ });
76062
76388
  }
76063
76389
  async resolveRedirectUri() {
76064
76390
  await this.refreshPublicOrigin();
@@ -76095,8 +76421,5 @@ var GoogleServicesAddon = class extends BaseAddon {
76095
76421
  function asSettingsCap(cap) {
76096
76422
  if (cap === "calendar-source" || cap === "smtp-provider") return cap;
76097
76423
  }
76098
- function escapeHtml(s) {
76099
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
76100
- }
76101
76424
  //#endregion
76102
76425
  export { GOOGLE_GMAIL_SEND_SCOPE as C, OAUTH_START_PATH as E, GoogleServicesAddon, GoogleServicesAddon as default, GOOGLE_BROKER_KIND as S, GoogleServicesConfigSchema as T, GoogleBrokerRegistry as _, refreshAccessToken as a, stampCalendarId as b, selectGmailAccount as c, mapGoogleEvent as d, GOOGLE_CONSOLE_APIS_URL as f, redirectUriDisplay as g, introHtml as h, needsRefresh as i, createGoogleCalendarApi as l, connectHtml as m, buildAuthorizeUrl as n, buildRawMime as o, GOOGLE_CONSOLE_CREDENTIALS_URL as p, exchangeCode as r, createGmailSmtpProvider as s, applyTokenResponse as t, createGoogleCalendarSource as u, migrateLegacyToBrokers as v, GOOGLE_OAUTH_SCOPES as w, DEFAULT_GOOGLE_CONFIG as x, parseStampedCalendarId as y };