@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.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  let node_crypto = require("node:crypto");
6
- //#region ../types/dist/event-category-BvOFi6PY.mjs
6
+ //#region ../types/dist/event-category-DOl51-Ij.mjs
7
7
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
8
8
  EventCategory["SystemBoot"] = "system.boot";
9
9
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -82,9 +82,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
82
82
  /**
83
83
  * The node's one-shot runner boot check (D877): planned runners against the
84
84
  * process service, `degraded` when any is failed, refused or missing. The
85
- * alert centre turns it into one alert and a direct critical push.
85
+ * alert centre turns it into one critical alert.
86
86
  */
87
87
  EventCategory["SystemRunnerCheck"] = "system.runner-check";
88
+ /**
89
+ * An alert EPISODE opened or closed, emitted by the alert centre for alerts
90
+ * of severity `critical` or `error` (`AlertEpisodePayload`). The alert
91
+ * centre sends no notification of its own: Notification Center turns this
92
+ * into the `alert-critical` / `alert-error` / `alert-critical-resolved`
93
+ * system-event kinds, and reconciles from the alerts store because a bus
94
+ * event may be dropped (D8, D11).
95
+ */
96
+ EventCategory["AlertEpisode"] = "alert.episode";
88
97
  EventCategory["AddonError"] = "addon.error";
89
98
  EventCategory["AddonPageReady"] = "addon.page-ready";
90
99
  EventCategory["AddonWidgetReady"] = "addon.widget-ready";
@@ -5542,7 +5551,7 @@ var ZodIssueCode = {
5542
5551
  var ZodFirstPartyTypeKind;
5543
5552
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5544
5553
  //#endregion
5545
- //#region ../types/dist/sleep-BOjuyoPC.mjs
5554
+ //#region ../types/dist/sleep-BxjU_fJM.mjs
5546
5555
  /**
5547
5556
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5548
5557
  * window to float samples (D455).
@@ -13641,7 +13650,14 @@ var AlertSeveritySchema = _enum([
13641
13650
  ]);
13642
13651
  var AlertActionSchema = object({
13643
13652
  label: string(),
13644
- href: string().optional()
13653
+ href: string().optional(),
13654
+ /**
13655
+ * How `href` is followed. Absent / `app`: an admin-ui route (client-side).
13656
+ * `server`: a route the HUB serves (an addon HTTP route such as an OAuth
13657
+ * start) — the browser must leave the SPA with a full navigation, because
13658
+ * the SPA has no such route and would bounce to the home page.
13659
+ */
13660
+ navigation: _enum(["app", "server"]).optional()
13645
13661
  });
13646
13662
  var AlertStatusSchema = _enum([
13647
13663
  "active",
@@ -17854,7 +17870,11 @@ DeviceType.Hub, DeviceDiscoveryStatusSchema.extend({ lastFetchedAt: number().int
17854
17870
  });
17855
17871
  /**
17856
17872
  * `device-adoption` — generic discovery + adoption surface,
17857
- * system-scoped singleton. The de-HA-ified successor to `ha-discovery`:
17873
+ * system-scoped collection (one provider per integration addon), routed
17874
+ * by explicit `{ addonId }` on direct `deviceAdoption.*` calls — the same
17875
+ * pin model as `connection-test`. The admin UI keeps using
17876
+ * `deviceManager.adoption*` (which already threads `addonId`). The de-HA-ified
17877
+ * successor to `ha-discovery`:
17858
17878
  * keyed by `integrationId` (the addon resolves integrationId → broker
17859
17879
  * internally) rather than a connection-specific `brokerId`, and it
17860
17880
  * REUSES the shared `DiscoveredChildDevice` candidate type from
@@ -42062,6 +42082,9 @@ var NcSystemEventKindSchema = _enum([
42062
42082
  "server-updated",
42063
42083
  "export-completed",
42064
42084
  "backup-completed",
42085
+ "alert-critical",
42086
+ "alert-error",
42087
+ "alert-critical-resolved",
42065
42088
  "camera-online",
42066
42089
  "camera-offline",
42067
42090
  "camera-disabled",
@@ -51107,7 +51130,10 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
51107
51130
  }), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
51108
51131
  kind: "mutation",
51109
51132
  auth: "admin"
51110
- }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
51133
+ }), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({ locationIds: array(string()).readonly() }), object({
51134
+ clear: boolean(),
51135
+ repointLocationId: string().optional()
51136
+ }), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
51111
51137
  kind: "mutation",
51112
51138
  auth: "admin"
51113
51139
  }), method(StorageCleanupInputSchema, object({ jobId: string() }), {
@@ -54552,6 +54578,11 @@ object({
54552
54578
  *
54553
54579
  * `kind: 'poll'` — cloud REST APIs typically have no push channel; the
54554
54580
  * provider refreshes the slice on an interval and eagerly after commands.
54581
+ *
54582
+ * UNKNOWN IS NOT OFF. Every field the appliance reports separately is
54583
+ * nullable: `null` means "not known right now" (the appliance has not said,
54584
+ * the link is down, or the description does not carry it) and is never
54585
+ * rendered or exported as `false` / `0`. `availability` says why.
54555
54586
  */
54556
54587
  var DishwasherStateSchema = _enum([
54557
54588
  "idle",
@@ -54561,80 +54592,227 @@ var DishwasherStateSchema = _enum([
54561
54592
  "error",
54562
54593
  "unavailable"
54563
54594
  ]);
54595
+ /**
54596
+ * The appliance's own operation state, uncollapsed. `state` above is the coarse
54597
+ * view for compact surfaces; this is what the appliance said.
54598
+ */
54599
+ var DishwasherOperationStateSchema = _enum([
54600
+ "Inactive",
54601
+ "Ready",
54602
+ "DelayedStart",
54603
+ "Run",
54604
+ "Pause",
54605
+ "ActionRequired",
54606
+ "Finished",
54607
+ "Error",
54608
+ "Aborting"
54609
+ ]);
54610
+ /**
54611
+ * Why fields may be `null`:
54612
+ * - `available` — the link is up and the description is loaded;
54613
+ * - `offline` — the local link is down (values are unknown, not last-known);
54614
+ * - `description-missing` — the link is up but the appliance's description could not
54615
+ * be loaded, so its values cannot be decoded.
54616
+ */
54617
+ var DishwasherAvailabilitySchema = _enum([
54618
+ "available",
54619
+ "offline",
54620
+ "description-missing"
54621
+ ]);
54564
54622
  var DishwasherConsumableLevelSchema = _enum([
54565
54623
  "ok",
54566
54624
  "low",
54567
54625
  "empty"
54568
54626
  ]);
54569
54627
  object({
54628
+ availability: DishwasherAvailabilitySchema,
54629
+ /** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
54570
54630
  state: DishwasherStateSchema,
54571
- /** Active program id/name, or null when none selected / unknown. */
54631
+ /** The appliance's operation state as reported; null when not known. */
54632
+ operationState: DishwasherOperationStateSchema.nullable(),
54633
+ /** Feature key of the ACTIVE (running) program, or null when none runs / not known. */
54572
54634
  program: string().nullable(),
54573
- /** Program tokens the hardware accepts — drives the UI selector. */
54635
+ /** Feature key of the SELECTED program (what Start would run), or null when none / not known. */
54636
+ selectedProgram: string().nullable(),
54637
+ /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
54574
54638
  availablePrograms: array(string()),
54575
- /** Minutes remaining on the active cycle, or null when not running. */
54576
- remainingMinutes: number().int().nonnegative().nullable(),
54639
+ /** Seconds remaining on the active cycle, or null when not running / not known. */
54640
+ remainingSeconds: number().int().nonnegative().nullable(),
54641
+ /** Seconds the delayed start is set to (time until the cycle starts), or null. */
54642
+ delayedStartSeconds: number().int().nonnegative().nullable(),
54577
54643
  /** Cycle completion 0..100, or null when not applicable. */
54578
54644
  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(),
54645
+ /** True when the door is reported open; null when the door state is not known. */
54646
+ doorOpen: boolean().nullable(),
54647
+ /** True when the door is latched (locked) during a cycle; null when not reported. */
54648
+ doorLocked: boolean().nullable(),
54649
+ /** True when remote start is allowed (door closed, power on, etc.); null when not known. */
54650
+ remoteStartAllowed: boolean().nullable(),
54651
+ /** True when the appliance accepts remote control now; null when not known. */
54652
+ remoteControlActive: boolean().nullable(),
54653
+ /** True while somebody operates the appliance at its panel; null when not reported. */
54654
+ localControlActive: boolean().nullable(),
54655
+ /** Main power / switched-on state; null when not known. */
54656
+ powerOn: boolean().nullable(),
54657
+ /** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
54658
+ powerState: string().nullable(),
54585
54659
  saltLevel: DishwasherConsumableLevelSchema.nullable(),
54586
54660
  rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
54587
- halfLoad: boolean(),
54588
- hygienePlus: boolean(),
54661
+ /** Option states: null when the appliance does not expose the option or has not said. */
54662
+ halfLoad: boolean().nullable(),
54663
+ hygienePlus: boolean().nullable(),
54664
+ /** Extra dry option when the appliance exposes it. */
54665
+ extraDry: boolean().nullable(),
54666
+ /** Intensive zone (lower basket) when the appliance exposes it. */
54667
+ intensiveZone: boolean().nullable(),
54668
+ /** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
54669
+ varioSpeedPlus: boolean().nullable(),
54670
+ /** Child lock (controls disabled on the appliance); null when not known. */
54671
+ childLock: boolean().nullable(),
54672
+ /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
54673
+ activeEvents: array(string()),
54674
+ /** Numeric error code when the appliance reports one; null otherwise. */
54589
54675
  errorCode: number().nullable(),
54676
+ /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
54590
54677
  errorLabel: string().nullable(),
54678
+ /** Ms epoch of the last value received from the appliance; 0 = never. */
54591
54679
  lastFetchedAt: number()
54592
54680
  });
54593
- DeviceType.Dishwasher, method(object({ deviceId: number().int().nonnegative() }), _void(), {
54681
+ var DeviceIdSchema = number().int().nonnegative();
54682
+ /** A value a program option or setting can take. */
54683
+ var DishwasherValueSchema = union([
54684
+ boolean(),
54685
+ string(),
54686
+ number()
54687
+ ]);
54688
+ var DishwasherAccessSchema = _enum([
54689
+ "none",
54690
+ "read",
54691
+ "readWrite",
54692
+ "writeOnly",
54693
+ "readStatic"
54694
+ ]);
54695
+ /**
54696
+ * One option or setting the appliance declares, with the constraints its description
54697
+ * gives. A `readWrite` descriptor that is `available` is what `setOption` accepts.
54698
+ */
54699
+ var DishwasherOptionDescriptorSchema = object({
54700
+ /** Feature key (`Dishcare.Dishwasher.Option.ExtraDry`). */
54701
+ key: string(),
54702
+ /** Last segment of the key, for display. */
54703
+ name: string(),
54704
+ type: _enum([
54705
+ "boolean",
54706
+ "number",
54707
+ "enum",
54708
+ "string"
54709
+ ]),
54710
+ access: DishwasherAccessSchema,
54711
+ /** The appliance reports this entity as currently available. */
54712
+ available: boolean(),
54713
+ /** Unit the description declares for a number; null when it declares none. */
54714
+ unit: string().nullable(),
54715
+ min: number().nullable(),
54716
+ max: number().nullable(),
54717
+ step: number().nullable(),
54718
+ /** Allowed members of an enum; null for other types. */
54719
+ enumValues: array(string()).nullable(),
54720
+ defaultValue: DishwasherValueSchema.nullable()
54721
+ });
54722
+ var DishwasherProgramSchema = object({
54723
+ /** Feature key (`Dishcare.Dishwasher.Program.Eco50`) — what `setProgram` / `start` take. */
54724
+ key: string(),
54725
+ name: string(),
54726
+ /** The appliance currently offers this program. */
54727
+ available: boolean(),
54728
+ /** The options this program lists, with the program-level access / availability / default. */
54729
+ options: array(DishwasherOptionDescriptorSchema)
54730
+ });
54731
+ var DishwasherOptionsSchema = object({
54732
+ availability: DishwasherAvailabilitySchema,
54733
+ /** What the appliance can be told to do, from the commands its description declares. */
54734
+ supportsStart: boolean(),
54735
+ supportsPause: boolean(),
54736
+ supportsResume: boolean(),
54737
+ supportsStop: boolean(),
54738
+ supportsPower: boolean(),
54739
+ /** Every program the description lists, with its options. */
54740
+ programs: array(DishwasherProgramSchema),
54741
+ /** Every option and setting entity the description lists (programs reference these by key). */
54742
+ options: array(DishwasherOptionDescriptorSchema)
54743
+ });
54744
+ DeviceType.Dishwasher, method(object({
54745
+ deviceId: DeviceIdSchema,
54746
+ program: string().min(1).optional(),
54747
+ options: record(string(), DishwasherValueSchema).optional()
54748
+ }), _void(), {
54594
54749
  kind: "mutation",
54595
54750
  auth: "protected"
54596
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54751
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
54597
54752
  kind: "mutation",
54598
54753
  auth: "protected"
54599
- }), method(object({ deviceId: number().int().nonnegative() }), _void(), {
54754
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
54755
+ kind: "mutation",
54756
+ auth: "protected"
54757
+ }), method(object({ deviceId: DeviceIdSchema }), _void(), {
54600
54758
  kind: "mutation",
54601
54759
  auth: "protected"
54602
54760
  }), method(object({
54603
- deviceId: number().int().nonnegative(),
54761
+ deviceId: DeviceIdSchema,
54604
54762
  program: string().min(1)
54605
54763
  }), _void(), {
54606
54764
  kind: "mutation",
54607
54765
  auth: "protected"
54608
54766
  }), method(object({
54609
- deviceId: number().int().nonnegative(),
54767
+ deviceId: DeviceIdSchema,
54610
54768
  on: boolean()
54611
54769
  }), _void(), {
54612
54770
  kind: "mutation",
54613
54771
  auth: "protected"
54614
54772
  }), method(object({
54615
- deviceId: number().int().nonnegative(),
54773
+ deviceId: DeviceIdSchema,
54616
54774
  on: boolean()
54617
54775
  }), _void(), {
54618
54776
  kind: "mutation",
54619
54777
  auth: "protected"
54620
54778
  }), method(object({
54621
- deviceId: number().int().nonnegative(),
54779
+ deviceId: DeviceIdSchema,
54622
54780
  on: boolean()
54623
54781
  }), _void(), {
54624
54782
  kind: "mutation",
54625
54783
  auth: "protected"
54626
54784
  }), method(object({
54627
- deviceId: number().int().nonnegative(),
54785
+ deviceId: DeviceIdSchema,
54786
+ on: boolean()
54787
+ }), _void(), {
54788
+ kind: "mutation",
54789
+ auth: "protected"
54790
+ }), method(object({
54791
+ deviceId: DeviceIdSchema,
54792
+ on: boolean()
54793
+ }), _void(), {
54794
+ kind: "mutation",
54795
+ auth: "protected"
54796
+ }), method(object({
54797
+ deviceId: DeviceIdSchema,
54798
+ on: boolean()
54799
+ }), _void(), {
54800
+ kind: "mutation",
54801
+ auth: "protected"
54802
+ }), method(object({
54803
+ deviceId: DeviceIdSchema,
54804
+ on: boolean()
54805
+ }), _void(), {
54806
+ kind: "mutation",
54807
+ auth: "protected"
54808
+ }), method(object({
54809
+ deviceId: DeviceIdSchema,
54628
54810
  key: string().min(1),
54629
- value: union([
54630
- boolean(),
54631
- string(),
54632
- number()
54633
- ])
54811
+ value: DishwasherValueSchema
54634
54812
  }), _void(), {
54635
54813
  kind: "mutation",
54636
54814
  auth: "protected"
54637
- });
54815
+ }), method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" });
54638
54816
  object({
54639
54817
  /** Ms epoch of the last press. null = never observed since this provider started. */
54640
54818
  lastPressedAt: number().nullable(),
@@ -64694,6 +64872,12 @@ Object.freeze({
64694
64872
  addonId: null,
64695
64873
  access: "view"
64696
64874
  },
64875
+ "dishwasher.getOptions": {
64876
+ capName: "dishwasher",
64877
+ capScope: "device",
64878
+ addonId: null,
64879
+ access: "view"
64880
+ },
64697
64881
  "dishwasher.getStatus": {
64698
64882
  capName: "dishwasher",
64699
64883
  capScope: "device",
@@ -64706,6 +64890,24 @@ Object.freeze({
64706
64890
  addonId: null,
64707
64891
  access: "create"
64708
64892
  },
64893
+ "dishwasher.resume": {
64894
+ capName: "dishwasher",
64895
+ capScope: "device",
64896
+ addonId: null,
64897
+ access: "create"
64898
+ },
64899
+ "dishwasher.setChildLock": {
64900
+ capName: "dishwasher",
64901
+ capScope: "device",
64902
+ addonId: null,
64903
+ access: "create"
64904
+ },
64905
+ "dishwasher.setExtraDry": {
64906
+ capName: "dishwasher",
64907
+ capScope: "device",
64908
+ addonId: null,
64909
+ access: "create"
64910
+ },
64709
64911
  "dishwasher.setHalfLoad": {
64710
64912
  capName: "dishwasher",
64711
64913
  capScope: "device",
@@ -64718,6 +64920,12 @@ Object.freeze({
64718
64920
  addonId: null,
64719
64921
  access: "create"
64720
64922
  },
64923
+ "dishwasher.setIntensiveZone": {
64924
+ capName: "dishwasher",
64925
+ capScope: "device",
64926
+ addonId: null,
64927
+ access: "create"
64928
+ },
64721
64929
  "dishwasher.setOption": {
64722
64930
  capName: "dishwasher",
64723
64931
  capScope: "device",
@@ -64736,6 +64944,12 @@ Object.freeze({
64736
64944
  addonId: null,
64737
64945
  access: "create"
64738
64946
  },
64947
+ "dishwasher.setVarioSpeedPlus": {
64948
+ capName: "dishwasher",
64949
+ capScope: "device",
64950
+ addonId: null,
64951
+ access: "create"
64952
+ },
64739
64953
  "dishwasher.start": {
64740
64954
  capName: "dishwasher",
64741
64955
  capScope: "device",
@@ -69182,6 +69396,12 @@ Object.freeze({
69182
69396
  addonId: null,
69183
69397
  access: "view"
69184
69398
  },
69399
+ "storageMigration.moverClearance": {
69400
+ capName: "storage-migration",
69401
+ capScope: "system",
69402
+ addonId: null,
69403
+ access: "view"
69404
+ },
69185
69405
  "storageMigration.movers": {
69186
69406
  capName: "storage-migration",
69187
69407
  capScope: "system",
@@ -71375,6 +71595,11 @@ Object.freeze({
71375
71595
  form: "single",
71376
71596
  optional: false
71377
71597
  }],
71598
+ "dishwasher.getOptions": [{
71599
+ name: "deviceId",
71600
+ form: "single",
71601
+ optional: false
71602
+ }],
71378
71603
  "dishwasher.getStatus": [{
71379
71604
  name: "deviceId",
71380
71605
  form: "single",
@@ -71385,6 +71610,21 @@ Object.freeze({
71385
71610
  form: "single",
71386
71611
  optional: false
71387
71612
  }],
71613
+ "dishwasher.resume": [{
71614
+ name: "deviceId",
71615
+ form: "single",
71616
+ optional: false
71617
+ }],
71618
+ "dishwasher.setChildLock": [{
71619
+ name: "deviceId",
71620
+ form: "single",
71621
+ optional: false
71622
+ }],
71623
+ "dishwasher.setExtraDry": [{
71624
+ name: "deviceId",
71625
+ form: "single",
71626
+ optional: false
71627
+ }],
71388
71628
  "dishwasher.setHalfLoad": [{
71389
71629
  name: "deviceId",
71390
71630
  form: "single",
@@ -71395,6 +71635,11 @@ Object.freeze({
71395
71635
  form: "single",
71396
71636
  optional: false
71397
71637
  }],
71638
+ "dishwasher.setIntensiveZone": [{
71639
+ name: "deviceId",
71640
+ form: "single",
71641
+ optional: false
71642
+ }],
71398
71643
  "dishwasher.setOption": [{
71399
71644
  name: "deviceId",
71400
71645
  form: "single",
@@ -71410,6 +71655,11 @@ Object.freeze({
71410
71655
  form: "single",
71411
71656
  optional: false
71412
71657
  }],
71658
+ "dishwasher.setVarioSpeedPlus": [{
71659
+ name: "deviceId",
71660
+ form: "single",
71661
+ optional: false
71662
+ }],
71413
71663
  "dishwasher.start": [{
71414
71664
  name: "deviceId",
71415
71665
  form: "single",
@@ -73451,6 +73701,9 @@ new Map(Object.entries({
73451
73701
  "wrapper-update-available": "system",
73452
73702
  "backup-completed": "system",
73453
73703
  "export-completed": "system",
73704
+ "alert-critical": "system",
73705
+ "alert-error": "system",
73706
+ "alert-critical-resolved": "system",
73454
73707
  "storage-location-low": "system",
73455
73708
  "storage-location-critical": "system",
73456
73709
  "storage-location-ok": "system",
@@ -73575,6 +73828,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
73575
73828
  "storage-location-ok"
73576
73829
  ]
73577
73830
  },
73831
+ {
73832
+ id: "alerts",
73833
+ family: "system",
73834
+ label: "Alerts",
73835
+ kinds: [
73836
+ "alert-critical",
73837
+ "alert-error",
73838
+ "alert-critical-resolved"
73839
+ ]
73840
+ },
73578
73841
  {
73579
73842
  id: "alarm",
73580
73843
  family: "alarm",
@@ -73619,6 +73882,21 @@ var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.ent
73619
73882
  return [[descriptor.kind, field]];
73620
73883
  })));
73621
73884
  Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
73885
+ object({
73886
+ phase: _enum(["raised", "resolved"]),
73887
+ episodeId: string().min(1),
73888
+ alertId: string().min(1),
73889
+ severity: AlertSeveritySchema,
73890
+ category: string(),
73891
+ title: string(),
73892
+ message: string(),
73893
+ sourceType: string().optional(),
73894
+ sourceId: string().optional(),
73895
+ nodeId: string().optional(),
73896
+ /** The alert's own action link (where the operator acts on it), when it has one. */
73897
+ actionUrl: string().optional(),
73898
+ at: number()
73899
+ });
73622
73900
  var RULE_BODY_ONLY = ["body"];
73623
73901
  var RULE_TEXT = ["title", "body"];
73624
73902
  var BATTERY = ["device-battery-low", "device-battery-normal"];
@@ -73628,6 +73906,11 @@ var TANK = [
73628
73906
  "device-tank-full",
73629
73907
  "device-tank-normal"
73630
73908
  ];
73909
+ var ALERT_EPISODE = [
73910
+ "alert-critical",
73911
+ "alert-error",
73912
+ "alert-critical-resolved"
73913
+ ];
73631
73914
  var UPDATES = [
73632
73915
  "addon-update-available",
73633
73916
  "server-update-available",
@@ -73820,14 +74103,14 @@ var STATE_SUMMARY_VARS = [
73820
74103
  ([...RULE_TEXT], [...CONSUMABLE]),
73821
74104
  ([...RULE_TEXT], [...TANK]),
73822
74105
  [...RULE_TEXT],
73823
- ([...RULE_TEXT], [...UPDATES]),
73824
- ([...RULE_TEXT], [...UPDATES]),
73825
- [...RULE_TEXT],
74106
+ ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
74107
+ ([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
73826
74108
  [...RULE_TEXT],
73827
74109
  [...RULE_TEXT],
73828
74110
  [...RULE_TEXT],
73829
74111
  [...RULE_TEXT],
73830
74112
  [...RULE_TEXT],
74113
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
73831
74114
  [...RULE_TEXT],
73832
74115
  [...RULE_TEXT],
73833
74116
  [...RULE_TEXT],
@@ -73845,7 +74128,11 @@ var STATE_SUMMARY_VARS = [
73845
74128
  [...RULE_TEXT],
73846
74129
  [...RULE_TEXT],
73847
74130
  [...RULE_TEXT],
73848
- [...RULE_TEXT],
74131
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74132
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74133
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74134
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
74135
+ ([...RULE_TEXT], [...ALERT_EPISODE]),
73849
74136
  [...RULE_TEXT],
73850
74137
  [...RULE_TEXT],
73851
74138
  [...RULE_TEXT],
@@ -74383,6 +74670,37 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
74383
74670
  DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
74384
74671
  DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
74385
74672
  DEFAULT_NATIVE_LEASE_SETTINGS.admission;
74673
+ /**
74674
+ * Where an OAuth round-trip ends: the admin-ui completion route
74675
+ * (`/sso-complete`, the page every popup sign-in already lands on), with the
74676
+ * OUTCOME in the URL fragment.
74677
+ *
74678
+ * The outcome is a status plus a reason CODE from a closed list. It never
74679
+ * carries a token, a code, a client secret or a free-form error message: the
74680
+ * page words each code itself, and an unknown code reads as `unknown` rather
74681
+ * than being echoed. Fragment only, so nothing here reaches a server log.
74682
+ *
74683
+ * Pure and browser-safe.
74684
+ */
74685
+ /** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
74686
+ var OAUTH_LANDING_PATH = "/sso-complete";
74687
+ /**
74688
+ * The landing URL for `outcome`. `origin` is the hub address the flow already
74689
+ * runs on (from the `local-network` palette role, never a literal); `null`
74690
+ * gives a hub-relative URL.
74691
+ */
74692
+ function oauthLandingUrl(origin, outcome) {
74693
+ const params = new URLSearchParams({
74694
+ return: "oauth",
74695
+ status: outcome.status
74696
+ });
74697
+ if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
74698
+ if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
74699
+ if (outcome.integration !== void 0) params.set("integration", outcome.integration);
74700
+ if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
74701
+ if (outcome.action === "disconnect") params.set("action", "disconnect");
74702
+ return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
74703
+ }
74386
74704
  var MB = 1024 * 1024;
74387
74705
  1024 * MB, 3072 * MB;
74388
74706
  //#endregion
@@ -74430,13 +74748,22 @@ var DEFAULT_OAUTH_STATE = {
74430
74748
  accessExpiresAt: 0,
74431
74749
  accountEmail: ""
74432
74750
  };
74433
- var GoogleOauthAccountsStateSchema = object({ accounts: array(object({
74751
+ /** Per-broker OAuth tokens (durable state `oauthAccounts`). */
74752
+ var GoogleOauthAccountSchema = object({
74434
74753
  id: string().min(1),
74435
74754
  refreshToken: string().default("").meta({ secret: true }),
74436
74755
  accessToken: string().default("").meta({ secret: true }),
74437
74756
  accessExpiresAt: number().default(0),
74438
- accountEmail: string().default("")
74439
- })).default([]) });
74757
+ accountEmail: string().default(""),
74758
+ /**
74759
+ * Google refused the refresh token (`invalid_grant`): it is dead and only a
74760
+ * new sign-in replaces it. Set where the refusal is seen, cleared by the
74761
+ * OAuth callback that stores a fresh token — so the status, the Sign-in
74762
+ * button and the alert all read the same fact.
74763
+ */
74764
+ needsReauth: boolean().default(false)
74765
+ });
74766
+ var GoogleOauthAccountsStateSchema = object({ accounts: array(GoogleOauthAccountSchema).default([]) });
74440
74767
  var GOOGLE_BROKER_KIND = "google";
74441
74768
  var GOOGLE_BROKER_KIND_LABEL = "Google";
74442
74769
  var GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
@@ -74525,7 +74852,8 @@ function migrateLegacyToBrokers(rawConfig, legacyOauth) {
74525
74852
  refreshToken: legacyOauth.refreshToken,
74526
74853
  accessToken: legacyOauth.accessToken,
74527
74854
  accessExpiresAt: legacyOauth.accessExpiresAt,
74528
- accountEmail: legacyOauth.accountEmail
74855
+ accountEmail: legacyOauth.accountEmail,
74856
+ needsReauth: false
74529
74857
  }] : [];
74530
74858
  return {
74531
74859
  brokers: [entry],
@@ -74560,7 +74888,10 @@ var GoogleBrokerRegistry = class {
74560
74888
  oauthById = /* @__PURE__ */ new Map();
74561
74889
  seedFrom(entries, accounts) {
74562
74890
  this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
74563
- this.oauthById = new Map(accounts.map((a) => [a.id, { ...a }]));
74891
+ this.oauthById = new Map(accounts.map((a) => {
74892
+ const account = GoogleOauthAccountSchema.parse(a);
74893
+ return [account.id, account];
74894
+ }));
74564
74895
  this.seedCounterFromEntries(this.entries);
74565
74896
  }
74566
74897
  listEntries() {
@@ -74576,14 +74907,28 @@ var GoogleBrokerRegistry = class {
74576
74907
  return [...this.oauthById.values()];
74577
74908
  }
74578
74909
  setOauth(account) {
74579
- this.oauthById.set(account.id, { ...account });
74910
+ const parsed = GoogleOauthAccountSchema.parse(account);
74911
+ this.oauthById.set(parsed.id, parsed);
74580
74912
  }
74581
74913
  removeOauth(id) {
74582
74914
  this.oauthById.delete(id);
74583
74915
  }
74584
74916
  isConnected(id) {
74585
74917
  const o = this.oauthById.get(id);
74586
- return o !== void 0 && o.refreshToken.length > 0;
74918
+ return o !== void 0 && o.refreshToken.length > 0 && !o.needsReauth;
74919
+ }
74920
+ /** A refresh token Google refused: kept (a sign-in replaces it) but not usable. */
74921
+ needsReauth(id) {
74922
+ return this.oauthById.get(id)?.needsReauth === true;
74923
+ }
74924
+ markNeedsReauth(id) {
74925
+ const o = this.oauthById.get(id);
74926
+ if (o === void 0 || o.needsReauth) return false;
74927
+ this.oauthById.set(id, {
74928
+ ...o,
74929
+ needsReauth: true
74930
+ });
74931
+ return true;
74587
74932
  }
74588
74933
  allocateId() {
74589
74934
  const id = `google_${String(this.nextIdCounter).padStart(3, "0")}`;
@@ -74633,18 +74978,19 @@ var GoogleBrokerRegistry = class {
74633
74978
  toBrokerInfo(entry, addonId) {
74634
74979
  const oauth = this.oauthById.get(entry.id);
74635
74980
  const connected = this.isConnected(entry.id);
74981
+ const reauth = this.needsReauth(entry.id);
74636
74982
  return {
74637
74983
  id: entry.id,
74638
74984
  addonId,
74639
74985
  name: entry.name,
74640
74986
  kind: GOOGLE_BROKER_KIND,
74641
- status: connected ? "connected" : "disconnected",
74987
+ status: connected ? "connected" : reauth ? "auth-failed" : "disconnected",
74642
74988
  info: {
74643
74989
  accountEmail: oauth?.accountEmail ?? "",
74644
74990
  hasClient: entry.clientId.length > 0
74645
74991
  },
74646
74992
  lastCheckedAt: connected ? Date.now() : null,
74647
- error: connected ? null : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
74993
+ 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"
74648
74994
  };
74649
74995
  }
74650
74996
  listBrokerInfos(addonId, kind) {
@@ -74696,7 +75042,7 @@ function consoleLinksHtml$1() {
74696
75042
  ].join("");
74697
75043
  }
74698
75044
  function introHtml(cap, publicOrigin) {
74699
- const redirect = escapeHtml$2(redirectUriDisplay(publicOrigin));
75045
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
74700
75046
  const console = consoleLinksHtml$1();
74701
75047
  if (cap === "smtp-provider") return [
74702
75048
  "<ol>",
@@ -74729,7 +75075,7 @@ function introHtml(cap, publicOrigin) {
74729
75075
  ].join("");
74730
75076
  }
74731
75077
  function connectHtml(cap, publicOrigin) {
74732
- const redirect = escapeHtml$2(redirectUriDisplay(publicOrigin));
75078
+ const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
74733
75079
  if (cap === "smtp-provider") return [
74734
75080
  `After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
74735
75081
  "Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
@@ -74749,7 +75095,7 @@ function connectHtml(cap, publicOrigin) {
74749
75095
  `Console: ${consoleLinksHtml$1()}.`
74750
75096
  ].join("");
74751
75097
  }
74752
- function escapeHtml$2(s) {
75098
+ function escapeHtml$1(s) {
74753
75099
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
74754
75100
  }
74755
75101
  //#endregion
@@ -74768,7 +75114,7 @@ function consoleLinksHtml() {
74768
75114
  }
74769
75115
  /** Setup steps shown on Add / Edit — includes Audience / Test users. */
74770
75116
  function googleBrokerSetupHtml(publicOrigin) {
74771
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
75117
+ const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
74772
75118
  return [
74773
75119
  "<ol>",
74774
75120
  `<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>`,
@@ -74844,7 +75190,7 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
74844
75190
  }
74845
75191
  /** Connectivity Calendars / Email pages — calendar import picker lives here (not on broker Add). */
74846
75192
  function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
74847
- const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
75193
+ const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
74848
75194
  if (cap === "calendar-source") return { sections: [{
74849
75195
  id: "setup-intro",
74850
75196
  title: "Google brokers",
@@ -74902,7 +75248,7 @@ function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
74902
75248
  }]
74903
75249
  }] };
74904
75250
  }
74905
- function escapeHtml$1(s) {
75251
+ function escapeHtml(s) {
74906
75252
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
74907
75253
  }
74908
75254
  //#endregion
@@ -75197,7 +75543,6 @@ function isInvalidGrantRefreshError(err) {
75197
75543
  return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
75198
75544
  }
75199
75545
  function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75200
- const pushLine = `${brokerName}: Google session expired — sign in again`;
75201
75546
  return {
75202
75547
  id: googleBrokerReconnectAlertId(brokerId),
75203
75548
  category: "google.broker-reconnect",
@@ -75206,7 +75551,8 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75206
75551
  message: `${brokerName} needs you to sign in to Google again.`,
75207
75552
  action: {
75208
75553
  label: "Sign in again",
75209
- href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`
75554
+ href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`,
75555
+ navigation: "server"
75210
75556
  },
75211
75557
  status: "active",
75212
75558
  read: false,
@@ -75216,10 +75562,7 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
75216
75562
  type: "google-broker",
75217
75563
  id: brokerId
75218
75564
  },
75219
- metadata: {
75220
- episodeKey: "reconnect",
75221
- pushLine
75222
- }
75565
+ metadata: { episodeKey: "reconnect" }
75223
75566
  };
75224
75567
  }
75225
75568
  //#endregion
@@ -75428,6 +75771,14 @@ var GoogleBrokerStore = class GoogleBrokerStore {
75428
75771
  * One OAuth account = one broker entry; feature caps aggregate connected brokers.
75429
75772
  */
75430
75773
  var ADDON_ID = "google-services";
75774
+ /** A callback failure the landing page can name by code. */
75775
+ var GoogleCallbackError = class extends Error {
75776
+ reason;
75777
+ constructor(reason, message) {
75778
+ super(message);
75779
+ this.reason = reason;
75780
+ }
75781
+ };
75431
75782
  var CALLBACK_PATH = "/oauth/callback";
75432
75783
  var START_PATH = "/oauth/start";
75433
75784
  var DISCONNECT_PATH = "/oauth/disconnect";
@@ -75448,7 +75799,7 @@ var GoogleServicesAddon = class extends BaseAddon {
75448
75799
  registry = new GoogleBrokerRegistry();
75449
75800
  brokerStore = null;
75450
75801
  activeBrokers = [];
75451
- /** CSRF state → { brokerId, popup, issuedAt } — popup mirrors OIDC D794. */
75802
+ /** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
75452
75803
  pendingStates = /* @__PURE__ */ new Map();
75453
75804
  publicOrigin = null;
75454
75805
  constructor() {
@@ -75484,7 +75835,8 @@ var GoogleServicesAddon = class extends BaseAddon {
75484
75835
  refreshToken: legacyOauth.refreshToken,
75485
75836
  accessToken: legacyOauth.accessToken,
75486
75837
  accessExpiresAt: legacyOauth.accessExpiresAt,
75487
- accountEmail: legacyOauth.accountEmail
75838
+ accountEmail: legacyOauth.accountEmail,
75839
+ needsReauth: false
75488
75840
  }] };
75489
75841
  await this.oauthAccountsHandle.set(oauthAccounts);
75490
75842
  await legacyHandle.set({ ...DEFAULT_OAUTH_STATE });
@@ -75755,12 +76107,9 @@ var GoogleServicesAddon = class extends BaseAddon {
75755
76107
  reply.status(500).send({ error: err instanceof Error ? err.message : String(err) });
75756
76108
  return;
75757
76109
  }
75758
- const popupRaw = req.query["popup"];
75759
- const popup = popupRaw === "1" || popupRaw === "true";
75760
76110
  const state = (0, node_crypto.randomBytes)(16).toString("hex");
75761
76111
  this.pendingStates.set(state, {
75762
76112
  brokerId,
75763
- popup,
75764
76113
  issuedAt: Date.now()
75765
76114
  });
75766
76115
  this.prunePendingStates();
@@ -75772,42 +76121,39 @@ var GoogleServicesAddon = class extends BaseAddon {
75772
76121
  reply.redirect(url);
75773
76122
  }
75774
76123
  async handleCallback(req, reply) {
76124
+ const state = typeof req.query["state"] === "string" ? req.query["state"] : "";
75775
76125
  const err = req.query["error"];
75776
76126
  if (err) {
75777
- const statePeek = typeof req.query["state"] === "string" ? req.query["state"] : "";
75778
- const pendingPeek = statePeek ? this.pendingStates.get(statePeek) : void 0;
75779
- if (pendingPeek?.popup) {
75780
- this.pendingStates.delete(statePeek);
75781
- this.popupComplete(reply, {
75782
- ok: false,
75783
- brokerId: pendingPeek.brokerId,
75784
- error: String(err)
75785
- });
75786
- return;
75787
- }
75788
- this.html(reply, 400, `Google OAuth error: ${err}`);
76127
+ const pendingPeek = state ? this.pendingStates.get(state) : void 0;
76128
+ if (pendingPeek !== void 0) this.pendingStates.delete(state);
76129
+ await this.landOnAdminUi(reply, {
76130
+ status: "error",
76131
+ addonId: ADDON_ID,
76132
+ ...pendingPeek !== void 0 ? { brokerId: pendingPeek.brokerId } : {},
76133
+ reason: err === "access_denied" ? "access_denied" : "invalid_request"
76134
+ });
75789
76135
  return;
75790
76136
  }
75791
76137
  const code = req.query["code"];
75792
- const state = req.query["state"];
75793
- const pending = typeof state === "string" ? this.pendingStates.get(state) : void 0;
76138
+ const pending = state ? this.pendingStates.get(state) : void 0;
75794
76139
  if (!code || !state || pending === void 0) {
75795
- this.html(reply, 400, "Invalid or expired OAuth state.");
76140
+ await this.landOnAdminUi(reply, {
76141
+ status: "error",
76142
+ addonId: ADDON_ID,
76143
+ reason: "invalid_state"
76144
+ });
75796
76145
  return;
75797
76146
  }
75798
76147
  this.pendingStates.delete(state);
75799
- const { brokerId, popup } = pending;
76148
+ const { brokerId } = pending;
75800
76149
  const entry = this.registry.getEntry(brokerId);
75801
76150
  if (entry === null) {
75802
- if (popup) {
75803
- this.popupComplete(reply, {
75804
- ok: false,
75805
- brokerId,
75806
- error: `Unknown Google broker: ${brokerId}`
75807
- });
75808
- return;
75809
- }
75810
- this.html(reply, 404, `Unknown Google broker: ${brokerId}`);
76151
+ await this.landOnAdminUi(reply, {
76152
+ status: "error",
76153
+ addonId: ADDON_ID,
76154
+ brokerId,
76155
+ reason: "unknown_broker"
76156
+ });
75811
76157
  return;
75812
76158
  }
75813
76159
  try {
@@ -75819,11 +76165,12 @@ var GoogleServicesAddon = class extends BaseAddon {
75819
76165
  }, code);
75820
76166
  const prev = this.registry.getOauth(brokerId) ?? {
75821
76167
  id: brokerId,
75822
- ...DEFAULT_OAUTH_STATE
76168
+ ...DEFAULT_OAUTH_STATE,
76169
+ needsReauth: false
75823
76170
  };
75824
- 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.");
76171
+ 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.");
75825
76172
  const applied = applyTokenResponse({
75826
- refreshToken: prev.refreshToken,
76173
+ refreshToken: prev.needsReauth && tokens.refresh_token ? "" : prev.refreshToken,
75827
76174
  accessToken: prev.accessToken,
75828
76175
  accessExpiresAt: prev.accessExpiresAt,
75829
76176
  accountEmail: prev.accountEmail
@@ -75834,7 +76181,8 @@ var GoogleServicesAddon = class extends BaseAddon {
75834
76181
  refreshToken: applied.refreshToken,
75835
76182
  accessToken: applied.accessToken,
75836
76183
  accessExpiresAt: applied.accessExpiresAt,
75837
- accountEmail: email || applied.accountEmail
76184
+ accountEmail: email || applied.accountEmail,
76185
+ needsReauth: false
75838
76186
  };
75839
76187
  this.registry.setOauth(next);
75840
76188
  await this.persistOauthAccounts();
@@ -75847,65 +76195,44 @@ var GoogleServicesAddon = class extends BaseAddon {
75847
76195
  this.registry.replaceEntries(this.registry.listEntries().map((e) => e.id === brokerId ? renamed : e));
75848
76196
  await this.persistBrokers();
75849
76197
  }
75850
- this.ctx.logger.info("google oauth connected", { meta: {
75851
- brokerId,
75852
- hasRefresh: next.refreshToken.length > 0,
75853
- accountEmail: next.accountEmail || void 0
75854
- } });
75855
- if (popup) {
75856
- this.popupComplete(reply, {
75857
- ok: true,
76198
+ this.ctx.logger.info("google oauth connected", {
76199
+ tags: { brokerId },
76200
+ meta: {
75858
76201
  brokerId,
75859
- message: "Google account connected. This window should close."
75860
- });
75861
- return;
75862
- }
75863
- this.html(reply, 200, `Google account connected for ${brokerId} (Calendar + Gmail send). You can close this window.`);
76202
+ hasRefresh: next.refreshToken.length > 0,
76203
+ accountEmail: next.accountEmail || void 0
76204
+ }
76205
+ });
76206
+ await this.landOnAdminUi(reply, {
76207
+ status: "success",
76208
+ addonId: ADDON_ID,
76209
+ brokerId
76210
+ });
75864
76211
  } catch (e) {
75865
76212
  const message = e instanceof Error ? e.message : String(e);
75866
76213
  this.ctx.logger.warn("google oauth callback failed", { meta: {
75867
76214
  brokerId,
75868
76215
  error: message
75869
76216
  } });
75870
- if (popup) {
75871
- this.popupComplete(reply, {
75872
- ok: false,
75873
- brokerId,
75874
- error: message
75875
- });
75876
- return;
75877
- }
75878
- this.html(reply, 500, `OAuth failed: ${message}`);
76217
+ await this.landOnAdminUi(reply, {
76218
+ status: "error",
76219
+ addonId: ADDON_ID,
76220
+ brokerId,
76221
+ reason: e instanceof GoogleCallbackError ? e.reason : "exchange_failed"
76222
+ });
75879
76223
  }
75880
76224
  }
75881
76225
  /**
75882
- * Popup completion page (OIDC D794 pattern): postMessage to opener at the
75883
- * hub origin, then close. Google never redirects back to admin-ui — this
75884
- * page is the only return surface.
76226
+ * Every OAuth completion ends in CamStack: a redirect to the admin-ui
76227
+ * completion page (`/sso-complete`) on the hub's public address (the
76228
+ * `local-network` palette role), carrying a status and a reason CODE — never
76229
+ * a token, a code or an error message. The page hands a popup's outcome to
76230
+ * its opener and closes it, or, in a full-page flow, shows the result and
76231
+ * returns to Connectivity.
75885
76232
  */
75886
- popupComplete(reply, outcome) {
75887
- const payload = JSON.stringify({
75888
- type: "camstack:broker-oauth",
75889
- ok: outcome.ok,
75890
- brokerId: outcome.brokerId,
75891
- ...outcome.error !== void 0 ? { error: outcome.error } : {}
75892
- });
75893
- const body = outcome.ok ? outcome.message ?? "Done. This window should close." : `OAuth failed: ${outcome.error ?? "unknown"}`;
75894
- reply.header("content-type", "text/html; charset=utf-8");
75895
- reply.status(outcome.ok ? 200 : 400).send(`<!doctype html><html><head><title>Google</title></head><body>
75896
- <p>${escapeHtml(body)}</p>
75897
- <script>
75898
- (function () {
75899
- var msg = ${payload};
75900
- try {
75901
- if (window.opener) {
75902
- window.opener.postMessage(msg, window.location.origin);
75903
- }
75904
- } catch (e) {}
75905
- setTimeout(function () { window.close(); }, 200);
75906
- })();
75907
- <\/script>
75908
- </body></html>`);
76233
+ async landOnAdminUi(reply, outcome) {
76234
+ await this.refreshPublicOrigin();
76235
+ reply.redirect(oauthLandingUrl(this.publicOrigin, outcome));
75909
76236
  }
75910
76237
  async fetchAccountEmail(accessToken) {
75911
76238
  try {
@@ -75917,10 +76244,6 @@ var GoogleServicesAddon = class extends BaseAddon {
75917
76244
  return "";
75918
76245
  }
75919
76246
  }
75920
- html(reply, code, body) {
75921
- reply.header("content-type", "text/html; charset=utf-8");
75922
- reply.status(code).send(`<!doctype html><html><body><p>${escapeHtml(body)}</p></body></html>`);
75923
- }
75924
76247
  prunePendingStates() {
75925
76248
  const cutoff = Date.now() - 10 * 6e4;
75926
76249
  for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
@@ -76008,6 +76331,7 @@ var GoogleServicesAddon = class extends BaseAddon {
76008
76331
  const entry = this.registry.getEntry(brokerId);
76009
76332
  const oauth = this.registry.getOauth(brokerId);
76010
76333
  if (entry === null || oauth === null || !oauth.refreshToken.trim()) throw new Error(`Google broker ${brokerId} is not connected — use Sign in under Connectivity → Brokers`);
76334
+ if (oauth.needsReauth) throw new Error(`Google broker ${brokerId} needs a new sign-in — use Sign in under Connectivity → Brokers`);
76011
76335
  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");
76012
76336
  const state = {
76013
76337
  refreshToken: oauth.refreshToken.trim(),
@@ -76023,7 +76347,10 @@ var GoogleServicesAddon = class extends BaseAddon {
76023
76347
  clientSecret: entry.clientSecret.trim()
76024
76348
  }, state.refreshToken);
76025
76349
  } catch (err) {
76026
- if (isInvalidGrantRefreshError(err)) this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76350
+ if (isInvalidGrantRefreshError(err)) {
76351
+ if (this.registry.markNeedsReauth(brokerId)) await this.persistOauthAccounts();
76352
+ this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
76353
+ }
76027
76354
  throw err;
76028
76355
  }
76029
76356
  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");
@@ -76033,7 +76360,8 @@ var GoogleServicesAddon = class extends BaseAddon {
76033
76360
  refreshToken: nextState.refreshToken,
76034
76361
  accessToken: nextState.accessToken,
76035
76362
  accessExpiresAt: nextState.accessExpiresAt,
76036
- accountEmail: nextState.accountEmail
76363
+ accountEmail: nextState.accountEmail,
76364
+ needsReauth: false
76037
76365
  });
76038
76366
  await this.persistOauthAccounts();
76039
76367
  return nextState.accessToken;
@@ -76049,20 +76377,18 @@ var GoogleServicesAddon = class extends BaseAddon {
76049
76377
  reply.status(404).send({ error: `Unknown Google broker: ${brokerId}` });
76050
76378
  return;
76051
76379
  }
76052
- const popupRaw = req.query["popup"];
76053
- const popup = popupRaw === "1" || popupRaw === "true";
76054
76380
  this.registry.removeOauth(brokerId);
76055
76381
  await this.persistOauthAccounts();
76056
- this.ctx.logger.info("google oauth disconnected", { meta: { brokerId } });
76057
- if (popup) {
76058
- this.popupComplete(reply, {
76059
- ok: true,
76060
- brokerId,
76061
- message: "Google account disconnected. This window should close."
76062
- });
76063
- return;
76064
- }
76065
- this.html(reply, 200, `Google account disconnected for ${brokerId}. You can close this window.`);
76382
+ this.ctx.logger.info("google oauth disconnected", {
76383
+ tags: { brokerId },
76384
+ meta: { brokerId }
76385
+ });
76386
+ await this.landOnAdminUi(reply, {
76387
+ status: "success",
76388
+ addonId: ADDON_ID,
76389
+ brokerId,
76390
+ action: "disconnect"
76391
+ });
76066
76392
  }
76067
76393
  async resolveRedirectUri() {
76068
76394
  await this.refreshPublicOrigin();
@@ -76099,9 +76425,6 @@ var GoogleServicesAddon = class extends BaseAddon {
76099
76425
  function asSettingsCap(cap) {
76100
76426
  if (cap === "calendar-source" || cap === "smtp-provider") return cap;
76101
76427
  }
76102
- function escapeHtml(s) {
76103
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
76104
- }
76105
76428
  //#endregion
76106
76429
  exports.DEFAULT_GOOGLE_CONFIG = DEFAULT_GOOGLE_CONFIG;
76107
76430
  exports.GOOGLE_BROKER_KIND = GOOGLE_BROKER_KIND;