@camstack/addon-google-services 0.2.11 → 0.2.13
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.
- package/dist/google-services.addon.js +640 -364
- package/dist/google-services.addon.mjs +640 -364
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
|
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-
|
|
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).
|
|
@@ -7924,6 +7933,12 @@ function normalizeAddonInitResult(result) {
|
|
|
7924
7933
|
if (Array.isArray(result)) return { providers: result };
|
|
7925
7934
|
return result;
|
|
7926
7935
|
}
|
|
7936
|
+
/**
|
|
7937
|
+
* The share-view scope kinds a `csv_*` share token can carry. The hub mints
|
|
7938
|
+
* and validates them (`share-token.service.ts` builds its enum from this
|
|
7939
|
+
* list); a data plane names the kinds it ACCEPTS in `share`.
|
|
7940
|
+
*/
|
|
7941
|
+
var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
|
|
7927
7942
|
/** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
|
|
7928
7943
|
var PeerBytesTicketSchema = object({
|
|
7929
7944
|
/** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
|
|
@@ -12906,6 +12921,229 @@ var ConvertResultSchema = object({
|
|
|
12906
12921
|
files: array(string()).readonly()
|
|
12907
12922
|
})).readonly()
|
|
12908
12923
|
});
|
|
12924
|
+
/**
|
|
12925
|
+
* Zod schemas for persisted record types.
|
|
12926
|
+
*
|
|
12927
|
+
* These schemas serve as the single source of truth for types that are
|
|
12928
|
+
* stored in the settings-store backend and retrieved via tRPC. They
|
|
12929
|
+
* replace the manual TypeScript interfaces with schema-derived types,
|
|
12930
|
+
* enabling runtime validation when reading from the backend.
|
|
12931
|
+
*
|
|
12932
|
+
* Convention: each schema is named `<Type>Schema`, the inferred TS type
|
|
12933
|
+
* keeps the original name for backward compatibility.
|
|
12934
|
+
*/
|
|
12935
|
+
/**
|
|
12936
|
+
* Auth model (v2 — 2026-05-12 caps-only):
|
|
12937
|
+
*
|
|
12938
|
+
* Two principals:
|
|
12939
|
+
* - `isAdmin: true` — full bypass. Admin scope set is ignored.
|
|
12940
|
+
* - `isAdmin: false` — every call gated by `scopes` (see TokenScope).
|
|
12941
|
+
*
|
|
12942
|
+
* No role enum. There used to be `viewer / agent / scoped` and earlier
|
|
12943
|
+
* `super_admin` — collapsed onto the single boolean because every
|
|
12944
|
+
* non-admin caller is governed by the same scope-check rules; the
|
|
12945
|
+
* distinction was only ever used to route role-specific procedures
|
|
12946
|
+
* which we no longer have. CLI / webhook integrations are `cst_*` tokens
|
|
12947
|
+
* with `isAdmin: false` + an explicit scope list.
|
|
12948
|
+
*/
|
|
12949
|
+
/** Per-method access flavour. Materialised on every cap method via codegen. */
|
|
12950
|
+
var MethodAccessSchema = _enum([
|
|
12951
|
+
"view",
|
|
12952
|
+
"create",
|
|
12953
|
+
"delete"
|
|
12954
|
+
]);
|
|
12955
|
+
/**
|
|
12956
|
+
* A single scope grant. Three forms — checked in matcher with OR
|
|
12957
|
+
* semantics; the first match decides whether the call passes:
|
|
12958
|
+
*
|
|
12959
|
+
* - `category` — broadest. `target: 'device' | 'system'` selects every
|
|
12960
|
+
* cap with the matching `scope` declaration. New caps inherit grants
|
|
12961
|
+
* automatically — adding a device cap to the codebase makes it
|
|
12962
|
+
* reachable to every user with `category:device [...]` without a
|
|
12963
|
+
* re-grant. Use this for "family viewer" / "operator" tiers.
|
|
12964
|
+
*
|
|
12965
|
+
* - `addon` — every cap that ships inside a given addon id. Niche;
|
|
12966
|
+
* useful when an integration needs to interact with one specific
|
|
12967
|
+
* addon's surface regardless of how many caps it exports.
|
|
12968
|
+
*
|
|
12969
|
+
* - `capability` — the most precise form. Single cap name. Use for
|
|
12970
|
+
* integration tokens where you want to grant `addons.installPackage`
|
|
12971
|
+
* and nothing else.
|
|
12972
|
+
*
|
|
12973
|
+
* `access` is REQUIRED with min 1 element. `admin` bypass is the
|
|
12974
|
+
* "give me everything" escape; there is no wildcard scope.
|
|
12975
|
+
*/
|
|
12976
|
+
var AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
12977
|
+
var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
12978
|
+
var CapScopeSchema = _enum(["device", "system"]);
|
|
12979
|
+
/**
|
|
12980
|
+
* DeviceSelector (scope model v3 — 2026-08-12).
|
|
12981
|
+
*
|
|
12982
|
+
* A `device` grant no longer carries a frozen list of deviceIds. It carries
|
|
12983
|
+
* a SELECTOR the matcher resolves against the live fleet, so the grant can be
|
|
12984
|
+
* DYNAMIC: a `types:['camera']` selector automatically covers a camera added
|
|
12985
|
+
* AFTER the grant was minted — no re-grant, no re-login.
|
|
12986
|
+
*
|
|
12987
|
+
* - `all` — every device in the deployment. The broad viewer/operator
|
|
12988
|
+
* lever without a `category` grant (a `category` grant also covers device
|
|
12989
|
+
* caps that carry no deviceId; `all` is specifically the device set).
|
|
12990
|
+
* - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
|
|
12991
|
+
* grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
|
|
12992
|
+
* is NOT covered until the grant is edited.
|
|
12993
|
+
* - `types` — every device of a `DeviceType` (e.g. every `camera`).
|
|
12994
|
+
* DYNAMIC. A device that changes type, or a new device of the type,
|
|
12995
|
+
* re-resolves on the next request.
|
|
12996
|
+
* - `locations` — every device whose operator-assigned `location` label is
|
|
12997
|
+
* in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
|
|
12998
|
+
* null/unset location matches NO `locations` selector.
|
|
12999
|
+
*/
|
|
13000
|
+
var DeviceSelectorSchema = discriminatedUnion("kind", [
|
|
13001
|
+
object({ kind: literal("all") }),
|
|
13002
|
+
object({
|
|
13003
|
+
kind: literal("ids"),
|
|
13004
|
+
ids: array(number().int()).min(1)
|
|
13005
|
+
}),
|
|
13006
|
+
object({
|
|
13007
|
+
kind: literal("types"),
|
|
13008
|
+
types: array(_enum(DeviceType)).min(1)
|
|
13009
|
+
}),
|
|
13010
|
+
object({
|
|
13011
|
+
kind: literal("locations"),
|
|
13012
|
+
locations: array(string().min(1)).min(1)
|
|
13013
|
+
})
|
|
13014
|
+
]);
|
|
13015
|
+
var DeviceTokenScopeSchema = object({
|
|
13016
|
+
type: literal("device"),
|
|
13017
|
+
/** The device SET this grant covers — resolved against the live fleet. */
|
|
13018
|
+
selector: DeviceSelectorSchema,
|
|
13019
|
+
access: array(MethodAccessSchema).min(1),
|
|
13020
|
+
/**
|
|
13021
|
+
* Whether a grant on a PARENT device transparently covers its accessory
|
|
13022
|
+
* CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
|
|
13023
|
+
* Direction is parent → children ONLY.
|
|
13024
|
+
*
|
|
13025
|
+
* Absent → the matcher DERIVES it from the access flavour: `view`
|
|
13026
|
+
* inherits (a camera viewer sees the camera's accessories), `create` /
|
|
13027
|
+
* `delete` do NOT (actuating/removing a child is an explicit act the
|
|
13028
|
+
* operator must grant on the child, not inherit from the parent). Set it
|
|
13029
|
+
* explicitly to override that default per grant.
|
|
13030
|
+
*/
|
|
13031
|
+
includeLinked: boolean().optional()
|
|
13032
|
+
});
|
|
13033
|
+
/**
|
|
13034
|
+
* v2 → v3 lazy migration. A pre-v3 `device` grant carried
|
|
13035
|
+
* `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
|
|
13036
|
+
* `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
|
|
13037
|
+
* EVERY parse path — stored records AND the JWT-carried scope arrays
|
|
13038
|
+
* normalised at the request boundary ({@link normalizeTokenScopes} in
|
|
13039
|
+
* `device-selector.ts`). Chosen over a one-time DB migration because a
|
|
13040
|
+
* migration cannot reach a JWT already in a client's hands; parse-time
|
|
13041
|
+
* migration covers both without a flag day. No cast — the raw object is read
|
|
13042
|
+
* through `Reflect.get` (its static type is `unknown`).
|
|
13043
|
+
*/
|
|
13044
|
+
function migrateLegacyTokenScope(raw) {
|
|
13045
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
13046
|
+
if (Reflect.get(raw, "type") !== "device") return raw;
|
|
13047
|
+
if (Reflect.get(raw, "selector") !== void 0) return raw;
|
|
13048
|
+
const targets = Reflect.get(raw, "targets");
|
|
13049
|
+
if (!Array.isArray(targets)) return raw;
|
|
13050
|
+
return {
|
|
13051
|
+
type: "device",
|
|
13052
|
+
selector: {
|
|
13053
|
+
kind: "ids",
|
|
13054
|
+
ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
|
|
13055
|
+
},
|
|
13056
|
+
access: Reflect.get(raw, "access")
|
|
13057
|
+
};
|
|
13058
|
+
}
|
|
13059
|
+
var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
|
|
13060
|
+
object({
|
|
13061
|
+
type: literal("category"),
|
|
13062
|
+
target: CapScopeSchema,
|
|
13063
|
+
access: array(MethodAccessSchema).min(1)
|
|
13064
|
+
}),
|
|
13065
|
+
object({
|
|
13066
|
+
type: literal("capability"),
|
|
13067
|
+
target: string(),
|
|
13068
|
+
access: array(MethodAccessSchema).min(1)
|
|
13069
|
+
}),
|
|
13070
|
+
object({
|
|
13071
|
+
type: literal("addon"),
|
|
13072
|
+
target: string(),
|
|
13073
|
+
access: array(MethodAccessSchema).min(1)
|
|
13074
|
+
}),
|
|
13075
|
+
DeviceTokenScopeSchema
|
|
13076
|
+
]));
|
|
13077
|
+
object({
|
|
13078
|
+
id: string(),
|
|
13079
|
+
username: string(),
|
|
13080
|
+
passwordHash: string(),
|
|
13081
|
+
/**
|
|
13082
|
+
* Admin bypass. When true, the middleware skips the scope-access
|
|
13083
|
+
* check entirely. There is no other axis of privilege; the legacy
|
|
13084
|
+
* role enum collapsed onto this boolean in v2.
|
|
13085
|
+
*/
|
|
13086
|
+
isAdmin: boolean().default(false),
|
|
13087
|
+
allowedProviders: AllowedProviderSchema,
|
|
13088
|
+
allowedDevices: AllowedDevicesSchema,
|
|
13089
|
+
/**
|
|
13090
|
+
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
13091
|
+
* ignored. Non-admins without scopes are locked out of every
|
|
13092
|
+
* protected call.
|
|
13093
|
+
*/
|
|
13094
|
+
scopes: array(TokenScopeSchema).default([]),
|
|
13095
|
+
createdAt: number(),
|
|
13096
|
+
updatedAt: number()
|
|
13097
|
+
});
|
|
13098
|
+
object({
|
|
13099
|
+
id: string(),
|
|
13100
|
+
label: string(),
|
|
13101
|
+
isAdmin: boolean().default(false),
|
|
13102
|
+
allowedProviders: AllowedProviderSchema,
|
|
13103
|
+
allowedDevices: AllowedDevicesSchema,
|
|
13104
|
+
tokenHash: string(),
|
|
13105
|
+
tokenPrefix: string(),
|
|
13106
|
+
createdAt: number(),
|
|
13107
|
+
lastUsedAt: number().optional()
|
|
13108
|
+
});
|
|
13109
|
+
object({
|
|
13110
|
+
id: string(),
|
|
13111
|
+
userId: string(),
|
|
13112
|
+
name: string(),
|
|
13113
|
+
tokenHash: string(),
|
|
13114
|
+
tokenPrefix: string(),
|
|
13115
|
+
scopes: array(TokenScopeSchema),
|
|
13116
|
+
expiresAt: number().nullish(),
|
|
13117
|
+
lastUsedAt: number().nullish(),
|
|
13118
|
+
createdAt: number()
|
|
13119
|
+
});
|
|
13120
|
+
/** What a share-view grant lets its holder see. */
|
|
13121
|
+
var ShareViewGrantSchema = object({
|
|
13122
|
+
kind: _enum(SHARE_VIEW_KINDS),
|
|
13123
|
+
deviceIds: array(number().int().nonnegative()).min(1).max(64),
|
|
13124
|
+
/** Talk-back, opt-in at mint time; absent means off. */
|
|
13125
|
+
talk: boolean().optional()
|
|
13126
|
+
});
|
|
13127
|
+
var ShareViewScopeSchema = object({
|
|
13128
|
+
type: literal("share-view"),
|
|
13129
|
+
grant: ShareViewGrantSchema
|
|
13130
|
+
});
|
|
13131
|
+
var AddonRoutesScopeSchema = object({
|
|
13132
|
+
type: literal("addon-routes"),
|
|
13133
|
+
access: array(MethodAccessSchema).min(1)
|
|
13134
|
+
});
|
|
13135
|
+
var CredentialScopeSchema = union([
|
|
13136
|
+
literal("*"),
|
|
13137
|
+
ShareViewScopeSchema,
|
|
13138
|
+
AddonRoutesScopeSchema,
|
|
13139
|
+
TokenScopeSchema
|
|
13140
|
+
]);
|
|
13141
|
+
object({
|
|
13142
|
+
kind: literal("credential"),
|
|
13143
|
+
sub: string().min(1),
|
|
13144
|
+
jti: string().min(16),
|
|
13145
|
+
scopes: array(CredentialScopeSchema).min(1)
|
|
13146
|
+
});
|
|
12909
13147
|
_enum(["new-session", "step-up"]);
|
|
12910
13148
|
/** The languages the hub's app landing pages are written in (D858). */
|
|
12911
13149
|
var AppPageLangSchema = _enum(["en", "it"]);
|
|
@@ -13641,7 +13879,14 @@ var AlertSeveritySchema = _enum([
|
|
|
13641
13879
|
]);
|
|
13642
13880
|
var AlertActionSchema = object({
|
|
13643
13881
|
label: string(),
|
|
13644
|
-
href: string().optional()
|
|
13882
|
+
href: string().optional(),
|
|
13883
|
+
/**
|
|
13884
|
+
* How `href` is followed. Absent / `app`: an admin-ui route (client-side).
|
|
13885
|
+
* `server`: a route the HUB serves (an addon HTTP route such as an OAuth
|
|
13886
|
+
* start) — the browser must leave the SPA with a full navigation, because
|
|
13887
|
+
* the SPA has no such route and would bounce to the home page.
|
|
13888
|
+
*/
|
|
13889
|
+
navigation: _enum(["app", "server"]).optional()
|
|
13645
13890
|
});
|
|
13646
13891
|
var AlertStatusSchema = _enum([
|
|
13647
13892
|
"active",
|
|
@@ -42066,6 +42311,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
42066
42311
|
"server-updated",
|
|
42067
42312
|
"export-completed",
|
|
42068
42313
|
"backup-completed",
|
|
42314
|
+
"alert-critical",
|
|
42315
|
+
"alert-error",
|
|
42316
|
+
"alert-critical-resolved",
|
|
42069
42317
|
"camera-online",
|
|
42070
42318
|
"camera-offline",
|
|
42071
42319
|
"camera-disabled",
|
|
@@ -44101,202 +44349,6 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
44101
44349
|
auth: "admin"
|
|
44102
44350
|
}), method(NcAlarmSensorsInputSchema, NcAlarmSensorsSchema, { auth: "admin" });
|
|
44103
44351
|
/**
|
|
44104
|
-
* Zod schemas for persisted record types.
|
|
44105
|
-
*
|
|
44106
|
-
* These schemas serve as the single source of truth for types that are
|
|
44107
|
-
* stored in the settings-store backend and retrieved via tRPC. They
|
|
44108
|
-
* replace the manual TypeScript interfaces with schema-derived types,
|
|
44109
|
-
* enabling runtime validation when reading from the backend.
|
|
44110
|
-
*
|
|
44111
|
-
* Convention: each schema is named `<Type>Schema`, the inferred TS type
|
|
44112
|
-
* keeps the original name for backward compatibility.
|
|
44113
|
-
*/
|
|
44114
|
-
/**
|
|
44115
|
-
* Auth model (v2 — 2026-05-12 caps-only):
|
|
44116
|
-
*
|
|
44117
|
-
* Two principals:
|
|
44118
|
-
* - `isAdmin: true` — full bypass. Admin scope set is ignored.
|
|
44119
|
-
* - `isAdmin: false` — every call gated by `scopes` (see TokenScope).
|
|
44120
|
-
*
|
|
44121
|
-
* No role enum. There used to be `viewer / agent / scoped` and earlier
|
|
44122
|
-
* `super_admin` — collapsed onto the single boolean because every
|
|
44123
|
-
* non-admin caller is governed by the same scope-check rules; the
|
|
44124
|
-
* distinction was only ever used to route role-specific procedures
|
|
44125
|
-
* which we no longer have. CLI / webhook integrations are `cst_*` tokens
|
|
44126
|
-
* with `isAdmin: false` + an explicit scope list.
|
|
44127
|
-
*/
|
|
44128
|
-
/** Per-method access flavour. Materialised on every cap method via codegen. */
|
|
44129
|
-
var MethodAccessSchema = _enum([
|
|
44130
|
-
"view",
|
|
44131
|
-
"create",
|
|
44132
|
-
"delete"
|
|
44133
|
-
]);
|
|
44134
|
-
/**
|
|
44135
|
-
* A single scope grant. Three forms — checked in matcher with OR
|
|
44136
|
-
* semantics; the first match decides whether the call passes:
|
|
44137
|
-
*
|
|
44138
|
-
* - `category` — broadest. `target: 'device' | 'system'` selects every
|
|
44139
|
-
* cap with the matching `scope` declaration. New caps inherit grants
|
|
44140
|
-
* automatically — adding a device cap to the codebase makes it
|
|
44141
|
-
* reachable to every user with `category:device [...]` without a
|
|
44142
|
-
* re-grant. Use this for "family viewer" / "operator" tiers.
|
|
44143
|
-
*
|
|
44144
|
-
* - `addon` — every cap that ships inside a given addon id. Niche;
|
|
44145
|
-
* useful when an integration needs to interact with one specific
|
|
44146
|
-
* addon's surface regardless of how many caps it exports.
|
|
44147
|
-
*
|
|
44148
|
-
* - `capability` — the most precise form. Single cap name. Use for
|
|
44149
|
-
* integration tokens where you want to grant `addons.installPackage`
|
|
44150
|
-
* and nothing else.
|
|
44151
|
-
*
|
|
44152
|
-
* `access` is REQUIRED with min 1 element. `admin` bypass is the
|
|
44153
|
-
* "give me everything" escape; there is no wildcard scope.
|
|
44154
|
-
*/
|
|
44155
|
-
var AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
44156
|
-
var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
44157
|
-
var CapScopeSchema = _enum(["device", "system"]);
|
|
44158
|
-
/**
|
|
44159
|
-
* DeviceSelector (scope model v3 — 2026-08-12).
|
|
44160
|
-
*
|
|
44161
|
-
* A `device` grant no longer carries a frozen list of deviceIds. It carries
|
|
44162
|
-
* a SELECTOR the matcher resolves against the live fleet, so the grant can be
|
|
44163
|
-
* DYNAMIC: a `types:['camera']` selector automatically covers a camera added
|
|
44164
|
-
* AFTER the grant was minted — no re-grant, no re-login.
|
|
44165
|
-
*
|
|
44166
|
-
* - `all` — every device in the deployment. The broad viewer/operator
|
|
44167
|
-
* lever without a `category` grant (a `category` grant also covers device
|
|
44168
|
-
* caps that carry no deviceId; `all` is specifically the device set).
|
|
44169
|
-
* - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
|
|
44170
|
-
* grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
|
|
44171
|
-
* is NOT covered until the grant is edited.
|
|
44172
|
-
* - `types` — every device of a `DeviceType` (e.g. every `camera`).
|
|
44173
|
-
* DYNAMIC. A device that changes type, or a new device of the type,
|
|
44174
|
-
* re-resolves on the next request.
|
|
44175
|
-
* - `locations` — every device whose operator-assigned `location` label is
|
|
44176
|
-
* in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
|
|
44177
|
-
* null/unset location matches NO `locations` selector.
|
|
44178
|
-
*/
|
|
44179
|
-
var DeviceSelectorSchema = discriminatedUnion("kind", [
|
|
44180
|
-
object({ kind: literal("all") }),
|
|
44181
|
-
object({
|
|
44182
|
-
kind: literal("ids"),
|
|
44183
|
-
ids: array(number().int()).min(1)
|
|
44184
|
-
}),
|
|
44185
|
-
object({
|
|
44186
|
-
kind: literal("types"),
|
|
44187
|
-
types: array(_enum(DeviceType)).min(1)
|
|
44188
|
-
}),
|
|
44189
|
-
object({
|
|
44190
|
-
kind: literal("locations"),
|
|
44191
|
-
locations: array(string().min(1)).min(1)
|
|
44192
|
-
})
|
|
44193
|
-
]);
|
|
44194
|
-
var DeviceTokenScopeSchema = object({
|
|
44195
|
-
type: literal("device"),
|
|
44196
|
-
/** The device SET this grant covers — resolved against the live fleet. */
|
|
44197
|
-
selector: DeviceSelectorSchema,
|
|
44198
|
-
access: array(MethodAccessSchema).min(1),
|
|
44199
|
-
/**
|
|
44200
|
-
* Whether a grant on a PARENT device transparently covers its accessory
|
|
44201
|
-
* CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
|
|
44202
|
-
* Direction is parent → children ONLY.
|
|
44203
|
-
*
|
|
44204
|
-
* Absent → the matcher DERIVES it from the access flavour: `view`
|
|
44205
|
-
* inherits (a camera viewer sees the camera's accessories), `create` /
|
|
44206
|
-
* `delete` do NOT (actuating/removing a child is an explicit act the
|
|
44207
|
-
* operator must grant on the child, not inherit from the parent). Set it
|
|
44208
|
-
* explicitly to override that default per grant.
|
|
44209
|
-
*/
|
|
44210
|
-
includeLinked: boolean().optional()
|
|
44211
|
-
});
|
|
44212
|
-
/**
|
|
44213
|
-
* v2 → v3 lazy migration. A pre-v3 `device` grant carried
|
|
44214
|
-
* `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
|
|
44215
|
-
* `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
|
|
44216
|
-
* EVERY parse path — stored records AND the JWT-carried scope arrays
|
|
44217
|
-
* normalised at the request boundary ({@link normalizeTokenScopes} in
|
|
44218
|
-
* `device-selector.ts`). Chosen over a one-time DB migration because a
|
|
44219
|
-
* migration cannot reach a JWT already in a client's hands; parse-time
|
|
44220
|
-
* migration covers both without a flag day. No cast — the raw object is read
|
|
44221
|
-
* through `Reflect.get` (its static type is `unknown`).
|
|
44222
|
-
*/
|
|
44223
|
-
function migrateLegacyTokenScope(raw) {
|
|
44224
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
44225
|
-
if (Reflect.get(raw, "type") !== "device") return raw;
|
|
44226
|
-
if (Reflect.get(raw, "selector") !== void 0) return raw;
|
|
44227
|
-
const targets = Reflect.get(raw, "targets");
|
|
44228
|
-
if (!Array.isArray(targets)) return raw;
|
|
44229
|
-
return {
|
|
44230
|
-
type: "device",
|
|
44231
|
-
selector: {
|
|
44232
|
-
kind: "ids",
|
|
44233
|
-
ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
|
|
44234
|
-
},
|
|
44235
|
-
access: Reflect.get(raw, "access")
|
|
44236
|
-
};
|
|
44237
|
-
}
|
|
44238
|
-
var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
|
|
44239
|
-
object({
|
|
44240
|
-
type: literal("category"),
|
|
44241
|
-
target: CapScopeSchema,
|
|
44242
|
-
access: array(MethodAccessSchema).min(1)
|
|
44243
|
-
}),
|
|
44244
|
-
object({
|
|
44245
|
-
type: literal("capability"),
|
|
44246
|
-
target: string(),
|
|
44247
|
-
access: array(MethodAccessSchema).min(1)
|
|
44248
|
-
}),
|
|
44249
|
-
object({
|
|
44250
|
-
type: literal("addon"),
|
|
44251
|
-
target: string(),
|
|
44252
|
-
access: array(MethodAccessSchema).min(1)
|
|
44253
|
-
}),
|
|
44254
|
-
DeviceTokenScopeSchema
|
|
44255
|
-
]));
|
|
44256
|
-
object({
|
|
44257
|
-
id: string(),
|
|
44258
|
-
username: string(),
|
|
44259
|
-
passwordHash: string(),
|
|
44260
|
-
/**
|
|
44261
|
-
* Admin bypass. When true, the middleware skips the scope-access
|
|
44262
|
-
* check entirely. There is no other axis of privilege; the legacy
|
|
44263
|
-
* role enum collapsed onto this boolean in v2.
|
|
44264
|
-
*/
|
|
44265
|
-
isAdmin: boolean().default(false),
|
|
44266
|
-
allowedProviders: AllowedProviderSchema,
|
|
44267
|
-
allowedDevices: AllowedDevicesSchema,
|
|
44268
|
-
/**
|
|
44269
|
-
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
44270
|
-
* ignored. Non-admins without scopes are locked out of every
|
|
44271
|
-
* protected call.
|
|
44272
|
-
*/
|
|
44273
|
-
scopes: array(TokenScopeSchema).default([]),
|
|
44274
|
-
createdAt: number(),
|
|
44275
|
-
updatedAt: number()
|
|
44276
|
-
});
|
|
44277
|
-
object({
|
|
44278
|
-
id: string(),
|
|
44279
|
-
label: string(),
|
|
44280
|
-
isAdmin: boolean().default(false),
|
|
44281
|
-
allowedProviders: AllowedProviderSchema,
|
|
44282
|
-
allowedDevices: AllowedDevicesSchema,
|
|
44283
|
-
tokenHash: string(),
|
|
44284
|
-
tokenPrefix: string(),
|
|
44285
|
-
createdAt: number(),
|
|
44286
|
-
lastUsedAt: number().optional()
|
|
44287
|
-
});
|
|
44288
|
-
object({
|
|
44289
|
-
id: string(),
|
|
44290
|
-
userId: string(),
|
|
44291
|
-
name: string(),
|
|
44292
|
-
tokenHash: string(),
|
|
44293
|
-
tokenPrefix: string(),
|
|
44294
|
-
scopes: array(TokenScopeSchema),
|
|
44295
|
-
expiresAt: number().nullish(),
|
|
44296
|
-
lastUsedAt: number().nullish(),
|
|
44297
|
-
createdAt: number()
|
|
44298
|
-
});
|
|
44299
|
-
/**
|
|
44300
44352
|
* `oauth-integration` — a collection cap by which an addon declares
|
|
44301
44353
|
* itself a linkable OAuth client of the hub's core `/oauth2/*` server.
|
|
44302
44354
|
* Each provider returns a static descriptor; the core enumerates them
|
|
@@ -51111,7 +51163,10 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
|
|
|
51111
51163
|
}), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
51112
51164
|
kind: "mutation",
|
|
51113
51165
|
auth: "admin"
|
|
51114
|
-
}), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({
|
|
51166
|
+
}), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({ locationIds: array(string()).readonly() }), object({
|
|
51167
|
+
clear: boolean(),
|
|
51168
|
+
repointLocationId: string().optional()
|
|
51169
|
+
}), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
|
|
51115
51170
|
kind: "mutation",
|
|
51116
51171
|
auth: "admin"
|
|
51117
51172
|
}), method(StorageCleanupInputSchema, object({ jobId: string() }), {
|
|
@@ -54556,6 +54611,11 @@ object({
|
|
|
54556
54611
|
*
|
|
54557
54612
|
* `kind: 'poll'` — cloud REST APIs typically have no push channel; the
|
|
54558
54613
|
* provider refreshes the slice on an interval and eagerly after commands.
|
|
54614
|
+
*
|
|
54615
|
+
* UNKNOWN IS NOT OFF. Every field the appliance reports separately is
|
|
54616
|
+
* nullable: `null` means "not known right now" (the appliance has not said,
|
|
54617
|
+
* the link is down, or the description does not carry it) and is never
|
|
54618
|
+
* rendered or exported as `false` / `0`. `availability` says why.
|
|
54559
54619
|
*/
|
|
54560
54620
|
var DishwasherStateSchema = _enum([
|
|
54561
54621
|
"idle",
|
|
@@ -54565,112 +54625,227 @@ var DishwasherStateSchema = _enum([
|
|
|
54565
54625
|
"error",
|
|
54566
54626
|
"unavailable"
|
|
54567
54627
|
]);
|
|
54628
|
+
/**
|
|
54629
|
+
* The appliance's own operation state, uncollapsed. `state` above is the coarse
|
|
54630
|
+
* view for compact surfaces; this is what the appliance said.
|
|
54631
|
+
*/
|
|
54632
|
+
var DishwasherOperationStateSchema = _enum([
|
|
54633
|
+
"Inactive",
|
|
54634
|
+
"Ready",
|
|
54635
|
+
"DelayedStart",
|
|
54636
|
+
"Run",
|
|
54637
|
+
"Pause",
|
|
54638
|
+
"ActionRequired",
|
|
54639
|
+
"Finished",
|
|
54640
|
+
"Error",
|
|
54641
|
+
"Aborting"
|
|
54642
|
+
]);
|
|
54643
|
+
/**
|
|
54644
|
+
* Why fields may be `null`:
|
|
54645
|
+
* - `available` — the link is up and the description is loaded;
|
|
54646
|
+
* - `offline` — the local link is down (values are unknown, not last-known);
|
|
54647
|
+
* - `description-missing` — the link is up but the appliance's description could not
|
|
54648
|
+
* be loaded, so its values cannot be decoded.
|
|
54649
|
+
*/
|
|
54650
|
+
var DishwasherAvailabilitySchema = _enum([
|
|
54651
|
+
"available",
|
|
54652
|
+
"offline",
|
|
54653
|
+
"description-missing"
|
|
54654
|
+
]);
|
|
54568
54655
|
var DishwasherConsumableLevelSchema = _enum([
|
|
54569
54656
|
"ok",
|
|
54570
54657
|
"low",
|
|
54571
54658
|
"empty"
|
|
54572
54659
|
]);
|
|
54573
54660
|
object({
|
|
54661
|
+
availability: DishwasherAvailabilitySchema,
|
|
54662
|
+
/** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
|
|
54574
54663
|
state: DishwasherStateSchema,
|
|
54575
|
-
/**
|
|
54664
|
+
/** The appliance's operation state as reported; null when not known. */
|
|
54665
|
+
operationState: DishwasherOperationStateSchema.nullable(),
|
|
54666
|
+
/** Feature key of the ACTIVE (running) program, or null when none runs / not known. */
|
|
54576
54667
|
program: string().nullable(),
|
|
54577
|
-
/**
|
|
54668
|
+
/** Feature key of the SELECTED program (what Start would run), or null when none / not known. */
|
|
54669
|
+
selectedProgram: string().nullable(),
|
|
54670
|
+
/** Feature keys of the programs the appliance currently offers — drives the UI selector. */
|
|
54578
54671
|
availablePrograms: array(string()),
|
|
54579
|
-
/**
|
|
54580
|
-
|
|
54672
|
+
/** Seconds remaining on the active cycle, or null when not running / not known. */
|
|
54673
|
+
remainingSeconds: number().int().nonnegative().nullable(),
|
|
54674
|
+
/** Seconds the delayed start is set to (time until the cycle starts), or null. */
|
|
54675
|
+
delayedStartSeconds: number().int().nonnegative().nullable(),
|
|
54581
54676
|
/** Cycle completion 0..100, or null when not applicable. */
|
|
54582
54677
|
progressPercent: number().min(0).max(100).nullable(),
|
|
54583
|
-
/** True when the door is reported open. */
|
|
54584
|
-
doorOpen: boolean(),
|
|
54585
|
-
/** True when
|
|
54586
|
-
|
|
54587
|
-
/**
|
|
54588
|
-
|
|
54678
|
+
/** True when the door is reported open; null when the door state is not known. */
|
|
54679
|
+
doorOpen: boolean().nullable(),
|
|
54680
|
+
/** True when the door is latched (locked) during a cycle; null when not reported. */
|
|
54681
|
+
doorLocked: boolean().nullable(),
|
|
54682
|
+
/** True when remote start is allowed (door closed, power on, etc.); null when not known. */
|
|
54683
|
+
remoteStartAllowed: boolean().nullable(),
|
|
54684
|
+
/** True when the appliance accepts remote control now; null when not known. */
|
|
54685
|
+
remoteControlActive: boolean().nullable(),
|
|
54686
|
+
/** True while somebody operates the appliance at its panel; null when not reported. */
|
|
54687
|
+
localControlActive: boolean().nullable(),
|
|
54688
|
+
/** Main power / switched-on state; null when not known. */
|
|
54689
|
+
powerOn: boolean().nullable(),
|
|
54690
|
+
/** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
|
|
54691
|
+
powerState: string().nullable(),
|
|
54589
54692
|
saltLevel: DishwasherConsumableLevelSchema.nullable(),
|
|
54590
54693
|
rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
|
|
54591
|
-
|
|
54592
|
-
|
|
54694
|
+
/** Option states: null when the appliance does not expose the option or has not said. */
|
|
54695
|
+
halfLoad: boolean().nullable(),
|
|
54696
|
+
hygienePlus: boolean().nullable(),
|
|
54593
54697
|
/** Extra dry option when the appliance exposes it. */
|
|
54594
|
-
extraDry: boolean(),
|
|
54698
|
+
extraDry: boolean().nullable(),
|
|
54595
54699
|
/** Intensive zone (lower basket) when the appliance exposes it. */
|
|
54596
|
-
intensiveZone: boolean(),
|
|
54700
|
+
intensiveZone: boolean().nullable(),
|
|
54597
54701
|
/** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
|
|
54598
|
-
varioSpeedPlus: boolean(),
|
|
54599
|
-
/** Child lock (controls disabled on the appliance). */
|
|
54600
|
-
childLock: boolean(),
|
|
54702
|
+
varioSpeedPlus: boolean().nullable(),
|
|
54703
|
+
/** Child lock (controls disabled on the appliance); null when not known. */
|
|
54704
|
+
childLock: boolean().nullable(),
|
|
54705
|
+
/** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
|
|
54706
|
+
activeEvents: array(string()),
|
|
54707
|
+
/** Numeric error code when the appliance reports one; null otherwise. */
|
|
54601
54708
|
errorCode: number().nullable(),
|
|
54709
|
+
/** Name of the raised alert-level event while the appliance is in error; null otherwise. */
|
|
54602
54710
|
errorLabel: string().nullable(),
|
|
54711
|
+
/** Ms epoch of the last value received from the appliance; 0 = never. */
|
|
54603
54712
|
lastFetchedAt: number()
|
|
54604
54713
|
});
|
|
54605
|
-
|
|
54714
|
+
var DeviceIdSchema = number().int().nonnegative();
|
|
54715
|
+
/** A value a program option or setting can take. */
|
|
54716
|
+
var DishwasherValueSchema = union([
|
|
54717
|
+
boolean(),
|
|
54718
|
+
string(),
|
|
54719
|
+
number()
|
|
54720
|
+
]);
|
|
54721
|
+
var DishwasherAccessSchema = _enum([
|
|
54722
|
+
"none",
|
|
54723
|
+
"read",
|
|
54724
|
+
"readWrite",
|
|
54725
|
+
"writeOnly",
|
|
54726
|
+
"readStatic"
|
|
54727
|
+
]);
|
|
54728
|
+
/**
|
|
54729
|
+
* One option or setting the appliance declares, with the constraints its description
|
|
54730
|
+
* gives. A `readWrite` descriptor that is `available` is what `setOption` accepts.
|
|
54731
|
+
*/
|
|
54732
|
+
var DishwasherOptionDescriptorSchema = object({
|
|
54733
|
+
/** Feature key (`Dishcare.Dishwasher.Option.ExtraDry`). */
|
|
54734
|
+
key: string(),
|
|
54735
|
+
/** Last segment of the key, for display. */
|
|
54736
|
+
name: string(),
|
|
54737
|
+
type: _enum([
|
|
54738
|
+
"boolean",
|
|
54739
|
+
"number",
|
|
54740
|
+
"enum",
|
|
54741
|
+
"string"
|
|
54742
|
+
]),
|
|
54743
|
+
access: DishwasherAccessSchema,
|
|
54744
|
+
/** The appliance reports this entity as currently available. */
|
|
54745
|
+
available: boolean(),
|
|
54746
|
+
/** Unit the description declares for a number; null when it declares none. */
|
|
54747
|
+
unit: string().nullable(),
|
|
54748
|
+
min: number().nullable(),
|
|
54749
|
+
max: number().nullable(),
|
|
54750
|
+
step: number().nullable(),
|
|
54751
|
+
/** Allowed members of an enum; null for other types. */
|
|
54752
|
+
enumValues: array(string()).nullable(),
|
|
54753
|
+
defaultValue: DishwasherValueSchema.nullable()
|
|
54754
|
+
});
|
|
54755
|
+
var DishwasherProgramSchema = object({
|
|
54756
|
+
/** Feature key (`Dishcare.Dishwasher.Program.Eco50`) — what `setProgram` / `start` take. */
|
|
54757
|
+
key: string(),
|
|
54758
|
+
name: string(),
|
|
54759
|
+
/** The appliance currently offers this program. */
|
|
54760
|
+
available: boolean(),
|
|
54761
|
+
/** The options this program lists, with the program-level access / availability / default. */
|
|
54762
|
+
options: array(DishwasherOptionDescriptorSchema)
|
|
54763
|
+
});
|
|
54764
|
+
var DishwasherOptionsSchema = object({
|
|
54765
|
+
availability: DishwasherAvailabilitySchema,
|
|
54766
|
+
/** What the appliance can be told to do, from the commands its description declares. */
|
|
54767
|
+
supportsStart: boolean(),
|
|
54768
|
+
supportsPause: boolean(),
|
|
54769
|
+
supportsResume: boolean(),
|
|
54770
|
+
supportsStop: boolean(),
|
|
54771
|
+
supportsPower: boolean(),
|
|
54772
|
+
/** Every program the description lists, with its options. */
|
|
54773
|
+
programs: array(DishwasherProgramSchema),
|
|
54774
|
+
/** Every option and setting entity the description lists (programs reference these by key). */
|
|
54775
|
+
options: array(DishwasherOptionDescriptorSchema)
|
|
54776
|
+
});
|
|
54777
|
+
DeviceType.Dishwasher, method(object({
|
|
54778
|
+
deviceId: DeviceIdSchema,
|
|
54779
|
+
program: string().min(1).optional(),
|
|
54780
|
+
options: record(string(), DishwasherValueSchema).optional()
|
|
54781
|
+
}), _void(), {
|
|
54606
54782
|
kind: "mutation",
|
|
54607
54783
|
auth: "protected"
|
|
54608
|
-
}), method(object({ deviceId:
|
|
54784
|
+
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
54609
54785
|
kind: "mutation",
|
|
54610
54786
|
auth: "protected"
|
|
54611
|
-
}), method(object({ deviceId:
|
|
54787
|
+
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
54788
|
+
kind: "mutation",
|
|
54789
|
+
auth: "protected"
|
|
54790
|
+
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
54612
54791
|
kind: "mutation",
|
|
54613
54792
|
auth: "protected"
|
|
54614
54793
|
}), method(object({
|
|
54615
|
-
deviceId:
|
|
54794
|
+
deviceId: DeviceIdSchema,
|
|
54616
54795
|
program: string().min(1)
|
|
54617
54796
|
}), _void(), {
|
|
54618
54797
|
kind: "mutation",
|
|
54619
54798
|
auth: "protected"
|
|
54620
54799
|
}), method(object({
|
|
54621
|
-
deviceId:
|
|
54800
|
+
deviceId: DeviceIdSchema,
|
|
54622
54801
|
on: boolean()
|
|
54623
54802
|
}), _void(), {
|
|
54624
54803
|
kind: "mutation",
|
|
54625
54804
|
auth: "protected"
|
|
54626
54805
|
}), method(object({
|
|
54627
|
-
deviceId:
|
|
54806
|
+
deviceId: DeviceIdSchema,
|
|
54628
54807
|
on: boolean()
|
|
54629
54808
|
}), _void(), {
|
|
54630
54809
|
kind: "mutation",
|
|
54631
54810
|
auth: "protected"
|
|
54632
54811
|
}), method(object({
|
|
54633
|
-
deviceId:
|
|
54812
|
+
deviceId: DeviceIdSchema,
|
|
54634
54813
|
on: boolean()
|
|
54635
54814
|
}), _void(), {
|
|
54636
54815
|
kind: "mutation",
|
|
54637
54816
|
auth: "protected"
|
|
54638
54817
|
}), method(object({
|
|
54639
|
-
deviceId:
|
|
54818
|
+
deviceId: DeviceIdSchema,
|
|
54640
54819
|
on: boolean()
|
|
54641
54820
|
}), _void(), {
|
|
54642
54821
|
kind: "mutation",
|
|
54643
54822
|
auth: "protected"
|
|
54644
54823
|
}), method(object({
|
|
54645
|
-
deviceId:
|
|
54824
|
+
deviceId: DeviceIdSchema,
|
|
54646
54825
|
on: boolean()
|
|
54647
54826
|
}), _void(), {
|
|
54648
54827
|
kind: "mutation",
|
|
54649
54828
|
auth: "protected"
|
|
54650
54829
|
}), method(object({
|
|
54651
|
-
deviceId:
|
|
54830
|
+
deviceId: DeviceIdSchema,
|
|
54652
54831
|
on: boolean()
|
|
54653
54832
|
}), _void(), {
|
|
54654
54833
|
kind: "mutation",
|
|
54655
54834
|
auth: "protected"
|
|
54656
54835
|
}), method(object({
|
|
54657
|
-
deviceId:
|
|
54836
|
+
deviceId: DeviceIdSchema,
|
|
54658
54837
|
on: boolean()
|
|
54659
54838
|
}), _void(), {
|
|
54660
54839
|
kind: "mutation",
|
|
54661
54840
|
auth: "protected"
|
|
54662
54841
|
}), method(object({
|
|
54663
|
-
deviceId:
|
|
54842
|
+
deviceId: DeviceIdSchema,
|
|
54664
54843
|
key: string().min(1),
|
|
54665
|
-
value:
|
|
54666
|
-
boolean(),
|
|
54667
|
-
string(),
|
|
54668
|
-
number()
|
|
54669
|
-
])
|
|
54844
|
+
value: DishwasherValueSchema
|
|
54670
54845
|
}), _void(), {
|
|
54671
54846
|
kind: "mutation",
|
|
54672
54847
|
auth: "protected"
|
|
54673
|
-
});
|
|
54848
|
+
}), method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" });
|
|
54674
54849
|
object({
|
|
54675
54850
|
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
54676
54851
|
lastPressedAt: number().nullable(),
|
|
@@ -64730,6 +64905,12 @@ Object.freeze({
|
|
|
64730
64905
|
addonId: null,
|
|
64731
64906
|
access: "view"
|
|
64732
64907
|
},
|
|
64908
|
+
"dishwasher.getOptions": {
|
|
64909
|
+
capName: "dishwasher",
|
|
64910
|
+
capScope: "device",
|
|
64911
|
+
addonId: null,
|
|
64912
|
+
access: "view"
|
|
64913
|
+
},
|
|
64733
64914
|
"dishwasher.getStatus": {
|
|
64734
64915
|
capName: "dishwasher",
|
|
64735
64916
|
capScope: "device",
|
|
@@ -64742,6 +64923,12 @@ Object.freeze({
|
|
|
64742
64923
|
addonId: null,
|
|
64743
64924
|
access: "create"
|
|
64744
64925
|
},
|
|
64926
|
+
"dishwasher.resume": {
|
|
64927
|
+
capName: "dishwasher",
|
|
64928
|
+
capScope: "device",
|
|
64929
|
+
addonId: null,
|
|
64930
|
+
access: "create"
|
|
64931
|
+
},
|
|
64745
64932
|
"dishwasher.setChildLock": {
|
|
64746
64933
|
capName: "dishwasher",
|
|
64747
64934
|
capScope: "device",
|
|
@@ -69242,6 +69429,12 @@ Object.freeze({
|
|
|
69242
69429
|
addonId: null,
|
|
69243
69430
|
access: "view"
|
|
69244
69431
|
},
|
|
69432
|
+
"storageMigration.moverClearance": {
|
|
69433
|
+
capName: "storage-migration",
|
|
69434
|
+
capScope: "system",
|
|
69435
|
+
addonId: null,
|
|
69436
|
+
access: "view"
|
|
69437
|
+
},
|
|
69245
69438
|
"storageMigration.movers": {
|
|
69246
69439
|
capName: "storage-migration",
|
|
69247
69440
|
capScope: "system",
|
|
@@ -71435,6 +71628,11 @@ Object.freeze({
|
|
|
71435
71628
|
form: "single",
|
|
71436
71629
|
optional: false
|
|
71437
71630
|
}],
|
|
71631
|
+
"dishwasher.getOptions": [{
|
|
71632
|
+
name: "deviceId",
|
|
71633
|
+
form: "single",
|
|
71634
|
+
optional: false
|
|
71635
|
+
}],
|
|
71438
71636
|
"dishwasher.getStatus": [{
|
|
71439
71637
|
name: "deviceId",
|
|
71440
71638
|
form: "single",
|
|
@@ -71445,6 +71643,11 @@ Object.freeze({
|
|
|
71445
71643
|
form: "single",
|
|
71446
71644
|
optional: false
|
|
71447
71645
|
}],
|
|
71646
|
+
"dishwasher.resume": [{
|
|
71647
|
+
name: "deviceId",
|
|
71648
|
+
form: "single",
|
|
71649
|
+
optional: false
|
|
71650
|
+
}],
|
|
71448
71651
|
"dishwasher.setChildLock": [{
|
|
71449
71652
|
name: "deviceId",
|
|
71450
71653
|
form: "single",
|
|
@@ -73531,6 +73734,9 @@ new Map(Object.entries({
|
|
|
73531
73734
|
"wrapper-update-available": "system",
|
|
73532
73735
|
"backup-completed": "system",
|
|
73533
73736
|
"export-completed": "system",
|
|
73737
|
+
"alert-critical": "system",
|
|
73738
|
+
"alert-error": "system",
|
|
73739
|
+
"alert-critical-resolved": "system",
|
|
73534
73740
|
"storage-location-low": "system",
|
|
73535
73741
|
"storage-location-critical": "system",
|
|
73536
73742
|
"storage-location-ok": "system",
|
|
@@ -73655,6 +73861,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
73655
73861
|
"storage-location-ok"
|
|
73656
73862
|
]
|
|
73657
73863
|
},
|
|
73864
|
+
{
|
|
73865
|
+
id: "alerts",
|
|
73866
|
+
family: "system",
|
|
73867
|
+
label: "Alerts",
|
|
73868
|
+
kinds: [
|
|
73869
|
+
"alert-critical",
|
|
73870
|
+
"alert-error",
|
|
73871
|
+
"alert-critical-resolved"
|
|
73872
|
+
]
|
|
73873
|
+
},
|
|
73658
73874
|
{
|
|
73659
73875
|
id: "alarm",
|
|
73660
73876
|
family: "alarm",
|
|
@@ -73699,6 +73915,21 @@ var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.ent
|
|
|
73699
73915
|
return [[descriptor.kind, field]];
|
|
73700
73916
|
})));
|
|
73701
73917
|
Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
|
|
73918
|
+
object({
|
|
73919
|
+
phase: _enum(["raised", "resolved"]),
|
|
73920
|
+
episodeId: string().min(1),
|
|
73921
|
+
alertId: string().min(1),
|
|
73922
|
+
severity: AlertSeveritySchema,
|
|
73923
|
+
category: string(),
|
|
73924
|
+
title: string(),
|
|
73925
|
+
message: string(),
|
|
73926
|
+
sourceType: string().optional(),
|
|
73927
|
+
sourceId: string().optional(),
|
|
73928
|
+
nodeId: string().optional(),
|
|
73929
|
+
/** The alert's own action link (where the operator acts on it), when it has one. */
|
|
73930
|
+
actionUrl: string().optional(),
|
|
73931
|
+
at: number()
|
|
73932
|
+
});
|
|
73702
73933
|
var RULE_BODY_ONLY = ["body"];
|
|
73703
73934
|
var RULE_TEXT = ["title", "body"];
|
|
73704
73935
|
var BATTERY = ["device-battery-low", "device-battery-normal"];
|
|
@@ -73708,6 +73939,11 @@ var TANK = [
|
|
|
73708
73939
|
"device-tank-full",
|
|
73709
73940
|
"device-tank-normal"
|
|
73710
73941
|
];
|
|
73942
|
+
var ALERT_EPISODE = [
|
|
73943
|
+
"alert-critical",
|
|
73944
|
+
"alert-error",
|
|
73945
|
+
"alert-critical-resolved"
|
|
73946
|
+
];
|
|
73711
73947
|
var UPDATES = [
|
|
73712
73948
|
"addon-update-available",
|
|
73713
73949
|
"server-update-available",
|
|
@@ -73900,14 +74136,14 @@ var STATE_SUMMARY_VARS = [
|
|
|
73900
74136
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
73901
74137
|
([...RULE_TEXT], [...TANK]),
|
|
73902
74138
|
[...RULE_TEXT],
|
|
73903
|
-
([...RULE_TEXT], [...UPDATES]),
|
|
73904
|
-
([...RULE_TEXT], [...UPDATES]),
|
|
73905
|
-
[...RULE_TEXT],
|
|
74139
|
+
([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
|
|
74140
|
+
([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
|
|
73906
74141
|
[...RULE_TEXT],
|
|
73907
74142
|
[...RULE_TEXT],
|
|
73908
74143
|
[...RULE_TEXT],
|
|
73909
74144
|
[...RULE_TEXT],
|
|
73910
74145
|
[...RULE_TEXT],
|
|
74146
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
73911
74147
|
[...RULE_TEXT],
|
|
73912
74148
|
[...RULE_TEXT],
|
|
73913
74149
|
[...RULE_TEXT],
|
|
@@ -73925,7 +74161,11 @@ var STATE_SUMMARY_VARS = [
|
|
|
73925
74161
|
[...RULE_TEXT],
|
|
73926
74162
|
[...RULE_TEXT],
|
|
73927
74163
|
[...RULE_TEXT],
|
|
73928
|
-
[...RULE_TEXT],
|
|
74164
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74165
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74166
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74167
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74168
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
73929
74169
|
[...RULE_TEXT],
|
|
73930
74170
|
[...RULE_TEXT],
|
|
73931
74171
|
[...RULE_TEXT],
|
|
@@ -74463,6 +74703,37 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
74463
74703
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
74464
74704
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
74465
74705
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
74706
|
+
/**
|
|
74707
|
+
* Where an OAuth round-trip ends: the admin-ui completion route
|
|
74708
|
+
* (`/sso-complete`, the page every popup sign-in already lands on), with the
|
|
74709
|
+
* OUTCOME in the URL fragment.
|
|
74710
|
+
*
|
|
74711
|
+
* The outcome is a status plus a reason CODE from a closed list. It never
|
|
74712
|
+
* carries a token, a code, a client secret or a free-form error message: the
|
|
74713
|
+
* page words each code itself, and an unknown code reads as `unknown` rather
|
|
74714
|
+
* than being echoed. Fragment only, so nothing here reaches a server log.
|
|
74715
|
+
*
|
|
74716
|
+
* Pure and browser-safe.
|
|
74717
|
+
*/
|
|
74718
|
+
/** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
|
|
74719
|
+
var OAUTH_LANDING_PATH = "/sso-complete";
|
|
74720
|
+
/**
|
|
74721
|
+
* The landing URL for `outcome`. `origin` is the hub address the flow already
|
|
74722
|
+
* runs on (from the `local-network` palette role, never a literal); `null`
|
|
74723
|
+
* gives a hub-relative URL.
|
|
74724
|
+
*/
|
|
74725
|
+
function oauthLandingUrl(origin, outcome) {
|
|
74726
|
+
const params = new URLSearchParams({
|
|
74727
|
+
return: "oauth",
|
|
74728
|
+
status: outcome.status
|
|
74729
|
+
});
|
|
74730
|
+
if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
|
|
74731
|
+
if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
|
|
74732
|
+
if (outcome.integration !== void 0) params.set("integration", outcome.integration);
|
|
74733
|
+
if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
|
|
74734
|
+
if (outcome.action === "disconnect") params.set("action", "disconnect");
|
|
74735
|
+
return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
|
|
74736
|
+
}
|
|
74466
74737
|
var MB = 1024 * 1024;
|
|
74467
74738
|
1024 * MB, 3072 * MB;
|
|
74468
74739
|
//#endregion
|
|
@@ -74510,13 +74781,22 @@ var DEFAULT_OAUTH_STATE = {
|
|
|
74510
74781
|
accessExpiresAt: 0,
|
|
74511
74782
|
accountEmail: ""
|
|
74512
74783
|
};
|
|
74513
|
-
|
|
74784
|
+
/** Per-broker OAuth tokens (durable state `oauthAccounts`). */
|
|
74785
|
+
var GoogleOauthAccountSchema = object({
|
|
74514
74786
|
id: string().min(1),
|
|
74515
74787
|
refreshToken: string().default("").meta({ secret: true }),
|
|
74516
74788
|
accessToken: string().default("").meta({ secret: true }),
|
|
74517
74789
|
accessExpiresAt: number().default(0),
|
|
74518
|
-
accountEmail: string().default("")
|
|
74519
|
-
|
|
74790
|
+
accountEmail: string().default(""),
|
|
74791
|
+
/**
|
|
74792
|
+
* Google refused the refresh token (`invalid_grant`): it is dead and only a
|
|
74793
|
+
* new sign-in replaces it. Set where the refusal is seen, cleared by the
|
|
74794
|
+
* OAuth callback that stores a fresh token — so the status, the Sign-in
|
|
74795
|
+
* button and the alert all read the same fact.
|
|
74796
|
+
*/
|
|
74797
|
+
needsReauth: boolean().default(false)
|
|
74798
|
+
});
|
|
74799
|
+
var GoogleOauthAccountsStateSchema = object({ accounts: array(GoogleOauthAccountSchema).default([]) });
|
|
74520
74800
|
var GOOGLE_BROKER_KIND = "google";
|
|
74521
74801
|
var GOOGLE_BROKER_KIND_LABEL = "Google";
|
|
74522
74802
|
var GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
|
|
@@ -74605,7 +74885,8 @@ function migrateLegacyToBrokers(rawConfig, legacyOauth) {
|
|
|
74605
74885
|
refreshToken: legacyOauth.refreshToken,
|
|
74606
74886
|
accessToken: legacyOauth.accessToken,
|
|
74607
74887
|
accessExpiresAt: legacyOauth.accessExpiresAt,
|
|
74608
|
-
accountEmail: legacyOauth.accountEmail
|
|
74888
|
+
accountEmail: legacyOauth.accountEmail,
|
|
74889
|
+
needsReauth: false
|
|
74609
74890
|
}] : [];
|
|
74610
74891
|
return {
|
|
74611
74892
|
brokers: [entry],
|
|
@@ -74640,7 +74921,10 @@ var GoogleBrokerRegistry = class {
|
|
|
74640
74921
|
oauthById = /* @__PURE__ */ new Map();
|
|
74641
74922
|
seedFrom(entries, accounts) {
|
|
74642
74923
|
this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
|
|
74643
|
-
this.oauthById = new Map(accounts.map((a) =>
|
|
74924
|
+
this.oauthById = new Map(accounts.map((a) => {
|
|
74925
|
+
const account = GoogleOauthAccountSchema.parse(a);
|
|
74926
|
+
return [account.id, account];
|
|
74927
|
+
}));
|
|
74644
74928
|
this.seedCounterFromEntries(this.entries);
|
|
74645
74929
|
}
|
|
74646
74930
|
listEntries() {
|
|
@@ -74656,14 +74940,28 @@ var GoogleBrokerRegistry = class {
|
|
|
74656
74940
|
return [...this.oauthById.values()];
|
|
74657
74941
|
}
|
|
74658
74942
|
setOauth(account) {
|
|
74659
|
-
|
|
74943
|
+
const parsed = GoogleOauthAccountSchema.parse(account);
|
|
74944
|
+
this.oauthById.set(parsed.id, parsed);
|
|
74660
74945
|
}
|
|
74661
74946
|
removeOauth(id) {
|
|
74662
74947
|
this.oauthById.delete(id);
|
|
74663
74948
|
}
|
|
74664
74949
|
isConnected(id) {
|
|
74665
74950
|
const o = this.oauthById.get(id);
|
|
74666
|
-
return o !== void 0 && o.refreshToken.length > 0;
|
|
74951
|
+
return o !== void 0 && o.refreshToken.length > 0 && !o.needsReauth;
|
|
74952
|
+
}
|
|
74953
|
+
/** A refresh token Google refused: kept (a sign-in replaces it) but not usable. */
|
|
74954
|
+
needsReauth(id) {
|
|
74955
|
+
return this.oauthById.get(id)?.needsReauth === true;
|
|
74956
|
+
}
|
|
74957
|
+
markNeedsReauth(id) {
|
|
74958
|
+
const o = this.oauthById.get(id);
|
|
74959
|
+
if (o === void 0 || o.needsReauth) return false;
|
|
74960
|
+
this.oauthById.set(id, {
|
|
74961
|
+
...o,
|
|
74962
|
+
needsReauth: true
|
|
74963
|
+
});
|
|
74964
|
+
return true;
|
|
74667
74965
|
}
|
|
74668
74966
|
allocateId() {
|
|
74669
74967
|
const id = `google_${String(this.nextIdCounter).padStart(3, "0")}`;
|
|
@@ -74713,18 +75011,19 @@ var GoogleBrokerRegistry = class {
|
|
|
74713
75011
|
toBrokerInfo(entry, addonId) {
|
|
74714
75012
|
const oauth = this.oauthById.get(entry.id);
|
|
74715
75013
|
const connected = this.isConnected(entry.id);
|
|
75014
|
+
const reauth = this.needsReauth(entry.id);
|
|
74716
75015
|
return {
|
|
74717
75016
|
id: entry.id,
|
|
74718
75017
|
addonId,
|
|
74719
75018
|
name: entry.name,
|
|
74720
75019
|
kind: GOOGLE_BROKER_KIND,
|
|
74721
|
-
status: connected ? "connected" : "disconnected",
|
|
75020
|
+
status: connected ? "connected" : reauth ? "auth-failed" : "disconnected",
|
|
74722
75021
|
info: {
|
|
74723
75022
|
accountEmail: oauth?.accountEmail ?? "",
|
|
74724
75023
|
hasClient: entry.clientId.length > 0
|
|
74725
75024
|
},
|
|
74726
75025
|
lastCheckedAt: connected ? Date.now() : null,
|
|
74727
|
-
error: connected ? null : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
|
|
75026
|
+
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"
|
|
74728
75027
|
};
|
|
74729
75028
|
}
|
|
74730
75029
|
listBrokerInfos(addonId, kind) {
|
|
@@ -74776,7 +75075,7 @@ function consoleLinksHtml$1() {
|
|
|
74776
75075
|
].join("");
|
|
74777
75076
|
}
|
|
74778
75077
|
function introHtml(cap, publicOrigin) {
|
|
74779
|
-
const redirect = escapeHtml$
|
|
75078
|
+
const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
|
|
74780
75079
|
const console = consoleLinksHtml$1();
|
|
74781
75080
|
if (cap === "smtp-provider") return [
|
|
74782
75081
|
"<ol>",
|
|
@@ -74809,7 +75108,7 @@ function introHtml(cap, publicOrigin) {
|
|
|
74809
75108
|
].join("");
|
|
74810
75109
|
}
|
|
74811
75110
|
function connectHtml(cap, publicOrigin) {
|
|
74812
|
-
const redirect = escapeHtml$
|
|
75111
|
+
const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
|
|
74813
75112
|
if (cap === "smtp-provider") return [
|
|
74814
75113
|
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
|
|
74815
75114
|
"Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
|
|
@@ -74829,7 +75128,7 @@ function connectHtml(cap, publicOrigin) {
|
|
|
74829
75128
|
`Console: ${consoleLinksHtml$1()}.`
|
|
74830
75129
|
].join("");
|
|
74831
75130
|
}
|
|
74832
|
-
function escapeHtml$
|
|
75131
|
+
function escapeHtml$1(s) {
|
|
74833
75132
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
74834
75133
|
}
|
|
74835
75134
|
//#endregion
|
|
@@ -74848,7 +75147,7 @@ function consoleLinksHtml() {
|
|
|
74848
75147
|
}
|
|
74849
75148
|
/** Setup steps shown on Add / Edit — includes Audience / Test users. */
|
|
74850
75149
|
function googleBrokerSetupHtml(publicOrigin) {
|
|
74851
|
-
const redirect = escapeHtml
|
|
75150
|
+
const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
|
|
74852
75151
|
return [
|
|
74853
75152
|
"<ol>",
|
|
74854
75153
|
`<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>`,
|
|
@@ -74924,7 +75223,7 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
|
|
|
74924
75223
|
}
|
|
74925
75224
|
/** Connectivity Calendars / Email pages — calendar import picker lives here (not on broker Add). */
|
|
74926
75225
|
function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
74927
|
-
const redirect = escapeHtml
|
|
75226
|
+
const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
|
|
74928
75227
|
if (cap === "calendar-source") return { sections: [{
|
|
74929
75228
|
id: "setup-intro",
|
|
74930
75229
|
title: "Google brokers",
|
|
@@ -74982,7 +75281,7 @@ function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
|
74982
75281
|
}]
|
|
74983
75282
|
}] };
|
|
74984
75283
|
}
|
|
74985
|
-
function escapeHtml
|
|
75284
|
+
function escapeHtml(s) {
|
|
74986
75285
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
74987
75286
|
}
|
|
74988
75287
|
//#endregion
|
|
@@ -75277,7 +75576,6 @@ function isInvalidGrantRefreshError(err) {
|
|
|
75277
75576
|
return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
|
|
75278
75577
|
}
|
|
75279
75578
|
function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
|
|
75280
|
-
const pushLine = `${brokerName}: Google session expired — sign in again`;
|
|
75281
75579
|
return {
|
|
75282
75580
|
id: googleBrokerReconnectAlertId(brokerId),
|
|
75283
75581
|
category: "google.broker-reconnect",
|
|
@@ -75286,7 +75584,8 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
|
|
|
75286
75584
|
message: `${brokerName} needs you to sign in to Google again.`,
|
|
75287
75585
|
action: {
|
|
75288
75586
|
label: "Sign in again",
|
|
75289
|
-
href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}
|
|
75587
|
+
href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`,
|
|
75588
|
+
navigation: "server"
|
|
75290
75589
|
},
|
|
75291
75590
|
status: "active",
|
|
75292
75591
|
read: false,
|
|
@@ -75296,10 +75595,7 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
|
|
|
75296
75595
|
type: "google-broker",
|
|
75297
75596
|
id: brokerId
|
|
75298
75597
|
},
|
|
75299
|
-
metadata: {
|
|
75300
|
-
episodeKey: "reconnect",
|
|
75301
|
-
pushLine
|
|
75302
|
-
}
|
|
75598
|
+
metadata: { episodeKey: "reconnect" }
|
|
75303
75599
|
};
|
|
75304
75600
|
}
|
|
75305
75601
|
//#endregion
|
|
@@ -75508,6 +75804,14 @@ var GoogleBrokerStore = class GoogleBrokerStore {
|
|
|
75508
75804
|
* One OAuth account = one broker entry; feature caps aggregate connected brokers.
|
|
75509
75805
|
*/
|
|
75510
75806
|
var ADDON_ID = "google-services";
|
|
75807
|
+
/** A callback failure the landing page can name by code. */
|
|
75808
|
+
var GoogleCallbackError = class extends Error {
|
|
75809
|
+
reason;
|
|
75810
|
+
constructor(reason, message) {
|
|
75811
|
+
super(message);
|
|
75812
|
+
this.reason = reason;
|
|
75813
|
+
}
|
|
75814
|
+
};
|
|
75511
75815
|
var CALLBACK_PATH = "/oauth/callback";
|
|
75512
75816
|
var START_PATH = "/oauth/start";
|
|
75513
75817
|
var DISCONNECT_PATH = "/oauth/disconnect";
|
|
@@ -75528,7 +75832,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75528
75832
|
registry = new GoogleBrokerRegistry();
|
|
75529
75833
|
brokerStore = null;
|
|
75530
75834
|
activeBrokers = [];
|
|
75531
|
-
/** CSRF state → { brokerId,
|
|
75835
|
+
/** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
|
|
75532
75836
|
pendingStates = /* @__PURE__ */ new Map();
|
|
75533
75837
|
publicOrigin = null;
|
|
75534
75838
|
constructor() {
|
|
@@ -75564,7 +75868,8 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75564
75868
|
refreshToken: legacyOauth.refreshToken,
|
|
75565
75869
|
accessToken: legacyOauth.accessToken,
|
|
75566
75870
|
accessExpiresAt: legacyOauth.accessExpiresAt,
|
|
75567
|
-
accountEmail: legacyOauth.accountEmail
|
|
75871
|
+
accountEmail: legacyOauth.accountEmail,
|
|
75872
|
+
needsReauth: false
|
|
75568
75873
|
}] };
|
|
75569
75874
|
await this.oauthAccountsHandle.set(oauthAccounts);
|
|
75570
75875
|
await legacyHandle.set({ ...DEFAULT_OAUTH_STATE });
|
|
@@ -75835,12 +76140,9 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75835
76140
|
reply.status(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
75836
76141
|
return;
|
|
75837
76142
|
}
|
|
75838
|
-
const popupRaw = req.query["popup"];
|
|
75839
|
-
const popup = popupRaw === "1" || popupRaw === "true";
|
|
75840
76143
|
const state = (0, node_crypto.randomBytes)(16).toString("hex");
|
|
75841
76144
|
this.pendingStates.set(state, {
|
|
75842
76145
|
brokerId,
|
|
75843
|
-
popup,
|
|
75844
76146
|
issuedAt: Date.now()
|
|
75845
76147
|
});
|
|
75846
76148
|
this.prunePendingStates();
|
|
@@ -75852,42 +76154,39 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75852
76154
|
reply.redirect(url);
|
|
75853
76155
|
}
|
|
75854
76156
|
async handleCallback(req, reply) {
|
|
76157
|
+
const state = typeof req.query["state"] === "string" ? req.query["state"] : "";
|
|
75855
76158
|
const err = req.query["error"];
|
|
75856
76159
|
if (err) {
|
|
75857
|
-
const
|
|
75858
|
-
|
|
75859
|
-
|
|
75860
|
-
|
|
75861
|
-
|
|
75862
|
-
|
|
75863
|
-
|
|
75864
|
-
|
|
75865
|
-
});
|
|
75866
|
-
return;
|
|
75867
|
-
}
|
|
75868
|
-
this.html(reply, 400, `Google OAuth error: ${err}`);
|
|
76160
|
+
const pendingPeek = state ? this.pendingStates.get(state) : void 0;
|
|
76161
|
+
if (pendingPeek !== void 0) this.pendingStates.delete(state);
|
|
76162
|
+
await this.landOnAdminUi(reply, {
|
|
76163
|
+
status: "error",
|
|
76164
|
+
addonId: ADDON_ID,
|
|
76165
|
+
...pendingPeek !== void 0 ? { brokerId: pendingPeek.brokerId } : {},
|
|
76166
|
+
reason: err === "access_denied" ? "access_denied" : "invalid_request"
|
|
76167
|
+
});
|
|
75869
76168
|
return;
|
|
75870
76169
|
}
|
|
75871
76170
|
const code = req.query["code"];
|
|
75872
|
-
const
|
|
75873
|
-
const pending = typeof state === "string" ? this.pendingStates.get(state) : void 0;
|
|
76171
|
+
const pending = state ? this.pendingStates.get(state) : void 0;
|
|
75874
76172
|
if (!code || !state || pending === void 0) {
|
|
75875
|
-
this.
|
|
76173
|
+
await this.landOnAdminUi(reply, {
|
|
76174
|
+
status: "error",
|
|
76175
|
+
addonId: ADDON_ID,
|
|
76176
|
+
reason: "invalid_state"
|
|
76177
|
+
});
|
|
75876
76178
|
return;
|
|
75877
76179
|
}
|
|
75878
76180
|
this.pendingStates.delete(state);
|
|
75879
|
-
const { brokerId
|
|
76181
|
+
const { brokerId } = pending;
|
|
75880
76182
|
const entry = this.registry.getEntry(brokerId);
|
|
75881
76183
|
if (entry === null) {
|
|
75882
|
-
|
|
75883
|
-
|
|
75884
|
-
|
|
75885
|
-
|
|
75886
|
-
|
|
75887
|
-
|
|
75888
|
-
return;
|
|
75889
|
-
}
|
|
75890
|
-
this.html(reply, 404, `Unknown Google broker: ${brokerId}`);
|
|
76184
|
+
await this.landOnAdminUi(reply, {
|
|
76185
|
+
status: "error",
|
|
76186
|
+
addonId: ADDON_ID,
|
|
76187
|
+
brokerId,
|
|
76188
|
+
reason: "unknown_broker"
|
|
76189
|
+
});
|
|
75891
76190
|
return;
|
|
75892
76191
|
}
|
|
75893
76192
|
try {
|
|
@@ -75899,11 +76198,12 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75899
76198
|
}, code);
|
|
75900
76199
|
const prev = this.registry.getOauth(brokerId) ?? {
|
|
75901
76200
|
id: brokerId,
|
|
75902
|
-
...DEFAULT_OAUTH_STATE
|
|
76201
|
+
...DEFAULT_OAUTH_STATE,
|
|
76202
|
+
needsReauth: false
|
|
75903
76203
|
};
|
|
75904
|
-
if (!tokens.refresh_token && !prev.refreshToken) throw new
|
|
76204
|
+
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.");
|
|
75905
76205
|
const applied = applyTokenResponse({
|
|
75906
|
-
refreshToken: prev.refreshToken,
|
|
76206
|
+
refreshToken: prev.needsReauth && tokens.refresh_token ? "" : prev.refreshToken,
|
|
75907
76207
|
accessToken: prev.accessToken,
|
|
75908
76208
|
accessExpiresAt: prev.accessExpiresAt,
|
|
75909
76209
|
accountEmail: prev.accountEmail
|
|
@@ -75914,7 +76214,8 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75914
76214
|
refreshToken: applied.refreshToken,
|
|
75915
76215
|
accessToken: applied.accessToken,
|
|
75916
76216
|
accessExpiresAt: applied.accessExpiresAt,
|
|
75917
|
-
accountEmail: email || applied.accountEmail
|
|
76217
|
+
accountEmail: email || applied.accountEmail,
|
|
76218
|
+
needsReauth: false
|
|
75918
76219
|
};
|
|
75919
76220
|
this.registry.setOauth(next);
|
|
75920
76221
|
await this.persistOauthAccounts();
|
|
@@ -75927,65 +76228,44 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75927
76228
|
this.registry.replaceEntries(this.registry.listEntries().map((e) => e.id === brokerId ? renamed : e));
|
|
75928
76229
|
await this.persistBrokers();
|
|
75929
76230
|
}
|
|
75930
|
-
this.ctx.logger.info("google oauth connected", {
|
|
75931
|
-
brokerId,
|
|
75932
|
-
|
|
75933
|
-
accountEmail: next.accountEmail || void 0
|
|
75934
|
-
} });
|
|
75935
|
-
if (popup) {
|
|
75936
|
-
this.popupComplete(reply, {
|
|
75937
|
-
ok: true,
|
|
76231
|
+
this.ctx.logger.info("google oauth connected", {
|
|
76232
|
+
tags: { brokerId },
|
|
76233
|
+
meta: {
|
|
75938
76234
|
brokerId,
|
|
75939
|
-
|
|
75940
|
-
|
|
75941
|
-
|
|
75942
|
-
}
|
|
75943
|
-
this.
|
|
76235
|
+
hasRefresh: next.refreshToken.length > 0,
|
|
76236
|
+
accountEmail: next.accountEmail || void 0
|
|
76237
|
+
}
|
|
76238
|
+
});
|
|
76239
|
+
await this.landOnAdminUi(reply, {
|
|
76240
|
+
status: "success",
|
|
76241
|
+
addonId: ADDON_ID,
|
|
76242
|
+
brokerId
|
|
76243
|
+
});
|
|
75944
76244
|
} catch (e) {
|
|
75945
76245
|
const message = e instanceof Error ? e.message : String(e);
|
|
75946
76246
|
this.ctx.logger.warn("google oauth callback failed", { meta: {
|
|
75947
76247
|
brokerId,
|
|
75948
76248
|
error: message
|
|
75949
76249
|
} });
|
|
75950
|
-
|
|
75951
|
-
|
|
75952
|
-
|
|
75953
|
-
|
|
75954
|
-
|
|
75955
|
-
|
|
75956
|
-
return;
|
|
75957
|
-
}
|
|
75958
|
-
this.html(reply, 500, `OAuth failed: ${message}`);
|
|
76250
|
+
await this.landOnAdminUi(reply, {
|
|
76251
|
+
status: "error",
|
|
76252
|
+
addonId: ADDON_ID,
|
|
76253
|
+
brokerId,
|
|
76254
|
+
reason: e instanceof GoogleCallbackError ? e.reason : "exchange_failed"
|
|
76255
|
+
});
|
|
75959
76256
|
}
|
|
75960
76257
|
}
|
|
75961
76258
|
/**
|
|
75962
|
-
*
|
|
75963
|
-
*
|
|
75964
|
-
*
|
|
76259
|
+
* Every OAuth completion ends in CamStack: a redirect to the admin-ui
|
|
76260
|
+
* completion page (`/sso-complete`) on the hub's public address (the
|
|
76261
|
+
* `local-network` palette role), carrying a status and a reason CODE — never
|
|
76262
|
+
* a token, a code or an error message. The page hands a popup's outcome to
|
|
76263
|
+
* its opener and closes it, or, in a full-page flow, shows the result and
|
|
76264
|
+
* returns to Connectivity.
|
|
75965
76265
|
*/
|
|
75966
|
-
|
|
75967
|
-
|
|
75968
|
-
|
|
75969
|
-
ok: outcome.ok,
|
|
75970
|
-
brokerId: outcome.brokerId,
|
|
75971
|
-
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
75972
|
-
});
|
|
75973
|
-
const body = outcome.ok ? outcome.message ?? "Done. This window should close." : `OAuth failed: ${outcome.error ?? "unknown"}`;
|
|
75974
|
-
reply.header("content-type", "text/html; charset=utf-8");
|
|
75975
|
-
reply.status(outcome.ok ? 200 : 400).send(`<!doctype html><html><head><title>Google</title></head><body>
|
|
75976
|
-
<p>${escapeHtml(body)}</p>
|
|
75977
|
-
<script>
|
|
75978
|
-
(function () {
|
|
75979
|
-
var msg = ${payload};
|
|
75980
|
-
try {
|
|
75981
|
-
if (window.opener) {
|
|
75982
|
-
window.opener.postMessage(msg, window.location.origin);
|
|
75983
|
-
}
|
|
75984
|
-
} catch (e) {}
|
|
75985
|
-
setTimeout(function () { window.close(); }, 200);
|
|
75986
|
-
})();
|
|
75987
|
-
<\/script>
|
|
75988
|
-
</body></html>`);
|
|
76266
|
+
async landOnAdminUi(reply, outcome) {
|
|
76267
|
+
await this.refreshPublicOrigin();
|
|
76268
|
+
reply.redirect(oauthLandingUrl(this.publicOrigin, outcome));
|
|
75989
76269
|
}
|
|
75990
76270
|
async fetchAccountEmail(accessToken) {
|
|
75991
76271
|
try {
|
|
@@ -75997,10 +76277,6 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75997
76277
|
return "";
|
|
75998
76278
|
}
|
|
75999
76279
|
}
|
|
76000
|
-
html(reply, code, body) {
|
|
76001
|
-
reply.header("content-type", "text/html; charset=utf-8");
|
|
76002
|
-
reply.status(code).send(`<!doctype html><html><body><p>${escapeHtml(body)}</p></body></html>`);
|
|
76003
|
-
}
|
|
76004
76280
|
prunePendingStates() {
|
|
76005
76281
|
const cutoff = Date.now() - 10 * 6e4;
|
|
76006
76282
|
for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
|
|
@@ -76088,6 +76364,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76088
76364
|
const entry = this.registry.getEntry(brokerId);
|
|
76089
76365
|
const oauth = this.registry.getOauth(brokerId);
|
|
76090
76366
|
if (entry === null || oauth === null || !oauth.refreshToken.trim()) throw new Error(`Google broker ${brokerId} is not connected — use Sign in under Connectivity → Brokers`);
|
|
76367
|
+
if (oauth.needsReauth) throw new Error(`Google broker ${brokerId} needs a new sign-in — use Sign in under Connectivity → Brokers`);
|
|
76091
76368
|
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");
|
|
76092
76369
|
const state = {
|
|
76093
76370
|
refreshToken: oauth.refreshToken.trim(),
|
|
@@ -76103,7 +76380,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76103
76380
|
clientSecret: entry.clientSecret.trim()
|
|
76104
76381
|
}, state.refreshToken);
|
|
76105
76382
|
} catch (err) {
|
|
76106
|
-
if (isInvalidGrantRefreshError(err))
|
|
76383
|
+
if (isInvalidGrantRefreshError(err)) {
|
|
76384
|
+
if (this.registry.markNeedsReauth(brokerId)) await this.persistOauthAccounts();
|
|
76385
|
+
this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
|
|
76386
|
+
}
|
|
76107
76387
|
throw err;
|
|
76108
76388
|
}
|
|
76109
76389
|
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");
|
|
@@ -76113,7 +76393,8 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76113
76393
|
refreshToken: nextState.refreshToken,
|
|
76114
76394
|
accessToken: nextState.accessToken,
|
|
76115
76395
|
accessExpiresAt: nextState.accessExpiresAt,
|
|
76116
|
-
accountEmail: nextState.accountEmail
|
|
76396
|
+
accountEmail: nextState.accountEmail,
|
|
76397
|
+
needsReauth: false
|
|
76117
76398
|
});
|
|
76118
76399
|
await this.persistOauthAccounts();
|
|
76119
76400
|
return nextState.accessToken;
|
|
@@ -76129,20 +76410,18 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76129
76410
|
reply.status(404).send({ error: `Unknown Google broker: ${brokerId}` });
|
|
76130
76411
|
return;
|
|
76131
76412
|
}
|
|
76132
|
-
const popupRaw = req.query["popup"];
|
|
76133
|
-
const popup = popupRaw === "1" || popupRaw === "true";
|
|
76134
76413
|
this.registry.removeOauth(brokerId);
|
|
76135
76414
|
await this.persistOauthAccounts();
|
|
76136
|
-
this.ctx.logger.info("google oauth disconnected", {
|
|
76137
|
-
|
|
76138
|
-
|
|
76139
|
-
|
|
76140
|
-
|
|
76141
|
-
|
|
76142
|
-
|
|
76143
|
-
|
|
76144
|
-
|
|
76145
|
-
|
|
76415
|
+
this.ctx.logger.info("google oauth disconnected", {
|
|
76416
|
+
tags: { brokerId },
|
|
76417
|
+
meta: { brokerId }
|
|
76418
|
+
});
|
|
76419
|
+
await this.landOnAdminUi(reply, {
|
|
76420
|
+
status: "success",
|
|
76421
|
+
addonId: ADDON_ID,
|
|
76422
|
+
brokerId,
|
|
76423
|
+
action: "disconnect"
|
|
76424
|
+
});
|
|
76146
76425
|
}
|
|
76147
76426
|
async resolveRedirectUri() {
|
|
76148
76427
|
await this.refreshPublicOrigin();
|
|
@@ -76179,9 +76458,6 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76179
76458
|
function asSettingsCap(cap) {
|
|
76180
76459
|
if (cap === "calendar-source" || cap === "smtp-provider") return cap;
|
|
76181
76460
|
}
|
|
76182
|
-
function escapeHtml(s) {
|
|
76183
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
76184
|
-
}
|
|
76185
76461
|
//#endregion
|
|
76186
76462
|
exports.DEFAULT_GOOGLE_CONFIG = DEFAULT_GOOGLE_CONFIG;
|
|
76187
76463
|
exports.GOOGLE_BROKER_KIND = GOOGLE_BROKER_KIND;
|