@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
//#region ../types/dist/event-category-
|
|
2
|
+
//#region ../types/dist/event-category-DOl51-Ij.mjs
|
|
3
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
5
5
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -78,9 +78,18 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
78
78
|
/**
|
|
79
79
|
* The node's one-shot runner boot check (D877): planned runners against the
|
|
80
80
|
* process service, `degraded` when any is failed, refused or missing. The
|
|
81
|
-
* alert centre turns it into one
|
|
81
|
+
* alert centre turns it into one critical alert.
|
|
82
82
|
*/
|
|
83
83
|
EventCategory["SystemRunnerCheck"] = "system.runner-check";
|
|
84
|
+
/**
|
|
85
|
+
* An alert EPISODE opened or closed, emitted by the alert centre for alerts
|
|
86
|
+
* of severity `critical` or `error` (`AlertEpisodePayload`). The alert
|
|
87
|
+
* centre sends no notification of its own: Notification Center turns this
|
|
88
|
+
* into the `alert-critical` / `alert-error` / `alert-critical-resolved`
|
|
89
|
+
* system-event kinds, and reconciles from the alerts store because a bus
|
|
90
|
+
* event may be dropped (D8, D11).
|
|
91
|
+
*/
|
|
92
|
+
EventCategory["AlertEpisode"] = "alert.episode";
|
|
84
93
|
EventCategory["AddonError"] = "addon.error";
|
|
85
94
|
EventCategory["AddonPageReady"] = "addon.page-ready";
|
|
86
95
|
EventCategory["AddonWidgetReady"] = "addon.widget-ready";
|
|
@@ -5538,7 +5547,7 @@ var ZodIssueCode = {
|
|
|
5538
5547
|
var ZodFirstPartyTypeKind;
|
|
5539
5548
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5540
5549
|
//#endregion
|
|
5541
|
-
//#region ../types/dist/sleep-
|
|
5550
|
+
//#region ../types/dist/sleep-BxjU_fJM.mjs
|
|
5542
5551
|
/**
|
|
5543
5552
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5544
5553
|
* window to float samples (D455).
|
|
@@ -7920,6 +7929,12 @@ function normalizeAddonInitResult(result) {
|
|
|
7920
7929
|
if (Array.isArray(result)) return { providers: result };
|
|
7921
7930
|
return result;
|
|
7922
7931
|
}
|
|
7932
|
+
/**
|
|
7933
|
+
* The share-view scope kinds a `csv_*` share token can carry. The hub mints
|
|
7934
|
+
* and validates them (`share-token.service.ts` builds its enum from this
|
|
7935
|
+
* list); a data plane names the kinds it ACCEPTS in `share`.
|
|
7936
|
+
*/
|
|
7937
|
+
var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
|
|
7923
7938
|
/** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
|
|
7924
7939
|
var PeerBytesTicketSchema = object({
|
|
7925
7940
|
/** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
|
|
@@ -12902,6 +12917,229 @@ var ConvertResultSchema = object({
|
|
|
12902
12917
|
files: array(string()).readonly()
|
|
12903
12918
|
})).readonly()
|
|
12904
12919
|
});
|
|
12920
|
+
/**
|
|
12921
|
+
* Zod schemas for persisted record types.
|
|
12922
|
+
*
|
|
12923
|
+
* These schemas serve as the single source of truth for types that are
|
|
12924
|
+
* stored in the settings-store backend and retrieved via tRPC. They
|
|
12925
|
+
* replace the manual TypeScript interfaces with schema-derived types,
|
|
12926
|
+
* enabling runtime validation when reading from the backend.
|
|
12927
|
+
*
|
|
12928
|
+
* Convention: each schema is named `<Type>Schema`, the inferred TS type
|
|
12929
|
+
* keeps the original name for backward compatibility.
|
|
12930
|
+
*/
|
|
12931
|
+
/**
|
|
12932
|
+
* Auth model (v2 — 2026-05-12 caps-only):
|
|
12933
|
+
*
|
|
12934
|
+
* Two principals:
|
|
12935
|
+
* - `isAdmin: true` — full bypass. Admin scope set is ignored.
|
|
12936
|
+
* - `isAdmin: false` — every call gated by `scopes` (see TokenScope).
|
|
12937
|
+
*
|
|
12938
|
+
* No role enum. There used to be `viewer / agent / scoped` and earlier
|
|
12939
|
+
* `super_admin` — collapsed onto the single boolean because every
|
|
12940
|
+
* non-admin caller is governed by the same scope-check rules; the
|
|
12941
|
+
* distinction was only ever used to route role-specific procedures
|
|
12942
|
+
* which we no longer have. CLI / webhook integrations are `cst_*` tokens
|
|
12943
|
+
* with `isAdmin: false` + an explicit scope list.
|
|
12944
|
+
*/
|
|
12945
|
+
/** Per-method access flavour. Materialised on every cap method via codegen. */
|
|
12946
|
+
var MethodAccessSchema = _enum([
|
|
12947
|
+
"view",
|
|
12948
|
+
"create",
|
|
12949
|
+
"delete"
|
|
12950
|
+
]);
|
|
12951
|
+
/**
|
|
12952
|
+
* A single scope grant. Three forms — checked in matcher with OR
|
|
12953
|
+
* semantics; the first match decides whether the call passes:
|
|
12954
|
+
*
|
|
12955
|
+
* - `category` — broadest. `target: 'device' | 'system'` selects every
|
|
12956
|
+
* cap with the matching `scope` declaration. New caps inherit grants
|
|
12957
|
+
* automatically — adding a device cap to the codebase makes it
|
|
12958
|
+
* reachable to every user with `category:device [...]` without a
|
|
12959
|
+
* re-grant. Use this for "family viewer" / "operator" tiers.
|
|
12960
|
+
*
|
|
12961
|
+
* - `addon` — every cap that ships inside a given addon id. Niche;
|
|
12962
|
+
* useful when an integration needs to interact with one specific
|
|
12963
|
+
* addon's surface regardless of how many caps it exports.
|
|
12964
|
+
*
|
|
12965
|
+
* - `capability` — the most precise form. Single cap name. Use for
|
|
12966
|
+
* integration tokens where you want to grant `addons.installPackage`
|
|
12967
|
+
* and nothing else.
|
|
12968
|
+
*
|
|
12969
|
+
* `access` is REQUIRED with min 1 element. `admin` bypass is the
|
|
12970
|
+
* "give me everything" escape; there is no wildcard scope.
|
|
12971
|
+
*/
|
|
12972
|
+
var AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
12973
|
+
var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
12974
|
+
var CapScopeSchema = _enum(["device", "system"]);
|
|
12975
|
+
/**
|
|
12976
|
+
* DeviceSelector (scope model v3 — 2026-08-12).
|
|
12977
|
+
*
|
|
12978
|
+
* A `device` grant no longer carries a frozen list of deviceIds. It carries
|
|
12979
|
+
* a SELECTOR the matcher resolves against the live fleet, so the grant can be
|
|
12980
|
+
* DYNAMIC: a `types:['camera']` selector automatically covers a camera added
|
|
12981
|
+
* AFTER the grant was minted — no re-grant, no re-login.
|
|
12982
|
+
*
|
|
12983
|
+
* - `all` — every device in the deployment. The broad viewer/operator
|
|
12984
|
+
* lever without a `category` grant (a `category` grant also covers device
|
|
12985
|
+
* caps that carry no deviceId; `all` is specifically the device set).
|
|
12986
|
+
* - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
|
|
12987
|
+
* grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
|
|
12988
|
+
* is NOT covered until the grant is edited.
|
|
12989
|
+
* - `types` — every device of a `DeviceType` (e.g. every `camera`).
|
|
12990
|
+
* DYNAMIC. A device that changes type, or a new device of the type,
|
|
12991
|
+
* re-resolves on the next request.
|
|
12992
|
+
* - `locations` — every device whose operator-assigned `location` label is
|
|
12993
|
+
* in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
|
|
12994
|
+
* null/unset location matches NO `locations` selector.
|
|
12995
|
+
*/
|
|
12996
|
+
var DeviceSelectorSchema = discriminatedUnion("kind", [
|
|
12997
|
+
object({ kind: literal("all") }),
|
|
12998
|
+
object({
|
|
12999
|
+
kind: literal("ids"),
|
|
13000
|
+
ids: array(number().int()).min(1)
|
|
13001
|
+
}),
|
|
13002
|
+
object({
|
|
13003
|
+
kind: literal("types"),
|
|
13004
|
+
types: array(_enum(DeviceType)).min(1)
|
|
13005
|
+
}),
|
|
13006
|
+
object({
|
|
13007
|
+
kind: literal("locations"),
|
|
13008
|
+
locations: array(string().min(1)).min(1)
|
|
13009
|
+
})
|
|
13010
|
+
]);
|
|
13011
|
+
var DeviceTokenScopeSchema = object({
|
|
13012
|
+
type: literal("device"),
|
|
13013
|
+
/** The device SET this grant covers — resolved against the live fleet. */
|
|
13014
|
+
selector: DeviceSelectorSchema,
|
|
13015
|
+
access: array(MethodAccessSchema).min(1),
|
|
13016
|
+
/**
|
|
13017
|
+
* Whether a grant on a PARENT device transparently covers its accessory
|
|
13018
|
+
* CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
|
|
13019
|
+
* Direction is parent → children ONLY.
|
|
13020
|
+
*
|
|
13021
|
+
* Absent → the matcher DERIVES it from the access flavour: `view`
|
|
13022
|
+
* inherits (a camera viewer sees the camera's accessories), `create` /
|
|
13023
|
+
* `delete` do NOT (actuating/removing a child is an explicit act the
|
|
13024
|
+
* operator must grant on the child, not inherit from the parent). Set it
|
|
13025
|
+
* explicitly to override that default per grant.
|
|
13026
|
+
*/
|
|
13027
|
+
includeLinked: boolean().optional()
|
|
13028
|
+
});
|
|
13029
|
+
/**
|
|
13030
|
+
* v2 → v3 lazy migration. A pre-v3 `device` grant carried
|
|
13031
|
+
* `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
|
|
13032
|
+
* `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
|
|
13033
|
+
* EVERY parse path — stored records AND the JWT-carried scope arrays
|
|
13034
|
+
* normalised at the request boundary ({@link normalizeTokenScopes} in
|
|
13035
|
+
* `device-selector.ts`). Chosen over a one-time DB migration because a
|
|
13036
|
+
* migration cannot reach a JWT already in a client's hands; parse-time
|
|
13037
|
+
* migration covers both without a flag day. No cast — the raw object is read
|
|
13038
|
+
* through `Reflect.get` (its static type is `unknown`).
|
|
13039
|
+
*/
|
|
13040
|
+
function migrateLegacyTokenScope(raw) {
|
|
13041
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
13042
|
+
if (Reflect.get(raw, "type") !== "device") return raw;
|
|
13043
|
+
if (Reflect.get(raw, "selector") !== void 0) return raw;
|
|
13044
|
+
const targets = Reflect.get(raw, "targets");
|
|
13045
|
+
if (!Array.isArray(targets)) return raw;
|
|
13046
|
+
return {
|
|
13047
|
+
type: "device",
|
|
13048
|
+
selector: {
|
|
13049
|
+
kind: "ids",
|
|
13050
|
+
ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
|
|
13051
|
+
},
|
|
13052
|
+
access: Reflect.get(raw, "access")
|
|
13053
|
+
};
|
|
13054
|
+
}
|
|
13055
|
+
var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
|
|
13056
|
+
object({
|
|
13057
|
+
type: literal("category"),
|
|
13058
|
+
target: CapScopeSchema,
|
|
13059
|
+
access: array(MethodAccessSchema).min(1)
|
|
13060
|
+
}),
|
|
13061
|
+
object({
|
|
13062
|
+
type: literal("capability"),
|
|
13063
|
+
target: string(),
|
|
13064
|
+
access: array(MethodAccessSchema).min(1)
|
|
13065
|
+
}),
|
|
13066
|
+
object({
|
|
13067
|
+
type: literal("addon"),
|
|
13068
|
+
target: string(),
|
|
13069
|
+
access: array(MethodAccessSchema).min(1)
|
|
13070
|
+
}),
|
|
13071
|
+
DeviceTokenScopeSchema
|
|
13072
|
+
]));
|
|
13073
|
+
object({
|
|
13074
|
+
id: string(),
|
|
13075
|
+
username: string(),
|
|
13076
|
+
passwordHash: string(),
|
|
13077
|
+
/**
|
|
13078
|
+
* Admin bypass. When true, the middleware skips the scope-access
|
|
13079
|
+
* check entirely. There is no other axis of privilege; the legacy
|
|
13080
|
+
* role enum collapsed onto this boolean in v2.
|
|
13081
|
+
*/
|
|
13082
|
+
isAdmin: boolean().default(false),
|
|
13083
|
+
allowedProviders: AllowedProviderSchema,
|
|
13084
|
+
allowedDevices: AllowedDevicesSchema,
|
|
13085
|
+
/**
|
|
13086
|
+
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
13087
|
+
* ignored. Non-admins without scopes are locked out of every
|
|
13088
|
+
* protected call.
|
|
13089
|
+
*/
|
|
13090
|
+
scopes: array(TokenScopeSchema).default([]),
|
|
13091
|
+
createdAt: number(),
|
|
13092
|
+
updatedAt: number()
|
|
13093
|
+
});
|
|
13094
|
+
object({
|
|
13095
|
+
id: string(),
|
|
13096
|
+
label: string(),
|
|
13097
|
+
isAdmin: boolean().default(false),
|
|
13098
|
+
allowedProviders: AllowedProviderSchema,
|
|
13099
|
+
allowedDevices: AllowedDevicesSchema,
|
|
13100
|
+
tokenHash: string(),
|
|
13101
|
+
tokenPrefix: string(),
|
|
13102
|
+
createdAt: number(),
|
|
13103
|
+
lastUsedAt: number().optional()
|
|
13104
|
+
});
|
|
13105
|
+
object({
|
|
13106
|
+
id: string(),
|
|
13107
|
+
userId: string(),
|
|
13108
|
+
name: string(),
|
|
13109
|
+
tokenHash: string(),
|
|
13110
|
+
tokenPrefix: string(),
|
|
13111
|
+
scopes: array(TokenScopeSchema),
|
|
13112
|
+
expiresAt: number().nullish(),
|
|
13113
|
+
lastUsedAt: number().nullish(),
|
|
13114
|
+
createdAt: number()
|
|
13115
|
+
});
|
|
13116
|
+
/** What a share-view grant lets its holder see. */
|
|
13117
|
+
var ShareViewGrantSchema = object({
|
|
13118
|
+
kind: _enum(SHARE_VIEW_KINDS),
|
|
13119
|
+
deviceIds: array(number().int().nonnegative()).min(1).max(64),
|
|
13120
|
+
/** Talk-back, opt-in at mint time; absent means off. */
|
|
13121
|
+
talk: boolean().optional()
|
|
13122
|
+
});
|
|
13123
|
+
var ShareViewScopeSchema = object({
|
|
13124
|
+
type: literal("share-view"),
|
|
13125
|
+
grant: ShareViewGrantSchema
|
|
13126
|
+
});
|
|
13127
|
+
var AddonRoutesScopeSchema = object({
|
|
13128
|
+
type: literal("addon-routes"),
|
|
13129
|
+
access: array(MethodAccessSchema).min(1)
|
|
13130
|
+
});
|
|
13131
|
+
var CredentialScopeSchema = union([
|
|
13132
|
+
literal("*"),
|
|
13133
|
+
ShareViewScopeSchema,
|
|
13134
|
+
AddonRoutesScopeSchema,
|
|
13135
|
+
TokenScopeSchema
|
|
13136
|
+
]);
|
|
13137
|
+
object({
|
|
13138
|
+
kind: literal("credential"),
|
|
13139
|
+
sub: string().min(1),
|
|
13140
|
+
jti: string().min(16),
|
|
13141
|
+
scopes: array(CredentialScopeSchema).min(1)
|
|
13142
|
+
});
|
|
12905
13143
|
_enum(["new-session", "step-up"]);
|
|
12906
13144
|
/** The languages the hub's app landing pages are written in (D858). */
|
|
12907
13145
|
var AppPageLangSchema = _enum(["en", "it"]);
|
|
@@ -13637,7 +13875,14 @@ var AlertSeveritySchema = _enum([
|
|
|
13637
13875
|
]);
|
|
13638
13876
|
var AlertActionSchema = object({
|
|
13639
13877
|
label: string(),
|
|
13640
|
-
href: string().optional()
|
|
13878
|
+
href: string().optional(),
|
|
13879
|
+
/**
|
|
13880
|
+
* How `href` is followed. Absent / `app`: an admin-ui route (client-side).
|
|
13881
|
+
* `server`: a route the HUB serves (an addon HTTP route such as an OAuth
|
|
13882
|
+
* start) — the browser must leave the SPA with a full navigation, because
|
|
13883
|
+
* the SPA has no such route and would bounce to the home page.
|
|
13884
|
+
*/
|
|
13885
|
+
navigation: _enum(["app", "server"]).optional()
|
|
13641
13886
|
});
|
|
13642
13887
|
var AlertStatusSchema = _enum([
|
|
13643
13888
|
"active",
|
|
@@ -42062,6 +42307,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
42062
42307
|
"server-updated",
|
|
42063
42308
|
"export-completed",
|
|
42064
42309
|
"backup-completed",
|
|
42310
|
+
"alert-critical",
|
|
42311
|
+
"alert-error",
|
|
42312
|
+
"alert-critical-resolved",
|
|
42065
42313
|
"camera-online",
|
|
42066
42314
|
"camera-offline",
|
|
42067
42315
|
"camera-disabled",
|
|
@@ -44097,202 +44345,6 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
44097
44345
|
auth: "admin"
|
|
44098
44346
|
}), method(NcAlarmSensorsInputSchema, NcAlarmSensorsSchema, { auth: "admin" });
|
|
44099
44347
|
/**
|
|
44100
|
-
* Zod schemas for persisted record types.
|
|
44101
|
-
*
|
|
44102
|
-
* These schemas serve as the single source of truth for types that are
|
|
44103
|
-
* stored in the settings-store backend and retrieved via tRPC. They
|
|
44104
|
-
* replace the manual TypeScript interfaces with schema-derived types,
|
|
44105
|
-
* enabling runtime validation when reading from the backend.
|
|
44106
|
-
*
|
|
44107
|
-
* Convention: each schema is named `<Type>Schema`, the inferred TS type
|
|
44108
|
-
* keeps the original name for backward compatibility.
|
|
44109
|
-
*/
|
|
44110
|
-
/**
|
|
44111
|
-
* Auth model (v2 — 2026-05-12 caps-only):
|
|
44112
|
-
*
|
|
44113
|
-
* Two principals:
|
|
44114
|
-
* - `isAdmin: true` — full bypass. Admin scope set is ignored.
|
|
44115
|
-
* - `isAdmin: false` — every call gated by `scopes` (see TokenScope).
|
|
44116
|
-
*
|
|
44117
|
-
* No role enum. There used to be `viewer / agent / scoped` and earlier
|
|
44118
|
-
* `super_admin` — collapsed onto the single boolean because every
|
|
44119
|
-
* non-admin caller is governed by the same scope-check rules; the
|
|
44120
|
-
* distinction was only ever used to route role-specific procedures
|
|
44121
|
-
* which we no longer have. CLI / webhook integrations are `cst_*` tokens
|
|
44122
|
-
* with `isAdmin: false` + an explicit scope list.
|
|
44123
|
-
*/
|
|
44124
|
-
/** Per-method access flavour. Materialised on every cap method via codegen. */
|
|
44125
|
-
var MethodAccessSchema = _enum([
|
|
44126
|
-
"view",
|
|
44127
|
-
"create",
|
|
44128
|
-
"delete"
|
|
44129
|
-
]);
|
|
44130
|
-
/**
|
|
44131
|
-
* A single scope grant. Three forms — checked in matcher with OR
|
|
44132
|
-
* semantics; the first match decides whether the call passes:
|
|
44133
|
-
*
|
|
44134
|
-
* - `category` — broadest. `target: 'device' | 'system'` selects every
|
|
44135
|
-
* cap with the matching `scope` declaration. New caps inherit grants
|
|
44136
|
-
* automatically — adding a device cap to the codebase makes it
|
|
44137
|
-
* reachable to every user with `category:device [...]` without a
|
|
44138
|
-
* re-grant. Use this for "family viewer" / "operator" tiers.
|
|
44139
|
-
*
|
|
44140
|
-
* - `addon` — every cap that ships inside a given addon id. Niche;
|
|
44141
|
-
* useful when an integration needs to interact with one specific
|
|
44142
|
-
* addon's surface regardless of how many caps it exports.
|
|
44143
|
-
*
|
|
44144
|
-
* - `capability` — the most precise form. Single cap name. Use for
|
|
44145
|
-
* integration tokens where you want to grant `addons.installPackage`
|
|
44146
|
-
* and nothing else.
|
|
44147
|
-
*
|
|
44148
|
-
* `access` is REQUIRED with min 1 element. `admin` bypass is the
|
|
44149
|
-
* "give me everything" escape; there is no wildcard scope.
|
|
44150
|
-
*/
|
|
44151
|
-
var AllowedProviderSchema = union([literal("*"), array(string())]);
|
|
44152
|
-
var AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
|
|
44153
|
-
var CapScopeSchema = _enum(["device", "system"]);
|
|
44154
|
-
/**
|
|
44155
|
-
* DeviceSelector (scope model v3 — 2026-08-12).
|
|
44156
|
-
*
|
|
44157
|
-
* A `device` grant no longer carries a frozen list of deviceIds. It carries
|
|
44158
|
-
* a SELECTOR the matcher resolves against the live fleet, so the grant can be
|
|
44159
|
-
* DYNAMIC: a `types:['camera']` selector automatically covers a camera added
|
|
44160
|
-
* AFTER the grant was minted — no re-grant, no re-login.
|
|
44161
|
-
*
|
|
44162
|
-
* - `all` — every device in the deployment. The broad viewer/operator
|
|
44163
|
-
* lever without a `category` grant (a `category` grant also covers device
|
|
44164
|
-
* caps that carry no deviceId; `all` is specifically the device set).
|
|
44165
|
-
* - `ids` — an explicit deviceId list. This is what a v2 `device:[…]`
|
|
44166
|
-
* grant migrates to (see {@link TokenScopeSchema}); STATIC — a new camera
|
|
44167
|
-
* is NOT covered until the grant is edited.
|
|
44168
|
-
* - `types` — every device of a `DeviceType` (e.g. every `camera`).
|
|
44169
|
-
* DYNAMIC. A device that changes type, or a new device of the type,
|
|
44170
|
-
* re-resolves on the next request.
|
|
44171
|
-
* - `locations` — every device whose operator-assigned `location` label is
|
|
44172
|
-
* in the set (e.g. "Garden", "Front door"). DYNAMIC. A device with a
|
|
44173
|
-
* null/unset location matches NO `locations` selector.
|
|
44174
|
-
*/
|
|
44175
|
-
var DeviceSelectorSchema = discriminatedUnion("kind", [
|
|
44176
|
-
object({ kind: literal("all") }),
|
|
44177
|
-
object({
|
|
44178
|
-
kind: literal("ids"),
|
|
44179
|
-
ids: array(number().int()).min(1)
|
|
44180
|
-
}),
|
|
44181
|
-
object({
|
|
44182
|
-
kind: literal("types"),
|
|
44183
|
-
types: array(_enum(DeviceType)).min(1)
|
|
44184
|
-
}),
|
|
44185
|
-
object({
|
|
44186
|
-
kind: literal("locations"),
|
|
44187
|
-
locations: array(string().min(1)).min(1)
|
|
44188
|
-
})
|
|
44189
|
-
]);
|
|
44190
|
-
var DeviceTokenScopeSchema = object({
|
|
44191
|
-
type: literal("device"),
|
|
44192
|
-
/** The device SET this grant covers — resolved against the live fleet. */
|
|
44193
|
-
selector: DeviceSelectorSchema,
|
|
44194
|
-
access: array(MethodAccessSchema).min(1),
|
|
44195
|
-
/**
|
|
44196
|
-
* Whether a grant on a PARENT device transparently covers its accessory
|
|
44197
|
-
* CHILDREN (siren / floodlight / PIR) via the persisted-parentage walk.
|
|
44198
|
-
* Direction is parent → children ONLY.
|
|
44199
|
-
*
|
|
44200
|
-
* Absent → the matcher DERIVES it from the access flavour: `view`
|
|
44201
|
-
* inherits (a camera viewer sees the camera's accessories), `create` /
|
|
44202
|
-
* `delete` do NOT (actuating/removing a child is an explicit act the
|
|
44203
|
-
* operator must grant on the child, not inherit from the parent). Set it
|
|
44204
|
-
* explicitly to override that default per grant.
|
|
44205
|
-
*/
|
|
44206
|
-
includeLinked: boolean().optional()
|
|
44207
|
-
});
|
|
44208
|
-
/**
|
|
44209
|
-
* v2 → v3 lazy migration. A pre-v3 `device` grant carried
|
|
44210
|
-
* `targets: string[]` (stringified deviceIds); it rewrites to the equivalent
|
|
44211
|
-
* `selector: {kind:'ids', ids}`. Applied as a `preprocess` so it runs on
|
|
44212
|
-
* EVERY parse path — stored records AND the JWT-carried scope arrays
|
|
44213
|
-
* normalised at the request boundary ({@link normalizeTokenScopes} in
|
|
44214
|
-
* `device-selector.ts`). Chosen over a one-time DB migration because a
|
|
44215
|
-
* migration cannot reach a JWT already in a client's hands; parse-time
|
|
44216
|
-
* migration covers both without a flag day. No cast — the raw object is read
|
|
44217
|
-
* through `Reflect.get` (its static type is `unknown`).
|
|
44218
|
-
*/
|
|
44219
|
-
function migrateLegacyTokenScope(raw) {
|
|
44220
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
44221
|
-
if (Reflect.get(raw, "type") !== "device") return raw;
|
|
44222
|
-
if (Reflect.get(raw, "selector") !== void 0) return raw;
|
|
44223
|
-
const targets = Reflect.get(raw, "targets");
|
|
44224
|
-
if (!Array.isArray(targets)) return raw;
|
|
44225
|
-
return {
|
|
44226
|
-
type: "device",
|
|
44227
|
-
selector: {
|
|
44228
|
-
kind: "ids",
|
|
44229
|
-
ids: targets.map((t) => typeof t === "string" ? Number(t) : t).filter((n) => typeof n === "number" && Number.isInteger(n))
|
|
44230
|
-
},
|
|
44231
|
-
access: Reflect.get(raw, "access")
|
|
44232
|
-
};
|
|
44233
|
-
}
|
|
44234
|
-
var TokenScopeSchema = preprocess(migrateLegacyTokenScope, discriminatedUnion("type", [
|
|
44235
|
-
object({
|
|
44236
|
-
type: literal("category"),
|
|
44237
|
-
target: CapScopeSchema,
|
|
44238
|
-
access: array(MethodAccessSchema).min(1)
|
|
44239
|
-
}),
|
|
44240
|
-
object({
|
|
44241
|
-
type: literal("capability"),
|
|
44242
|
-
target: string(),
|
|
44243
|
-
access: array(MethodAccessSchema).min(1)
|
|
44244
|
-
}),
|
|
44245
|
-
object({
|
|
44246
|
-
type: literal("addon"),
|
|
44247
|
-
target: string(),
|
|
44248
|
-
access: array(MethodAccessSchema).min(1)
|
|
44249
|
-
}),
|
|
44250
|
-
DeviceTokenScopeSchema
|
|
44251
|
-
]));
|
|
44252
|
-
object({
|
|
44253
|
-
id: string(),
|
|
44254
|
-
username: string(),
|
|
44255
|
-
passwordHash: string(),
|
|
44256
|
-
/**
|
|
44257
|
-
* Admin bypass. When true, the middleware skips the scope-access
|
|
44258
|
-
* check entirely. There is no other axis of privilege; the legacy
|
|
44259
|
-
* role enum collapsed onto this boolean in v2.
|
|
44260
|
-
*/
|
|
44261
|
-
isAdmin: boolean().default(false),
|
|
44262
|
-
allowedProviders: AllowedProviderSchema,
|
|
44263
|
-
allowedDevices: AllowedDevicesSchema,
|
|
44264
|
-
/**
|
|
44265
|
-
* Scopes granted to this user. Admins bypass; their `scopes` is
|
|
44266
|
-
* ignored. Non-admins without scopes are locked out of every
|
|
44267
|
-
* protected call.
|
|
44268
|
-
*/
|
|
44269
|
-
scopes: array(TokenScopeSchema).default([]),
|
|
44270
|
-
createdAt: number(),
|
|
44271
|
-
updatedAt: number()
|
|
44272
|
-
});
|
|
44273
|
-
object({
|
|
44274
|
-
id: string(),
|
|
44275
|
-
label: string(),
|
|
44276
|
-
isAdmin: boolean().default(false),
|
|
44277
|
-
allowedProviders: AllowedProviderSchema,
|
|
44278
|
-
allowedDevices: AllowedDevicesSchema,
|
|
44279
|
-
tokenHash: string(),
|
|
44280
|
-
tokenPrefix: string(),
|
|
44281
|
-
createdAt: number(),
|
|
44282
|
-
lastUsedAt: number().optional()
|
|
44283
|
-
});
|
|
44284
|
-
object({
|
|
44285
|
-
id: string(),
|
|
44286
|
-
userId: string(),
|
|
44287
|
-
name: string(),
|
|
44288
|
-
tokenHash: string(),
|
|
44289
|
-
tokenPrefix: string(),
|
|
44290
|
-
scopes: array(TokenScopeSchema),
|
|
44291
|
-
expiresAt: number().nullish(),
|
|
44292
|
-
lastUsedAt: number().nullish(),
|
|
44293
|
-
createdAt: number()
|
|
44294
|
-
});
|
|
44295
|
-
/**
|
|
44296
44348
|
* `oauth-integration` — a collection cap by which an addon declares
|
|
44297
44349
|
* itself a linkable OAuth client of the hub's core `/oauth2/*` server.
|
|
44298
44350
|
* Each provider returns a static descriptor; the core enumerates them
|
|
@@ -51107,7 +51159,10 @@ method(StorageMigrationInputSchema, StorageMigrationPlanSchema, { auth: "admin"
|
|
|
51107
51159
|
}), method(object({ jobId: string().optional() }), StorageMigrationJobSchema.nullable(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
51108
51160
|
kind: "mutation",
|
|
51109
51161
|
auth: "admin"
|
|
51110
|
-
}), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({
|
|
51162
|
+
}), method(object({}), array(StorageMigrationMoverSchema).readonly(), { auth: "admin" }), method(object({ locationIds: array(string()).readonly() }), object({
|
|
51163
|
+
clear: boolean(),
|
|
51164
|
+
repointLocationId: string().optional()
|
|
51165
|
+
}), { auth: "admin" }), method(object({}), array(StorageMigrationResidueSchema).readonly(), { auth: "admin" }), method(StorageMigrationDrainInputSchema, object({ jobId: string() }), {
|
|
51111
51166
|
kind: "mutation",
|
|
51112
51167
|
auth: "admin"
|
|
51113
51168
|
}), method(StorageCleanupInputSchema, object({ jobId: string() }), {
|
|
@@ -54552,6 +54607,11 @@ object({
|
|
|
54552
54607
|
*
|
|
54553
54608
|
* `kind: 'poll'` — cloud REST APIs typically have no push channel; the
|
|
54554
54609
|
* provider refreshes the slice on an interval and eagerly after commands.
|
|
54610
|
+
*
|
|
54611
|
+
* UNKNOWN IS NOT OFF. Every field the appliance reports separately is
|
|
54612
|
+
* nullable: `null` means "not known right now" (the appliance has not said,
|
|
54613
|
+
* the link is down, or the description does not carry it) and is never
|
|
54614
|
+
* rendered or exported as `false` / `0`. `availability` says why.
|
|
54555
54615
|
*/
|
|
54556
54616
|
var DishwasherStateSchema = _enum([
|
|
54557
54617
|
"idle",
|
|
@@ -54561,112 +54621,227 @@ var DishwasherStateSchema = _enum([
|
|
|
54561
54621
|
"error",
|
|
54562
54622
|
"unavailable"
|
|
54563
54623
|
]);
|
|
54624
|
+
/**
|
|
54625
|
+
* The appliance's own operation state, uncollapsed. `state` above is the coarse
|
|
54626
|
+
* view for compact surfaces; this is what the appliance said.
|
|
54627
|
+
*/
|
|
54628
|
+
var DishwasherOperationStateSchema = _enum([
|
|
54629
|
+
"Inactive",
|
|
54630
|
+
"Ready",
|
|
54631
|
+
"DelayedStart",
|
|
54632
|
+
"Run",
|
|
54633
|
+
"Pause",
|
|
54634
|
+
"ActionRequired",
|
|
54635
|
+
"Finished",
|
|
54636
|
+
"Error",
|
|
54637
|
+
"Aborting"
|
|
54638
|
+
]);
|
|
54639
|
+
/**
|
|
54640
|
+
* Why fields may be `null`:
|
|
54641
|
+
* - `available` — the link is up and the description is loaded;
|
|
54642
|
+
* - `offline` — the local link is down (values are unknown, not last-known);
|
|
54643
|
+
* - `description-missing` — the link is up but the appliance's description could not
|
|
54644
|
+
* be loaded, so its values cannot be decoded.
|
|
54645
|
+
*/
|
|
54646
|
+
var DishwasherAvailabilitySchema = _enum([
|
|
54647
|
+
"available",
|
|
54648
|
+
"offline",
|
|
54649
|
+
"description-missing"
|
|
54650
|
+
]);
|
|
54564
54651
|
var DishwasherConsumableLevelSchema = _enum([
|
|
54565
54652
|
"ok",
|
|
54566
54653
|
"low",
|
|
54567
54654
|
"empty"
|
|
54568
54655
|
]);
|
|
54569
54656
|
object({
|
|
54657
|
+
availability: DishwasherAvailabilitySchema,
|
|
54658
|
+
/** Coarse state for compact surfaces; `unavailable` whenever the appliance state is not known. */
|
|
54570
54659
|
state: DishwasherStateSchema,
|
|
54571
|
-
/**
|
|
54660
|
+
/** The appliance's operation state as reported; null when not known. */
|
|
54661
|
+
operationState: DishwasherOperationStateSchema.nullable(),
|
|
54662
|
+
/** Feature key of the ACTIVE (running) program, or null when none runs / not known. */
|
|
54572
54663
|
program: string().nullable(),
|
|
54573
|
-
/**
|
|
54664
|
+
/** Feature key of the SELECTED program (what Start would run), or null when none / not known. */
|
|
54665
|
+
selectedProgram: string().nullable(),
|
|
54666
|
+
/** Feature keys of the programs the appliance currently offers — drives the UI selector. */
|
|
54574
54667
|
availablePrograms: array(string()),
|
|
54575
|
-
/**
|
|
54576
|
-
|
|
54668
|
+
/** Seconds remaining on the active cycle, or null when not running / not known. */
|
|
54669
|
+
remainingSeconds: number().int().nonnegative().nullable(),
|
|
54670
|
+
/** Seconds the delayed start is set to (time until the cycle starts), or null. */
|
|
54671
|
+
delayedStartSeconds: number().int().nonnegative().nullable(),
|
|
54577
54672
|
/** Cycle completion 0..100, or null when not applicable. */
|
|
54578
54673
|
progressPercent: number().min(0).max(100).nullable(),
|
|
54579
|
-
/** True when the door is reported open. */
|
|
54580
|
-
doorOpen: boolean(),
|
|
54581
|
-
/** True when
|
|
54582
|
-
|
|
54583
|
-
/**
|
|
54584
|
-
|
|
54674
|
+
/** True when the door is reported open; null when the door state is not known. */
|
|
54675
|
+
doorOpen: boolean().nullable(),
|
|
54676
|
+
/** True when the door is latched (locked) during a cycle; null when not reported. */
|
|
54677
|
+
doorLocked: boolean().nullable(),
|
|
54678
|
+
/** True when remote start is allowed (door closed, power on, etc.); null when not known. */
|
|
54679
|
+
remoteStartAllowed: boolean().nullable(),
|
|
54680
|
+
/** True when the appliance accepts remote control now; null when not known. */
|
|
54681
|
+
remoteControlActive: boolean().nullable(),
|
|
54682
|
+
/** True while somebody operates the appliance at its panel; null when not reported. */
|
|
54683
|
+
localControlActive: boolean().nullable(),
|
|
54684
|
+
/** Main power / switched-on state; null when not known. */
|
|
54685
|
+
powerOn: boolean().nullable(),
|
|
54686
|
+
/** The raw power state (`On`, `Off`, `Standby`, …); null when not known. */
|
|
54687
|
+
powerState: string().nullable(),
|
|
54585
54688
|
saltLevel: DishwasherConsumableLevelSchema.nullable(),
|
|
54586
54689
|
rinseAidLevel: DishwasherConsumableLevelSchema.nullable(),
|
|
54587
|
-
|
|
54588
|
-
|
|
54690
|
+
/** Option states: null when the appliance does not expose the option or has not said. */
|
|
54691
|
+
halfLoad: boolean().nullable(),
|
|
54692
|
+
hygienePlus: boolean().nullable(),
|
|
54589
54693
|
/** Extra dry option when the appliance exposes it. */
|
|
54590
|
-
extraDry: boolean(),
|
|
54694
|
+
extraDry: boolean().nullable(),
|
|
54591
54695
|
/** Intensive zone (lower basket) when the appliance exposes it. */
|
|
54592
|
-
intensiveZone: boolean(),
|
|
54696
|
+
intensiveZone: boolean().nullable(),
|
|
54593
54697
|
/** VarioSpeed / VarioSpeed+ when the appliance exposes it. */
|
|
54594
|
-
varioSpeedPlus: boolean(),
|
|
54595
|
-
/** Child lock (controls disabled on the appliance). */
|
|
54596
|
-
childLock: boolean(),
|
|
54698
|
+
varioSpeedPlus: boolean().nullable(),
|
|
54699
|
+
/** Child lock (controls disabled on the appliance); null when not known. */
|
|
54700
|
+
childLock: boolean().nullable(),
|
|
54701
|
+
/** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
|
|
54702
|
+
activeEvents: array(string()),
|
|
54703
|
+
/** Numeric error code when the appliance reports one; null otherwise. */
|
|
54597
54704
|
errorCode: number().nullable(),
|
|
54705
|
+
/** Name of the raised alert-level event while the appliance is in error; null otherwise. */
|
|
54598
54706
|
errorLabel: string().nullable(),
|
|
54707
|
+
/** Ms epoch of the last value received from the appliance; 0 = never. */
|
|
54599
54708
|
lastFetchedAt: number()
|
|
54600
54709
|
});
|
|
54601
|
-
|
|
54710
|
+
var DeviceIdSchema = number().int().nonnegative();
|
|
54711
|
+
/** A value a program option or setting can take. */
|
|
54712
|
+
var DishwasherValueSchema = union([
|
|
54713
|
+
boolean(),
|
|
54714
|
+
string(),
|
|
54715
|
+
number()
|
|
54716
|
+
]);
|
|
54717
|
+
var DishwasherAccessSchema = _enum([
|
|
54718
|
+
"none",
|
|
54719
|
+
"read",
|
|
54720
|
+
"readWrite",
|
|
54721
|
+
"writeOnly",
|
|
54722
|
+
"readStatic"
|
|
54723
|
+
]);
|
|
54724
|
+
/**
|
|
54725
|
+
* One option or setting the appliance declares, with the constraints its description
|
|
54726
|
+
* gives. A `readWrite` descriptor that is `available` is what `setOption` accepts.
|
|
54727
|
+
*/
|
|
54728
|
+
var DishwasherOptionDescriptorSchema = object({
|
|
54729
|
+
/** Feature key (`Dishcare.Dishwasher.Option.ExtraDry`). */
|
|
54730
|
+
key: string(),
|
|
54731
|
+
/** Last segment of the key, for display. */
|
|
54732
|
+
name: string(),
|
|
54733
|
+
type: _enum([
|
|
54734
|
+
"boolean",
|
|
54735
|
+
"number",
|
|
54736
|
+
"enum",
|
|
54737
|
+
"string"
|
|
54738
|
+
]),
|
|
54739
|
+
access: DishwasherAccessSchema,
|
|
54740
|
+
/** The appliance reports this entity as currently available. */
|
|
54741
|
+
available: boolean(),
|
|
54742
|
+
/** Unit the description declares for a number; null when it declares none. */
|
|
54743
|
+
unit: string().nullable(),
|
|
54744
|
+
min: number().nullable(),
|
|
54745
|
+
max: number().nullable(),
|
|
54746
|
+
step: number().nullable(),
|
|
54747
|
+
/** Allowed members of an enum; null for other types. */
|
|
54748
|
+
enumValues: array(string()).nullable(),
|
|
54749
|
+
defaultValue: DishwasherValueSchema.nullable()
|
|
54750
|
+
});
|
|
54751
|
+
var DishwasherProgramSchema = object({
|
|
54752
|
+
/** Feature key (`Dishcare.Dishwasher.Program.Eco50`) — what `setProgram` / `start` take. */
|
|
54753
|
+
key: string(),
|
|
54754
|
+
name: string(),
|
|
54755
|
+
/** The appliance currently offers this program. */
|
|
54756
|
+
available: boolean(),
|
|
54757
|
+
/** The options this program lists, with the program-level access / availability / default. */
|
|
54758
|
+
options: array(DishwasherOptionDescriptorSchema)
|
|
54759
|
+
});
|
|
54760
|
+
var DishwasherOptionsSchema = object({
|
|
54761
|
+
availability: DishwasherAvailabilitySchema,
|
|
54762
|
+
/** What the appliance can be told to do, from the commands its description declares. */
|
|
54763
|
+
supportsStart: boolean(),
|
|
54764
|
+
supportsPause: boolean(),
|
|
54765
|
+
supportsResume: boolean(),
|
|
54766
|
+
supportsStop: boolean(),
|
|
54767
|
+
supportsPower: boolean(),
|
|
54768
|
+
/** Every program the description lists, with its options. */
|
|
54769
|
+
programs: array(DishwasherProgramSchema),
|
|
54770
|
+
/** Every option and setting entity the description lists (programs reference these by key). */
|
|
54771
|
+
options: array(DishwasherOptionDescriptorSchema)
|
|
54772
|
+
});
|
|
54773
|
+
DeviceType.Dishwasher, method(object({
|
|
54774
|
+
deviceId: DeviceIdSchema,
|
|
54775
|
+
program: string().min(1).optional(),
|
|
54776
|
+
options: record(string(), DishwasherValueSchema).optional()
|
|
54777
|
+
}), _void(), {
|
|
54602
54778
|
kind: "mutation",
|
|
54603
54779
|
auth: "protected"
|
|
54604
|
-
}), method(object({ deviceId:
|
|
54780
|
+
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
54605
54781
|
kind: "mutation",
|
|
54606
54782
|
auth: "protected"
|
|
54607
|
-
}), method(object({ deviceId:
|
|
54783
|
+
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
54784
|
+
kind: "mutation",
|
|
54785
|
+
auth: "protected"
|
|
54786
|
+
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
54608
54787
|
kind: "mutation",
|
|
54609
54788
|
auth: "protected"
|
|
54610
54789
|
}), method(object({
|
|
54611
|
-
deviceId:
|
|
54790
|
+
deviceId: DeviceIdSchema,
|
|
54612
54791
|
program: string().min(1)
|
|
54613
54792
|
}), _void(), {
|
|
54614
54793
|
kind: "mutation",
|
|
54615
54794
|
auth: "protected"
|
|
54616
54795
|
}), method(object({
|
|
54617
|
-
deviceId:
|
|
54796
|
+
deviceId: DeviceIdSchema,
|
|
54618
54797
|
on: boolean()
|
|
54619
54798
|
}), _void(), {
|
|
54620
54799
|
kind: "mutation",
|
|
54621
54800
|
auth: "protected"
|
|
54622
54801
|
}), method(object({
|
|
54623
|
-
deviceId:
|
|
54802
|
+
deviceId: DeviceIdSchema,
|
|
54624
54803
|
on: boolean()
|
|
54625
54804
|
}), _void(), {
|
|
54626
54805
|
kind: "mutation",
|
|
54627
54806
|
auth: "protected"
|
|
54628
54807
|
}), method(object({
|
|
54629
|
-
deviceId:
|
|
54808
|
+
deviceId: DeviceIdSchema,
|
|
54630
54809
|
on: boolean()
|
|
54631
54810
|
}), _void(), {
|
|
54632
54811
|
kind: "mutation",
|
|
54633
54812
|
auth: "protected"
|
|
54634
54813
|
}), method(object({
|
|
54635
|
-
deviceId:
|
|
54814
|
+
deviceId: DeviceIdSchema,
|
|
54636
54815
|
on: boolean()
|
|
54637
54816
|
}), _void(), {
|
|
54638
54817
|
kind: "mutation",
|
|
54639
54818
|
auth: "protected"
|
|
54640
54819
|
}), method(object({
|
|
54641
|
-
deviceId:
|
|
54820
|
+
deviceId: DeviceIdSchema,
|
|
54642
54821
|
on: boolean()
|
|
54643
54822
|
}), _void(), {
|
|
54644
54823
|
kind: "mutation",
|
|
54645
54824
|
auth: "protected"
|
|
54646
54825
|
}), method(object({
|
|
54647
|
-
deviceId:
|
|
54826
|
+
deviceId: DeviceIdSchema,
|
|
54648
54827
|
on: boolean()
|
|
54649
54828
|
}), _void(), {
|
|
54650
54829
|
kind: "mutation",
|
|
54651
54830
|
auth: "protected"
|
|
54652
54831
|
}), method(object({
|
|
54653
|
-
deviceId:
|
|
54832
|
+
deviceId: DeviceIdSchema,
|
|
54654
54833
|
on: boolean()
|
|
54655
54834
|
}), _void(), {
|
|
54656
54835
|
kind: "mutation",
|
|
54657
54836
|
auth: "protected"
|
|
54658
54837
|
}), method(object({
|
|
54659
|
-
deviceId:
|
|
54838
|
+
deviceId: DeviceIdSchema,
|
|
54660
54839
|
key: string().min(1),
|
|
54661
|
-
value:
|
|
54662
|
-
boolean(),
|
|
54663
|
-
string(),
|
|
54664
|
-
number()
|
|
54665
|
-
])
|
|
54840
|
+
value: DishwasherValueSchema
|
|
54666
54841
|
}), _void(), {
|
|
54667
54842
|
kind: "mutation",
|
|
54668
54843
|
auth: "protected"
|
|
54669
|
-
});
|
|
54844
|
+
}), method(object({ deviceId: DeviceIdSchema }), DishwasherOptionsSchema, { auth: "protected" });
|
|
54670
54845
|
object({
|
|
54671
54846
|
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
54672
54847
|
lastPressedAt: number().nullable(),
|
|
@@ -64726,6 +64901,12 @@ Object.freeze({
|
|
|
64726
64901
|
addonId: null,
|
|
64727
64902
|
access: "view"
|
|
64728
64903
|
},
|
|
64904
|
+
"dishwasher.getOptions": {
|
|
64905
|
+
capName: "dishwasher",
|
|
64906
|
+
capScope: "device",
|
|
64907
|
+
addonId: null,
|
|
64908
|
+
access: "view"
|
|
64909
|
+
},
|
|
64729
64910
|
"dishwasher.getStatus": {
|
|
64730
64911
|
capName: "dishwasher",
|
|
64731
64912
|
capScope: "device",
|
|
@@ -64738,6 +64919,12 @@ Object.freeze({
|
|
|
64738
64919
|
addonId: null,
|
|
64739
64920
|
access: "create"
|
|
64740
64921
|
},
|
|
64922
|
+
"dishwasher.resume": {
|
|
64923
|
+
capName: "dishwasher",
|
|
64924
|
+
capScope: "device",
|
|
64925
|
+
addonId: null,
|
|
64926
|
+
access: "create"
|
|
64927
|
+
},
|
|
64741
64928
|
"dishwasher.setChildLock": {
|
|
64742
64929
|
capName: "dishwasher",
|
|
64743
64930
|
capScope: "device",
|
|
@@ -69238,6 +69425,12 @@ Object.freeze({
|
|
|
69238
69425
|
addonId: null,
|
|
69239
69426
|
access: "view"
|
|
69240
69427
|
},
|
|
69428
|
+
"storageMigration.moverClearance": {
|
|
69429
|
+
capName: "storage-migration",
|
|
69430
|
+
capScope: "system",
|
|
69431
|
+
addonId: null,
|
|
69432
|
+
access: "view"
|
|
69433
|
+
},
|
|
69241
69434
|
"storageMigration.movers": {
|
|
69242
69435
|
capName: "storage-migration",
|
|
69243
69436
|
capScope: "system",
|
|
@@ -71431,6 +71624,11 @@ Object.freeze({
|
|
|
71431
71624
|
form: "single",
|
|
71432
71625
|
optional: false
|
|
71433
71626
|
}],
|
|
71627
|
+
"dishwasher.getOptions": [{
|
|
71628
|
+
name: "deviceId",
|
|
71629
|
+
form: "single",
|
|
71630
|
+
optional: false
|
|
71631
|
+
}],
|
|
71434
71632
|
"dishwasher.getStatus": [{
|
|
71435
71633
|
name: "deviceId",
|
|
71436
71634
|
form: "single",
|
|
@@ -71441,6 +71639,11 @@ Object.freeze({
|
|
|
71441
71639
|
form: "single",
|
|
71442
71640
|
optional: false
|
|
71443
71641
|
}],
|
|
71642
|
+
"dishwasher.resume": [{
|
|
71643
|
+
name: "deviceId",
|
|
71644
|
+
form: "single",
|
|
71645
|
+
optional: false
|
|
71646
|
+
}],
|
|
71444
71647
|
"dishwasher.setChildLock": [{
|
|
71445
71648
|
name: "deviceId",
|
|
71446
71649
|
form: "single",
|
|
@@ -73527,6 +73730,9 @@ new Map(Object.entries({
|
|
|
73527
73730
|
"wrapper-update-available": "system",
|
|
73528
73731
|
"backup-completed": "system",
|
|
73529
73732
|
"export-completed": "system",
|
|
73733
|
+
"alert-critical": "system",
|
|
73734
|
+
"alert-error": "system",
|
|
73735
|
+
"alert-critical-resolved": "system",
|
|
73530
73736
|
"storage-location-low": "system",
|
|
73531
73737
|
"storage-location-critical": "system",
|
|
73532
73738
|
"storage-location-ok": "system",
|
|
@@ -73651,6 +73857,16 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
73651
73857
|
"storage-location-ok"
|
|
73652
73858
|
]
|
|
73653
73859
|
},
|
|
73860
|
+
{
|
|
73861
|
+
id: "alerts",
|
|
73862
|
+
family: "system",
|
|
73863
|
+
label: "Alerts",
|
|
73864
|
+
kinds: [
|
|
73865
|
+
"alert-critical",
|
|
73866
|
+
"alert-error",
|
|
73867
|
+
"alert-critical-resolved"
|
|
73868
|
+
]
|
|
73869
|
+
},
|
|
73654
73870
|
{
|
|
73655
73871
|
id: "alarm",
|
|
73656
73872
|
family: "alarm",
|
|
@@ -73695,6 +73911,21 @@ var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.ent
|
|
|
73695
73911
|
return [[descriptor.kind, field]];
|
|
73696
73912
|
})));
|
|
73697
73913
|
Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
|
|
73914
|
+
object({
|
|
73915
|
+
phase: _enum(["raised", "resolved"]),
|
|
73916
|
+
episodeId: string().min(1),
|
|
73917
|
+
alertId: string().min(1),
|
|
73918
|
+
severity: AlertSeveritySchema,
|
|
73919
|
+
category: string(),
|
|
73920
|
+
title: string(),
|
|
73921
|
+
message: string(),
|
|
73922
|
+
sourceType: string().optional(),
|
|
73923
|
+
sourceId: string().optional(),
|
|
73924
|
+
nodeId: string().optional(),
|
|
73925
|
+
/** The alert's own action link (where the operator acts on it), when it has one. */
|
|
73926
|
+
actionUrl: string().optional(),
|
|
73927
|
+
at: number()
|
|
73928
|
+
});
|
|
73698
73929
|
var RULE_BODY_ONLY = ["body"];
|
|
73699
73930
|
var RULE_TEXT = ["title", "body"];
|
|
73700
73931
|
var BATTERY = ["device-battery-low", "device-battery-normal"];
|
|
@@ -73704,6 +73935,11 @@ var TANK = [
|
|
|
73704
73935
|
"device-tank-full",
|
|
73705
73936
|
"device-tank-normal"
|
|
73706
73937
|
];
|
|
73938
|
+
var ALERT_EPISODE = [
|
|
73939
|
+
"alert-critical",
|
|
73940
|
+
"alert-error",
|
|
73941
|
+
"alert-critical-resolved"
|
|
73942
|
+
];
|
|
73707
73943
|
var UPDATES = [
|
|
73708
73944
|
"addon-update-available",
|
|
73709
73945
|
"server-update-available",
|
|
@@ -73896,14 +74132,14 @@ var STATE_SUMMARY_VARS = [
|
|
|
73896
74132
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
73897
74133
|
([...RULE_TEXT], [...TANK]),
|
|
73898
74134
|
[...RULE_TEXT],
|
|
73899
|
-
([...RULE_TEXT], [...UPDATES]),
|
|
73900
|
-
([...RULE_TEXT], [...UPDATES]),
|
|
73901
|
-
[...RULE_TEXT],
|
|
74135
|
+
([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
|
|
74136
|
+
([...RULE_TEXT], [...UPDATES, ...ALERT_EPISODE]),
|
|
73902
74137
|
[...RULE_TEXT],
|
|
73903
74138
|
[...RULE_TEXT],
|
|
73904
74139
|
[...RULE_TEXT],
|
|
73905
74140
|
[...RULE_TEXT],
|
|
73906
74141
|
[...RULE_TEXT],
|
|
74142
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
73907
74143
|
[...RULE_TEXT],
|
|
73908
74144
|
[...RULE_TEXT],
|
|
73909
74145
|
[...RULE_TEXT],
|
|
@@ -73921,7 +74157,11 @@ var STATE_SUMMARY_VARS = [
|
|
|
73921
74157
|
[...RULE_TEXT],
|
|
73922
74158
|
[...RULE_TEXT],
|
|
73923
74159
|
[...RULE_TEXT],
|
|
73924
|
-
[...RULE_TEXT],
|
|
74160
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74161
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74162
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74163
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
74164
|
+
([...RULE_TEXT], [...ALERT_EPISODE]),
|
|
73925
74165
|
[...RULE_TEXT],
|
|
73926
74166
|
[...RULE_TEXT],
|
|
73927
74167
|
[...RULE_TEXT],
|
|
@@ -74459,6 +74699,37 @@ DEFAULT_NATIVE_LEASE_SETTINGS.activityMs;
|
|
|
74459
74699
|
DEFAULT_NATIVE_LEASE_SETTINGS.tileBudgetMb;
|
|
74460
74700
|
DEFAULT_NATIVE_LEASE_SETTINGS.sceneBudgetMb;
|
|
74461
74701
|
DEFAULT_NATIVE_LEASE_SETTINGS.admission;
|
|
74702
|
+
/**
|
|
74703
|
+
* Where an OAuth round-trip ends: the admin-ui completion route
|
|
74704
|
+
* (`/sso-complete`, the page every popup sign-in already lands on), with the
|
|
74705
|
+
* OUTCOME in the URL fragment.
|
|
74706
|
+
*
|
|
74707
|
+
* The outcome is a status plus a reason CODE from a closed list. It never
|
|
74708
|
+
* carries a token, a code, a client secret or a free-form error message: the
|
|
74709
|
+
* page words each code itself, and an unknown code reads as `unknown` rather
|
|
74710
|
+
* than being echoed. Fragment only, so nothing here reaches a server log.
|
|
74711
|
+
*
|
|
74712
|
+
* Pure and browser-safe.
|
|
74713
|
+
*/
|
|
74714
|
+
/** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
|
|
74715
|
+
var OAUTH_LANDING_PATH = "/sso-complete";
|
|
74716
|
+
/**
|
|
74717
|
+
* The landing URL for `outcome`. `origin` is the hub address the flow already
|
|
74718
|
+
* runs on (from the `local-network` palette role, never a literal); `null`
|
|
74719
|
+
* gives a hub-relative URL.
|
|
74720
|
+
*/
|
|
74721
|
+
function oauthLandingUrl(origin, outcome) {
|
|
74722
|
+
const params = new URLSearchParams({
|
|
74723
|
+
return: "oauth",
|
|
74724
|
+
status: outcome.status
|
|
74725
|
+
});
|
|
74726
|
+
if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
|
|
74727
|
+
if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
|
|
74728
|
+
if (outcome.integration !== void 0) params.set("integration", outcome.integration);
|
|
74729
|
+
if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
|
|
74730
|
+
if (outcome.action === "disconnect") params.set("action", "disconnect");
|
|
74731
|
+
return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
|
|
74732
|
+
}
|
|
74462
74733
|
var MB = 1024 * 1024;
|
|
74463
74734
|
1024 * MB, 3072 * MB;
|
|
74464
74735
|
//#endregion
|
|
@@ -74506,13 +74777,22 @@ var DEFAULT_OAUTH_STATE = {
|
|
|
74506
74777
|
accessExpiresAt: 0,
|
|
74507
74778
|
accountEmail: ""
|
|
74508
74779
|
};
|
|
74509
|
-
|
|
74780
|
+
/** Per-broker OAuth tokens (durable state `oauthAccounts`). */
|
|
74781
|
+
var GoogleOauthAccountSchema = object({
|
|
74510
74782
|
id: string().min(1),
|
|
74511
74783
|
refreshToken: string().default("").meta({ secret: true }),
|
|
74512
74784
|
accessToken: string().default("").meta({ secret: true }),
|
|
74513
74785
|
accessExpiresAt: number().default(0),
|
|
74514
|
-
accountEmail: string().default("")
|
|
74515
|
-
|
|
74786
|
+
accountEmail: string().default(""),
|
|
74787
|
+
/**
|
|
74788
|
+
* Google refused the refresh token (`invalid_grant`): it is dead and only a
|
|
74789
|
+
* new sign-in replaces it. Set where the refusal is seen, cleared by the
|
|
74790
|
+
* OAuth callback that stores a fresh token — so the status, the Sign-in
|
|
74791
|
+
* button and the alert all read the same fact.
|
|
74792
|
+
*/
|
|
74793
|
+
needsReauth: boolean().default(false)
|
|
74794
|
+
});
|
|
74795
|
+
var GoogleOauthAccountsStateSchema = object({ accounts: array(GoogleOauthAccountSchema).default([]) });
|
|
74516
74796
|
var GOOGLE_BROKER_KIND = "google";
|
|
74517
74797
|
var GOOGLE_BROKER_KIND_LABEL = "Google";
|
|
74518
74798
|
var GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
|
|
@@ -74601,7 +74881,8 @@ function migrateLegacyToBrokers(rawConfig, legacyOauth) {
|
|
|
74601
74881
|
refreshToken: legacyOauth.refreshToken,
|
|
74602
74882
|
accessToken: legacyOauth.accessToken,
|
|
74603
74883
|
accessExpiresAt: legacyOauth.accessExpiresAt,
|
|
74604
|
-
accountEmail: legacyOauth.accountEmail
|
|
74884
|
+
accountEmail: legacyOauth.accountEmail,
|
|
74885
|
+
needsReauth: false
|
|
74605
74886
|
}] : [];
|
|
74606
74887
|
return {
|
|
74607
74888
|
brokers: [entry],
|
|
@@ -74636,7 +74917,10 @@ var GoogleBrokerRegistry = class {
|
|
|
74636
74917
|
oauthById = /* @__PURE__ */ new Map();
|
|
74637
74918
|
seedFrom(entries, accounts) {
|
|
74638
74919
|
this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
|
|
74639
|
-
this.oauthById = new Map(accounts.map((a) =>
|
|
74920
|
+
this.oauthById = new Map(accounts.map((a) => {
|
|
74921
|
+
const account = GoogleOauthAccountSchema.parse(a);
|
|
74922
|
+
return [account.id, account];
|
|
74923
|
+
}));
|
|
74640
74924
|
this.seedCounterFromEntries(this.entries);
|
|
74641
74925
|
}
|
|
74642
74926
|
listEntries() {
|
|
@@ -74652,14 +74936,28 @@ var GoogleBrokerRegistry = class {
|
|
|
74652
74936
|
return [...this.oauthById.values()];
|
|
74653
74937
|
}
|
|
74654
74938
|
setOauth(account) {
|
|
74655
|
-
|
|
74939
|
+
const parsed = GoogleOauthAccountSchema.parse(account);
|
|
74940
|
+
this.oauthById.set(parsed.id, parsed);
|
|
74656
74941
|
}
|
|
74657
74942
|
removeOauth(id) {
|
|
74658
74943
|
this.oauthById.delete(id);
|
|
74659
74944
|
}
|
|
74660
74945
|
isConnected(id) {
|
|
74661
74946
|
const o = this.oauthById.get(id);
|
|
74662
|
-
return o !== void 0 && o.refreshToken.length > 0;
|
|
74947
|
+
return o !== void 0 && o.refreshToken.length > 0 && !o.needsReauth;
|
|
74948
|
+
}
|
|
74949
|
+
/** A refresh token Google refused: kept (a sign-in replaces it) but not usable. */
|
|
74950
|
+
needsReauth(id) {
|
|
74951
|
+
return this.oauthById.get(id)?.needsReauth === true;
|
|
74952
|
+
}
|
|
74953
|
+
markNeedsReauth(id) {
|
|
74954
|
+
const o = this.oauthById.get(id);
|
|
74955
|
+
if (o === void 0 || o.needsReauth) return false;
|
|
74956
|
+
this.oauthById.set(id, {
|
|
74957
|
+
...o,
|
|
74958
|
+
needsReauth: true
|
|
74959
|
+
});
|
|
74960
|
+
return true;
|
|
74663
74961
|
}
|
|
74664
74962
|
allocateId() {
|
|
74665
74963
|
const id = `google_${String(this.nextIdCounter).padStart(3, "0")}`;
|
|
@@ -74709,18 +75007,19 @@ var GoogleBrokerRegistry = class {
|
|
|
74709
75007
|
toBrokerInfo(entry, addonId) {
|
|
74710
75008
|
const oauth = this.oauthById.get(entry.id);
|
|
74711
75009
|
const connected = this.isConnected(entry.id);
|
|
75010
|
+
const reauth = this.needsReauth(entry.id);
|
|
74712
75011
|
return {
|
|
74713
75012
|
id: entry.id,
|
|
74714
75013
|
addonId,
|
|
74715
75014
|
name: entry.name,
|
|
74716
75015
|
kind: GOOGLE_BROKER_KIND,
|
|
74717
|
-
status: connected ? "connected" : "disconnected",
|
|
75016
|
+
status: connected ? "connected" : reauth ? "auth-failed" : "disconnected",
|
|
74718
75017
|
info: {
|
|
74719
75018
|
accountEmail: oauth?.accountEmail ?? "",
|
|
74720
75019
|
hasClient: entry.clientId.length > 0
|
|
74721
75020
|
},
|
|
74722
75021
|
lastCheckedAt: connected ? Date.now() : null,
|
|
74723
|
-
error: connected ? null : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
|
|
75022
|
+
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"
|
|
74724
75023
|
};
|
|
74725
75024
|
}
|
|
74726
75025
|
listBrokerInfos(addonId, kind) {
|
|
@@ -74772,7 +75071,7 @@ function consoleLinksHtml$1() {
|
|
|
74772
75071
|
].join("");
|
|
74773
75072
|
}
|
|
74774
75073
|
function introHtml(cap, publicOrigin) {
|
|
74775
|
-
const redirect = escapeHtml$
|
|
75074
|
+
const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
|
|
74776
75075
|
const console = consoleLinksHtml$1();
|
|
74777
75076
|
if (cap === "smtp-provider") return [
|
|
74778
75077
|
"<ol>",
|
|
@@ -74805,7 +75104,7 @@ function introHtml(cap, publicOrigin) {
|
|
|
74805
75104
|
].join("");
|
|
74806
75105
|
}
|
|
74807
75106
|
function connectHtml(cap, publicOrigin) {
|
|
74808
|
-
const redirect = escapeHtml$
|
|
75107
|
+
const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
|
|
74809
75108
|
if (cap === "smtp-provider") return [
|
|
74810
75109
|
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
|
|
74811
75110
|
"Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
|
|
@@ -74825,7 +75124,7 @@ function connectHtml(cap, publicOrigin) {
|
|
|
74825
75124
|
`Console: ${consoleLinksHtml$1()}.`
|
|
74826
75125
|
].join("");
|
|
74827
75126
|
}
|
|
74828
|
-
function escapeHtml$
|
|
75127
|
+
function escapeHtml$1(s) {
|
|
74829
75128
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
74830
75129
|
}
|
|
74831
75130
|
//#endregion
|
|
@@ -74844,7 +75143,7 @@ function consoleLinksHtml() {
|
|
|
74844
75143
|
}
|
|
74845
75144
|
/** Setup steps shown on Add / Edit — includes Audience / Test users. */
|
|
74846
75145
|
function googleBrokerSetupHtml(publicOrigin) {
|
|
74847
|
-
const redirect = escapeHtml
|
|
75146
|
+
const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
|
|
74848
75147
|
return [
|
|
74849
75148
|
"<ol>",
|
|
74850
75149
|
`<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>`,
|
|
@@ -74920,7 +75219,7 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
|
|
|
74920
75219
|
}
|
|
74921
75220
|
/** Connectivity Calendars / Email pages — calendar import picker lives here (not on broker Add). */
|
|
74922
75221
|
function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
74923
|
-
const redirect = escapeHtml
|
|
75222
|
+
const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
|
|
74924
75223
|
if (cap === "calendar-source") return { sections: [{
|
|
74925
75224
|
id: "setup-intro",
|
|
74926
75225
|
title: "Google brokers",
|
|
@@ -74978,7 +75277,7 @@ function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
|
74978
75277
|
}]
|
|
74979
75278
|
}] };
|
|
74980
75279
|
}
|
|
74981
|
-
function escapeHtml
|
|
75280
|
+
function escapeHtml(s) {
|
|
74982
75281
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
74983
75282
|
}
|
|
74984
75283
|
//#endregion
|
|
@@ -75273,7 +75572,6 @@ function isInvalidGrantRefreshError(err) {
|
|
|
75273
75572
|
return (err instanceof Error ? err.message : String(err)).includes("invalid_grant");
|
|
75274
75573
|
}
|
|
75275
75574
|
function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
|
|
75276
|
-
const pushLine = `${brokerName}: Google session expired — sign in again`;
|
|
75277
75575
|
return {
|
|
75278
75576
|
id: googleBrokerReconnectAlertId(brokerId),
|
|
75279
75577
|
category: "google.broker-reconnect",
|
|
@@ -75282,7 +75580,8 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
|
|
|
75282
75580
|
message: `${brokerName} needs you to sign in to Google again.`,
|
|
75283
75581
|
action: {
|
|
75284
75582
|
label: "Sign in again",
|
|
75285
|
-
href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}
|
|
75583
|
+
href: `/addon/google-services/oauth/start?brokerId=${encodeURIComponent(brokerId)}`,
|
|
75584
|
+
navigation: "server"
|
|
75286
75585
|
},
|
|
75287
75586
|
status: "active",
|
|
75288
75587
|
read: false,
|
|
@@ -75292,10 +75591,7 @@ function buildGoogleBrokerReconnectAlert(brokerId, brokerName) {
|
|
|
75292
75591
|
type: "google-broker",
|
|
75293
75592
|
id: brokerId
|
|
75294
75593
|
},
|
|
75295
|
-
metadata: {
|
|
75296
|
-
episodeKey: "reconnect",
|
|
75297
|
-
pushLine
|
|
75298
|
-
}
|
|
75594
|
+
metadata: { episodeKey: "reconnect" }
|
|
75299
75595
|
};
|
|
75300
75596
|
}
|
|
75301
75597
|
//#endregion
|
|
@@ -75504,6 +75800,14 @@ var GoogleBrokerStore = class GoogleBrokerStore {
|
|
|
75504
75800
|
* One OAuth account = one broker entry; feature caps aggregate connected brokers.
|
|
75505
75801
|
*/
|
|
75506
75802
|
var ADDON_ID = "google-services";
|
|
75803
|
+
/** A callback failure the landing page can name by code. */
|
|
75804
|
+
var GoogleCallbackError = class extends Error {
|
|
75805
|
+
reason;
|
|
75806
|
+
constructor(reason, message) {
|
|
75807
|
+
super(message);
|
|
75808
|
+
this.reason = reason;
|
|
75809
|
+
}
|
|
75810
|
+
};
|
|
75507
75811
|
var CALLBACK_PATH = "/oauth/callback";
|
|
75508
75812
|
var START_PATH = "/oauth/start";
|
|
75509
75813
|
var DISCONNECT_PATH = "/oauth/disconnect";
|
|
@@ -75524,7 +75828,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75524
75828
|
registry = new GoogleBrokerRegistry();
|
|
75525
75829
|
brokerStore = null;
|
|
75526
75830
|
activeBrokers = [];
|
|
75527
|
-
/** CSRF state → { brokerId,
|
|
75831
|
+
/** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
|
|
75528
75832
|
pendingStates = /* @__PURE__ */ new Map();
|
|
75529
75833
|
publicOrigin = null;
|
|
75530
75834
|
constructor() {
|
|
@@ -75560,7 +75864,8 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75560
75864
|
refreshToken: legacyOauth.refreshToken,
|
|
75561
75865
|
accessToken: legacyOauth.accessToken,
|
|
75562
75866
|
accessExpiresAt: legacyOauth.accessExpiresAt,
|
|
75563
|
-
accountEmail: legacyOauth.accountEmail
|
|
75867
|
+
accountEmail: legacyOauth.accountEmail,
|
|
75868
|
+
needsReauth: false
|
|
75564
75869
|
}] };
|
|
75565
75870
|
await this.oauthAccountsHandle.set(oauthAccounts);
|
|
75566
75871
|
await legacyHandle.set({ ...DEFAULT_OAUTH_STATE });
|
|
@@ -75831,12 +76136,9 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75831
76136
|
reply.status(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
75832
76137
|
return;
|
|
75833
76138
|
}
|
|
75834
|
-
const popupRaw = req.query["popup"];
|
|
75835
|
-
const popup = popupRaw === "1" || popupRaw === "true";
|
|
75836
76139
|
const state = randomBytes(16).toString("hex");
|
|
75837
76140
|
this.pendingStates.set(state, {
|
|
75838
76141
|
brokerId,
|
|
75839
|
-
popup,
|
|
75840
76142
|
issuedAt: Date.now()
|
|
75841
76143
|
});
|
|
75842
76144
|
this.prunePendingStates();
|
|
@@ -75848,42 +76150,39 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75848
76150
|
reply.redirect(url);
|
|
75849
76151
|
}
|
|
75850
76152
|
async handleCallback(req, reply) {
|
|
76153
|
+
const state = typeof req.query["state"] === "string" ? req.query["state"] : "";
|
|
75851
76154
|
const err = req.query["error"];
|
|
75852
76155
|
if (err) {
|
|
75853
|
-
const
|
|
75854
|
-
|
|
75855
|
-
|
|
75856
|
-
|
|
75857
|
-
|
|
75858
|
-
|
|
75859
|
-
|
|
75860
|
-
|
|
75861
|
-
});
|
|
75862
|
-
return;
|
|
75863
|
-
}
|
|
75864
|
-
this.html(reply, 400, `Google OAuth error: ${err}`);
|
|
76156
|
+
const pendingPeek = state ? this.pendingStates.get(state) : void 0;
|
|
76157
|
+
if (pendingPeek !== void 0) this.pendingStates.delete(state);
|
|
76158
|
+
await this.landOnAdminUi(reply, {
|
|
76159
|
+
status: "error",
|
|
76160
|
+
addonId: ADDON_ID,
|
|
76161
|
+
...pendingPeek !== void 0 ? { brokerId: pendingPeek.brokerId } : {},
|
|
76162
|
+
reason: err === "access_denied" ? "access_denied" : "invalid_request"
|
|
76163
|
+
});
|
|
75865
76164
|
return;
|
|
75866
76165
|
}
|
|
75867
76166
|
const code = req.query["code"];
|
|
75868
|
-
const
|
|
75869
|
-
const pending = typeof state === "string" ? this.pendingStates.get(state) : void 0;
|
|
76167
|
+
const pending = state ? this.pendingStates.get(state) : void 0;
|
|
75870
76168
|
if (!code || !state || pending === void 0) {
|
|
75871
|
-
this.
|
|
76169
|
+
await this.landOnAdminUi(reply, {
|
|
76170
|
+
status: "error",
|
|
76171
|
+
addonId: ADDON_ID,
|
|
76172
|
+
reason: "invalid_state"
|
|
76173
|
+
});
|
|
75872
76174
|
return;
|
|
75873
76175
|
}
|
|
75874
76176
|
this.pendingStates.delete(state);
|
|
75875
|
-
const { brokerId
|
|
76177
|
+
const { brokerId } = pending;
|
|
75876
76178
|
const entry = this.registry.getEntry(brokerId);
|
|
75877
76179
|
if (entry === null) {
|
|
75878
|
-
|
|
75879
|
-
|
|
75880
|
-
|
|
75881
|
-
|
|
75882
|
-
|
|
75883
|
-
|
|
75884
|
-
return;
|
|
75885
|
-
}
|
|
75886
|
-
this.html(reply, 404, `Unknown Google broker: ${brokerId}`);
|
|
76180
|
+
await this.landOnAdminUi(reply, {
|
|
76181
|
+
status: "error",
|
|
76182
|
+
addonId: ADDON_ID,
|
|
76183
|
+
brokerId,
|
|
76184
|
+
reason: "unknown_broker"
|
|
76185
|
+
});
|
|
75887
76186
|
return;
|
|
75888
76187
|
}
|
|
75889
76188
|
try {
|
|
@@ -75895,11 +76194,12 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75895
76194
|
}, code);
|
|
75896
76195
|
const prev = this.registry.getOauth(brokerId) ?? {
|
|
75897
76196
|
id: brokerId,
|
|
75898
|
-
...DEFAULT_OAUTH_STATE
|
|
76197
|
+
...DEFAULT_OAUTH_STATE,
|
|
76198
|
+
needsReauth: false
|
|
75899
76199
|
};
|
|
75900
|
-
if (!tokens.refresh_token && !prev.refreshToken) throw new
|
|
76200
|
+
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.");
|
|
75901
76201
|
const applied = applyTokenResponse({
|
|
75902
|
-
refreshToken: prev.refreshToken,
|
|
76202
|
+
refreshToken: prev.needsReauth && tokens.refresh_token ? "" : prev.refreshToken,
|
|
75903
76203
|
accessToken: prev.accessToken,
|
|
75904
76204
|
accessExpiresAt: prev.accessExpiresAt,
|
|
75905
76205
|
accountEmail: prev.accountEmail
|
|
@@ -75910,7 +76210,8 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75910
76210
|
refreshToken: applied.refreshToken,
|
|
75911
76211
|
accessToken: applied.accessToken,
|
|
75912
76212
|
accessExpiresAt: applied.accessExpiresAt,
|
|
75913
|
-
accountEmail: email || applied.accountEmail
|
|
76213
|
+
accountEmail: email || applied.accountEmail,
|
|
76214
|
+
needsReauth: false
|
|
75914
76215
|
};
|
|
75915
76216
|
this.registry.setOauth(next);
|
|
75916
76217
|
await this.persistOauthAccounts();
|
|
@@ -75923,65 +76224,44 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75923
76224
|
this.registry.replaceEntries(this.registry.listEntries().map((e) => e.id === brokerId ? renamed : e));
|
|
75924
76225
|
await this.persistBrokers();
|
|
75925
76226
|
}
|
|
75926
|
-
this.ctx.logger.info("google oauth connected", {
|
|
75927
|
-
brokerId,
|
|
75928
|
-
|
|
75929
|
-
accountEmail: next.accountEmail || void 0
|
|
75930
|
-
} });
|
|
75931
|
-
if (popup) {
|
|
75932
|
-
this.popupComplete(reply, {
|
|
75933
|
-
ok: true,
|
|
76227
|
+
this.ctx.logger.info("google oauth connected", {
|
|
76228
|
+
tags: { brokerId },
|
|
76229
|
+
meta: {
|
|
75934
76230
|
brokerId,
|
|
75935
|
-
|
|
75936
|
-
|
|
75937
|
-
|
|
75938
|
-
}
|
|
75939
|
-
this.
|
|
76231
|
+
hasRefresh: next.refreshToken.length > 0,
|
|
76232
|
+
accountEmail: next.accountEmail || void 0
|
|
76233
|
+
}
|
|
76234
|
+
});
|
|
76235
|
+
await this.landOnAdminUi(reply, {
|
|
76236
|
+
status: "success",
|
|
76237
|
+
addonId: ADDON_ID,
|
|
76238
|
+
brokerId
|
|
76239
|
+
});
|
|
75940
76240
|
} catch (e) {
|
|
75941
76241
|
const message = e instanceof Error ? e.message : String(e);
|
|
75942
76242
|
this.ctx.logger.warn("google oauth callback failed", { meta: {
|
|
75943
76243
|
brokerId,
|
|
75944
76244
|
error: message
|
|
75945
76245
|
} });
|
|
75946
|
-
|
|
75947
|
-
|
|
75948
|
-
|
|
75949
|
-
|
|
75950
|
-
|
|
75951
|
-
|
|
75952
|
-
return;
|
|
75953
|
-
}
|
|
75954
|
-
this.html(reply, 500, `OAuth failed: ${message}`);
|
|
76246
|
+
await this.landOnAdminUi(reply, {
|
|
76247
|
+
status: "error",
|
|
76248
|
+
addonId: ADDON_ID,
|
|
76249
|
+
brokerId,
|
|
76250
|
+
reason: e instanceof GoogleCallbackError ? e.reason : "exchange_failed"
|
|
76251
|
+
});
|
|
75955
76252
|
}
|
|
75956
76253
|
}
|
|
75957
76254
|
/**
|
|
75958
|
-
*
|
|
75959
|
-
*
|
|
75960
|
-
*
|
|
76255
|
+
* Every OAuth completion ends in CamStack: a redirect to the admin-ui
|
|
76256
|
+
* completion page (`/sso-complete`) on the hub's public address (the
|
|
76257
|
+
* `local-network` palette role), carrying a status and a reason CODE — never
|
|
76258
|
+
* a token, a code or an error message. The page hands a popup's outcome to
|
|
76259
|
+
* its opener and closes it, or, in a full-page flow, shows the result and
|
|
76260
|
+
* returns to Connectivity.
|
|
75961
76261
|
*/
|
|
75962
|
-
|
|
75963
|
-
|
|
75964
|
-
|
|
75965
|
-
ok: outcome.ok,
|
|
75966
|
-
brokerId: outcome.brokerId,
|
|
75967
|
-
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
75968
|
-
});
|
|
75969
|
-
const body = outcome.ok ? outcome.message ?? "Done. This window should close." : `OAuth failed: ${outcome.error ?? "unknown"}`;
|
|
75970
|
-
reply.header("content-type", "text/html; charset=utf-8");
|
|
75971
|
-
reply.status(outcome.ok ? 200 : 400).send(`<!doctype html><html><head><title>Google</title></head><body>
|
|
75972
|
-
<p>${escapeHtml(body)}</p>
|
|
75973
|
-
<script>
|
|
75974
|
-
(function () {
|
|
75975
|
-
var msg = ${payload};
|
|
75976
|
-
try {
|
|
75977
|
-
if (window.opener) {
|
|
75978
|
-
window.opener.postMessage(msg, window.location.origin);
|
|
75979
|
-
}
|
|
75980
|
-
} catch (e) {}
|
|
75981
|
-
setTimeout(function () { window.close(); }, 200);
|
|
75982
|
-
})();
|
|
75983
|
-
<\/script>
|
|
75984
|
-
</body></html>`);
|
|
76262
|
+
async landOnAdminUi(reply, outcome) {
|
|
76263
|
+
await this.refreshPublicOrigin();
|
|
76264
|
+
reply.redirect(oauthLandingUrl(this.publicOrigin, outcome));
|
|
75985
76265
|
}
|
|
75986
76266
|
async fetchAccountEmail(accessToken) {
|
|
75987
76267
|
try {
|
|
@@ -75993,10 +76273,6 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
75993
76273
|
return "";
|
|
75994
76274
|
}
|
|
75995
76275
|
}
|
|
75996
|
-
html(reply, code, body) {
|
|
75997
|
-
reply.header("content-type", "text/html; charset=utf-8");
|
|
75998
|
-
reply.status(code).send(`<!doctype html><html><body><p>${escapeHtml(body)}</p></body></html>`);
|
|
75999
|
-
}
|
|
76000
76276
|
prunePendingStates() {
|
|
76001
76277
|
const cutoff = Date.now() - 10 * 6e4;
|
|
76002
76278
|
for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
|
|
@@ -76084,6 +76360,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76084
76360
|
const entry = this.registry.getEntry(brokerId);
|
|
76085
76361
|
const oauth = this.registry.getOauth(brokerId);
|
|
76086
76362
|
if (entry === null || oauth === null || !oauth.refreshToken.trim()) throw new Error(`Google broker ${brokerId} is not connected — use Sign in under Connectivity → Brokers`);
|
|
76363
|
+
if (oauth.needsReauth) throw new Error(`Google broker ${brokerId} needs a new sign-in — use Sign in under Connectivity → Brokers`);
|
|
76087
76364
|
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");
|
|
76088
76365
|
const state = {
|
|
76089
76366
|
refreshToken: oauth.refreshToken.trim(),
|
|
@@ -76099,7 +76376,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76099
76376
|
clientSecret: entry.clientSecret.trim()
|
|
76100
76377
|
}, state.refreshToken);
|
|
76101
76378
|
} catch (err) {
|
|
76102
|
-
if (isInvalidGrantRefreshError(err))
|
|
76379
|
+
if (isInvalidGrantRefreshError(err)) {
|
|
76380
|
+
if (this.registry.markNeedsReauth(brokerId)) await this.persistOauthAccounts();
|
|
76381
|
+
this.emitGoogleBrokerReconnectAlert(brokerId, entry.name);
|
|
76382
|
+
}
|
|
76103
76383
|
throw err;
|
|
76104
76384
|
}
|
|
76105
76385
|
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");
|
|
@@ -76109,7 +76389,8 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76109
76389
|
refreshToken: nextState.refreshToken,
|
|
76110
76390
|
accessToken: nextState.accessToken,
|
|
76111
76391
|
accessExpiresAt: nextState.accessExpiresAt,
|
|
76112
|
-
accountEmail: nextState.accountEmail
|
|
76392
|
+
accountEmail: nextState.accountEmail,
|
|
76393
|
+
needsReauth: false
|
|
76113
76394
|
});
|
|
76114
76395
|
await this.persistOauthAccounts();
|
|
76115
76396
|
return nextState.accessToken;
|
|
@@ -76125,20 +76406,18 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76125
76406
|
reply.status(404).send({ error: `Unknown Google broker: ${brokerId}` });
|
|
76126
76407
|
return;
|
|
76127
76408
|
}
|
|
76128
|
-
const popupRaw = req.query["popup"];
|
|
76129
|
-
const popup = popupRaw === "1" || popupRaw === "true";
|
|
76130
76409
|
this.registry.removeOauth(brokerId);
|
|
76131
76410
|
await this.persistOauthAccounts();
|
|
76132
|
-
this.ctx.logger.info("google oauth disconnected", {
|
|
76133
|
-
|
|
76134
|
-
|
|
76135
|
-
|
|
76136
|
-
|
|
76137
|
-
|
|
76138
|
-
|
|
76139
|
-
|
|
76140
|
-
|
|
76141
|
-
|
|
76411
|
+
this.ctx.logger.info("google oauth disconnected", {
|
|
76412
|
+
tags: { brokerId },
|
|
76413
|
+
meta: { brokerId }
|
|
76414
|
+
});
|
|
76415
|
+
await this.landOnAdminUi(reply, {
|
|
76416
|
+
status: "success",
|
|
76417
|
+
addonId: ADDON_ID,
|
|
76418
|
+
brokerId,
|
|
76419
|
+
action: "disconnect"
|
|
76420
|
+
});
|
|
76142
76421
|
}
|
|
76143
76422
|
async resolveRedirectUri() {
|
|
76144
76423
|
await this.refreshPublicOrigin();
|
|
@@ -76175,8 +76454,5 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
76175
76454
|
function asSettingsCap(cap) {
|
|
76176
76455
|
if (cap === "calendar-source" || cap === "smtp-provider") return cap;
|
|
76177
76456
|
}
|
|
76178
|
-
function escapeHtml(s) {
|
|
76179
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
76180
|
-
}
|
|
76181
76457
|
//#endregion
|
|
76182
76458
|
export { GOOGLE_GMAIL_SEND_SCOPE as C, OAUTH_START_PATH as E, GoogleServicesAddon, GoogleServicesAddon as default, GOOGLE_BROKER_KIND as S, GoogleServicesConfigSchema as T, GoogleBrokerRegistry as _, refreshAccessToken as a, stampCalendarId as b, selectGmailAccount as c, mapGoogleEvent as d, GOOGLE_CONSOLE_APIS_URL as f, redirectUriDisplay as g, introHtml as h, needsRefresh as i, createGoogleCalendarApi as l, connectHtml as m, buildAuthorizeUrl as n, buildRawMime as o, GOOGLE_CONSOLE_CREDENTIALS_URL as p, exchangeCode as r, createGmailSmtpProvider as s, applyTokenResponse as t, createGoogleCalendarSource as u, migrateLegacyToBrokers as v, GOOGLE_OAUTH_SCOPES as w, DEFAULT_GOOGLE_CONFIG as x, parseStampedCalendarId as y };
|