@camstack/addon-google-services 0.2.45 → 0.2.46

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/CHANGELOG.md CHANGED
@@ -7,11 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.46] - 2026-11-09
11
+
12
+ ### Changed
13
+
14
+ - Gmail send provider drops the unused verify call
15
+
10
16
  ## [0.2.45] - 2026-11-09
11
17
 
12
18
  ### Fixed
13
19
 
14
20
  - Each imported calendar names the record that owns it, so its switch is found on the Calendars page
21
+ ### Changed
22
+
15
23
 
16
24
  ## [0.2.44] - 2026-11-08
17
25
 
@@ -6016,7 +6016,7 @@ var ZodIssueCode = {
6016
6016
  var ZodFirstPartyTypeKind;
6017
6017
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6018
6018
  //#endregion
6019
- //#region ../types/dist/sleep-CS5Xp1iQ.mjs
6019
+ //#region ../types/dist/sleep-CkinsFFb.mjs
6020
6020
  /**
6021
6021
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6022
6022
  * window to float samples (D455).
@@ -8695,16 +8695,6 @@ var BaseAddon = class {
8695
8695
  return null;
8696
8696
  }
8697
8697
  /**
8698
- * The form schema of ONE integration instance, or `null` to use the scoped form.
8699
- *
8700
- * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8701
- * needs its connection fields, the default integration the addon's own sections. It is the
8702
- * SHAPE of the form only — the values live on the instance record.
8703
- */
8704
- async integrationFormSchema(_instance) {
8705
- return null;
8706
- }
8707
- /**
8708
8698
  * D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
8709
8699
  * provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
8710
8700
  * declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
@@ -8732,8 +8722,7 @@ var BaseAddon = class {
8732
8722
  }
8733
8723
  /**
8734
8724
  * The host's read of the scoped declarations: the schema one capability declares for `scope`,
8735
- * or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
8736
- * host asks this method.
8725
+ * or `null`. The hook is protected, the host asks this method.
8737
8726
  */
8738
8727
  async getScopedSettingsSchema(scope, cap) {
8739
8728
  switch (scope) {
@@ -8901,22 +8890,6 @@ var BaseAddon = class {
8901
8890
  return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8902
8891
  }
8903
8892
  /**
8904
- * What the create wizard and the integration edit page render for one instance: the addon's
8905
- * per-instance schema when it declares one, else its global schema (what they always showed).
8906
- */
8907
- async getIntegrationFormSchema(instance, nodeId) {
8908
- const scoped = await this.integrationFormSchema(instance);
8909
- if (scoped === null) return this.scopedForm(nodeId);
8910
- return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8911
- }
8912
- /** The form of the scoped schemas this addon declared, hydrated from its store. */
8913
- async scopedForm(nodeId) {
8914
- await this.loadScopedSchemas();
8915
- const schema = this.scopedFormSchema();
8916
- if (!schema) return { sections: [] };
8917
- return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8918
- }
8919
- /**
8920
8893
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
8921
8894
  * every `perNode: true` field carries THAT node's scoped value on its bare
8922
8895
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -9103,12 +9076,6 @@ var BaseAddon = class {
9103
9076
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
9104
9077
  this._addonSecretTree = null;
9105
9078
  }
9106
- /** The form of an addon's scoped schemas: their sections, each shown once. */
9107
- scopedFormSchema(cap) {
9108
- const byId = /* @__PURE__ */ new Map();
9109
- for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
9110
- return byId.size === 0 ? null : { sections: [...byId.values()] };
9111
- }
9112
9079
  /** Every loaded scoped schema of `cap`, or of every loaded capability. */
9113
9080
  loadedScopedSchemas(cap) {
9114
9081
  if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
@@ -9698,6 +9665,24 @@ function normalizeAddonInitResult(result) {
9698
9665
  if (Array.isArray(result)) return { providers: result };
9699
9666
  return result;
9700
9667
  }
9668
+ object({
9669
+ code: string().regex(/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/),
9670
+ params: record(string(), union([string(), number$1()])).optional()
9671
+ });
9672
+ var CamstackError = class extends Error {
9673
+ i18nCode;
9674
+ i18nParams;
9675
+ constructor(code, params, englishMessage) {
9676
+ super(englishMessage);
9677
+ this.name = "CamstackError";
9678
+ this.i18nCode = code;
9679
+ this.i18nParams = params;
9680
+ }
9681
+ };
9682
+ /** `throw userError('device.not-found', { id }, 'Device 12 not found')` */
9683
+ function userError(code, params, englishFallback) {
9684
+ return new CamstackError(code, params, englishFallback);
9685
+ }
9701
9686
  object({
9702
9687
  field: string().min(1),
9703
9688
  on: literal("advance")
@@ -9774,12 +9759,6 @@ function systemMethod(input, output, options) {
9774
9759
  systemOnly: true
9775
9760
  };
9776
9761
  }
9777
- /**
9778
- * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9779
- * on `InferProvider`. The `providerOptional: true` literal is what
9780
- * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9781
- * for when this is the honest answer and when it is a soft stub.
9782
- */
9783
9762
  function optionalMethod(input, output, options) {
9784
9763
  return {
9785
9764
  ...method(input, output, options),
@@ -17174,24 +17153,6 @@ Object.freeze({
17174
17153
  "uploadDataset": 72e5
17175
17154
  }
17176
17155
  });
17177
- object({
17178
- code: string().regex(/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/),
17179
- params: record(string(), union([string(), number$1()])).optional()
17180
- });
17181
- var CamstackError = class extends Error {
17182
- i18nCode;
17183
- i18nParams;
17184
- constructor(code, params, englishMessage) {
17185
- super(englishMessage);
17186
- this.name = "CamstackError";
17187
- this.i18nCode = code;
17188
- this.i18nParams = params;
17189
- }
17190
- };
17191
- /** `throw userError('device.not-found', { id }, 'Device 12 not found')` */
17192
- function userError(code, params, englishFallback) {
17193
- return new CamstackError(code, params, englishFallback);
17194
- }
17195
17156
  //#endregion
17196
17157
  //#region ../types/dist/composition-CXCX78IR.mjs
17197
17158
  /**
@@ -18903,25 +18864,6 @@ var RequiredAddonUpdateSchema = object({
18903
18864
  installed: string().nullable()
18904
18865
  });
18905
18866
  /**
18906
- * Which integration instance a settings form is for. An addon that serves several instances
18907
- * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
18908
- * needs its connection, the default integration needs the addon's own sections.
18909
- *
18910
- * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
18911
- * setting up a NEW instance.
18912
- * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
18913
- * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
18914
- */
18915
- var IntegrationInstanceRefSchema = object({
18916
- integrationId: string().nullable(),
18917
- mode: _enum([
18918
- "standalone",
18919
- "account",
18920
- "broker"
18921
- ]),
18922
- candidateStableId: string().optional()
18923
- });
18924
- /**
18925
18867
  * Build an `IAddonRouteProvider` from a list of routes. Implements
18926
18868
  * both the operator-facing `getRoutes` (returning route descriptors
18927
18869
  * minus the handlers, which can't cross JSON) and the framework-
@@ -19098,8 +19040,8 @@ function composeInstanceKinds(parts, base) {
19098
19040
  instanceFormSchema: async ({ id, kind }) => {
19099
19041
  return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
19100
19042
  },
19101
- listInstances: async () => {
19102
- return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
19043
+ listInstances: async (input) => {
19044
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances(input)).map((instance) => tagged(part, instance))))).flat();
19103
19045
  },
19104
19046
  getInstance: async ({ id }) => {
19105
19047
  for (const part of parts) {
@@ -45244,10 +45186,6 @@ method(object({
45244
45186
  scope: ScopedSettingsScopeSchema,
45245
45187
  nodeId: string().optional()
45246
45188
  }), record(string(), unknown()).nullable()), method(object({
45247
- addonId: string(),
45248
- nodeId: string().optional(),
45249
- instance: IntegrationInstanceRefSchema
45250
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45251
45189
  addonId: string(),
45252
45190
  nodeId: string().optional()
45253
45191
  }), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
@@ -55652,6 +55590,12 @@ var IntegrationWithStateSchema = object({
55652
55590
  name: string(),
55653
55591
  /** A projection of the instance body's switch (`setInstanceEnabled` is the only writer). */
55654
55592
  enabled: boolean(),
55593
+ /**
55594
+ * True when the addon that holds the switch did not answer (down, failed, or keeps no record for
55595
+ * this row): `enabled` is then the stored registry flag, a stale value, not the state. A reader
55596
+ * must not draw it as on or off (D315).
55597
+ */
55598
+ switchUnknown: literal(true).optional(),
55655
55599
  info: record(string(), unknown()).readonly(),
55656
55600
  /** The discovery candidate this integration was set up from (D984). */
55657
55601
  originStableId: string().optional(),
@@ -55665,6 +55609,12 @@ var IntegrationLiteSchema = object({
55665
55609
  name: string(),
55666
55610
  /** A projection of the instance body's switch (`setInstanceEnabled` is the only writer). */
55667
55611
  enabled: boolean(),
55612
+ /**
55613
+ * True when the addon that holds the switch did not answer (down, failed, or keeps no record for
55614
+ * this row): `enabled` is then the stored registry flag, a stale value, not the state. A reader
55615
+ * must not draw it as on or off (D315).
55616
+ */
55617
+ switchUnknown: literal(true).optional(),
55668
55618
  info: record(string(), unknown()).readonly(),
55669
55619
  /** The discovery candidate this integration was set up from (D984). */
55670
55620
  originStableId: string().optional(),
@@ -55763,9 +55713,9 @@ var UpdateIntegrationInputSchema = object({
55763
55713
  id: string(),
55764
55714
  name: string().optional(),
55765
55715
  /**
55766
- * Accepted for a FIXED builtin only (`info.fixed`: alarm panel, camera grid, core blocks,
55767
- * terminal), which keeps no instance record: the registry flag is its one store. Any other
55768
- * integration is switched through `integration-instances.setInstanceEnabled`.
55716
+ * Honoured ONLY for a fixed builtin (`info.fixed`), whose switch is the hub's own registry flag
55717
+ * and works while its addon is down. Any other row is refused (a typed `BAD_REQUEST`): its
55718
+ * switch is `integration-instances.setInstanceEnabled`.
55769
55719
  */
55770
55720
  enabled: boolean().optional(),
55771
55721
  info: record(string(), unknown()).optional()
@@ -55799,6 +55749,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55799
55749
  * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
55800
55750
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
55801
55751
  * clears it.
55752
+ *
55753
+ * Who may call (epic #440): every method but `describe` and `instanceFormSchema` is open to any
55754
+ * session (`caller: 'scoped'`) and default-denies. An admin passes everywhere. A non-admin passes
55755
+ * only on a call pinned to one addon whose `describe().userScoped` is true, and that provider
55756
+ * filters every row on the `caller` the router injected (a notification target is somebody's
55757
+ * phone: a user sees and changes only their own). An addon that says nothing keeps the admin door.
55802
55758
  */
55803
55759
  var ConfigSchemaPassthrough$2 = custom();
55804
55760
  var IntegrationInstanceIdSchema = string().min(1);
@@ -55912,7 +55868,15 @@ var IntegrationInstanceDescriptionSchema = object({
55912
55868
  */
55913
55869
  mintsId: boolean().optional(),
55914
55870
  /** The links to other instances the provider's kinds declare; absent means none. */
55915
- links: array(IntegrationInstanceLinkSchema).optional()
55871
+ links: array(IntegrationInstanceLinkSchema).optional(),
55872
+ /**
55873
+ * The rows belong to individual users (a notification target is somebody's phone). The router
55874
+ * then opens `listInstances`, `getInstance`, `createInstance`, `updateInstance`,
55875
+ * `setInstanceEnabled`, `deleteInstance` and `testInstance` to a NON-admin session on a call
55876
+ * pinned to this addon, and hands the provider the `caller` so it keeps a user to their own rows.
55877
+ * Absent means admin only, as it always was.
55878
+ */
55879
+ userScoped: boolean().optional()
55916
55880
  });
55917
55881
  var IntegrationInstanceStatusSchema = object({
55918
55882
  state: _enum([
@@ -55970,8 +55934,14 @@ var integrationInstancesCapability = {
55970
55934
  /** The discovery candidate the wizard was seeded from (`stableId`), if any. */
55971
55935
  candidateStableId: string().min(1).optional()
55972
55936
  }), ConfigSchemaPassthrough$2, { auth: "admin" }),
55973
- listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
55974
- getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
55937
+ listInstances: method(looseObject({}), array(IntegrationInstanceSchema).readonly(), {
55938
+ auth: "protected",
55939
+ caller: "scoped"
55940
+ }),
55941
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), {
55942
+ auth: "protected",
55943
+ caller: "scoped"
55944
+ }),
55975
55945
  /**
55976
55946
  * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
55977
55947
  * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
@@ -56004,11 +55974,13 @@ var integrationInstancesCapability = {
56004
55974
  seed: object({ info: record(string(), unknown()) }).optional()
56005
55975
  }), object({ id: string() }), {
56006
55976
  kind: "mutation",
56007
- auth: "admin"
55977
+ auth: "protected",
55978
+ caller: "scoped"
56008
55979
  }),
56009
55980
  updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56010
55981
  kind: "mutation",
56011
- auth: "admin"
55982
+ auth: "protected",
55983
+ caller: "scoped"
56012
55984
  }),
56013
55985
  /**
56014
55986
  * Switch ONE instance on or off, for every role; a missing record is created from the addon's
@@ -56020,7 +55992,8 @@ var integrationInstancesCapability = {
56020
55992
  enabled: boolean()
56021
55993
  }), object({ ok: literal(true) }), {
56022
55994
  kind: "mutation",
56023
- auth: "admin"
55995
+ auth: "protected",
55996
+ caller: "scoped"
56024
55997
  }),
56025
55998
  /**
56026
55999
  * Delete the record and its keychain refs together. For an instance that brings devices the hub
@@ -56029,7 +56002,8 @@ var integrationInstancesCapability = {
56029
56002
  */
56030
56003
  deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56031
56004
  kind: "mutation",
56032
- auth: "admin"
56005
+ auth: "protected",
56006
+ caller: "scoped"
56033
56007
  }),
56034
56008
  testInstance: optionalMethod(object({
56035
56009
  id: IntegrationInstanceIdSchema.nullable(),
@@ -56038,7 +56012,8 @@ var integrationInstancesCapability = {
56038
56012
  kind: string().min(1).optional()
56039
56013
  }), TestConnectionResultSchema, {
56040
56014
  kind: "mutation",
56041
- auth: "admin"
56015
+ auth: "protected",
56016
+ caller: "scoped"
56042
56017
  })
56043
56018
  }
56044
56019
  };
@@ -73888,6 +73863,11 @@ var SendEmailResultSchema = object({
73888
73863
  var SmtpStatusSchema = object({
73889
73864
  /** True iff the addon has successfully verified the relay. */
73890
73865
  ready: boolean(),
73866
+ /**
73867
+ * The tri-state behind `ready` (D315): true when the last send worked, false when it failed,
73868
+ * ABSENT when the relay has not sent yet (unknown, never "not verified").
73869
+ */
73870
+ verified: boolean().optional(),
73891
73871
  /** Operator-visible host string (no credentials). */
73892
73872
  host: string(),
73893
73873
  /** Last error message reported by the relay, when not ready. */
@@ -73907,13 +73887,6 @@ var smtpProviderCapability = {
73907
73887
  auth: "admin",
73908
73888
  access: "create"
73909
73889
  }),
73910
- /** Round-trip ping against the SMTP relay (EHLO + AUTH if configured).
73911
- * Used by the operator's "Test connection" button. */
73912
- verify: method(RelayPinSchema, SmtpStatusSchema, {
73913
- kind: "mutation",
73914
- auth: "admin",
73915
- access: "view"
73916
- }),
73917
73890
  getStatus: method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" })
73918
73891
  }
73919
73892
  };
@@ -80584,13 +80557,6 @@ Object.freeze({
80584
80557
  access: "view",
80585
80558
  role: "admin"
80586
80559
  },
80587
- "addonSettings.getIntegrationFormSchema": {
80588
- capName: "addon-settings",
80589
- capScope: "system",
80590
- addonId: null,
80591
- access: "view",
80592
- role: "admin"
80593
- },
80594
80560
  "addonSettings.getScopedSettings": {
80595
80561
  capName: "addon-settings",
80596
80562
  capScope: "system",
@@ -88613,13 +88579,6 @@ Object.freeze({
88613
88579
  access: "create",
88614
88580
  role: "admin"
88615
88581
  },
88616
- "smtpProvider.verify": {
88617
- capName: "smtp-provider",
88618
- capScope: "system",
88619
- addonId: null,
88620
- access: "view",
88621
- role: "admin"
88622
- },
88623
88582
  "snapshot.applyDeviceSettingsPatch": {
88624
88583
  capName: "snapshot",
88625
88584
  capScope: "device",
@@ -95597,28 +95556,6 @@ function createGmailSmtpProvider(deps) {
95597
95556
  };
95598
95557
  return {
95599
95558
  getStatus: async () => status(),
95600
- verify: async () => {
95601
- const account = selectGmailAccount(deps.accounts(), void 0);
95602
- if (account === null) {
95603
- lastError = "No Google broker connected.";
95604
- return status();
95605
- }
95606
- try {
95607
- const token = await account.getAccessToken();
95608
- const res = await http(`${GOOGLE_GMAIL_API}/users/me/profile`, { headers: { Authorization: `Bearer ${token}` } });
95609
- if (!res.ok) {
95610
- const text = await res.text();
95611
- lastError = `Gmail profile failed (${res.status}): ${text.slice(0, 200)}`;
95612
- return status();
95613
- }
95614
- lastError = void 0;
95615
- lastVerifiedAt = Date.now();
95616
- return status();
95617
- } catch (err) {
95618
- lastError = err instanceof Error ? err.message : String(err);
95619
- return status();
95620
- }
95621
- },
95622
95559
  sendEmail: async (input) => {
95623
95560
  const account = selectGmailAccount(deps.accounts(), input.from);
95624
95561
  if (account === null) throw new Error("No Google broker connected — add one under Connectivity → Brokers and Connect.");
@@ -6012,7 +6012,7 @@ var ZodIssueCode = {
6012
6012
  var ZodFirstPartyTypeKind;
6013
6013
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6014
6014
  //#endregion
6015
- //#region ../types/dist/sleep-CS5Xp1iQ.mjs
6015
+ //#region ../types/dist/sleep-CkinsFFb.mjs
6016
6016
  /**
6017
6017
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6018
6018
  * window to float samples (D455).
@@ -8691,16 +8691,6 @@ var BaseAddon = class {
8691
8691
  return null;
8692
8692
  }
8693
8693
  /**
8694
- * The form schema of ONE integration instance, or `null` to use the scoped form.
8695
- *
8696
- * Override it only when an `instanceMode: multiple` addon's instances differ: an NVR integration
8697
- * needs its connection fields, the default integration the addon's own sections. It is the
8698
- * SHAPE of the form only — the values live on the instance record.
8699
- */
8700
- async integrationFormSchema(_instance) {
8701
- return null;
8702
- }
8703
- /**
8704
8694
  * D986 scoped settings: the form shape of the CLUSTER-wide settings one capability this addon
8705
8695
  * provides owns, or `null` when it has none. `cap` is a capability name the addon's manifest
8706
8696
  * declares. Stored as `addon-settings` rows `<addonId>.<field>`. The keys of this schema, of
@@ -8728,8 +8718,7 @@ var BaseAddon = class {
8728
8718
  }
8729
8719
  /**
8730
8720
  * The host's read of the scoped declarations: the schema one capability declares for `scope`,
8731
- * or `null`. Beside {@link getIntegrationFormSchema}, the same shape: the hook is protected, the
8732
- * host asks this method.
8721
+ * or `null`. The hook is protected, the host asks this method.
8733
8722
  */
8734
8723
  async getScopedSettingsSchema(scope, cap) {
8735
8724
  switch (scope) {
@@ -8897,22 +8886,6 @@ var BaseAddon = class {
8897
8886
  return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
8898
8887
  }
8899
8888
  /**
8900
- * What the create wizard and the integration edit page render for one instance: the addon's
8901
- * per-instance schema when it declares one, else its global schema (what they always showed).
8902
- */
8903
- async getIntegrationFormSchema(instance, nodeId) {
8904
- const scoped = await this.integrationFormSchema(instance);
8905
- if (scoped === null) return this.scopedForm(nodeId);
8906
- return hydrateSchema(scoped, {}, { inlineSecretsAsViews: true });
8907
- }
8908
- /** The form of the scoped schemas this addon declared, hydrated from its store. */
8909
- async scopedForm(nodeId) {
8910
- await this.loadScopedSchemas();
8911
- const schema = this.scopedFormSchema();
8912
- if (!schema) return { sections: [] };
8913
- return hydrateSchema(schema, await this.readRawProjectedStore(nodeId), { inlineSecretsAsViews: true });
8914
- }
8915
- /**
8916
8889
  * The raw addon store PROJECTED onto the target node's bare per-node keys:
8917
8890
  * every `perNode: true` field carries THAT node's scoped value on its bare
8918
8891
  * key (absent scoped key ⇒ key absent, so the schema `default` wins — no
@@ -9099,12 +9072,6 @@ var BaseAddon = class {
9099
9072
  this._nodeKeysByCap = /* @__PURE__ */ new Map();
9100
9073
  this._addonSecretTree = null;
9101
9074
  }
9102
- /** The form of an addon's scoped schemas: their sections, each shown once. */
9103
- scopedFormSchema(cap) {
9104
- const byId = /* @__PURE__ */ new Map();
9105
- for (const entry of this.loadedScopedSchemas(cap)) for (const section of entry.schema.sections) if (!byId.has(section.id)) byId.set(section.id, section);
9106
- return byId.size === 0 ? null : { sections: [...byId.values()] };
9107
- }
9108
9075
  /** Every loaded scoped schema of `cap`, or of every loaded capability. */
9109
9076
  loadedScopedSchemas(cap) {
9110
9077
  if (cap !== void 0) return this._scopedByCap.get(cap) ?? [];
@@ -9694,6 +9661,24 @@ function normalizeAddonInitResult(result) {
9694
9661
  if (Array.isArray(result)) return { providers: result };
9695
9662
  return result;
9696
9663
  }
9664
+ object({
9665
+ code: string().regex(/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/),
9666
+ params: record(string(), union([string(), number$1()])).optional()
9667
+ });
9668
+ var CamstackError = class extends Error {
9669
+ i18nCode;
9670
+ i18nParams;
9671
+ constructor(code, params, englishMessage) {
9672
+ super(englishMessage);
9673
+ this.name = "CamstackError";
9674
+ this.i18nCode = code;
9675
+ this.i18nParams = params;
9676
+ }
9677
+ };
9678
+ /** `throw userError('device.not-found', { id }, 'Device 12 not found')` */
9679
+ function userError(code, params, englishFallback) {
9680
+ return new CamstackError(code, params, englishFallback);
9681
+ }
9697
9682
  object({
9698
9683
  field: string().min(1),
9699
9684
  on: literal("advance")
@@ -9770,12 +9755,6 @@ function systemMethod(input, output, options) {
9770
9755
  systemOnly: true
9771
9756
  };
9772
9757
  }
9773
- /**
9774
- * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9775
- * on `InferProvider`. The `providerOptional: true` literal is what
9776
- * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9777
- * for when this is the honest answer and when it is a soft stub.
9778
- */
9779
9758
  function optionalMethod(input, output, options) {
9780
9759
  return {
9781
9760
  ...method(input, output, options),
@@ -17170,24 +17149,6 @@ Object.freeze({
17170
17149
  "uploadDataset": 72e5
17171
17150
  }
17172
17151
  });
17173
- object({
17174
- code: string().regex(/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/),
17175
- params: record(string(), union([string(), number$1()])).optional()
17176
- });
17177
- var CamstackError = class extends Error {
17178
- i18nCode;
17179
- i18nParams;
17180
- constructor(code, params, englishMessage) {
17181
- super(englishMessage);
17182
- this.name = "CamstackError";
17183
- this.i18nCode = code;
17184
- this.i18nParams = params;
17185
- }
17186
- };
17187
- /** `throw userError('device.not-found', { id }, 'Device 12 not found')` */
17188
- function userError(code, params, englishFallback) {
17189
- return new CamstackError(code, params, englishFallback);
17190
- }
17191
17152
  //#endregion
17192
17153
  //#region ../types/dist/composition-CXCX78IR.mjs
17193
17154
  /**
@@ -18899,25 +18860,6 @@ var RequiredAddonUpdateSchema = object({
18899
18860
  installed: string().nullable()
18900
18861
  });
18901
18862
  /**
18902
- * Which integration instance a settings form is for. An addon that serves several instances
18903
- * (`instanceMode: multiple`) can show a different form per instance — an NVR integration
18904
- * needs its connection, the default integration needs the addon's own sections.
18905
- *
18906
- * - `integrationId` is `null` in the create wizard: nothing exists yet, the operator is
18907
- * setting up a NEW instance.
18908
- * - `mode` is the wizard mode of the integration type (`getAvailableTypes`).
18909
- * - `candidateStableId` is the discovery candidate a wizard was seeded from, if any.
18910
- */
18911
- var IntegrationInstanceRefSchema = object({
18912
- integrationId: string().nullable(),
18913
- mode: _enum([
18914
- "standalone",
18915
- "account",
18916
- "broker"
18917
- ]),
18918
- candidateStableId: string().optional()
18919
- });
18920
- /**
18921
18863
  * Build an `IAddonRouteProvider` from a list of routes. Implements
18922
18864
  * both the operator-facing `getRoutes` (returning route descriptors
18923
18865
  * minus the handlers, which can't cross JSON) and the framework-
@@ -19094,8 +19036,8 @@ function composeInstanceKinds(parts, base) {
19094
19036
  instanceFormSchema: async ({ id, kind }) => {
19095
19037
  return (id === null ? byKind(kind) : await holderOf(id)).provider.instanceFormSchema({ id });
19096
19038
  },
19097
- listInstances: async () => {
19098
- return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances()).map((instance) => tagged(part, instance))))).flat();
19039
+ listInstances: async (input) => {
19040
+ return (await Promise.all(parts.map(async (part) => (await part.provider.listInstances(input)).map((instance) => tagged(part, instance))))).flat();
19099
19041
  },
19100
19042
  getInstance: async ({ id }) => {
19101
19043
  for (const part of parts) {
@@ -45240,10 +45182,6 @@ method(object({
45240
45182
  scope: ScopedSettingsScopeSchema,
45241
45183
  nodeId: string().optional()
45242
45184
  }), record(string(), unknown()).nullable()), method(object({
45243
- addonId: string(),
45244
- nodeId: string().optional(),
45245
- instance: IntegrationInstanceRefSchema
45246
- }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45247
45185
  addonId: string(),
45248
45186
  nodeId: string().optional()
45249
45187
  }), object({ keys: array(string()) }).nullable(), { auth: "admin" }), method(object({
@@ -55648,6 +55586,12 @@ var IntegrationWithStateSchema = object({
55648
55586
  name: string(),
55649
55587
  /** A projection of the instance body's switch (`setInstanceEnabled` is the only writer). */
55650
55588
  enabled: boolean(),
55589
+ /**
55590
+ * True when the addon that holds the switch did not answer (down, failed, or keeps no record for
55591
+ * this row): `enabled` is then the stored registry flag, a stale value, not the state. A reader
55592
+ * must not draw it as on or off (D315).
55593
+ */
55594
+ switchUnknown: literal(true).optional(),
55651
55595
  info: record(string(), unknown()).readonly(),
55652
55596
  /** The discovery candidate this integration was set up from (D984). */
55653
55597
  originStableId: string().optional(),
@@ -55661,6 +55605,12 @@ var IntegrationLiteSchema = object({
55661
55605
  name: string(),
55662
55606
  /** A projection of the instance body's switch (`setInstanceEnabled` is the only writer). */
55663
55607
  enabled: boolean(),
55608
+ /**
55609
+ * True when the addon that holds the switch did not answer (down, failed, or keeps no record for
55610
+ * this row): `enabled` is then the stored registry flag, a stale value, not the state. A reader
55611
+ * must not draw it as on or off (D315).
55612
+ */
55613
+ switchUnknown: literal(true).optional(),
55664
55614
  info: record(string(), unknown()).readonly(),
55665
55615
  /** The discovery candidate this integration was set up from (D984). */
55666
55616
  originStableId: string().optional(),
@@ -55759,9 +55709,9 @@ var UpdateIntegrationInputSchema = object({
55759
55709
  id: string(),
55760
55710
  name: string().optional(),
55761
55711
  /**
55762
- * Accepted for a FIXED builtin only (`info.fixed`: alarm panel, camera grid, core blocks,
55763
- * terminal), which keeps no instance record: the registry flag is its one store. Any other
55764
- * integration is switched through `integration-instances.setInstanceEnabled`.
55712
+ * Honoured ONLY for a fixed builtin (`info.fixed`), whose switch is the hub's own registry flag
55713
+ * and works while its addon is down. Any other row is refused (a typed `BAD_REQUEST`): its
55714
+ * switch is `integration-instances.setInstanceEnabled`.
55765
55715
  */
55766
55716
  enabled: boolean().optional(),
55767
55717
  info: record(string(), unknown()).optional()
@@ -55795,6 +55745,12 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55795
55745
  * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
55796
55746
  * or the redaction token KEEPS the stored secret; `{ $secretClear: true }`
55797
55747
  * clears it.
55748
+ *
55749
+ * Who may call (epic #440): every method but `describe` and `instanceFormSchema` is open to any
55750
+ * session (`caller: 'scoped'`) and default-denies. An admin passes everywhere. A non-admin passes
55751
+ * only on a call pinned to one addon whose `describe().userScoped` is true, and that provider
55752
+ * filters every row on the `caller` the router injected (a notification target is somebody's
55753
+ * phone: a user sees and changes only their own). An addon that says nothing keeps the admin door.
55798
55754
  */
55799
55755
  var ConfigSchemaPassthrough$2 = custom();
55800
55756
  var IntegrationInstanceIdSchema = string().min(1);
@@ -55908,7 +55864,15 @@ var IntegrationInstanceDescriptionSchema = object({
55908
55864
  */
55909
55865
  mintsId: boolean().optional(),
55910
55866
  /** The links to other instances the provider's kinds declare; absent means none. */
55911
- links: array(IntegrationInstanceLinkSchema).optional()
55867
+ links: array(IntegrationInstanceLinkSchema).optional(),
55868
+ /**
55869
+ * The rows belong to individual users (a notification target is somebody's phone). The router
55870
+ * then opens `listInstances`, `getInstance`, `createInstance`, `updateInstance`,
55871
+ * `setInstanceEnabled`, `deleteInstance` and `testInstance` to a NON-admin session on a call
55872
+ * pinned to this addon, and hands the provider the `caller` so it keeps a user to their own rows.
55873
+ * Absent means admin only, as it always was.
55874
+ */
55875
+ userScoped: boolean().optional()
55912
55876
  });
55913
55877
  var IntegrationInstanceStatusSchema = object({
55914
55878
  state: _enum([
@@ -55966,8 +55930,14 @@ var integrationInstancesCapability = {
55966
55930
  /** The discovery candidate the wizard was seeded from (`stableId`), if any. */
55967
55931
  candidateStableId: string().min(1).optional()
55968
55932
  }), ConfigSchemaPassthrough$2, { auth: "admin" }),
55969
- listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
55970
- getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
55933
+ listInstances: method(looseObject({}), array(IntegrationInstanceSchema).readonly(), {
55934
+ auth: "protected",
55935
+ caller: "scoped"
55936
+ }),
55937
+ getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), {
55938
+ auth: "protected",
55939
+ caller: "scoped"
55940
+ }),
55971
55941
  /**
55972
55942
  * Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
55973
55943
  * without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
@@ -56000,11 +55970,13 @@ var integrationInstancesCapability = {
56000
55970
  seed: object({ info: record(string(), unknown()) }).optional()
56001
55971
  }), object({ id: string() }), {
56002
55972
  kind: "mutation",
56003
- auth: "admin"
55973
+ auth: "protected",
55974
+ caller: "scoped"
56004
55975
  }),
56005
55976
  updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
56006
55977
  kind: "mutation",
56007
- auth: "admin"
55978
+ auth: "protected",
55979
+ caller: "scoped"
56008
55980
  }),
56009
55981
  /**
56010
55982
  * Switch ONE instance on or off, for every role; a missing record is created from the addon's
@@ -56016,7 +55988,8 @@ var integrationInstancesCapability = {
56016
55988
  enabled: boolean()
56017
55989
  }), object({ ok: literal(true) }), {
56018
55990
  kind: "mutation",
56019
- auth: "admin"
55991
+ auth: "protected",
55992
+ caller: "scoped"
56020
55993
  }),
56021
55994
  /**
56022
55995
  * Delete the record and its keychain refs together. For an instance that brings devices the hub
@@ -56025,7 +55998,8 @@ var integrationInstancesCapability = {
56025
55998
  */
56026
55999
  deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56027
56000
  kind: "mutation",
56028
- auth: "admin"
56001
+ auth: "protected",
56002
+ caller: "scoped"
56029
56003
  }),
56030
56004
  testInstance: optionalMethod(object({
56031
56005
  id: IntegrationInstanceIdSchema.nullable(),
@@ -56034,7 +56008,8 @@ var integrationInstancesCapability = {
56034
56008
  kind: string().min(1).optional()
56035
56009
  }), TestConnectionResultSchema, {
56036
56010
  kind: "mutation",
56037
- auth: "admin"
56011
+ auth: "protected",
56012
+ caller: "scoped"
56038
56013
  })
56039
56014
  }
56040
56015
  };
@@ -73884,6 +73859,11 @@ var SendEmailResultSchema = object({
73884
73859
  var SmtpStatusSchema = object({
73885
73860
  /** True iff the addon has successfully verified the relay. */
73886
73861
  ready: boolean(),
73862
+ /**
73863
+ * The tri-state behind `ready` (D315): true when the last send worked, false when it failed,
73864
+ * ABSENT when the relay has not sent yet (unknown, never "not verified").
73865
+ */
73866
+ verified: boolean().optional(),
73887
73867
  /** Operator-visible host string (no credentials). */
73888
73868
  host: string(),
73889
73869
  /** Last error message reported by the relay, when not ready. */
@@ -73903,13 +73883,6 @@ var smtpProviderCapability = {
73903
73883
  auth: "admin",
73904
73884
  access: "create"
73905
73885
  }),
73906
- /** Round-trip ping against the SMTP relay (EHLO + AUTH if configured).
73907
- * Used by the operator's "Test connection" button. */
73908
- verify: method(RelayPinSchema, SmtpStatusSchema, {
73909
- kind: "mutation",
73910
- auth: "admin",
73911
- access: "view"
73912
- }),
73913
73886
  getStatus: method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" })
73914
73887
  }
73915
73888
  };
@@ -80580,13 +80553,6 @@ Object.freeze({
80580
80553
  access: "view",
80581
80554
  role: "admin"
80582
80555
  },
80583
- "addonSettings.getIntegrationFormSchema": {
80584
- capName: "addon-settings",
80585
- capScope: "system",
80586
- addonId: null,
80587
- access: "view",
80588
- role: "admin"
80589
- },
80590
80556
  "addonSettings.getScopedSettings": {
80591
80557
  capName: "addon-settings",
80592
80558
  capScope: "system",
@@ -88609,13 +88575,6 @@ Object.freeze({
88609
88575
  access: "create",
88610
88576
  role: "admin"
88611
88577
  },
88612
- "smtpProvider.verify": {
88613
- capName: "smtp-provider",
88614
- capScope: "system",
88615
- addonId: null,
88616
- access: "view",
88617
- role: "admin"
88618
- },
88619
88578
  "snapshot.applyDeviceSettingsPatch": {
88620
88579
  capName: "snapshot",
88621
88580
  capScope: "device",
@@ -95593,28 +95552,6 @@ function createGmailSmtpProvider(deps) {
95593
95552
  };
95594
95553
  return {
95595
95554
  getStatus: async () => status(),
95596
- verify: async () => {
95597
- const account = selectGmailAccount(deps.accounts(), void 0);
95598
- if (account === null) {
95599
- lastError = "No Google broker connected.";
95600
- return status();
95601
- }
95602
- try {
95603
- const token = await account.getAccessToken();
95604
- const res = await http(`${GOOGLE_GMAIL_API}/users/me/profile`, { headers: { Authorization: `Bearer ${token}` } });
95605
- if (!res.ok) {
95606
- const text = await res.text();
95607
- lastError = `Gmail profile failed (${res.status}): ${text.slice(0, 200)}`;
95608
- return status();
95609
- }
95610
- lastError = void 0;
95611
- lastVerifiedAt = Date.now();
95612
- return status();
95613
- } catch (err) {
95614
- lastError = err instanceof Error ? err.message : String(err);
95615
- return status();
95616
- }
95617
- },
95618
95555
  sendEmail: async (input) => {
95619
95556
  const account = selectGmailAccount(deps.accounts(), input.from);
95620
95557
  if (account === null) throw new Error("No Google broker connected — add one under Connectivity → Brokers and Connect.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-google-services",
3
- "version": "0.2.45",
3
+ "version": "0.2.46",
4
4
  "description": "Google — Google brokers (OAuth2): Calendar as calendar-source, Gmail send as smtp-provider.",
5
5
  "keywords": [
6
6
  "camstack",
@@ -64,7 +64,7 @@
64
64
  }
65
65
  ],
66
66
  "requires": {
67
- "server": ">=1.2.818"
67
+ "server": ">=1.2.820"
68
68
  }
69
69
  },
70
70
  "files": [