@camstack/addon-google-services 0.1.6 → 0.2.7
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 +1288 -306
- package/dist/google-services.addon.mjs +1283 -307
- package/dist/index.js +6 -0
- package/dist/index.mjs +2 -2
- package/package.json +6 -3
|
@@ -13666,7 +13666,14 @@ var BrokerProviderInfoSchema = object({
|
|
|
13666
13666
|
/** Broker kinds this provider can create, with a display label. */
|
|
13667
13667
|
kinds: array(object({
|
|
13668
13668
|
kind: string(),
|
|
13669
|
-
label: string()
|
|
13669
|
+
label: string(),
|
|
13670
|
+
/**
|
|
13671
|
+
* Relative hub path to start an interactive account link (OAuth popup).
|
|
13672
|
+
* May include `{{brokerId}}`; admin-ui substitutes and appends `?popup=1`.
|
|
13673
|
+
*/
|
|
13674
|
+
interactiveConnectPath: string().min(1).optional(),
|
|
13675
|
+
/** Relative hub path to disconnect the linked account (same popup pattern). */
|
|
13676
|
+
interactiveDisconnectPath: string().min(1).optional()
|
|
13670
13677
|
}))
|
|
13671
13678
|
});
|
|
13672
13679
|
var ListInputSchema = object({
|
|
@@ -13752,34 +13759,83 @@ var GetStateInputSchema = object({
|
|
|
13752
13759
|
* HA: entity_id (returns the cached entity state). */
|
|
13753
13760
|
key: string()
|
|
13754
13761
|
});
|
|
13755
|
-
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
}),
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13762
|
+
var brokerCapability = {
|
|
13763
|
+
name: "broker",
|
|
13764
|
+
scope: "system",
|
|
13765
|
+
mode: "collection",
|
|
13766
|
+
providerKind: "broker",
|
|
13767
|
+
status: {
|
|
13768
|
+
schema: RegistryStatusSchema,
|
|
13769
|
+
kind: "poll"
|
|
13770
|
+
},
|
|
13771
|
+
methods: {
|
|
13772
|
+
list: method(ListInputSchema, array(BrokerInfoSchema)),
|
|
13773
|
+
get: method(GetInputSchema, BrokerInfoSchema.nullable()),
|
|
13774
|
+
/** Enumerate which addon provides which broker kind(s) for the
|
|
13775
|
+
* unified create picker. The auto-mount fans this array across
|
|
13776
|
+
* every registered `broker` provider (array-output method), so the
|
|
13777
|
+
* picker sees every kind from every provider in one call. */
|
|
13778
|
+
listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
|
|
13779
|
+
add: method(AddInputSchema, AddResultSchema, {
|
|
13780
|
+
kind: "mutation",
|
|
13781
|
+
auth: "admin"
|
|
13782
|
+
}),
|
|
13783
|
+
remove: method(RemoveInputSchema, _void(), {
|
|
13784
|
+
kind: "mutation",
|
|
13785
|
+
auth: "admin"
|
|
13786
|
+
}),
|
|
13787
|
+
testConnection: method(GetInputSchema, TestConnectionResultSchema, {
|
|
13788
|
+
kind: "mutation",
|
|
13789
|
+
auth: "admin"
|
|
13790
|
+
}),
|
|
13791
|
+
/** Read the persisted settings record for a broker (kind-specific
|
|
13792
|
+
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
13793
|
+
* when the broker id is unknown to the provider (the collection
|
|
13794
|
+
* fallback may route a foreign id to the first provider). */
|
|
13795
|
+
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
13796
|
+
/** Overwrite the persisted settings record. The kind-specific
|
|
13797
|
+
* provider validates the shape and applies the change (reconnects
|
|
13798
|
+
* if credentials changed). */
|
|
13799
|
+
setSettings: method(object({
|
|
13800
|
+
id: string(),
|
|
13801
|
+
settings: SettingsRecordSchema$1
|
|
13802
|
+
}), _void(), {
|
|
13803
|
+
kind: "mutation",
|
|
13804
|
+
auth: "admin"
|
|
13805
|
+
}),
|
|
13806
|
+
/** Returns the kind-specific connection config the consumer needs
|
|
13807
|
+
* to open its own client (MQTT pattern: `{url, username, password,
|
|
13808
|
+
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
13809
|
+
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
13810
|
+
* Returns `null` when the broker id is unknown to the provider. */
|
|
13811
|
+
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
13812
|
+
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
13813
|
+
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
13814
|
+
kind: "mutation",
|
|
13815
|
+
auth: "admin"
|
|
13816
|
+
}),
|
|
13817
|
+
publish: method(PublishInputSchema, unknown(), {
|
|
13818
|
+
kind: "mutation",
|
|
13819
|
+
auth: "admin"
|
|
13820
|
+
}),
|
|
13821
|
+
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
13822
|
+
kind: "mutation",
|
|
13823
|
+
auth: "admin"
|
|
13824
|
+
}),
|
|
13825
|
+
unsubscribe: method(UnsubscribeInputSchema, _void(), {
|
|
13826
|
+
kind: "mutation",
|
|
13827
|
+
auth: "admin"
|
|
13828
|
+
}),
|
|
13829
|
+
/** Read the broker's cached state for a key. Returns `null` when
|
|
13830
|
+
* unknown to the broker (never published / unknown entity). */
|
|
13831
|
+
getState: method(GetStateInputSchema, unknown().nullable(), { auth: "admin" }),
|
|
13832
|
+
/** Status method — explicit registration with a `z.void()` input so
|
|
13833
|
+
* the codegen-generated tRPC router types its input as
|
|
13834
|
+
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
13835
|
+
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
13836
|
+
getStatus: method(_void(), RegistryStatusSchema)
|
|
13837
|
+
}
|
|
13838
|
+
};
|
|
13783
13839
|
DeviceType.Camera;
|
|
13784
13840
|
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
13785
13841
|
var StreamFormatSchema = _enum([
|
|
@@ -18195,14 +18251,14 @@ var LlmProfileSchema = object({
|
|
|
18195
18251
|
/** ConfigUISchema tree passed through untyped on the wire (the
|
|
18196
18252
|
* notification-output `ConfigSchemaPassthrough` precedent at
|
|
18197
18253
|
* notification-output.cap.ts:151); the exported TS type re-tightens it. */
|
|
18198
|
-
var ConfigSchemaPassthrough$
|
|
18254
|
+
var ConfigSchemaPassthrough$3 = unknown();
|
|
18199
18255
|
var LlmProfileKindDescriptorSchema = object({
|
|
18200
18256
|
kind: LlmProfileKindSchema,
|
|
18201
18257
|
label: string(),
|
|
18202
18258
|
icon: string(),
|
|
18203
18259
|
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
18204
18260
|
addonId: string(),
|
|
18205
|
-
configSchema: ConfigSchemaPassthrough$
|
|
18261
|
+
configSchema: ConfigSchemaPassthrough$3
|
|
18206
18262
|
});
|
|
18207
18263
|
var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
|
|
18208
18264
|
var LlmDefaultSchema = object({
|
|
@@ -19323,7 +19379,7 @@ var TargetKindCapsSchema = object({
|
|
|
19323
19379
|
* the union is large and not meant for runtime validation here; the exported
|
|
19324
19380
|
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
19325
19381
|
*/
|
|
19326
|
-
var ConfigSchemaPassthrough$
|
|
19382
|
+
var ConfigSchemaPassthrough$2 = unknown();
|
|
19327
19383
|
var TargetKindSchema = object({
|
|
19328
19384
|
kind: string(),
|
|
19329
19385
|
label: string(),
|
|
@@ -19360,7 +19416,7 @@ var TargetKindSchema = object({
|
|
|
19360
19416
|
* its raster path, which is the safe default for an unknown image.
|
|
19361
19417
|
*/
|
|
19362
19418
|
iconMediaType: string().optional(),
|
|
19363
|
-
configSchema: ConfigSchemaPassthrough$
|
|
19419
|
+
configSchema: ConfigSchemaPassthrough$2,
|
|
19364
19420
|
/**
|
|
19365
19421
|
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
19366
19422
|
* base URL, instance, chat, recipient). A save that changes any of them
|
|
@@ -22659,6 +22715,61 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
22659
22715
|
});
|
|
22660
22716
|
method(_void(), OauthIntegrationDescriptorSchema, { auth: "admin" });
|
|
22661
22717
|
/**
|
|
22718
|
+
* `oidc-connections` — CRUD for IdP rows under `auth-oidc` (D828 sibling).
|
|
22719
|
+
*
|
|
22720
|
+
* One hub-only singleton (`auth-oidc`). The Settings UI is a DataTable +
|
|
22721
|
+
* ItemFormDialog; there is no `editable-array` in `globalSettingsSchema`.
|
|
22722
|
+
* Operator-chosen `id` is the URL slug (`/addon/auth-oidc/<id>/…`) and is
|
|
22723
|
+
* immutable after create.
|
|
22724
|
+
*/
|
|
22725
|
+
var ConfigSchemaPassthrough$1 = unknown();
|
|
22726
|
+
var OidcConnectionSchema = object({
|
|
22727
|
+
id: string().min(1),
|
|
22728
|
+
displayName: string(),
|
|
22729
|
+
icon: string(),
|
|
22730
|
+
issuerUrl: string(),
|
|
22731
|
+
clientId: string(),
|
|
22732
|
+
/** Client id + secret present — never the secret value. */
|
|
22733
|
+
configured: boolean(),
|
|
22734
|
+
/**
|
|
22735
|
+
* Values for the connection form schema. `clientSecret` is a keychain
|
|
22736
|
+
* {@link SecretFieldView} or `null` / `''`, never plaintext (D789).
|
|
22737
|
+
*/
|
|
22738
|
+
formValues: record(string(), unknown()).optional()
|
|
22739
|
+
});
|
|
22740
|
+
/**
|
|
22741
|
+
* Patch one connection. Omitted or empty `clientSecret` keeps the stored
|
|
22742
|
+
* secret (D787). `{ $secretClear: true }` clears it. `id` is never renamed.
|
|
22743
|
+
*/
|
|
22744
|
+
var OidcConnectionUpdateInputSchema = object({
|
|
22745
|
+
id: string().min(1),
|
|
22746
|
+
displayName: string().min(1),
|
|
22747
|
+
icon: string().optional(),
|
|
22748
|
+
issuerUrl: string().optional(),
|
|
22749
|
+
clientId: string().optional(),
|
|
22750
|
+
clientSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
|
|
22751
|
+
redirectUri: string().optional(),
|
|
22752
|
+
scopes: string().optional(),
|
|
22753
|
+
usernameClaim: _enum([
|
|
22754
|
+
"preferred_username",
|
|
22755
|
+
"email",
|
|
22756
|
+
"sub"
|
|
22757
|
+
]).optional()
|
|
22758
|
+
});
|
|
22759
|
+
method(object({}), ConfigSchemaPassthrough$1, { auth: "admin" }), method(object({}), array(OidcConnectionSchema).readonly(), { auth: "admin" }), method(object({
|
|
22760
|
+
id: string().min(1).regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "id must be a lowercase slug"),
|
|
22761
|
+
displayName: string().min(1)
|
|
22762
|
+
}), object({ id: string() }), {
|
|
22763
|
+
kind: "mutation",
|
|
22764
|
+
auth: "admin"
|
|
22765
|
+
}), method(OidcConnectionUpdateInputSchema, object({ ok: literal(true) }), {
|
|
22766
|
+
kind: "mutation",
|
|
22767
|
+
auth: "admin"
|
|
22768
|
+
}), method(object({ id: string().min(1) }), object({ ok: literal(true) }), {
|
|
22769
|
+
kind: "mutation",
|
|
22770
|
+
auth: "admin"
|
|
22771
|
+
});
|
|
22772
|
+
/**
|
|
22662
22773
|
* What started a camera's onboard motion (D672). `camera` is the camera's own
|
|
22663
22774
|
* firmware; `sensor` is a device the operator ATTACHED to it. Absent on the
|
|
22664
22775
|
* wire means the camera: only the camera-sensors bridge ever says `sensor`.
|
|
@@ -44022,6 +44133,36 @@ Object.freeze({
|
|
|
44022
44133
|
addonId: null,
|
|
44023
44134
|
access: "view"
|
|
44024
44135
|
},
|
|
44136
|
+
"oidcConnections.connectionFormSchema": {
|
|
44137
|
+
capName: "oidc-connections",
|
|
44138
|
+
capScope: "system",
|
|
44139
|
+
addonId: null,
|
|
44140
|
+
access: "view"
|
|
44141
|
+
},
|
|
44142
|
+
"oidcConnections.createConnection": {
|
|
44143
|
+
capName: "oidc-connections",
|
|
44144
|
+
capScope: "system",
|
|
44145
|
+
addonId: null,
|
|
44146
|
+
access: "create"
|
|
44147
|
+
},
|
|
44148
|
+
"oidcConnections.deleteConnection": {
|
|
44149
|
+
capName: "oidc-connections",
|
|
44150
|
+
capScope: "system",
|
|
44151
|
+
addonId: null,
|
|
44152
|
+
access: "delete"
|
|
44153
|
+
},
|
|
44154
|
+
"oidcConnections.listConnections": {
|
|
44155
|
+
capName: "oidc-connections",
|
|
44156
|
+
capScope: "system",
|
|
44157
|
+
addonId: null,
|
|
44158
|
+
access: "view"
|
|
44159
|
+
},
|
|
44160
|
+
"oidcConnections.updateConnection": {
|
|
44161
|
+
capName: "oidc-connections",
|
|
44162
|
+
capScope: "system",
|
|
44163
|
+
addonId: null,
|
|
44164
|
+
access: "create"
|
|
44165
|
+
},
|
|
44025
44166
|
"osd.getStatus": {
|
|
44026
44167
|
capName: "osd",
|
|
44027
44168
|
capScope: "device",
|
|
@@ -52800,34 +52941,506 @@ var MB = 1024 * 1024;
|
|
|
52800
52941
|
1024 * MB, 3072 * MB;
|
|
52801
52942
|
//#endregion
|
|
52802
52943
|
//#region src/google-types.ts
|
|
52803
|
-
|
|
52944
|
+
/** One Google account connection (broker entry). Credentials for the OAuth
|
|
52945
|
+
* *app* live here; tokens live in durable oauthAccounts state keyed by id. */
|
|
52946
|
+
var GoogleBrokerEntrySchema = object({
|
|
52947
|
+
id: string().min(1),
|
|
52948
|
+
name: string().min(1),
|
|
52804
52949
|
clientId: string().default(""),
|
|
52805
52950
|
clientSecret: string().default("").meta({ secret: true }),
|
|
52806
|
-
/**
|
|
52951
|
+
/**
|
|
52952
|
+
* @deprecated Per-broker filter; import selection lives on addon
|
|
52953
|
+
* `calendarIds` (Connectivity → Calendars). Kept for migrate only.
|
|
52954
|
+
*/
|
|
52807
52955
|
calendarFilter: string().default(""),
|
|
52808
52956
|
/** Default From for Gmail send — usually the connected account email. */
|
|
52809
52957
|
fromAddress: string().default(""),
|
|
52810
52958
|
fromName: string().default("")
|
|
52811
52959
|
});
|
|
52960
|
+
var GoogleServicesConfigSchema = object({
|
|
52961
|
+
brokers: array(GoogleBrokerEntrySchema).default([]),
|
|
52962
|
+
/**
|
|
52963
|
+
* Stamped calendar ids (`brokerId:googleCalendarId`) to import.
|
|
52964
|
+
* Empty = import none. Edited under Connectivity → Calendars.
|
|
52965
|
+
*/
|
|
52966
|
+
calendarIds: array(string()).default([]),
|
|
52967
|
+
/** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
|
|
52968
|
+
clientId: string().optional(),
|
|
52969
|
+
/** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
|
|
52970
|
+
clientSecret: string().optional().meta({ secret: true }),
|
|
52971
|
+
/** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
|
|
52972
|
+
calendarFilter: string().optional(),
|
|
52973
|
+
/** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
|
|
52974
|
+
fromAddress: string().optional(),
|
|
52975
|
+
/** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
|
|
52976
|
+
fromName: string().optional()
|
|
52977
|
+
});
|
|
52812
52978
|
var DEFAULT_GOOGLE_CONFIG = GoogleServicesConfigSchema.parse({});
|
|
52979
|
+
/** Flat pre-broker config (singleton). Parsed only for one-shot migration. */
|
|
52980
|
+
var LegacyGoogleFlatConfigSchema = GoogleServicesConfigSchema;
|
|
52813
52981
|
var DEFAULT_OAUTH_STATE = {
|
|
52814
52982
|
refreshToken: "",
|
|
52815
52983
|
accessToken: "",
|
|
52816
52984
|
accessExpiresAt: 0,
|
|
52817
52985
|
accountEmail: ""
|
|
52818
52986
|
};
|
|
52987
|
+
var GoogleOauthAccountsStateSchema = object({ accounts: array(object({
|
|
52988
|
+
id: string().min(1),
|
|
52989
|
+
refreshToken: string().default("").meta({ secret: true }),
|
|
52990
|
+
accessToken: string().default("").meta({ secret: true }),
|
|
52991
|
+
accessExpiresAt: number().default(0),
|
|
52992
|
+
accountEmail: string().default("")
|
|
52993
|
+
})).default([]) });
|
|
52994
|
+
var GOOGLE_BROKER_KIND = "google";
|
|
52995
|
+
var GOOGLE_BROKER_KIND_LABEL = "Google";
|
|
52819
52996
|
var GOOGLE_CALENDAR_SCOPE = "https://www.googleapis.com/auth/calendar.readonly";
|
|
52820
52997
|
var GOOGLE_GMAIL_SEND_SCOPE = "https://www.googleapis.com/auth/gmail.send";
|
|
52821
|
-
/** Space-separated scopes
|
|
52822
|
-
var GOOGLE_OAUTH_SCOPES = `${GOOGLE_CALENDAR_SCOPE} ${GOOGLE_GMAIL_SEND_SCOPE}`;
|
|
52998
|
+
/** Space-separated scopes on Connect (Calendar + Gmail + email). */
|
|
52999
|
+
var GOOGLE_OAUTH_SCOPES = `${GOOGLE_CALENDAR_SCOPE} ${GOOGLE_GMAIL_SEND_SCOPE} https://www.googleapis.com/auth/userinfo.email`;
|
|
52823
53000
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
52824
53001
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
52825
53002
|
var GOOGLE_CALENDAR_API = "https://www.googleapis.com/calendar/v3";
|
|
52826
53003
|
var GOOGLE_GMAIL_API = "https://gmail.googleapis.com/gmail/v1";
|
|
52827
53004
|
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
53005
|
+
/** Lightweight Calendar probe for Test — works with calendar.readonly alone. */
|
|
53006
|
+
var GOOGLE_CALENDAR_LIST_URL = `${GOOGLE_CALENDAR_API}/users/me/calendarList?maxResults=1`;
|
|
52828
53007
|
var OAUTH_START_PATH = `/addon/google-services/oauth/start`;
|
|
52829
53008
|
var OAUTH_CALLBACK_SUFFIX = `/addon/google-services/oauth/callback`;
|
|
52830
53009
|
//#endregion
|
|
53010
|
+
//#region src/google-broker-registry.ts
|
|
53011
|
+
var GoogleAddSettingsSchema = object({
|
|
53012
|
+
clientId: string().min(1),
|
|
53013
|
+
clientSecret: string().min(1).meta({ secret: true }),
|
|
53014
|
+
/** Selected Google calendar ids; empty = import all. */
|
|
53015
|
+
calendarIds: array(string()).optional(),
|
|
53016
|
+
/** @deprecated Prefer calendarIds — still accepted on save. */
|
|
53017
|
+
calendarFilter: string().optional(),
|
|
53018
|
+
fromAddress: string().optional(),
|
|
53019
|
+
fromName: string().optional()
|
|
53020
|
+
});
|
|
53021
|
+
/** Persist UI calendarIds (or legacy filter string) as comma-separated filter. */
|
|
53022
|
+
function calendarIdsToFilter(settings) {
|
|
53023
|
+
if (settings.calendarIds !== void 0) return settings.calendarIds.map((s) => s.trim()).filter((s) => s.length > 0).join(",");
|
|
53024
|
+
return settings.calendarFilter ?? "";
|
|
53025
|
+
}
|
|
53026
|
+
function settingsToBrokerFields(settings) {
|
|
53027
|
+
const parsed = GoogleAddSettingsSchema.parse(settings);
|
|
53028
|
+
return {
|
|
53029
|
+
clientId: parsed.clientId,
|
|
53030
|
+
clientSecret: parsed.clientSecret,
|
|
53031
|
+
calendarFilter: calendarIdsToFilter(parsed),
|
|
53032
|
+
fromAddress: parsed.fromAddress ?? "",
|
|
53033
|
+
fromName: parsed.fromName ?? ""
|
|
53034
|
+
};
|
|
53035
|
+
}
|
|
53036
|
+
function brokerEntryToSettings(entry) {
|
|
53037
|
+
return {
|
|
53038
|
+
clientId: entry.clientId,
|
|
53039
|
+
clientSecret: entry.clientSecret,
|
|
53040
|
+
calendarIds: [...parseCalendarFilter(entry.calendarFilter)],
|
|
53041
|
+
fromAddress: entry.fromAddress,
|
|
53042
|
+
fromName: entry.fromName
|
|
53043
|
+
};
|
|
53044
|
+
}
|
|
53045
|
+
/**
|
|
53046
|
+
* One-shot: flat singleton config + legacy `oauth` state → `google_001`.
|
|
53047
|
+
* Idempotent when `brokers` already present.
|
|
53048
|
+
*/
|
|
53049
|
+
function migrateLegacyToBrokers(rawConfig, legacyOauth) {
|
|
53050
|
+
const loose = LegacyGoogleFlatConfigSchema.safeParse(rawConfig);
|
|
53051
|
+
const flat = loose.success ? loose.data : {};
|
|
53052
|
+
const existingBrokers = flat.brokers ?? [];
|
|
53053
|
+
if (existingBrokers.length > 0) return {
|
|
53054
|
+
brokers: existingBrokers.map((b) => GoogleBrokerEntrySchema.parse(b)),
|
|
53055
|
+
oauthAccounts: [],
|
|
53056
|
+
didMigrate: false
|
|
53057
|
+
};
|
|
53058
|
+
const hasClient = (flat.clientId ?? "").length > 0 || (flat.clientSecret ?? "").length > 0;
|
|
53059
|
+
const hasOauth = legacyOauth !== null && legacyOauth.refreshToken.length > 0;
|
|
53060
|
+
if (!hasClient && !hasOauth) return {
|
|
53061
|
+
brokers: [],
|
|
53062
|
+
oauthAccounts: [],
|
|
53063
|
+
didMigrate: false
|
|
53064
|
+
};
|
|
53065
|
+
const id = "google_001";
|
|
53066
|
+
const email = legacyOauth?.accountEmail?.trim() ?? "";
|
|
53067
|
+
const name = email.length > 0 ? email : "Google account";
|
|
53068
|
+
const entry = GoogleBrokerEntrySchema.parse({
|
|
53069
|
+
id,
|
|
53070
|
+
name,
|
|
53071
|
+
clientId: flat.clientId ?? "",
|
|
53072
|
+
clientSecret: flat.clientSecret ?? "",
|
|
53073
|
+
calendarFilter: flat.calendarFilter ?? "",
|
|
53074
|
+
fromAddress: flat.fromAddress ?? "",
|
|
53075
|
+
fromName: flat.fromName ?? ""
|
|
53076
|
+
});
|
|
53077
|
+
const oauthAccounts = legacyOauth !== null && (legacyOauth.refreshToken.length > 0 || legacyOauth.accessToken.length > 0) ? [{
|
|
53078
|
+
id,
|
|
53079
|
+
refreshToken: legacyOauth.refreshToken,
|
|
53080
|
+
accessToken: legacyOauth.accessToken,
|
|
53081
|
+
accessExpiresAt: legacyOauth.accessExpiresAt,
|
|
53082
|
+
accountEmail: legacyOauth.accountEmail
|
|
53083
|
+
}] : [];
|
|
53084
|
+
return {
|
|
53085
|
+
brokers: [entry],
|
|
53086
|
+
oauthAccounts,
|
|
53087
|
+
didMigrate: true
|
|
53088
|
+
};
|
|
53089
|
+
}
|
|
53090
|
+
function parseCalendarFilter(raw) {
|
|
53091
|
+
const trimmed = raw.trim();
|
|
53092
|
+
if (!trimmed) return /* @__PURE__ */ new Set();
|
|
53093
|
+
return new Set(trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0));
|
|
53094
|
+
}
|
|
53095
|
+
/** Local calendar id stamped as `brokerId:googleCalendarId`. */
|
|
53096
|
+
function stampCalendarId(brokerId, googleCalendarId) {
|
|
53097
|
+
return `${brokerId}:${googleCalendarId}`;
|
|
53098
|
+
}
|
|
53099
|
+
function parseStampedCalendarId(stamped) {
|
|
53100
|
+
const idx = stamped.indexOf(":");
|
|
53101
|
+
if (idx <= 0 || idx === stamped.length - 1) return null;
|
|
53102
|
+
return {
|
|
53103
|
+
brokerId: stamped.slice(0, idx),
|
|
53104
|
+
googleCalendarId: stamped.slice(idx + 1)
|
|
53105
|
+
};
|
|
53106
|
+
}
|
|
53107
|
+
/**
|
|
53108
|
+
* In-memory index over broker entries + oauth accounts. The addon owns
|
|
53109
|
+
* persistence; this class only allocates ids and builds BrokerInfo rows.
|
|
53110
|
+
*/
|
|
53111
|
+
var GoogleBrokerRegistry = class {
|
|
53112
|
+
nextIdCounter = 1;
|
|
53113
|
+
entries = [];
|
|
53114
|
+
oauthById = /* @__PURE__ */ new Map();
|
|
53115
|
+
seedFrom(entries, accounts) {
|
|
53116
|
+
this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
|
|
53117
|
+
this.oauthById = new Map(accounts.map((a) => [a.id, { ...a }]));
|
|
53118
|
+
this.seedCounterFromEntries(this.entries);
|
|
53119
|
+
}
|
|
53120
|
+
listEntries() {
|
|
53121
|
+
return this.entries;
|
|
53122
|
+
}
|
|
53123
|
+
getEntry(id) {
|
|
53124
|
+
return this.entries.find((e) => e.id === id) ?? null;
|
|
53125
|
+
}
|
|
53126
|
+
getOauth(id) {
|
|
53127
|
+
return this.oauthById.get(id) ?? null;
|
|
53128
|
+
}
|
|
53129
|
+
listOauthAccounts() {
|
|
53130
|
+
return [...this.oauthById.values()];
|
|
53131
|
+
}
|
|
53132
|
+
setOauth(account) {
|
|
53133
|
+
this.oauthById.set(account.id, { ...account });
|
|
53134
|
+
}
|
|
53135
|
+
removeOauth(id) {
|
|
53136
|
+
this.oauthById.delete(id);
|
|
53137
|
+
}
|
|
53138
|
+
isConnected(id) {
|
|
53139
|
+
const o = this.oauthById.get(id);
|
|
53140
|
+
return o !== void 0 && o.refreshToken.length > 0;
|
|
53141
|
+
}
|
|
53142
|
+
allocateId() {
|
|
53143
|
+
const id = `google_${String(this.nextIdCounter).padStart(3, "0")}`;
|
|
53144
|
+
this.nextIdCounter += 1;
|
|
53145
|
+
return id;
|
|
53146
|
+
}
|
|
53147
|
+
adoptId(id) {
|
|
53148
|
+
if (this.entries.some((e) => e.id === id)) throw new Error(`broker id "${id}" is already in use — adopt an id only to restore a broker that is gone`);
|
|
53149
|
+
const match = id.match(/^google_(\d+)$/);
|
|
53150
|
+
if (match?.[1] !== void 0) this.nextIdCounter = Math.max(this.nextIdCounter, Number.parseInt(match[1], 10) + 1);
|
|
53151
|
+
return id;
|
|
53152
|
+
}
|
|
53153
|
+
createEntry(name, settings, opts = {}) {
|
|
53154
|
+
const fields = settingsToBrokerFields(settings);
|
|
53155
|
+
const id = opts.id !== void 0 ? this.adoptId(opts.id) : this.allocateId();
|
|
53156
|
+
const entry = GoogleBrokerEntrySchema.parse({
|
|
53157
|
+
id,
|
|
53158
|
+
name,
|
|
53159
|
+
...fields
|
|
53160
|
+
});
|
|
53161
|
+
this.entries = [...this.entries, entry];
|
|
53162
|
+
return entry;
|
|
53163
|
+
}
|
|
53164
|
+
updateEntry(id, settings) {
|
|
53165
|
+
const idx = this.entries.findIndex((e) => e.id === id);
|
|
53166
|
+
if (idx < 0) return null;
|
|
53167
|
+
const prev = this.entries[idx];
|
|
53168
|
+
const fields = settingsToBrokerFields(settings);
|
|
53169
|
+
const next = GoogleBrokerEntrySchema.parse({
|
|
53170
|
+
id: prev.id,
|
|
53171
|
+
name: prev.name,
|
|
53172
|
+
...fields
|
|
53173
|
+
});
|
|
53174
|
+
this.entries = this.entries.map((e, i) => i === idx ? next : e);
|
|
53175
|
+
return next;
|
|
53176
|
+
}
|
|
53177
|
+
removeEntry(id) {
|
|
53178
|
+
const before = this.entries.length;
|
|
53179
|
+
this.entries = this.entries.filter((e) => e.id !== id);
|
|
53180
|
+
this.oauthById.delete(id);
|
|
53181
|
+
return this.entries.length < before;
|
|
53182
|
+
}
|
|
53183
|
+
replaceEntries(entries) {
|
|
53184
|
+
this.entries = entries.map((e) => GoogleBrokerEntrySchema.parse(e));
|
|
53185
|
+
this.seedCounterFromEntries(this.entries);
|
|
53186
|
+
}
|
|
53187
|
+
toBrokerInfo(entry, addonId) {
|
|
53188
|
+
const oauth = this.oauthById.get(entry.id);
|
|
53189
|
+
const connected = this.isConnected(entry.id);
|
|
53190
|
+
return {
|
|
53191
|
+
id: entry.id,
|
|
53192
|
+
addonId,
|
|
53193
|
+
name: entry.name,
|
|
53194
|
+
kind: GOOGLE_BROKER_KIND,
|
|
53195
|
+
status: connected ? "connected" : "disconnected",
|
|
53196
|
+
info: {
|
|
53197
|
+
accountEmail: oauth?.accountEmail ?? "",
|
|
53198
|
+
hasClient: entry.clientId.length > 0
|
|
53199
|
+
},
|
|
53200
|
+
lastCheckedAt: connected ? Date.now() : null,
|
|
53201
|
+
error: connected ? null : entry.clientId.length === 0 ? "Configure Client ID / secret, then Connect Google" : "Not connected — open Connect Google on this broker"
|
|
53202
|
+
};
|
|
53203
|
+
}
|
|
53204
|
+
listBrokerInfos(addonId, kind) {
|
|
53205
|
+
if (kind !== void 0 && kind !== "google") return [];
|
|
53206
|
+
return this.entries.map((e) => this.toBrokerInfo(e, addonId));
|
|
53207
|
+
}
|
|
53208
|
+
size() {
|
|
53209
|
+
return this.entries.length;
|
|
53210
|
+
}
|
|
53211
|
+
connectedSize() {
|
|
53212
|
+
return this.entries.filter((e) => this.isConnected(e.id)).length;
|
|
53213
|
+
}
|
|
53214
|
+
toConfig(calendarIds = []) {
|
|
53215
|
+
return {
|
|
53216
|
+
brokers: [...this.entries],
|
|
53217
|
+
calendarIds: [...calendarIds]
|
|
53218
|
+
};
|
|
53219
|
+
}
|
|
53220
|
+
seedCounterFromEntries(entries) {
|
|
53221
|
+
let maxSeen = 0;
|
|
53222
|
+
for (const e of entries) {
|
|
53223
|
+
const match = e.id.match(/^google_(\d+)$/);
|
|
53224
|
+
if (match?.[1] !== void 0) maxSeen = Math.max(maxSeen, Number.parseInt(match[1], 10));
|
|
53225
|
+
}
|
|
53226
|
+
this.nextIdCounter = maxSeen + 1;
|
|
53227
|
+
}
|
|
53228
|
+
};
|
|
53229
|
+
//#endregion
|
|
53230
|
+
//#region src/google-settings-copy.ts
|
|
53231
|
+
/**
|
|
53232
|
+
* Operator-facing setup copy for Connectivity → Calendars / Email.
|
|
53233
|
+
* Keep Connect as a same-origin relative link so the admin session cookie
|
|
53234
|
+
* rides along (an absolute Public URL from a LAN admin tab drops the cookie).
|
|
53235
|
+
* Root-relative hrefs must survive `sanitizeInfoHtml` in ui-library.
|
|
53236
|
+
*/
|
|
53237
|
+
/** Enable APIs library in Google Cloud Console. */
|
|
53238
|
+
var GOOGLE_CONSOLE_APIS_URL = "https://console.cloud.google.com/apis/library";
|
|
53239
|
+
/** Create / manage OAuth Web clients. */
|
|
53240
|
+
var GOOGLE_CONSOLE_CREDENTIALS_URL = "https://console.cloud.google.com/apis/credentials";
|
|
53241
|
+
function redirectUriDisplay(publicOrigin) {
|
|
53242
|
+
if (publicOrigin === null || publicOrigin.length === 0) return `<public-hub>${OAUTH_CALLBACK_SUFFIX}`;
|
|
53243
|
+
return `${publicOrigin.replace(/\/$/, "")}${OAUTH_CALLBACK_SUFFIX}`;
|
|
53244
|
+
}
|
|
53245
|
+
function consoleLinksHtml$1() {
|
|
53246
|
+
return [
|
|
53247
|
+
`<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
|
|
53248
|
+
` · `,
|
|
53249
|
+
`<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
|
|
53250
|
+
].join("");
|
|
53251
|
+
}
|
|
53252
|
+
function introHtml(cap, publicOrigin) {
|
|
53253
|
+
const redirect = escapeHtml$2(redirectUriDisplay(publicOrigin));
|
|
53254
|
+
const console = consoleLinksHtml$1();
|
|
53255
|
+
if (cap === "smtp-provider") return [
|
|
53256
|
+
"<ol>",
|
|
53257
|
+
`<li>Open Google Cloud Console (${console}) and enable the <strong>Gmail API</strong> (and Calendar API if you also use Calendars).</li>`,
|
|
53258
|
+
"<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
|
|
53259
|
+
`<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
|
|
53260
|
+
"<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
|
|
53261
|
+
`<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
|
|
53262
|
+
"<li>Set From address (connected account or a verified send-as). Turn this integration off with the page <strong>Enable</strong> toggle when you do not want Gmail send — Calendar can stay connected.</li>",
|
|
53263
|
+
"</ol>"
|
|
53264
|
+
].join("");
|
|
53265
|
+
if (cap === "calendar-source") return [
|
|
53266
|
+
"<ol>",
|
|
53267
|
+
`<li>Open Google Cloud Console (${console}) and enable the <strong>Google Calendar API</strong> (and Gmail API if you also use Email).</li>`,
|
|
53268
|
+
"<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
|
|
53269
|
+
`<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
|
|
53270
|
+
"<li>Paste Client ID and Client secret below, then <strong>Save</strong>.</li>",
|
|
53271
|
+
`<li>Open <a href="${OAUTH_START_PATH}">Connect Google</a> while signed in as admin (same browser tab as this hub).</li>`,
|
|
53272
|
+
"<li>Optionally restrict which calendars are exposed with the filter below.</li>",
|
|
53273
|
+
"</ol>"
|
|
53274
|
+
].join("");
|
|
53275
|
+
return [
|
|
53276
|
+
"<ol>",
|
|
53277
|
+
`<li>Open Google Cloud Console (${console}) and enable <strong>Google Calendar API</strong> and <strong>Gmail API</strong>.</li>`,
|
|
53278
|
+
"<li>Create a <strong>Web application</strong> OAuth client under Credentials.</li>",
|
|
53279
|
+
`<li>Register redirect URI: <code>${redirect}</code> (Network → Public endpoint).</li>`,
|
|
53280
|
+
`<li>Paste Client ID / secret, Save, then <a href="${OAUTH_START_PATH}">Connect Google</a>.</li>`,
|
|
53281
|
+
"<li>Calendars and Email share one Google account; use each page’s <strong>Enable</strong> toggle to turn a capability off without revoking OAuth.</li>",
|
|
53282
|
+
"</ol>"
|
|
53283
|
+
].join("");
|
|
53284
|
+
}
|
|
53285
|
+
function connectHtml(cap, publicOrigin) {
|
|
53286
|
+
const redirect = escapeHtml$2(redirectUriDisplay(publicOrigin));
|
|
53287
|
+
if (cap === "smtp-provider") return [
|
|
53288
|
+
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
|
|
53289
|
+
"Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
|
|
53290
|
+
`Google must list exactly: <code>${redirect}</code>. `,
|
|
53291
|
+
"If an older Connect predates Gmail send, revoke CamStack in Google Account → Security and Connect again. ",
|
|
53292
|
+
`Console: ${consoleLinksHtml$1()}.`
|
|
53293
|
+
].join("");
|
|
53294
|
+
if (cap === "calendar-source") return [
|
|
53295
|
+
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
|
|
53296
|
+
"Connect requests <code>calendar.readonly</code> (and Gmail send for the Email page). ",
|
|
53297
|
+
`Google must list exactly: <code>${redirect}</code>. `,
|
|
53298
|
+
`Console: ${consoleLinksHtml$1()}.`
|
|
53299
|
+
].join("");
|
|
53300
|
+
return [
|
|
53301
|
+
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a>. `,
|
|
53302
|
+
`Redirect URI registered in Google Cloud must be <code>${redirect}</code>. `,
|
|
53303
|
+
`Console: ${consoleLinksHtml$1()}.`
|
|
53304
|
+
].join("");
|
|
53305
|
+
}
|
|
53306
|
+
function escapeHtml$2(s) {
|
|
53307
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
53308
|
+
}
|
|
53309
|
+
//#endregion
|
|
53310
|
+
//#region src/google-broker-settings-schema.ts
|
|
53311
|
+
/**
|
|
53312
|
+
* Per-kind broker creation/edit form for kind=`google`.
|
|
53313
|
+
* Account linking (Sign in) is driven by `listProviders.interactiveConnectPath`
|
|
53314
|
+
* in BrokerManager — not a hard-coded Connect link in this form.
|
|
53315
|
+
*/
|
|
53316
|
+
function consoleLinksHtml() {
|
|
53317
|
+
return [
|
|
53318
|
+
`<a href="${GOOGLE_CONSOLE_APIS_URL}">API library</a>`,
|
|
53319
|
+
` · `,
|
|
53320
|
+
`<a href="${GOOGLE_CONSOLE_CREDENTIALS_URL}">Credentials</a>`
|
|
53321
|
+
].join("");
|
|
53322
|
+
}
|
|
53323
|
+
/** Setup steps shown on Add / Edit — includes Audience / Test users. */
|
|
53324
|
+
function googleBrokerSetupHtml(publicOrigin) {
|
|
53325
|
+
const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
|
|
53326
|
+
return [
|
|
53327
|
+
"<ol>",
|
|
53328
|
+
`<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>`,
|
|
53329
|
+
"<li>In that project, enable <strong>Google Calendar API</strong> and/or <strong>Gmail API</strong> (API library).</li>",
|
|
53330
|
+
"<li>Open <strong>Google Auth Platform → Audience</strong> (or APIs & Services → OAuth consent screen → <strong>Audience</strong>). User type <strong>External</strong> + publishing status <strong>Testing</strong> is fine for a personal hub. Under <strong>Test users</strong>, add the Google account(s) that will Sign in — otherwise Google returns <code>403: access_denied</code>. (If you only see Branding / Data Access, open the <strong>Audience</strong> tab in the left sidebar of Google Auth Platform.) Publishing / verification is optional for a private hub.</li>",
|
|
53331
|
+
"<li>Under Credentials, create an OAuth client of type <strong>Web application</strong>.</li>",
|
|
53332
|
+
`<li>Add this authorized redirect URI (Network → <strong>Public endpoint</strong>, not Internal): <code>${redirect}</code></li>`,
|
|
53333
|
+
"<li>Paste Client ID and Client secret below. <strong>Test connection</strong> checks those with Google (not the account yet).</li>",
|
|
53334
|
+
"<li>Add the broker — the hub then opens Google sign-in in a popup to link the account (Calendar + Gmail). Use <strong>Sign in</strong> on the row if you skip or cancel.</li>",
|
|
53335
|
+
"</ol>"
|
|
53336
|
+
].join("");
|
|
53337
|
+
}
|
|
53338
|
+
function buildGoogleBrokerSettingsSchema(publicOrigin) {
|
|
53339
|
+
return { sections: [
|
|
53340
|
+
{
|
|
53341
|
+
id: "setup-intro",
|
|
53342
|
+
title: "Setup",
|
|
53343
|
+
description: "Google Cloud Console — register the hub redirect before Add / Sign in.",
|
|
53344
|
+
columns: 1,
|
|
53345
|
+
fields: [{
|
|
53346
|
+
type: "info",
|
|
53347
|
+
key: "_setupSteps",
|
|
53348
|
+
label: "Steps",
|
|
53349
|
+
format: "html",
|
|
53350
|
+
content: googleBrokerSetupHtml(publicOrigin)
|
|
53351
|
+
}]
|
|
53352
|
+
},
|
|
53353
|
+
{
|
|
53354
|
+
id: "google-oauth-client",
|
|
53355
|
+
title: "Google OAuth client",
|
|
53356
|
+
description: "Web application client from Google Cloud Console. The same Cloud project can be reused across Google brokers.",
|
|
53357
|
+
columns: 1,
|
|
53358
|
+
fields: [{
|
|
53359
|
+
type: "text",
|
|
53360
|
+
key: "clientId",
|
|
53361
|
+
label: "Client ID"
|
|
53362
|
+
}, {
|
|
53363
|
+
type: "password",
|
|
53364
|
+
key: "clientSecret",
|
|
53365
|
+
label: "Client secret",
|
|
53366
|
+
showToggle: true
|
|
53367
|
+
}]
|
|
53368
|
+
},
|
|
53369
|
+
{
|
|
53370
|
+
id: "gmail-send",
|
|
53371
|
+
title: "Gmail send",
|
|
53372
|
+
description: "From must be this connected account or a verified send-as. Disable Email on Connectivity to turn Gmail off without removing the broker.",
|
|
53373
|
+
columns: 1,
|
|
53374
|
+
fields: [{
|
|
53375
|
+
type: "text",
|
|
53376
|
+
key: "fromAddress",
|
|
53377
|
+
label: "From address",
|
|
53378
|
+
description: "Defaults to the connected Google account email when empty."
|
|
53379
|
+
}, {
|
|
53380
|
+
type: "text",
|
|
53381
|
+
key: "fromName",
|
|
53382
|
+
label: "From display name (optional)"
|
|
53383
|
+
}]
|
|
53384
|
+
}
|
|
53385
|
+
] };
|
|
53386
|
+
}
|
|
53387
|
+
/** Connectivity Calendars / Email pages — calendar import picker lives here (not on broker Add). */
|
|
53388
|
+
function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
53389
|
+
const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
|
|
53390
|
+
if (cap === "calendar-source") return { sections: [{
|
|
53391
|
+
id: "setup-intro",
|
|
53392
|
+
title: "Google brokers",
|
|
53393
|
+
columns: 1,
|
|
53394
|
+
fields: [{
|
|
53395
|
+
type: "info",
|
|
53396
|
+
key: "_setupSteps",
|
|
53397
|
+
label: "Setup",
|
|
53398
|
+
format: "html",
|
|
53399
|
+
content: [
|
|
53400
|
+
"<p>Link a Google account under Connectivity → <strong>Brokers</strong> ",
|
|
53401
|
+
"(Client ID / secret + Sign in). Then pick calendars below. ",
|
|
53402
|
+
`OAuth redirect URI: <code>${redirect}</code>.</p>`
|
|
53403
|
+
].join("")
|
|
53404
|
+
}]
|
|
53405
|
+
}, {
|
|
53406
|
+
id: "google-calendars-import",
|
|
53407
|
+
title: "Calendars to import",
|
|
53408
|
+
description: "Nothing selected = no calendars imported. Options appear after at least one Google broker is signed in.",
|
|
53409
|
+
columns: 1,
|
|
53410
|
+
fields: [{
|
|
53411
|
+
type: "multiselect",
|
|
53412
|
+
key: "calendarIds",
|
|
53413
|
+
label: "Calendars",
|
|
53414
|
+
options: [],
|
|
53415
|
+
optionsFrom: "_calendarCatalog",
|
|
53416
|
+
description: "Ids are stamped as brokerId:googleCalendarId across every Google broker."
|
|
53417
|
+
}]
|
|
53418
|
+
}] };
|
|
53419
|
+
return { sections: [{
|
|
53420
|
+
id: "setup-intro",
|
|
53421
|
+
title: cap === "smtp-provider" ? "Gmail send uses Google brokers" : "Google brokers",
|
|
53422
|
+
columns: 1,
|
|
53423
|
+
fields: [{
|
|
53424
|
+
type: "info",
|
|
53425
|
+
key: "_setupSteps",
|
|
53426
|
+
label: "Setup",
|
|
53427
|
+
format: "html",
|
|
53428
|
+
content: cap === "smtp-provider" ? [
|
|
53429
|
+
"<p>Add or edit a <strong>Google</strong> broker under Connectivity → <strong>Brokers</strong> ",
|
|
53430
|
+
"(Client ID / secret + Sign in). From address and send-as live on that broker. ",
|
|
53431
|
+
`OAuth redirect URI: <code>${redirect}</code>.</p>`
|
|
53432
|
+
].join("") : [
|
|
53433
|
+
"<p>Add or edit a <strong>Google</strong> broker under Connectivity → <strong>Brokers</strong> ",
|
|
53434
|
+
"(Client ID / secret + Sign in). ",
|
|
53435
|
+
`OAuth redirect URI: <code>${redirect}</code>.</p>`
|
|
53436
|
+
].join("")
|
|
53437
|
+
}]
|
|
53438
|
+
}] };
|
|
53439
|
+
}
|
|
53440
|
+
function escapeHtml$1(s) {
|
|
53441
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
53442
|
+
}
|
|
53443
|
+
//#endregion
|
|
52831
53444
|
//#region src/google-calendar-source.ts
|
|
52832
53445
|
function parseGoogleInstant(value) {
|
|
52833
53446
|
const ms = Date.parse(value);
|
|
@@ -52890,39 +53503,75 @@ function createGoogleCalendarApi(getAccessToken, http = { fetch }) {
|
|
|
52890
53503
|
function createGoogleCalendarSource(deps) {
|
|
52891
53504
|
return {
|
|
52892
53505
|
listCalendars: async () => {
|
|
52893
|
-
|
|
52894
|
-
|
|
52895
|
-
|
|
52896
|
-
|
|
52897
|
-
|
|
52898
|
-
|
|
52899
|
-
|
|
52900
|
-
|
|
53506
|
+
const selected = deps.selectedCalendarIds();
|
|
53507
|
+
if (selected.size === 0) return [];
|
|
53508
|
+
const out = [];
|
|
53509
|
+
for (const account of deps.accounts()) {
|
|
53510
|
+
if (!account.connected) continue;
|
|
53511
|
+
try {
|
|
53512
|
+
const list = await account.api.listCalendars();
|
|
53513
|
+
for (const c of list) {
|
|
53514
|
+
const stamped = stampCalendarId(account.brokerId, c.id);
|
|
53515
|
+
if (!selected.has(stamped)) continue;
|
|
53516
|
+
out.push({
|
|
53517
|
+
id: stamped,
|
|
53518
|
+
name: c.summary,
|
|
53519
|
+
source: "addon",
|
|
53520
|
+
enabled: true
|
|
53521
|
+
});
|
|
53522
|
+
}
|
|
53523
|
+
} catch (err) {
|
|
53524
|
+
deps.logger.warn("google calendar listCalendars failed", { meta: {
|
|
53525
|
+
brokerId: account.brokerId,
|
|
53526
|
+
error: err instanceof Error ? err.message : String(err)
|
|
53527
|
+
} });
|
|
53528
|
+
}
|
|
53529
|
+
}
|
|
53530
|
+
return out;
|
|
52901
53531
|
},
|
|
52902
53532
|
listEvents: async (input) => {
|
|
52903
|
-
|
|
52904
|
-
|
|
52905
|
-
let calendarIds = input.calendarIds;
|
|
52906
|
-
if (calendarIds === void 0) calendarIds = (await deps.api.listCalendars()).filter((c) => filter.size === 0 ? true : filter.has(c.id)).map((c) => c.id);
|
|
52907
|
-
else if (filter.size > 0) calendarIds = calendarIds.filter((id) => filter.has(id));
|
|
53533
|
+
const selected = deps.selectedCalendarIds();
|
|
53534
|
+
if (selected.size === 0) return [];
|
|
52908
53535
|
const timeMin = new Date(input.startMs).toISOString();
|
|
52909
53536
|
const timeMax = new Date(input.endMs).toISOString();
|
|
53537
|
+
const accounts = deps.accounts().filter((a) => a.connected);
|
|
53538
|
+
const byBroker = new Map(accounts.map((a) => [a.brokerId, a]));
|
|
53539
|
+
const requestIds = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? input.calendarIds : [...selected];
|
|
53540
|
+
/** brokerId → google calendar ids to fetch */
|
|
53541
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
53542
|
+
for (const stamped of requestIds) {
|
|
53543
|
+
if (!selected.has(stamped)) continue;
|
|
53544
|
+
const parsed = parseStampedCalendarId(stamped);
|
|
53545
|
+
if (parsed === null) continue;
|
|
53546
|
+
if (!byBroker.has(parsed.brokerId)) continue;
|
|
53547
|
+
const list = wanted.get(parsed.brokerId) ?? [];
|
|
53548
|
+
list.push(parsed.googleCalendarId);
|
|
53549
|
+
wanted.set(parsed.brokerId, list);
|
|
53550
|
+
}
|
|
52910
53551
|
const out = [];
|
|
52911
|
-
for (const
|
|
52912
|
-
const
|
|
52913
|
-
|
|
52914
|
-
|
|
52915
|
-
|
|
52916
|
-
|
|
52917
|
-
|
|
52918
|
-
|
|
52919
|
-
|
|
53552
|
+
for (const [brokerId, googleIds] of wanted) {
|
|
53553
|
+
const account = byBroker.get(brokerId);
|
|
53554
|
+
if (account === void 0) continue;
|
|
53555
|
+
for (const googleCalendarId of googleIds) {
|
|
53556
|
+
const stampedId = stampCalendarId(brokerId, googleCalendarId);
|
|
53557
|
+
try {
|
|
53558
|
+
const events = await account.api.listEvents({
|
|
53559
|
+
calendarId: googleCalendarId,
|
|
53560
|
+
timeMin,
|
|
53561
|
+
timeMax
|
|
53562
|
+
});
|
|
53563
|
+
for (const ev of events) {
|
|
53564
|
+
const mapped = mapGoogleEvent(stampedId, ev);
|
|
53565
|
+
if (mapped) out.push(mapped);
|
|
53566
|
+
}
|
|
53567
|
+
} catch (err) {
|
|
53568
|
+
deps.logger.warn("google calendar listEvents failed", { meta: {
|
|
53569
|
+
brokerId,
|
|
53570
|
+
calendarId: googleCalendarId,
|
|
53571
|
+
error: err instanceof Error ? err.message : String(err)
|
|
53572
|
+
} });
|
|
53573
|
+
}
|
|
52920
53574
|
}
|
|
52921
|
-
} catch (err) {
|
|
52922
|
-
deps.logger.warn("google calendar listEvents failed", { meta: {
|
|
52923
|
-
calendarId,
|
|
52924
|
-
error: err instanceof Error ? err.message : String(err)
|
|
52925
|
-
} });
|
|
52926
53575
|
}
|
|
52927
53576
|
return out;
|
|
52928
53577
|
}
|
|
@@ -52940,12 +53589,26 @@ function encodeHeaderWord(value) {
|
|
|
52940
53589
|
function formatAddressList(addrs) {
|
|
52941
53590
|
return (Array.isArray(addrs) ? addrs : [addrs]).join(", ");
|
|
52942
53591
|
}
|
|
52943
|
-
function buildFromHeader(
|
|
52944
|
-
const addr =
|
|
53592
|
+
function buildFromHeader(account) {
|
|
53593
|
+
const addr = account.fromAddress.trim() || account.accountEmail;
|
|
52945
53594
|
if (!addr) return "";
|
|
52946
|
-
if (
|
|
53595
|
+
if (account.fromName.trim()) return `${encodeHeaderWord(account.fromName.trim())} <${addr}>`;
|
|
52947
53596
|
return addr;
|
|
52948
53597
|
}
|
|
53598
|
+
function extractEmail(fromHeader) {
|
|
53599
|
+
const angled = /<([^>]+)>/.exec(fromHeader);
|
|
53600
|
+
if (angled?.[1] !== void 0) return angled[1].trim().toLowerCase();
|
|
53601
|
+
return fromHeader.trim().toLowerCase();
|
|
53602
|
+
}
|
|
53603
|
+
/** Pick account: match From to accountEmail/fromAddress; else first connected. */
|
|
53604
|
+
function selectGmailAccount(accounts, fromHeader) {
|
|
53605
|
+
const connected = accounts.filter((a) => a.connected);
|
|
53606
|
+
if (connected.length === 0) return null;
|
|
53607
|
+
if (fromHeader === void 0 || fromHeader.trim().length === 0) return connected[0] ?? null;
|
|
53608
|
+
const want = extractEmail(fromHeader);
|
|
53609
|
+
for (const a of connected) if ([a.accountEmail, a.fromAddress].map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0).includes(want)) return a;
|
|
53610
|
+
return connected[0] ?? null;
|
|
53611
|
+
}
|
|
52949
53612
|
/** Build a minimal multipart/alternative-capable plain (and optional HTML) message. */
|
|
52950
53613
|
function buildRawMime(input) {
|
|
52951
53614
|
const lines = [`From: ${input.from}`, `To: ${formatAddressList(input.to)}`];
|
|
@@ -52982,17 +53645,17 @@ function createGmailSmtpProvider(deps) {
|
|
|
52982
53645
|
let lastVerifiedAt;
|
|
52983
53646
|
let lastError;
|
|
52984
53647
|
const status = () => {
|
|
52985
|
-
const
|
|
52986
|
-
const
|
|
52987
|
-
if (
|
|
53648
|
+
const connected = deps.accounts().filter((a) => a.connected);
|
|
53649
|
+
const host = connected[0]?.accountEmail || connected[0]?.fromAddress.trim() || (connected.length > 0 ? "gmail" : "gmail");
|
|
53650
|
+
if (connected.length === 0) return {
|
|
52988
53651
|
ready: false,
|
|
52989
|
-
host
|
|
52990
|
-
error: "Google
|
|
53652
|
+
host,
|
|
53653
|
+
error: "No Google broker connected — add one under Connectivity → Brokers and Connect.",
|
|
52991
53654
|
lastVerifiedAt
|
|
52992
53655
|
};
|
|
52993
53656
|
return {
|
|
52994
53657
|
ready: lastError === void 0 && lastVerifiedAt !== void 0,
|
|
52995
|
-
host:
|
|
53658
|
+
host: connected.length === 1 ? host : `${host} (+${connected.length - 1})`,
|
|
52996
53659
|
error: lastError,
|
|
52997
53660
|
lastVerifiedAt
|
|
52998
53661
|
};
|
|
@@ -53000,12 +53663,13 @@ function createGmailSmtpProvider(deps) {
|
|
|
53000
53663
|
return {
|
|
53001
53664
|
getStatus: async () => status(),
|
|
53002
53665
|
verify: async () => {
|
|
53003
|
-
|
|
53004
|
-
|
|
53666
|
+
const account = selectGmailAccount(deps.accounts(), void 0);
|
|
53667
|
+
if (account === null) {
|
|
53668
|
+
lastError = "No Google broker connected.";
|
|
53005
53669
|
return status();
|
|
53006
53670
|
}
|
|
53007
53671
|
try {
|
|
53008
|
-
const token = await
|
|
53672
|
+
const token = await account.getAccessToken();
|
|
53009
53673
|
const res = await http(`${GOOGLE_GMAIL_API}/users/me/profile`, { headers: { Authorization: `Bearer ${token}` } });
|
|
53010
53674
|
if (!res.ok) {
|
|
53011
53675
|
const text = await res.text();
|
|
@@ -53021,10 +53685,10 @@ function createGmailSmtpProvider(deps) {
|
|
|
53021
53685
|
}
|
|
53022
53686
|
},
|
|
53023
53687
|
sendEmail: async (input) => {
|
|
53024
|
-
const
|
|
53025
|
-
if (
|
|
53026
|
-
const from = input.from?.trim() || buildFromHeader(
|
|
53027
|
-
if (!from) throw new Error("Set From address
|
|
53688
|
+
const account = selectGmailAccount(deps.accounts(), input.from);
|
|
53689
|
+
if (account === null) throw new Error("No Google broker connected — add one under Connectivity → Brokers and Connect.");
|
|
53690
|
+
const from = input.from?.trim() || buildFromHeader(account);
|
|
53691
|
+
if (!from) throw new Error("Set From address on the Google broker (or Connect an account).");
|
|
53028
53692
|
const raw = buildRawMime({
|
|
53029
53693
|
from,
|
|
53030
53694
|
to: input.to,
|
|
@@ -53035,7 +53699,7 @@ function createGmailSmtpProvider(deps) {
|
|
|
53035
53699
|
text: input.text,
|
|
53036
53700
|
html: input.html
|
|
53037
53701
|
});
|
|
53038
|
-
const token = await
|
|
53702
|
+
const token = await account.getAccessToken();
|
|
53039
53703
|
const res = await http(`${GOOGLE_GMAIL_API}/users/me/messages/send`, {
|
|
53040
53704
|
method: "POST",
|
|
53041
53705
|
headers: {
|
|
@@ -53126,85 +53790,59 @@ function needsRefresh(state, nowMs = Date.now()) {
|
|
|
53126
53790
|
if (!state.accessToken) return true;
|
|
53127
53791
|
return state.accessExpiresAt <= nowMs;
|
|
53128
53792
|
}
|
|
53129
|
-
//#endregion
|
|
53130
|
-
//#region src/google-settings-copy.ts
|
|
53131
53793
|
/**
|
|
53132
|
-
*
|
|
53133
|
-
*
|
|
53134
|
-
*
|
|
53135
|
-
* Root-relative hrefs must survive `sanitizeInfoHtml` in ui-library.
|
|
53794
|
+
* Probe Client ID / secret against Google's token endpoint with a throwaway
|
|
53795
|
+
* authorization code. `invalid_client` means bad credentials; `invalid_grant`
|
|
53796
|
+
* / `redirect_uri_mismatch` mean Google accepted the client (code was fake).
|
|
53136
53797
|
*/
|
|
53137
|
-
|
|
53138
|
-
|
|
53139
|
-
|
|
53140
|
-
|
|
53141
|
-
|
|
53142
|
-
|
|
53143
|
-
|
|
53144
|
-
|
|
53145
|
-
|
|
53146
|
-
|
|
53147
|
-
|
|
53148
|
-
|
|
53149
|
-
|
|
53150
|
-
|
|
53151
|
-
|
|
53152
|
-
|
|
53153
|
-
|
|
53154
|
-
|
|
53155
|
-
|
|
53156
|
-
|
|
53157
|
-
|
|
53158
|
-
|
|
53159
|
-
|
|
53160
|
-
|
|
53161
|
-
|
|
53162
|
-
|
|
53163
|
-
"
|
|
53164
|
-
|
|
53165
|
-
if (
|
|
53166
|
-
|
|
53167
|
-
|
|
53168
|
-
|
|
53169
|
-
|
|
53170
|
-
|
|
53171
|
-
|
|
53172
|
-
|
|
53173
|
-
|
|
53174
|
-
|
|
53175
|
-
|
|
53176
|
-
|
|
53177
|
-
|
|
53178
|
-
|
|
53179
|
-
|
|
53180
|
-
|
|
53181
|
-
|
|
53182
|
-
|
|
53183
|
-
|
|
53184
|
-
}
|
|
53185
|
-
function connectHtml(cap, publicOrigin) {
|
|
53186
|
-
const redirect = escapeHtml$1(redirectUriDisplay(publicOrigin));
|
|
53187
|
-
if (cap === "smtp-provider") return [
|
|
53188
|
-
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
|
|
53189
|
-
"Connect requests <code>gmail.send</code> (and Calendar read for the Calendars page). ",
|
|
53190
|
-
`Google must list exactly: <code>${redirect}</code>. `,
|
|
53191
|
-
"If an older Connect predates Gmail send, revoke CamStack in Google Account → Security and Connect again. ",
|
|
53192
|
-
`Console: ${consoleLinksHtml()}.`
|
|
53193
|
-
].join("");
|
|
53194
|
-
if (cap === "calendar-source") return [
|
|
53195
|
-
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a> in this hub (admin session required). `,
|
|
53196
|
-
"Connect requests <code>calendar.readonly</code> (and Gmail send for the Email page). ",
|
|
53197
|
-
`Google must list exactly: <code>${redirect}</code>. `,
|
|
53198
|
-
`Console: ${consoleLinksHtml()}.`
|
|
53199
|
-
].join("");
|
|
53200
|
-
return [
|
|
53201
|
-
`After Save, open <a href="${OAUTH_START_PATH}">Connect Google</a>. `,
|
|
53202
|
-
`Redirect URI registered in Google Cloud must be <code>${redirect}</code>. `,
|
|
53203
|
-
`Console: ${consoleLinksHtml()}.`
|
|
53204
|
-
].join("");
|
|
53205
|
-
}
|
|
53206
|
-
function escapeHtml$1(s) {
|
|
53207
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
53798
|
+
async function probeGoogleOauthClient(cfg, http = { fetch }) {
|
|
53799
|
+
const started = Date.now();
|
|
53800
|
+
const body = new URLSearchParams({
|
|
53801
|
+
code: "camstack-probe-invalid",
|
|
53802
|
+
client_id: cfg.clientId,
|
|
53803
|
+
client_secret: cfg.clientSecret,
|
|
53804
|
+
redirect_uri: cfg.redirectUri,
|
|
53805
|
+
grant_type: "authorization_code"
|
|
53806
|
+
});
|
|
53807
|
+
let res;
|
|
53808
|
+
try {
|
|
53809
|
+
res = await http.fetch(GOOGLE_TOKEN_URL, {
|
|
53810
|
+
method: "POST",
|
|
53811
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
53812
|
+
body
|
|
53813
|
+
});
|
|
53814
|
+
} catch (err) {
|
|
53815
|
+
return {
|
|
53816
|
+
ok: false,
|
|
53817
|
+
error: err instanceof Error ? err.message : String(err)
|
|
53818
|
+
};
|
|
53819
|
+
}
|
|
53820
|
+
const text = await res.text();
|
|
53821
|
+
let errorCode = "";
|
|
53822
|
+
try {
|
|
53823
|
+
const json = JSON.parse(text);
|
|
53824
|
+
if (typeof json.error === "string") errorCode = json.error;
|
|
53825
|
+
} catch {}
|
|
53826
|
+
if (errorCode === "invalid_client") return {
|
|
53827
|
+
ok: false,
|
|
53828
|
+
error: "Invalid Client ID or Client secret (Google rejected the client)"
|
|
53829
|
+
};
|
|
53830
|
+
if (errorCode === "unauthorized_client") return {
|
|
53831
|
+
ok: false,
|
|
53832
|
+
error: "This OAuth client is not allowed to use the authorization-code grant"
|
|
53833
|
+
};
|
|
53834
|
+
if (errorCode === "invalid_grant" || errorCode === "redirect_uri_mismatch" || errorCode === "invalid_request") return {
|
|
53835
|
+
ok: true,
|
|
53836
|
+
latencyMs: Math.max(0, Date.now() - started)
|
|
53837
|
+
};
|
|
53838
|
+
if (res.ok) return {
|
|
53839
|
+
ok: true,
|
|
53840
|
+
latencyMs: Math.max(0, Date.now() - started)
|
|
53841
|
+
};
|
|
53842
|
+
return {
|
|
53843
|
+
ok: false,
|
|
53844
|
+
error: errorCode.length > 0 ? `Google token probe failed (${errorCode}): ${text.slice(0, 200)}` : `Google token probe failed (${res.status}): ${text.slice(0, 200)}`
|
|
53845
|
+
};
|
|
53208
53846
|
}
|
|
53209
53847
|
//#endregion
|
|
53210
53848
|
//#region src/oauth-redirect-origin.ts
|
|
@@ -53219,65 +53857,125 @@ function pickOauthRedirectOrigin(book) {
|
|
|
53219
53857
|
//#endregion
|
|
53220
53858
|
//#region src/google-services.addon.ts
|
|
53221
53859
|
/**
|
|
53222
|
-
* Google Services — hub-only calendar-source + Gmail smtp
|
|
53860
|
+
* Google Services — hub-only broker kind `google` + calendar-source + Gmail smtp.
|
|
53861
|
+
* One OAuth account = one broker entry; feature caps aggregate connected brokers.
|
|
53223
53862
|
*/
|
|
53224
53863
|
var ADDON_ID = "google-services";
|
|
53225
53864
|
var CALLBACK_PATH = "/oauth/callback";
|
|
53226
53865
|
var START_PATH = "/oauth/start";
|
|
53227
|
-
var
|
|
53228
|
-
|
|
53229
|
-
|
|
53866
|
+
var DISCONNECT_PATH = "/oauth/disconnect";
|
|
53867
|
+
var LegacyOauthStateSchema = object({
|
|
53868
|
+
refreshToken: string().default("").meta({ secret: true }),
|
|
53869
|
+
accessToken: string().default("").meta({ secret: true }),
|
|
53230
53870
|
accessExpiresAt: number().default(0),
|
|
53231
53871
|
accountEmail: string().default("")
|
|
53232
53872
|
});
|
|
53233
53873
|
var GoogleServicesAddon = class extends BaseAddon {
|
|
53234
|
-
secretStatePaths = [
|
|
53235
|
-
|
|
53236
|
-
|
|
53237
|
-
|
|
53874
|
+
secretStatePaths = [
|
|
53875
|
+
"oauthAccounts.accounts[id].refreshToken",
|
|
53876
|
+
"oauthAccounts.accounts[id].accessToken",
|
|
53877
|
+
"oauth.refreshToken",
|
|
53878
|
+
"oauth.accessToken"
|
|
53879
|
+
];
|
|
53880
|
+
oauthAccountsHandle = null;
|
|
53881
|
+
registry = new GoogleBrokerRegistry();
|
|
53882
|
+
/** CSRF state → { brokerId, popup, issuedAt } — popup mirrors OIDC D794. */
|
|
53238
53883
|
pendingStates = /* @__PURE__ */ new Map();
|
|
53239
|
-
/** Cached Public-endpoint origin for settings copy (redirect URI display). */
|
|
53240
53884
|
publicOrigin = null;
|
|
53241
53885
|
constructor() {
|
|
53242
53886
|
super({ ...DEFAULT_GOOGLE_CONFIG });
|
|
53243
53887
|
}
|
|
53244
53888
|
async onInitialize() {
|
|
53245
|
-
this.
|
|
53246
|
-
|
|
53889
|
+
this.oauthAccountsHandle = this.state("oauthAccounts", GoogleOauthAccountsStateSchema, { accounts: [] });
|
|
53890
|
+
let oauthAccounts = await this.oauthAccountsHandle.get();
|
|
53891
|
+
const legacyHandle = this.state("oauth", LegacyOauthStateSchema, { ...DEFAULT_OAUTH_STATE });
|
|
53892
|
+
const legacyOauth = await legacyHandle.get();
|
|
53893
|
+
const migrated = migrateLegacyToBrokers(this.config, legacyOauth);
|
|
53894
|
+
if (migrated.didMigrate) {
|
|
53895
|
+
await this.updateGlobalSettings({
|
|
53896
|
+
brokers: [...migrated.brokers],
|
|
53897
|
+
clientId: void 0,
|
|
53898
|
+
clientSecret: void 0,
|
|
53899
|
+
calendarFilter: void 0,
|
|
53900
|
+
fromAddress: void 0,
|
|
53901
|
+
fromName: void 0
|
|
53902
|
+
});
|
|
53903
|
+
if (migrated.oauthAccounts.length > 0) {
|
|
53904
|
+
oauthAccounts = { accounts: [...migrated.oauthAccounts] };
|
|
53905
|
+
await this.oauthAccountsHandle.set(oauthAccounts);
|
|
53906
|
+
}
|
|
53907
|
+
await legacyHandle.set({ ...DEFAULT_OAUTH_STATE });
|
|
53908
|
+
this.ctx.logger.info("google-services: migrated singleton OAuth to broker google_001", { meta: {
|
|
53909
|
+
brokerId: "google_001",
|
|
53910
|
+
accountEmail: legacyOauth.accountEmail || void 0
|
|
53911
|
+
} });
|
|
53912
|
+
} else if (oauthAccounts.accounts.length === 0 && legacyOauth.refreshToken.length > 0 && this.config.brokers.length > 0) {
|
|
53913
|
+
oauthAccounts = { accounts: [{
|
|
53914
|
+
id: this.config.brokers[0].id,
|
|
53915
|
+
refreshToken: legacyOauth.refreshToken,
|
|
53916
|
+
accessToken: legacyOauth.accessToken,
|
|
53917
|
+
accessExpiresAt: legacyOauth.accessExpiresAt,
|
|
53918
|
+
accountEmail: legacyOauth.accountEmail
|
|
53919
|
+
}] };
|
|
53920
|
+
await this.oauthAccountsHandle.set(oauthAccounts);
|
|
53921
|
+
await legacyHandle.set({ ...DEFAULT_OAUTH_STATE });
|
|
53922
|
+
}
|
|
53923
|
+
this.registry.seedFrom(this.config.brokers, oauthAccounts.accounts);
|
|
53247
53924
|
await this.refreshPublicOrigin();
|
|
53248
|
-
const api = createGoogleCalendarApi(async () => this.ensureAccessToken());
|
|
53249
53925
|
const calendarProvider = createGoogleCalendarSource({
|
|
53250
53926
|
logger: this.ctx.logger,
|
|
53251
|
-
|
|
53252
|
-
|
|
53253
|
-
|
|
53927
|
+
accounts: () => this.registry.listEntries().map((entry) => ({
|
|
53928
|
+
brokerId: entry.id,
|
|
53929
|
+
connected: this.registry.isConnected(entry.id),
|
|
53930
|
+
api: createGoogleCalendarApi(() => this.ensureAccessToken(entry.id))
|
|
53931
|
+
})),
|
|
53932
|
+
selectedCalendarIds: () => new Set(this.config.calendarIds ?? [])
|
|
53254
53933
|
});
|
|
53255
|
-
const smtpProvider = createGmailSmtpProvider({
|
|
53256
|
-
|
|
53257
|
-
|
|
53258
|
-
|
|
53259
|
-
|
|
53260
|
-
|
|
53261
|
-
|
|
53262
|
-
|
|
53263
|
-
|
|
53264
|
-
|
|
53265
|
-
|
|
53266
|
-
|
|
53267
|
-
|
|
53268
|
-
|
|
53269
|
-
|
|
53270
|
-
|
|
53271
|
-
|
|
53272
|
-
|
|
53273
|
-
|
|
53274
|
-
|
|
53275
|
-
|
|
53276
|
-
|
|
53277
|
-
|
|
53934
|
+
const smtpProvider = createGmailSmtpProvider({ accounts: () => this.registry.listEntries().map((entry) => {
|
|
53935
|
+
const oauth = this.registry.getOauth(entry.id);
|
|
53936
|
+
return {
|
|
53937
|
+
brokerId: entry.id,
|
|
53938
|
+
accountEmail: oauth?.accountEmail ?? "",
|
|
53939
|
+
fromAddress: entry.fromAddress,
|
|
53940
|
+
fromName: entry.fromName,
|
|
53941
|
+
connected: this.registry.isConnected(entry.id),
|
|
53942
|
+
getAccessToken: () => this.ensureAccessToken(entry.id)
|
|
53943
|
+
};
|
|
53944
|
+
}) });
|
|
53945
|
+
const routeProvider = buildAddonRouteProvider(ADDON_ID, [
|
|
53946
|
+
{
|
|
53947
|
+
method: "GET",
|
|
53948
|
+
path: START_PATH,
|
|
53949
|
+
access: "admin",
|
|
53950
|
+
description: "Begin Google OAuth for a broker (Calendar + Gmail send)",
|
|
53951
|
+
handler: async (req, reply) => this.handleStart(req, reply)
|
|
53952
|
+
},
|
|
53953
|
+
{
|
|
53954
|
+
method: "GET",
|
|
53955
|
+
path: CALLBACK_PATH,
|
|
53956
|
+
access: "public",
|
|
53957
|
+
rateLimit: "auth",
|
|
53958
|
+
description: "Google OAuth callback",
|
|
53959
|
+
handler: async (req, reply) => this.handleCallback(req, reply)
|
|
53960
|
+
},
|
|
53961
|
+
{
|
|
53962
|
+
method: "GET",
|
|
53963
|
+
path: DISCONNECT_PATH,
|
|
53964
|
+
access: "admin",
|
|
53965
|
+
description: "Clear Google OAuth tokens for a broker (Sign out)",
|
|
53966
|
+
handler: async (req, reply) => this.handleDisconnect(req, reply)
|
|
53967
|
+
}
|
|
53968
|
+
]);
|
|
53969
|
+
this.ctx.logger.info("Initialized — google broker + calendar-source + gmail smtp", { meta: {
|
|
53970
|
+
brokerCount: this.registry.size(),
|
|
53971
|
+
connectedCount: this.registry.connectedSize(),
|
|
53278
53972
|
publicOrigin: this.publicOrigin ?? void 0
|
|
53279
53973
|
} });
|
|
53280
53974
|
return { providers: [
|
|
53975
|
+
{
|
|
53976
|
+
capability: brokerCapability,
|
|
53977
|
+
provider: this.buildBrokerProvider()
|
|
53978
|
+
},
|
|
53281
53979
|
{
|
|
53282
53980
|
capability: calendarSourceCapability,
|
|
53283
53981
|
provider: calendarProvider
|
|
@@ -53292,9 +53990,183 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
53292
53990
|
}
|
|
53293
53991
|
] };
|
|
53294
53992
|
}
|
|
53295
|
-
async
|
|
53296
|
-
|
|
53297
|
-
|
|
53993
|
+
async onConfigChanged() {
|
|
53994
|
+
const accounts = this.registry.listOauthAccounts();
|
|
53995
|
+
this.registry.seedFrom(this.config.brokers, accounts);
|
|
53996
|
+
}
|
|
53997
|
+
buildBrokerProvider() {
|
|
53998
|
+
const ownerAddonId = this.ctx.id;
|
|
53999
|
+
const owns = (id) => this.registry.getEntry(id) !== null;
|
|
54000
|
+
const notSupported = async () => {
|
|
54001
|
+
throw new Error("google-services broker: publish/subscribe/getState are not supported — use calendar-source / smtp-provider");
|
|
54002
|
+
};
|
|
54003
|
+
return {
|
|
54004
|
+
list: async ({ kind }) => this.registry.listBrokerInfos(ownerAddonId, kind),
|
|
54005
|
+
get: async ({ id }) => {
|
|
54006
|
+
const entry = this.registry.getEntry(id);
|
|
54007
|
+
return entry ? this.registry.toBrokerInfo(entry, ownerAddonId) : null;
|
|
54008
|
+
},
|
|
54009
|
+
listProviders: async () => [{
|
|
54010
|
+
addonId: ownerAddonId,
|
|
54011
|
+
kinds: [{
|
|
54012
|
+
kind: GOOGLE_BROKER_KIND,
|
|
54013
|
+
label: GOOGLE_BROKER_KIND_LABEL,
|
|
54014
|
+
interactiveConnectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
|
|
54015
|
+
interactiveDisconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`
|
|
54016
|
+
}]
|
|
54017
|
+
}],
|
|
54018
|
+
add: async ({ kind, name, settings, id }) => {
|
|
54019
|
+
if (kind !== "google") throw new Error(`google-services: only kind '${GOOGLE_BROKER_KIND}' is handled here (got '${kind}')`);
|
|
54020
|
+
const entry = this.registry.createEntry(name, settings, id !== void 0 ? { id } : {});
|
|
54021
|
+
await this.persistBrokers();
|
|
54022
|
+
return { id: entry.id };
|
|
54023
|
+
},
|
|
54024
|
+
remove: async ({ id }) => {
|
|
54025
|
+
if (!owns(id)) return;
|
|
54026
|
+
this.registry.removeEntry(id);
|
|
54027
|
+
await this.persistBrokers();
|
|
54028
|
+
await this.persistOauthAccounts();
|
|
54029
|
+
},
|
|
54030
|
+
testConnection: async ({ id }) => {
|
|
54031
|
+
if (!owns(id)) return {
|
|
54032
|
+
ok: false,
|
|
54033
|
+
error: "unknown broker"
|
|
54034
|
+
};
|
|
54035
|
+
const entry = this.registry.getEntry(id);
|
|
54036
|
+
if (entry === null) return {
|
|
54037
|
+
ok: false,
|
|
54038
|
+
error: "unknown broker"
|
|
54039
|
+
};
|
|
54040
|
+
if (!entry.clientId || !entry.clientSecret) return {
|
|
54041
|
+
ok: false,
|
|
54042
|
+
error: "Configure Client ID and Client secret first"
|
|
54043
|
+
};
|
|
54044
|
+
let redirectUri;
|
|
54045
|
+
try {
|
|
54046
|
+
redirectUri = await this.resolveRedirectUri();
|
|
54047
|
+
} catch (err) {
|
|
54048
|
+
return {
|
|
54049
|
+
ok: false,
|
|
54050
|
+
error: err instanceof Error ? err.message : String(err)
|
|
54051
|
+
};
|
|
54052
|
+
}
|
|
54053
|
+
if (!this.registry.isConnected(id)) return probeGoogleOauthClient({
|
|
54054
|
+
clientId: entry.clientId,
|
|
54055
|
+
clientSecret: entry.clientSecret,
|
|
54056
|
+
redirectUri
|
|
54057
|
+
});
|
|
54058
|
+
try {
|
|
54059
|
+
const started = Date.now();
|
|
54060
|
+
const token = await this.ensureAccessToken(id, { forceRefresh: true });
|
|
54061
|
+
if (token.trim().length === 0) return {
|
|
54062
|
+
ok: false,
|
|
54063
|
+
error: "No access token after refresh — Sign out and Sign in again"
|
|
54064
|
+
};
|
|
54065
|
+
let res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${token}` } });
|
|
54066
|
+
if (res.status === 401) {
|
|
54067
|
+
const retryToken = await this.ensureAccessToken(id, { forceRefresh: true });
|
|
54068
|
+
res = await fetch(GOOGLE_CALENDAR_LIST_URL, { headers: { Authorization: `Bearer ${retryToken}` } });
|
|
54069
|
+
}
|
|
54070
|
+
if (!res.ok) {
|
|
54071
|
+
const text = await res.text();
|
|
54072
|
+
return {
|
|
54073
|
+
ok: false,
|
|
54074
|
+
error: `Google Calendar probe failed (${res.status}): ${text.slice(0, 200)}`
|
|
54075
|
+
};
|
|
54076
|
+
}
|
|
54077
|
+
const email = await this.fetchAccountEmail(token);
|
|
54078
|
+
if (email.length > 0) {
|
|
54079
|
+
const oauth = this.registry.getOauth(id);
|
|
54080
|
+
if (oauth !== null && oauth.accountEmail !== email) {
|
|
54081
|
+
this.registry.setOauth({
|
|
54082
|
+
...oauth,
|
|
54083
|
+
accountEmail: email
|
|
54084
|
+
});
|
|
54085
|
+
await this.persistOauthAccounts();
|
|
54086
|
+
}
|
|
54087
|
+
}
|
|
54088
|
+
return {
|
|
54089
|
+
ok: true,
|
|
54090
|
+
latencyMs: Date.now() - started
|
|
54091
|
+
};
|
|
54092
|
+
} catch (err) {
|
|
54093
|
+
return {
|
|
54094
|
+
ok: false,
|
|
54095
|
+
error: err instanceof Error ? err.message : String(err)
|
|
54096
|
+
};
|
|
54097
|
+
}
|
|
54098
|
+
},
|
|
54099
|
+
getSettings: async ({ id }) => {
|
|
54100
|
+
const entry = this.registry.getEntry(id);
|
|
54101
|
+
if (entry === null) return null;
|
|
54102
|
+
return brokerEntryToSettings(entry);
|
|
54103
|
+
},
|
|
54104
|
+
setSettings: async ({ id, settings }) => {
|
|
54105
|
+
if (!owns(id)) return;
|
|
54106
|
+
this.registry.updateEntry(id, settings);
|
|
54107
|
+
await this.persistBrokers();
|
|
54108
|
+
},
|
|
54109
|
+
getBrokerConfig: async ({ id }) => {
|
|
54110
|
+
const entry = this.registry.getEntry(id);
|
|
54111
|
+
if (entry === null) return null;
|
|
54112
|
+
const oauth = this.registry.getOauth(id);
|
|
54113
|
+
return {
|
|
54114
|
+
...brokerEntryToSettings(entry),
|
|
54115
|
+
accountEmail: oauth?.accountEmail ?? "",
|
|
54116
|
+
connected: this.registry.isConnected(id)
|
|
54117
|
+
};
|
|
54118
|
+
},
|
|
54119
|
+
getSettingsSchema: async ({ kind }) => {
|
|
54120
|
+
if (kind !== "google") return null;
|
|
54121
|
+
return buildGoogleBrokerSettingsSchema(this.publicOrigin);
|
|
54122
|
+
},
|
|
54123
|
+
testSettings: async ({ kind, settings }) => {
|
|
54124
|
+
if (kind !== "google") return {
|
|
54125
|
+
ok: false,
|
|
54126
|
+
error: `unsupported kind: ${kind}`
|
|
54127
|
+
};
|
|
54128
|
+
try {
|
|
54129
|
+
const parsed = object({
|
|
54130
|
+
clientId: string().min(1),
|
|
54131
|
+
clientSecret: string().min(1).meta({ secret: true })
|
|
54132
|
+
}).parse(settings);
|
|
54133
|
+
const redirectUri = await this.resolveRedirectUri();
|
|
54134
|
+
return probeGoogleOauthClient({
|
|
54135
|
+
clientId: parsed.clientId,
|
|
54136
|
+
clientSecret: parsed.clientSecret,
|
|
54137
|
+
redirectUri
|
|
54138
|
+
});
|
|
54139
|
+
} catch (err) {
|
|
54140
|
+
return {
|
|
54141
|
+
ok: false,
|
|
54142
|
+
error: err instanceof Error ? err.message : String(err)
|
|
54143
|
+
};
|
|
54144
|
+
}
|
|
54145
|
+
},
|
|
54146
|
+
publish: notSupported,
|
|
54147
|
+
subscribe: notSupported,
|
|
54148
|
+
unsubscribe: notSupported,
|
|
54149
|
+
getState: notSupported,
|
|
54150
|
+
getStatus: async () => ({
|
|
54151
|
+
brokerCount: this.registry.size(),
|
|
54152
|
+
connectedCount: this.registry.connectedSize()
|
|
54153
|
+
})
|
|
54154
|
+
};
|
|
54155
|
+
}
|
|
54156
|
+
async handleStart(req, reply) {
|
|
54157
|
+
const brokerIdRaw = req.query["brokerId"];
|
|
54158
|
+
const brokerId = typeof brokerIdRaw === "string" ? brokerIdRaw.trim() : "";
|
|
54159
|
+
if (!brokerId) {
|
|
54160
|
+
reply.status(400).send({ error: "Missing brokerId query parameter — Connect from Connectivity → Brokers → Edit." });
|
|
54161
|
+
return;
|
|
54162
|
+
}
|
|
54163
|
+
const entry = this.registry.getEntry(brokerId);
|
|
54164
|
+
if (entry === null) {
|
|
54165
|
+
reply.status(404).send({ error: `Unknown Google broker: ${brokerId}` });
|
|
54166
|
+
return;
|
|
54167
|
+
}
|
|
54168
|
+
if (!entry.clientId || !entry.clientSecret) {
|
|
54169
|
+
reply.status(400).send({ error: "Configure Client ID and Client secret on this broker, Save, then Connect." });
|
|
53298
54170
|
return;
|
|
53299
54171
|
}
|
|
53300
54172
|
let redirectUri;
|
|
@@ -53304,12 +54176,18 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
53304
54176
|
reply.status(500).send({ error: err instanceof Error ? err.message : String(err) });
|
|
53305
54177
|
return;
|
|
53306
54178
|
}
|
|
54179
|
+
const popupRaw = req.query["popup"];
|
|
54180
|
+
const popup = popupRaw === "1" || popupRaw === "true";
|
|
53307
54181
|
const state = (0, node_crypto.randomBytes)(16).toString("hex");
|
|
53308
|
-
this.pendingStates.set(state,
|
|
54182
|
+
this.pendingStates.set(state, {
|
|
54183
|
+
brokerId,
|
|
54184
|
+
popup,
|
|
54185
|
+
issuedAt: Date.now()
|
|
54186
|
+
});
|
|
53309
54187
|
this.prunePendingStates();
|
|
53310
54188
|
const url = buildAuthorizeUrl({
|
|
53311
|
-
clientId:
|
|
53312
|
-
clientSecret:
|
|
54189
|
+
clientId: entry.clientId,
|
|
54190
|
+
clientSecret: entry.clientSecret,
|
|
53313
54191
|
redirectUri
|
|
53314
54192
|
}, state);
|
|
53315
54193
|
reply.redirect(url);
|
|
@@ -53317,42 +54195,138 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
53317
54195
|
async handleCallback(req, reply) {
|
|
53318
54196
|
const err = req.query["error"];
|
|
53319
54197
|
if (err) {
|
|
54198
|
+
const statePeek = typeof req.query["state"] === "string" ? req.query["state"] : "";
|
|
54199
|
+
const pendingPeek = statePeek ? this.pendingStates.get(statePeek) : void 0;
|
|
54200
|
+
if (pendingPeek?.popup) {
|
|
54201
|
+
this.pendingStates.delete(statePeek);
|
|
54202
|
+
this.popupComplete(reply, {
|
|
54203
|
+
ok: false,
|
|
54204
|
+
brokerId: pendingPeek.brokerId,
|
|
54205
|
+
error: String(err)
|
|
54206
|
+
});
|
|
54207
|
+
return;
|
|
54208
|
+
}
|
|
53320
54209
|
this.html(reply, 400, `Google OAuth error: ${err}`);
|
|
53321
54210
|
return;
|
|
53322
54211
|
}
|
|
53323
54212
|
const code = req.query["code"];
|
|
53324
54213
|
const state = req.query["state"];
|
|
53325
|
-
|
|
54214
|
+
const pending = typeof state === "string" ? this.pendingStates.get(state) : void 0;
|
|
54215
|
+
if (!code || !state || pending === void 0) {
|
|
53326
54216
|
this.html(reply, 400, "Invalid or expired OAuth state.");
|
|
53327
54217
|
return;
|
|
53328
54218
|
}
|
|
53329
54219
|
this.pendingStates.delete(state);
|
|
54220
|
+
const { brokerId, popup } = pending;
|
|
54221
|
+
const entry = this.registry.getEntry(brokerId);
|
|
54222
|
+
if (entry === null) {
|
|
54223
|
+
if (popup) {
|
|
54224
|
+
this.popupComplete(reply, {
|
|
54225
|
+
ok: false,
|
|
54226
|
+
brokerId,
|
|
54227
|
+
error: `Unknown Google broker: ${brokerId}`
|
|
54228
|
+
});
|
|
54229
|
+
return;
|
|
54230
|
+
}
|
|
54231
|
+
this.html(reply, 404, `Unknown Google broker: ${brokerId}`);
|
|
54232
|
+
return;
|
|
54233
|
+
}
|
|
53330
54234
|
try {
|
|
53331
54235
|
const redirectUri = await this.resolveRedirectUri();
|
|
53332
54236
|
const tokens = await exchangeCode({
|
|
53333
|
-
clientId:
|
|
53334
|
-
clientSecret:
|
|
54237
|
+
clientId: entry.clientId,
|
|
54238
|
+
clientSecret: entry.clientSecret,
|
|
53335
54239
|
redirectUri
|
|
53336
54240
|
}, code);
|
|
53337
|
-
|
|
53338
|
-
|
|
53339
|
-
|
|
53340
|
-
|
|
53341
|
-
|
|
53342
|
-
|
|
54241
|
+
const prev = this.registry.getOauth(brokerId) ?? {
|
|
54242
|
+
id: brokerId,
|
|
54243
|
+
...DEFAULT_OAUTH_STATE
|
|
54244
|
+
};
|
|
54245
|
+
if (!tokens.refresh_token && !prev.refreshToken) throw new Error("Google did not return a refresh_token. Revoke CamStack access in Google Account → Security and retry with prompt=consent.");
|
|
54246
|
+
const applied = applyTokenResponse({
|
|
54247
|
+
refreshToken: prev.refreshToken,
|
|
54248
|
+
accessToken: prev.accessToken,
|
|
54249
|
+
accessExpiresAt: prev.accessExpiresAt,
|
|
54250
|
+
accountEmail: prev.accountEmail
|
|
54251
|
+
}, tokens);
|
|
54252
|
+
const email = await this.fetchAccountEmail(applied.accessToken);
|
|
54253
|
+
const next = {
|
|
54254
|
+
id: brokerId,
|
|
54255
|
+
refreshToken: applied.refreshToken,
|
|
54256
|
+
accessToken: applied.accessToken,
|
|
54257
|
+
accessExpiresAt: applied.accessExpiresAt,
|
|
54258
|
+
accountEmail: email || applied.accountEmail
|
|
53343
54259
|
};
|
|
53344
|
-
|
|
54260
|
+
this.registry.setOauth(next);
|
|
54261
|
+
await this.persistOauthAccounts();
|
|
54262
|
+
if (next.accountEmail && (entry.name === "Google account" || entry.name.startsWith("google_"))) {
|
|
54263
|
+
const renamed = {
|
|
54264
|
+
...entry,
|
|
54265
|
+
name: next.accountEmail
|
|
54266
|
+
};
|
|
54267
|
+
this.registry.replaceEntries(this.registry.listEntries().map((e) => e.id === brokerId ? renamed : e));
|
|
54268
|
+
await this.persistBrokers();
|
|
54269
|
+
}
|
|
53345
54270
|
this.ctx.logger.info("google oauth connected", { meta: {
|
|
53346
|
-
|
|
53347
|
-
|
|
54271
|
+
brokerId,
|
|
54272
|
+
hasRefresh: next.refreshToken.length > 0,
|
|
54273
|
+
accountEmail: next.accountEmail || void 0
|
|
53348
54274
|
} });
|
|
53349
|
-
|
|
54275
|
+
if (popup) {
|
|
54276
|
+
this.popupComplete(reply, {
|
|
54277
|
+
ok: true,
|
|
54278
|
+
brokerId,
|
|
54279
|
+
message: "Google account connected. This window should close."
|
|
54280
|
+
});
|
|
54281
|
+
return;
|
|
54282
|
+
}
|
|
54283
|
+
this.html(reply, 200, `Google account connected for ${brokerId} (Calendar + Gmail send). You can close this window.`);
|
|
53350
54284
|
} catch (e) {
|
|
53351
54285
|
const message = e instanceof Error ? e.message : String(e);
|
|
53352
|
-
this.ctx.logger.warn("google oauth callback failed", { meta: {
|
|
54286
|
+
this.ctx.logger.warn("google oauth callback failed", { meta: {
|
|
54287
|
+
brokerId,
|
|
54288
|
+
error: message
|
|
54289
|
+
} });
|
|
54290
|
+
if (popup) {
|
|
54291
|
+
this.popupComplete(reply, {
|
|
54292
|
+
ok: false,
|
|
54293
|
+
brokerId,
|
|
54294
|
+
error: message
|
|
54295
|
+
});
|
|
54296
|
+
return;
|
|
54297
|
+
}
|
|
53353
54298
|
this.html(reply, 500, `OAuth failed: ${message}`);
|
|
53354
54299
|
}
|
|
53355
54300
|
}
|
|
54301
|
+
/**
|
|
54302
|
+
* Popup completion page (OIDC D794 pattern): postMessage to opener at the
|
|
54303
|
+
* hub origin, then close. Google never redirects back to admin-ui — this
|
|
54304
|
+
* page is the only return surface.
|
|
54305
|
+
*/
|
|
54306
|
+
popupComplete(reply, outcome) {
|
|
54307
|
+
const payload = JSON.stringify({
|
|
54308
|
+
type: "camstack:broker-oauth",
|
|
54309
|
+
ok: outcome.ok,
|
|
54310
|
+
brokerId: outcome.brokerId,
|
|
54311
|
+
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
54312
|
+
});
|
|
54313
|
+
const body = outcome.ok ? outcome.message ?? "Done. This window should close." : `OAuth failed: ${outcome.error ?? "unknown"}`;
|
|
54314
|
+
reply.header("content-type", "text/html; charset=utf-8");
|
|
54315
|
+
reply.status(outcome.ok ? 200 : 400).send(`<!doctype html><html><head><title>Google</title></head><body>
|
|
54316
|
+
<p>${escapeHtml(body)}</p>
|
|
54317
|
+
<script>
|
|
54318
|
+
(function () {
|
|
54319
|
+
var msg = ${payload};
|
|
54320
|
+
try {
|
|
54321
|
+
if (window.opener) {
|
|
54322
|
+
window.opener.postMessage(msg, window.location.origin);
|
|
54323
|
+
}
|
|
54324
|
+
} catch (e) {}
|
|
54325
|
+
setTimeout(function () { window.close(); }, 200);
|
|
54326
|
+
})();
|
|
54327
|
+
<\/script>
|
|
54328
|
+
</body></html>`);
|
|
54329
|
+
}
|
|
53356
54330
|
async fetchAccountEmail(accessToken) {
|
|
53357
54331
|
try {
|
|
53358
54332
|
const res = await fetch(GOOGLE_USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
@@ -53369,27 +54343,92 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
53369
54343
|
}
|
|
53370
54344
|
prunePendingStates() {
|
|
53371
54345
|
const cutoff = Date.now() - 10 * 6e4;
|
|
53372
|
-
for (const [k,
|
|
54346
|
+
for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
|
|
54347
|
+
}
|
|
54348
|
+
async persistBrokers() {
|
|
54349
|
+
await this.updateGlobalSettings({ brokers: [...this.registry.listEntries()] });
|
|
53373
54350
|
}
|
|
53374
|
-
async
|
|
53375
|
-
if (!this.
|
|
53376
|
-
await this.
|
|
54351
|
+
async persistOauthAccounts() {
|
|
54352
|
+
if (!this.oauthAccountsHandle) return;
|
|
54353
|
+
await this.oauthAccountsHandle.set({ accounts: [...this.registry.listOauthAccounts()] });
|
|
53377
54354
|
}
|
|
53378
|
-
|
|
53379
|
-
|
|
53380
|
-
|
|
54355
|
+
/** Stamped catalog for Connectivity → Calendars (`optionsFrom`). */
|
|
54356
|
+
async loadStampedCalendarCatalog() {
|
|
54357
|
+
const out = [];
|
|
54358
|
+
for (const entry of this.registry.listEntries()) {
|
|
54359
|
+
if (!this.registry.isConnected(entry.id)) continue;
|
|
54360
|
+
try {
|
|
54361
|
+
const list = await createGoogleCalendarApi(() => this.ensureAccessToken(entry.id)).listCalendars();
|
|
54362
|
+
const accountLabel = this.registry.getOauth(entry.id)?.accountEmail || entry.name;
|
|
54363
|
+
for (const c of list) {
|
|
54364
|
+
const summary = c.summary.trim().length > 0 ? c.summary : c.id;
|
|
54365
|
+
out.push({
|
|
54366
|
+
value: stampCalendarId(entry.id, c.id),
|
|
54367
|
+
label: `${summary} (${accountLabel})`
|
|
54368
|
+
});
|
|
54369
|
+
}
|
|
54370
|
+
} catch (err) {
|
|
54371
|
+
this.ctx.logger.warn("google calendar catalog failed", { meta: {
|
|
54372
|
+
brokerId: entry.id,
|
|
54373
|
+
error: err instanceof Error ? err.message : String(err)
|
|
54374
|
+
} });
|
|
54375
|
+
}
|
|
54376
|
+
}
|
|
54377
|
+
return out.toSorted((a, b) => a.label.localeCompare(b.label));
|
|
54378
|
+
}
|
|
54379
|
+
async ensureAccessToken(brokerId, opts = {}) {
|
|
54380
|
+
const entry = this.registry.getEntry(brokerId);
|
|
54381
|
+
const oauth = this.registry.getOauth(brokerId);
|
|
54382
|
+
if (entry === null || oauth === null || !oauth.refreshToken.trim()) throw new Error(`Google broker ${brokerId} is not connected — use Sign in under Connectivity → Brokers`);
|
|
54383
|
+
if (!entry.clientId.trim() || !entry.clientSecret.trim()) throw new Error("Configure Client ID and Client secret before refreshing the Google token");
|
|
54384
|
+
const state = {
|
|
54385
|
+
refreshToken: oauth.refreshToken.trim(),
|
|
54386
|
+
accessToken: oauth.accessToken.trim(),
|
|
54387
|
+
accessExpiresAt: oauth.accessExpiresAt,
|
|
54388
|
+
accountEmail: oauth.accountEmail
|
|
54389
|
+
};
|
|
54390
|
+
if (!opts.forceRefresh && !needsRefresh(state)) return state.accessToken;
|
|
53381
54391
|
const tokens = await refreshAccessToken({
|
|
53382
|
-
clientId:
|
|
53383
|
-
clientSecret:
|
|
53384
|
-
},
|
|
53385
|
-
|
|
53386
|
-
|
|
53387
|
-
|
|
54392
|
+
clientId: entry.clientId.trim(),
|
|
54393
|
+
clientSecret: entry.clientSecret.trim()
|
|
54394
|
+
}, state.refreshToken);
|
|
54395
|
+
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");
|
|
54396
|
+
const nextState = applyTokenResponse(state, tokens);
|
|
54397
|
+
this.registry.setOauth({
|
|
54398
|
+
id: brokerId,
|
|
54399
|
+
refreshToken: nextState.refreshToken,
|
|
54400
|
+
accessToken: nextState.accessToken,
|
|
54401
|
+
accessExpiresAt: nextState.accessExpiresAt,
|
|
54402
|
+
accountEmail: nextState.accountEmail
|
|
54403
|
+
});
|
|
54404
|
+
await this.persistOauthAccounts();
|
|
54405
|
+
return nextState.accessToken;
|
|
53388
54406
|
}
|
|
53389
|
-
|
|
53390
|
-
const
|
|
53391
|
-
|
|
53392
|
-
|
|
54407
|
+
async handleDisconnect(req, reply) {
|
|
54408
|
+
const brokerIdRaw = req.query["brokerId"];
|
|
54409
|
+
const brokerId = typeof brokerIdRaw === "string" ? brokerIdRaw.trim() : "";
|
|
54410
|
+
if (!brokerId) {
|
|
54411
|
+
reply.status(400).send({ error: "Missing brokerId query parameter" });
|
|
54412
|
+
return;
|
|
54413
|
+
}
|
|
54414
|
+
if (this.registry.getEntry(brokerId) === null) {
|
|
54415
|
+
reply.status(404).send({ error: `Unknown Google broker: ${brokerId}` });
|
|
54416
|
+
return;
|
|
54417
|
+
}
|
|
54418
|
+
const popupRaw = req.query["popup"];
|
|
54419
|
+
const popup = popupRaw === "1" || popupRaw === "true";
|
|
54420
|
+
this.registry.removeOauth(brokerId);
|
|
54421
|
+
await this.persistOauthAccounts();
|
|
54422
|
+
this.ctx.logger.info("google oauth disconnected", { meta: { brokerId } });
|
|
54423
|
+
if (popup) {
|
|
54424
|
+
this.popupComplete(reply, {
|
|
54425
|
+
ok: true,
|
|
54426
|
+
brokerId,
|
|
54427
|
+
message: "Google account disconnected. This window should close."
|
|
54428
|
+
});
|
|
54429
|
+
return;
|
|
54430
|
+
}
|
|
54431
|
+
this.html(reply, 200, `Google account disconnected for ${brokerId}. You can close this window.`);
|
|
53393
54432
|
}
|
|
53394
54433
|
async resolveRedirectUri() {
|
|
53395
54434
|
await this.refreshPublicOrigin();
|
|
@@ -53405,85 +54444,22 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
53405
54444
|
}
|
|
53406
54445
|
}
|
|
53407
54446
|
/**
|
|
53408
|
-
* Connectivity
|
|
53409
|
-
*
|
|
53410
|
-
* mqtt-broker under `broker`). The addon page (no cap) keeps every section.
|
|
54447
|
+
* Connectivity Calendars / Email: point at Brokers (no duplicate OAuth fields).
|
|
54448
|
+
* Addon page without cap: same pointer + note.
|
|
53411
54449
|
*/
|
|
53412
54450
|
globalSettingsSchema(cap) {
|
|
53413
54451
|
const scoped = asSettingsCap(cap);
|
|
53414
|
-
return this.schema(
|
|
53415
|
-
{
|
|
53416
|
-
id: "setup-intro",
|
|
53417
|
-
title: "Setup",
|
|
53418
|
-
description: scoped === "smtp-provider" ? "Connect a Google account for Gmail send (same OAuth as Calendars)." : scoped === "calendar-source" ? "Connect a Google account for Calendar (same OAuth as Email)." : "One Google account for Calendar and optional Gmail send.",
|
|
53419
|
-
columns: 1,
|
|
53420
|
-
fields: [{
|
|
53421
|
-
type: "info",
|
|
53422
|
-
key: "_setupSteps",
|
|
53423
|
-
label: "Steps",
|
|
53424
|
-
format: "html",
|
|
53425
|
-
content: introHtml(scoped, this.publicOrigin)
|
|
53426
|
-
}]
|
|
53427
|
-
},
|
|
53428
|
-
{
|
|
53429
|
-
id: "google-oauth",
|
|
53430
|
-
title: "Google account",
|
|
53431
|
-
description: "OAuth client credentials (Web application) stored in the hub keychain.",
|
|
53432
|
-
columns: 1,
|
|
53433
|
-
fields: [
|
|
53434
|
-
{
|
|
53435
|
-
type: "text",
|
|
53436
|
-
key: "clientId",
|
|
53437
|
-
label: "Client ID"
|
|
53438
|
-
},
|
|
53439
|
-
{
|
|
53440
|
-
type: "password",
|
|
53441
|
-
key: "clientSecret",
|
|
53442
|
-
label: "Client secret",
|
|
53443
|
-
showToggle: true
|
|
53444
|
-
},
|
|
53445
|
-
{
|
|
53446
|
-
type: "info",
|
|
53447
|
-
key: "_connectInfo",
|
|
53448
|
-
label: "Connect",
|
|
53449
|
-
format: "html",
|
|
53450
|
-
content: connectHtml(scoped, this.publicOrigin)
|
|
53451
|
-
}
|
|
53452
|
-
]
|
|
53453
|
-
},
|
|
53454
|
-
...scoped === "smtp-provider" ? [] : [{
|
|
53455
|
-
id: "google-calendar",
|
|
53456
|
-
title: "Google Calendar",
|
|
53457
|
-
description: "Which calendars this hub exposes as calendar-source entries.",
|
|
53458
|
-
columns: 1,
|
|
53459
|
-
fields: [{
|
|
53460
|
-
type: "text",
|
|
53461
|
-
key: "calendarFilter",
|
|
53462
|
-
label: "Calendar id filter (optional)",
|
|
53463
|
-
description: "Comma-separated Google calendar ids to expose. Empty = all calendars the account can read."
|
|
53464
|
-
}]
|
|
53465
|
-
}],
|
|
53466
|
-
...scoped === "calendar-source" ? [] : [{
|
|
53467
|
-
id: "gmail-send",
|
|
53468
|
-
title: "Gmail send",
|
|
53469
|
-
description: "Delivery via the Gmail API. Disable this capability with the page Enable toggle — not a second switch here. From must be the connected account or a verified send-as.",
|
|
53470
|
-
columns: 1,
|
|
53471
|
-
fields: [{
|
|
53472
|
-
type: "text",
|
|
53473
|
-
key: "fromAddress",
|
|
53474
|
-
label: "From address",
|
|
53475
|
-
description: "Defaults to the connected Google account email when empty."
|
|
53476
|
-
}, {
|
|
53477
|
-
type: "text",
|
|
53478
|
-
key: "fromName",
|
|
53479
|
-
label: "From display name (optional)"
|
|
53480
|
-
}]
|
|
53481
|
-
}]
|
|
53482
|
-
] });
|
|
54452
|
+
return this.schema(buildGoogleFeatureSettingsSchema(scoped, this.publicOrigin));
|
|
53483
54453
|
}
|
|
53484
54454
|
async getGlobalSettings(overlay, cap, nodeId) {
|
|
53485
54455
|
await this.refreshPublicOrigin();
|
|
53486
|
-
return super.getGlobalSettings(overlay, cap, nodeId);
|
|
54456
|
+
if (cap !== "calendar-source") return super.getGlobalSettings(overlay, cap, nodeId);
|
|
54457
|
+
const catalog = await this.loadStampedCalendarCatalog();
|
|
54458
|
+
return super.getGlobalSettings({
|
|
54459
|
+
...overlay,
|
|
54460
|
+
calendarIds: this.config.calendarIds ?? [],
|
|
54461
|
+
...catalog.length > 0 ? { _calendarCatalog: catalog } : {}
|
|
54462
|
+
}, cap, nodeId);
|
|
53487
54463
|
}
|
|
53488
54464
|
};
|
|
53489
54465
|
function asSettingsCap(cap) {
|
|
@@ -53494,10 +54470,12 @@ function escapeHtml(s) {
|
|
|
53494
54470
|
}
|
|
53495
54471
|
//#endregion
|
|
53496
54472
|
exports.DEFAULT_GOOGLE_CONFIG = DEFAULT_GOOGLE_CONFIG;
|
|
54473
|
+
exports.GOOGLE_BROKER_KIND = GOOGLE_BROKER_KIND;
|
|
53497
54474
|
exports.GOOGLE_CONSOLE_APIS_URL = GOOGLE_CONSOLE_APIS_URL;
|
|
53498
54475
|
exports.GOOGLE_CONSOLE_CREDENTIALS_URL = GOOGLE_CONSOLE_CREDENTIALS_URL;
|
|
53499
54476
|
exports.GOOGLE_GMAIL_SEND_SCOPE = GOOGLE_GMAIL_SEND_SCOPE;
|
|
53500
54477
|
exports.GOOGLE_OAUTH_SCOPES = GOOGLE_OAUTH_SCOPES;
|
|
54478
|
+
exports.GoogleBrokerRegistry = GoogleBrokerRegistry;
|
|
53501
54479
|
exports.GoogleServicesAddon = GoogleServicesAddon;
|
|
53502
54480
|
exports.default = GoogleServicesAddon;
|
|
53503
54481
|
exports.GoogleServicesConfigSchema = GoogleServicesConfigSchema;
|
|
@@ -53512,6 +54490,10 @@ exports.createGoogleCalendarSource = createGoogleCalendarSource;
|
|
|
53512
54490
|
exports.exchangeCode = exchangeCode;
|
|
53513
54491
|
exports.introHtml = introHtml;
|
|
53514
54492
|
exports.mapGoogleEvent = mapGoogleEvent;
|
|
54493
|
+
exports.migrateLegacyToBrokers = migrateLegacyToBrokers;
|
|
53515
54494
|
exports.needsRefresh = needsRefresh;
|
|
54495
|
+
exports.parseStampedCalendarId = parseStampedCalendarId;
|
|
53516
54496
|
exports.redirectUriDisplay = redirectUriDisplay;
|
|
53517
54497
|
exports.refreshAccessToken = refreshAccessToken;
|
|
54498
|
+
exports.selectGmailAccount = selectGmailAccount;
|
|
54499
|
+
exports.stampCalendarId = stampCalendarId;
|