@camstack/addon-provider-homematic 1.2.11 → 1.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +385 -12
  2. package/dist/addon.mjs +385 -12
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -8,7 +8,7 @@ let http = require("http");
8
8
  let crypto$1 = require("crypto");
9
9
  let fs_promises = require("fs/promises");
10
10
  let path = require("path");
11
- //#region ../types/dist/event-category-41fKf-q9.mjs
11
+ //#region ../types/dist/event-category-Cv9dO26A.mjs
12
12
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
13
13
  EventCategory["SystemBoot"] = "system.boot";
14
14
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -24,6 +24,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
24
24
  */
25
25
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
26
26
  /**
27
+ * A newer addon or server-root package version was found by the
28
+ * authoritative registry check. Emitted once per
29
+ * `(target, packageName, currentVersion, latestVersion)` transition; repeated
30
+ * polling of the same result is deduplicated by the checker.
31
+ */
32
+ EventCategory["UpdateAvailable"] = "update.available";
33
+ /**
27
34
  * Readiness transition for a capability provider. Every producer emits
28
35
  * this event on `onInitialize` completion, `onDestroy`, and
29
36
  * `$node.reconnect`; every consumer that needs to gate on a cross-process
@@ -7123,6 +7130,104 @@ object({
7123
7130
  })
7124
7131
  });
7125
7132
  /**
7133
+ * Adoption job — the background form of `device-adoption.adopt`.
7134
+ *
7135
+ * ## Why this exists
7136
+ *
7137
+ * `adopt({childNativeIds: [...]})` materialises one CamStack device per
7138
+ * candidate PLUS every accessory child, and the whole array shares ONE UDS
7139
+ * request deadline (60s). Measured on the live hub against Home Assistant:
7140
+ * each device the kernel creates costs ~450 ms — `devices.create` pre-seeds
7141
+ * meta with up to eleven SEQUENTIAL round trips (`setName`, `setType`,
7142
+ * `setRole`, … `persistConfig`) before the class is constructed — and an
7143
+ * accessory child costs the same as its parent. So the real unit of work is
7144
+ * the CHILD, not the candidate:
7145
+ *
7146
+ * - 25 candidates averaging 6 children → ~150 devices → **>60s, times out**
7147
+ * - ONE candidate with 217 children → ~217 devices → **>60s, times out**
7148
+ *
7149
+ * That second line is why this is a job and not a smaller batch. No chunking,
7150
+ * no bounded concurrency over candidates and no per-call tuning can fix a
7151
+ * shape where **N=1 already exceeds the deadline** — the count that blows the
7152
+ * budget is the source system's accessory fan-out, which the operator does not
7153
+ * choose and cannot see. A design that only works below some N is the same bug
7154
+ * deferred.
7155
+ *
7156
+ * ## What the timeout did NOT do
7157
+ *
7158
+ * It did not stop the work. The UDS deadline ends the CALLER's wait; the
7159
+ * provider's loop runs to completion. Measured: a 25-candidate adopt that
7160
+ * "failed" at 60s had adopted 17 by 87s and all 25 by ~130s. The operator saw
7161
+ * an error and had no way to learn that. Every field below exists so that
7162
+ * question has an answer.
7163
+ *
7164
+ * ## Idempotency
7165
+ *
7166
+ * Jobs are in-RAM; a restart forgets them. That is safe here because adoption
7167
+ * is keyed by a stable id (`ha:<broker>:dev:<nativeId>` and equivalents), so
7168
+ * re-running a job re-adopts nothing: an already-adopted candidate is SKIPPED
7169
+ * by the engine before any provider call and lands in `alreadyAdopted`. It is
7170
+ * never a duplicate device, and never an error the operator has to interpret.
7171
+ */
7172
+ var AdoptionJobStateSchema = _enum([
7173
+ "running",
7174
+ "done",
7175
+ "failed",
7176
+ "cancelled"
7177
+ ]);
7178
+ /**
7179
+ * Per-candidate result. Every candidate the job was asked to adopt ends in
7180
+ * exactly one of these buckets — there is no silent drop, and the operator can
7181
+ * always answer "which of my 25 landed?".
7182
+ *
7183
+ * - `adopted` — created now by this job.
7184
+ * - `already-adopted` — a device for this candidate existed before the job
7185
+ * reached it (a re-run, or a retry after a timeout). Not an error.
7186
+ * - `failed` — the provider threw; `error` carries the message.
7187
+ * - `cancelled` — the operator cancelled before this candidate was reached.
7188
+ */
7189
+ var AdoptionOutcomeSchema = _enum([
7190
+ "adopted",
7191
+ "already-adopted",
7192
+ "failed",
7193
+ "cancelled"
7194
+ ]);
7195
+ var AdoptionCandidateResultSchema = object({
7196
+ childNativeId: string(),
7197
+ outcome: AdoptionOutcomeSchema,
7198
+ /** The materialised parent device id — null for `failed` / `cancelled`. */
7199
+ parentDeviceId: number().int().nonnegative().nullable(),
7200
+ /** Accessory children created for this candidate. */
7201
+ accessoryCount: number().int().nonnegative(),
7202
+ /** Failure message; null unless `outcome === 'failed'`. */
7203
+ error: string().nullable()
7204
+ });
7205
+ var AdoptionJobSchema = object({
7206
+ jobId: string(),
7207
+ /** The integration provider this job adopts through (the `addonId` pin). */
7208
+ addonId: string(),
7209
+ integrationId: string(),
7210
+ state: AdoptionJobStateSchema,
7211
+ /** Candidates the job was asked to adopt. Known up front, so never null. */
7212
+ total: number().int().nonnegative(),
7213
+ /** Candidates that have reached a terminal bucket. */
7214
+ processed: number().int().nonnegative(),
7215
+ adopted: number().int().nonnegative(),
7216
+ alreadyAdopted: number().int().nonnegative(),
7217
+ failed: number().int().nonnegative(),
7218
+ /** Accessory child devices created across every candidate — the real unit
7219
+ * of work, surfaced so a slow job is legible rather than mysterious. */
7220
+ accessoriesCreated: number().int().nonnegative(),
7221
+ /** The candidate currently being adopted; null when idle or finished. */
7222
+ currentChildNativeId: string().nullable(),
7223
+ /** One entry per candidate, in the order they were processed. */
7224
+ results: array(AdoptionCandidateResultSchema).readonly(),
7225
+ startedAt: number(),
7226
+ finishedAt: number().nullable(),
7227
+ /** Set only when the job itself broke (not a per-candidate failure). */
7228
+ error: string().nullable()
7229
+ });
7230
+ /**
7126
7231
  * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7127
7232
  * pipeline functions an operator thinks in terms of.
7128
7233
  *
@@ -12105,6 +12210,15 @@ method(object({
12105
12210
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
12106
12211
  kind: "mutation",
12107
12212
  auth: "admin"
12213
+ }), method(AdoptInputSchema.extend({ addonId: string() }), object({ jobId: string() }), {
12214
+ kind: "mutation",
12215
+ auth: "admin"
12216
+ }), method(object({
12217
+ addonId: string(),
12218
+ integrationId: string().optional()
12219
+ }), array(AdoptionJobSchema).readonly(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
12220
+ kind: "mutation",
12221
+ auth: "admin"
12108
12222
  }), method(ResyncInputSchema, ResyncResultSchema, {
12109
12223
  kind: "mutation",
12110
12224
  auth: "admin"
@@ -14020,8 +14134,35 @@ var NcDeliverySchema = _enum([
14020
14134
  "immediate",
14021
14135
  "track-end",
14022
14136
  "device-event",
14023
- "package-event"
14137
+ "package-event",
14138
+ "system-event"
14024
14139
  ]);
14140
+ /**
14141
+ * Stable Notification Center vocabulary over infrastructure/liveness events.
14142
+ * Bus categories are normalized into these intent-level kinds so rules do not
14143
+ * depend on a provider's raw event name or payload shape.
14144
+ */
14145
+ var NcSystemEventKindSchema = _enum([
14146
+ "camera-online",
14147
+ "camera-offline",
14148
+ "stream-online",
14149
+ "stream-offline",
14150
+ "node-online",
14151
+ "node-offline",
14152
+ "addon-update-available",
14153
+ "server-update-available"
14154
+ ]);
14155
+ /**
14156
+ * One coherent system-event condition. `kinds` is the required opt-in safety
14157
+ * gate; the remaining lists are optional narrowing filters relevant to the
14158
+ * selected kinds.
14159
+ */
14160
+ var NcSystemEventConditionSchema = object({
14161
+ kinds: array(NcSystemEventKindSchema).min(1),
14162
+ deviceIds: array(number().int()).min(1).optional(),
14163
+ nodeIds: array(string().min(1)).min(1).optional(),
14164
+ packageNames: array(string().min(1)).min(1).optional()
14165
+ });
14025
14166
  /** Weekly schedule — OR of windows; absence on the rule = always active. */
14026
14167
  var NcScheduleSchema = object({
14027
14168
  windows: array(object({
@@ -14326,6 +14467,8 @@ var NcConditionsSchema = object({
14326
14467
  "picked-up",
14327
14468
  "both"
14328
14469
  ]).optional(),
14470
+ /** Infrastructure/liveness/update event matcher (`system-event` delivery). */
14471
+ systemEvent: NcSystemEventConditionSchema.optional(),
14329
14472
  /**
14330
14473
  * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
14331
14474
  * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
@@ -14545,7 +14688,8 @@ var NcTestResultSchema = object({
14545
14688
  "object-event",
14546
14689
  "track",
14547
14690
  "device-event",
14548
- "package-event"
14691
+ "package-event",
14692
+ "system-event"
14549
14693
  ]),
14550
14694
  deviceId: number(),
14551
14695
  timestamp: number(),
@@ -14567,7 +14711,8 @@ var NcConditionDescriptorSchema = object({
14567
14711
  "schedule",
14568
14712
  "device",
14569
14713
  "package",
14570
- "occupancy"
14714
+ "occupancy",
14715
+ "system"
14571
14716
  ]),
14572
14717
  label: string(),
14573
14718
  /** Editor widget the UI renders — never hardcode per-condition forms. */
@@ -14585,7 +14730,8 @@ var NcConditionDescriptorSchema = object({
14585
14730
  "crossingSelect",
14586
14731
  "polygonDraw",
14587
14732
  "occupancy",
14588
- "deviceState"
14733
+ "deviceState",
14734
+ "systemEvent"
14589
14735
  ]),
14590
14736
  operator: _enum([
14591
14737
  "in",
@@ -14644,7 +14790,8 @@ var NcHistoryRecordKindSchema = _enum([
14644
14790
  "object-event",
14645
14791
  "track-end",
14646
14792
  "device-event",
14647
- "package-event"
14793
+ "package-event",
14794
+ "system-event"
14648
14795
  ]);
14649
14796
  /** Subject summary frozen on the row at fire time (survives rule/record edits). */
14650
14797
  var NcHistorySubjectSchema = object({
@@ -14652,7 +14799,14 @@ var NcHistorySubjectSchema = object({
14652
14799
  label: string().optional(),
14653
14800
  confidence: number().optional(),
14654
14801
  zones: array(string()),
14655
- timestamp: number()
14802
+ timestamp: number(),
14803
+ systemEvent: object({
14804
+ kind: NcSystemEventKindSchema,
14805
+ subject: string(),
14806
+ title: string(),
14807
+ body: string(),
14808
+ data: record(string(), unknown())
14809
+ }).optional()
14656
14810
  });
14657
14811
  /**
14658
14812
  * One delivery-history row. This is a read-only VIEW over the durable
@@ -15012,6 +15166,76 @@ object({
15012
15166
  * Each provider returns a static descriptor; the core enumerates them
15013
15167
  * to validate the `integration=` query param and resolve the consent
15014
15168
  * label + the scopes baked into the issued token.
15169
+ *
15170
+ * ## Declaring one
15171
+ *
15172
+ * An OAuth client is integration-specific knowledge — who the client is, what
15173
+ * it may ask for, where it may be sent — so it is declared by the ADDON that
15174
+ * owns the integration, never by the kernel and never as a branch inside
15175
+ * `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
15176
+ * steps, no others:
15177
+ *
15178
+ * 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
15179
+ * manifest entry. This is also what tells the hub, at addon-LOAD time, that
15180
+ * a descriptor is owed — see "the boot window" below.
15181
+ * 2. Return a provider from `onInitialize()`:
15182
+ *
15183
+ * ```ts
15184
+ * const provider: IOauthIntegrationProvider = {
15185
+ * getDescriptor: async () => ({
15186
+ * integrationId: 'my-thing', // the `integration=` query param
15187
+ * displayName: 'My Thing',
15188
+ * requestedScopes: [ … ], // see below
15189
+ * allowedRedirectPrefixes: ['https://callback.example/'],
15190
+ * }),
15191
+ * }
15192
+ * return [{ capability: oauthIntegrationCapability, provider }]
15193
+ * ```
15194
+ *
15195
+ * The descriptor must be **static** — it is read on the authorize path, so
15196
+ * never put an await on network or disk behind it, and never register it
15197
+ * behind one either (a provider is registered only once `onInitialize`
15198
+ * RETURNS, so anything awaited before the return delays linking).
15199
+ * 3. Nothing else. There is no allow-list to join, no id to register with the
15200
+ * core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
15201
+ * `/api/oauth2/integrations` are built from this collection alone.
15202
+ *
15203
+ * **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
15204
+ * NEEDS to function.** Not a blast radius, not a conservative
15205
+ * under-declaration, not a description of some other path the addon happens to
15206
+ * have. Derive it from what the client actually calls **with this token** —
15207
+ * every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
15208
+ * addon HTTP route it posts to — and write the call that justifies each entry
15209
+ * next to it. Two integrations once used this field to mean two different
15210
+ * things; the operator ruled there is one meaning, and any third integration
15211
+ * inherits it (2026-08-09).
15212
+ *
15213
+ * This is not documentation, it is the ENFORCEMENT INPUT. Since
15214
+ * [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
15215
+ * checks an integration token's grant before letting it reach an
15216
+ * `access: 'authenticated'` route, so an **under-declaration is an integration
15217
+ * that stops working** — a missing `addon:` entry means `403 Token scope
15218
+ * mismatch` on every control the client tries to actuate. Widen the descriptor
15219
+ * honestly rather than weakening a check to make a route pass.
15220
+ *
15221
+ * Prefer a narrow `capability:` scope to a `category:` one unless the client
15222
+ * genuinely needs a whole family; a category scope grants every future member
15223
+ * of that category too. `category:system [create]` has been rejected once and
15224
+ * should stay rejected: it hands `addons.installPackage` to an integration.
15225
+ *
15226
+ * Calls the ADDON itself makes over `ctx.api` run as the addon and are not
15227
+ * scope-checked, so they are not what this field describes — but reaching the
15228
+ * addon's route in the first place IS, and that is the entry to declare.
15229
+ *
15230
+ * **The boot window.** An addon registers its provider after its runner forks
15231
+ * and initialises, so between hub start and that moment this collection is
15232
+ * incomplete and an `integrationId` can be legitimately absent. The core does
15233
+ * not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
15234
+ * it compares the manifest declarers against the registered providers and
15235
+ * answers `503 temporarily_unavailable` (with `Retry-After` and the pending
15236
+ * addon ids) instead of `400 unknown integration`, and reports
15237
+ * `complete: false` on `GET /api/oauth2/integrations`. A client should retry
15238
+ * while the list is incomplete rather than conclude the hub cannot do OAuth.
15015
15239
  */
15016
15240
  var OauthIntegrationDescriptorSchema = object({
15017
15241
  /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
@@ -15042,7 +15266,30 @@ var OauthIntegrationDescriptorSchema = object({
15042
15266
  * present, /api/oauth2/authorize bakes THIS into the code instead of the
15043
15267
  * hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
15044
15268
  * the hub's env) drives the claim that its cloud Lambda routes back on. */
15045
- hubUrl: string().optional()
15269
+ hubUrl: string().optional(),
15270
+ /**
15271
+ * How long a REFRESH token issued for this integration lives — seconds, or
15272
+ * `'never'` for a token minted with no `exp` claim at all. Omit to keep the
15273
+ * 30-day default, which is what every link used before this field existed.
15274
+ *
15275
+ * Declared here for the same reason `requestedScopes` is: the integration
15276
+ * knows what it needs. Amazon's account linking and a Home Assistant config
15277
+ * entry are both meant to survive indefinitely, and re-linking is a manual
15278
+ * user action, so a 30-day expiry silently unlinks a working integration.
15279
+ *
15280
+ * **The security posture, stated so it is owned deliberately.** A refresh
15281
+ * token that never expires is permanent access if it leaks. What bounds it is
15282
+ * revocation, not time: `oauthRefresh` re-reads the session on every use and
15283
+ * returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
15284
+ * The one gap is the ACCESS token — it is a plain signed JWT that nothing
15285
+ * re-checks against the session on the `/trpc` and `/addon/*` paths, so
15286
+ * revoking a link takes effect there only after its remaining hour. That hour
15287
+ * is why the access TTL is not configurable.
15288
+ *
15289
+ * The value is baked into the authorization code at `/authorize` and travels
15290
+ * on the tokens, so editing this field changes FUTURE links only.
15291
+ */
15292
+ refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
15046
15293
  });
15047
15294
  method(_void(), OauthIntegrationDescriptorSchema);
15048
15295
  /**
@@ -18179,11 +18426,29 @@ var SsoBridgeClaimsSchema = object({
18179
18426
  codeChallenge: string().optional(),
18180
18427
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
18181
18428
  * tokens so the verify path can check the session is not revoked. */
18182
- sessionId: string().optional()
18429
+ sessionId: string().optional(),
18430
+ /**
18431
+ * The refresh lifetime this LINK was created with, in seconds, or `'never'`.
18432
+ * Baked into the code at `/authorize` from the integration's descriptor and
18433
+ * carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
18434
+ * on the token rather than being re-read from the descriptor on purpose:
18435
+ * editing a descriptor must not retroactively extend or shorten a link the
18436
+ * operator already consented to.
18437
+ */
18438
+ refreshTtl: union([number().int().positive(), literal("never")]).optional()
18183
18439
  });
18184
18440
  method(object({
18185
18441
  claims: SsoBridgeClaimsSchema,
18186
- ttlSec: number().int().positive().optional()
18442
+ /**
18443
+ * Seconds, or `'never'` for a token minted with NO `exp` claim.
18444
+ *
18445
+ * `'never'` is a literal rather than `undefined`/`0` because omitting
18446
+ * this field already means "the 5-minute SSO hand-off default", and
18447
+ * `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
18448
+ * went through the numeric path would fail at mint time and break
18449
+ * linking rather than produce an eternal token.
18450
+ */
18451
+ ttlSec: union([number().int().positive(), literal("never")]).optional()
18187
18452
  }), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
18188
18453
  var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
18189
18454
  providerId: string().min(1),
@@ -20164,6 +20429,37 @@ onColorChanged: { data: object({
20164
20429
  */
20165
20430
  runtimeState: ColorStatusSchema
20166
20431
  };
20432
+ var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
20433
+ object({
20434
+ outcome: literal("validated"),
20435
+ /** Round-trip of the sign-in, when the provider measured it. */
20436
+ latencyMs: number().nonnegative().optional(),
20437
+ /** Optional human detail worth showing next to the tick
20438
+ * ("3 devices visible on this account"). */
20439
+ detail: string().optional()
20440
+ }).strict(),
20441
+ object({
20442
+ outcome: literal("rejected"),
20443
+ error: string()
20444
+ }).strict(),
20445
+ object({
20446
+ outcome: literal("inconclusive"),
20447
+ error: string()
20448
+ }).strict()
20449
+ ]);
20450
+ var ConnectionTestInputSchema = object({
20451
+ /** Candidate integration settings, exactly as the create form collected them. */
20452
+ settings: record(string(), unknown()) });
20453
+ /**
20454
+ * What the provider's test actually DOES, so the UI can say it in words before
20455
+ * the operator presses the button ("Signs in to the Dreo cloud"). Purely
20456
+ * descriptive — it never changes routing.
20457
+ */
20458
+ var ConnectionTestDescriptorSchema = object({ label: string() });
20459
+ method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
20460
+ kind: "mutation",
20461
+ auth: "admin"
20462
+ }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
20167
20463
  /**
20168
20464
  * Upstream-system connectivity sensor — distinct from `device-status`,
20169
20465
  * which is the kernel-managed online/offline flag for the device's
@@ -21525,15 +21821,57 @@ var AvailableIntegrationTypeSchema = object({
21525
21821
  * flow can import (e.g. HA areas). Drives the adopt modal's "import
21526
21822
  * locations" checkbox. Provider-declared in the addon manifest. */
21527
21823
  supportsLocationImport: boolean(),
21824
+ /**
21825
+ * True when this integration DECLARES a pre-creation test (the
21826
+ * `connection-test` cap, or a broker whose settings it stores). Drives the
21827
+ * Test button: an integration that cannot be tested must say so up front
21828
+ * rather than offering a button that always answers the same nonsense.
21829
+ */
21830
+ canTest: boolean(),
21528
21831
  existingInstances: array(object({
21529
21832
  id: string(),
21530
21833
  name: string()
21531
21834
  })),
21532
21835
  canAdd: boolean()
21533
21836
  });
21837
+ /**
21838
+ * Why a test could not be answered as a plain boolean.
21839
+ *
21840
+ * `success` alone collapsed four different situations into one red box, and the
21841
+ * one that mattered most — "nobody ever asked the remote anything" — looked
21842
+ * exactly like "the remote said no". The status is the discriminator:
21843
+ *
21844
+ * - `validated` — a provider-declared test ran and the remote ACCEPTED.
21845
+ * - `rejected` — a provider-declared test ran and the remote REFUSED.
21846
+ * The only status that blocks `integrations.create`.
21847
+ * - `inconclusive` — a test IS declared but could not complete (timeout,
21848
+ * DNS, 5xx). Nothing was observed; not a failure.
21849
+ * - `unsupported` — this integration declares NO test. Nothing was
21850
+ * observed either; not a failure, and not a pass.
21851
+ *
21852
+ * `unsupported` and `inconclusive` both carry `success: false` so an older
21853
+ * client can never read them as a green tick, and both carry an `error` string
21854
+ * that SAYS the test did not run rather than inventing a failure.
21855
+ */
21856
+ var TestConnectionStatusEnum = _enum([
21857
+ "validated",
21858
+ "rejected",
21859
+ "inconclusive",
21860
+ "unsupported"
21861
+ ]);
21534
21862
  var TestConnectionResultSchema$1 = object({
21863
+ /** True ONLY for `validated`. Never true for a test that did not run. */
21535
21864
  success: boolean(),
21536
- error: string().optional()
21865
+ error: string().optional(),
21866
+ /** Optional for wire back-compat with clients built before the tri-state;
21867
+ * the server always sets it. */
21868
+ status: TestConnectionStatusEnum.optional(),
21869
+ /** Addon id whose declared test answered — `null` when none did. Lets the UI
21870
+ * attribute a result instead of blaming "the integration". */
21871
+ testedBy: string().nullable().optional(),
21872
+ latencyMs: number().nonnegative().optional(),
21873
+ /** Human detail from a `validated` result ("3 devices on this account"). */
21874
+ detail: string().optional()
21537
21875
  });
21538
21876
  var CreateIntegrationInputSchema = object({
21539
21877
  addonId: string(),
@@ -25302,7 +25640,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
25302
25640
  hubUrl: string(),
25303
25641
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
25304
25642
  * that carries one can ONLY be exchanged with the matching verifier. */
25305
- codeChallenge: string().optional()
25643
+ codeChallenge: string().optional(),
25644
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
25645
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
25646
+ * code so the link carries its own lifetime; omit for the 30-day
25647
+ * default. */
25648
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
25306
25649
  }), object({ code: string() }), {
25307
25650
  kind: "mutation",
25308
25651
  access: "create"
@@ -27756,6 +28099,18 @@ Object.freeze({
27756
28099
  addonId: null,
27757
28100
  access: "create"
27758
28101
  },
28102
+ "connectionTest.describeTest": {
28103
+ capName: "connection-test",
28104
+ capScope: "system",
28105
+ addonId: null,
28106
+ access: "view"
28107
+ },
28108
+ "connectionTest.testSettings": {
28109
+ capName: "connection-test",
28110
+ capScope: "system",
28111
+ addonId: null,
28112
+ access: "create"
28113
+ },
27759
28114
  "consumables.reset": {
27760
28115
  capName: "consumables",
27761
28116
  capScope: "device",
@@ -28146,6 +28501,12 @@ Object.freeze({
28146
28501
  addonId: null,
28147
28502
  access: "create"
28148
28503
  },
28504
+ "deviceManager.adoptionCancelJob": {
28505
+ capName: "device-manager",
28506
+ capScope: "system",
28507
+ addonId: null,
28508
+ access: "create"
28509
+ },
28149
28510
  "deviceManager.adoptionListCandidateFilters": {
28150
28511
  capName: "device-manager",
28151
28512
  capScope: "system",
@@ -28158,6 +28519,12 @@ Object.freeze({
28158
28519
  addonId: null,
28159
28520
  access: "view"
28160
28521
  },
28522
+ "deviceManager.adoptionListJobs": {
28523
+ capName: "device-manager",
28524
+ capScope: "system",
28525
+ addonId: null,
28526
+ access: "view"
28527
+ },
28161
28528
  "deviceManager.adoptionRefresh": {
28162
28529
  capName: "device-manager",
28163
28530
  capScope: "system",
@@ -28176,6 +28543,12 @@ Object.freeze({
28176
28543
  addonId: null,
28177
28544
  access: "create"
28178
28545
  },
28546
+ "deviceManager.adoptionStartJob": {
28547
+ capName: "device-manager",
28548
+ capScope: "system",
28549
+ addonId: null,
28550
+ access: "create"
28551
+ },
28179
28552
  "deviceManager.allocateDeviceId": {
28180
28553
  capName: "device-manager",
28181
28554
  capScope: "system",
package/dist/addon.mjs CHANGED
@@ -9,7 +9,7 @@ import { join } from "path";
9
9
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
10
10
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
11
11
  //#endregion
12
- //#region ../types/dist/event-category-41fKf-q9.mjs
12
+ //#region ../types/dist/event-category-Cv9dO26A.mjs
13
13
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
14
14
  EventCategory["SystemBoot"] = "system.boot";
15
15
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -25,6 +25,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
25
25
  */
26
26
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
27
27
  /**
28
+ * A newer addon or server-root package version was found by the
29
+ * authoritative registry check. Emitted once per
30
+ * `(target, packageName, currentVersion, latestVersion)` transition; repeated
31
+ * polling of the same result is deduplicated by the checker.
32
+ */
33
+ EventCategory["UpdateAvailable"] = "update.available";
34
+ /**
28
35
  * Readiness transition for a capability provider. Every producer emits
29
36
  * this event on `onInitialize` completion, `onDestroy`, and
30
37
  * `$node.reconnect`; every consumer that needs to gate on a cross-process
@@ -7124,6 +7131,104 @@ object({
7124
7131
  })
7125
7132
  });
7126
7133
  /**
7134
+ * Adoption job — the background form of `device-adoption.adopt`.
7135
+ *
7136
+ * ## Why this exists
7137
+ *
7138
+ * `adopt({childNativeIds: [...]})` materialises one CamStack device per
7139
+ * candidate PLUS every accessory child, and the whole array shares ONE UDS
7140
+ * request deadline (60s). Measured on the live hub against Home Assistant:
7141
+ * each device the kernel creates costs ~450 ms — `devices.create` pre-seeds
7142
+ * meta with up to eleven SEQUENTIAL round trips (`setName`, `setType`,
7143
+ * `setRole`, … `persistConfig`) before the class is constructed — and an
7144
+ * accessory child costs the same as its parent. So the real unit of work is
7145
+ * the CHILD, not the candidate:
7146
+ *
7147
+ * - 25 candidates averaging 6 children → ~150 devices → **>60s, times out**
7148
+ * - ONE candidate with 217 children → ~217 devices → **>60s, times out**
7149
+ *
7150
+ * That second line is why this is a job and not a smaller batch. No chunking,
7151
+ * no bounded concurrency over candidates and no per-call tuning can fix a
7152
+ * shape where **N=1 already exceeds the deadline** — the count that blows the
7153
+ * budget is the source system's accessory fan-out, which the operator does not
7154
+ * choose and cannot see. A design that only works below some N is the same bug
7155
+ * deferred.
7156
+ *
7157
+ * ## What the timeout did NOT do
7158
+ *
7159
+ * It did not stop the work. The UDS deadline ends the CALLER's wait; the
7160
+ * provider's loop runs to completion. Measured: a 25-candidate adopt that
7161
+ * "failed" at 60s had adopted 17 by 87s and all 25 by ~130s. The operator saw
7162
+ * an error and had no way to learn that. Every field below exists so that
7163
+ * question has an answer.
7164
+ *
7165
+ * ## Idempotency
7166
+ *
7167
+ * Jobs are in-RAM; a restart forgets them. That is safe here because adoption
7168
+ * is keyed by a stable id (`ha:<broker>:dev:<nativeId>` and equivalents), so
7169
+ * re-running a job re-adopts nothing: an already-adopted candidate is SKIPPED
7170
+ * by the engine before any provider call and lands in `alreadyAdopted`. It is
7171
+ * never a duplicate device, and never an error the operator has to interpret.
7172
+ */
7173
+ var AdoptionJobStateSchema = _enum([
7174
+ "running",
7175
+ "done",
7176
+ "failed",
7177
+ "cancelled"
7178
+ ]);
7179
+ /**
7180
+ * Per-candidate result. Every candidate the job was asked to adopt ends in
7181
+ * exactly one of these buckets — there is no silent drop, and the operator can
7182
+ * always answer "which of my 25 landed?".
7183
+ *
7184
+ * - `adopted` — created now by this job.
7185
+ * - `already-adopted` — a device for this candidate existed before the job
7186
+ * reached it (a re-run, or a retry after a timeout). Not an error.
7187
+ * - `failed` — the provider threw; `error` carries the message.
7188
+ * - `cancelled` — the operator cancelled before this candidate was reached.
7189
+ */
7190
+ var AdoptionOutcomeSchema = _enum([
7191
+ "adopted",
7192
+ "already-adopted",
7193
+ "failed",
7194
+ "cancelled"
7195
+ ]);
7196
+ var AdoptionCandidateResultSchema = object({
7197
+ childNativeId: string(),
7198
+ outcome: AdoptionOutcomeSchema,
7199
+ /** The materialised parent device id — null for `failed` / `cancelled`. */
7200
+ parentDeviceId: number().int().nonnegative().nullable(),
7201
+ /** Accessory children created for this candidate. */
7202
+ accessoryCount: number().int().nonnegative(),
7203
+ /** Failure message; null unless `outcome === 'failed'`. */
7204
+ error: string().nullable()
7205
+ });
7206
+ var AdoptionJobSchema = object({
7207
+ jobId: string(),
7208
+ /** The integration provider this job adopts through (the `addonId` pin). */
7209
+ addonId: string(),
7210
+ integrationId: string(),
7211
+ state: AdoptionJobStateSchema,
7212
+ /** Candidates the job was asked to adopt. Known up front, so never null. */
7213
+ total: number().int().nonnegative(),
7214
+ /** Candidates that have reached a terminal bucket. */
7215
+ processed: number().int().nonnegative(),
7216
+ adopted: number().int().nonnegative(),
7217
+ alreadyAdopted: number().int().nonnegative(),
7218
+ failed: number().int().nonnegative(),
7219
+ /** Accessory child devices created across every candidate — the real unit
7220
+ * of work, surfaced so a slow job is legible rather than mysterious. */
7221
+ accessoriesCreated: number().int().nonnegative(),
7222
+ /** The candidate currently being adopted; null when idle or finished. */
7223
+ currentChildNativeId: string().nullable(),
7224
+ /** One entry per candidate, in the order they were processed. */
7225
+ results: array(AdoptionCandidateResultSchema).readonly(),
7226
+ startedAt: number(),
7227
+ finishedAt: number().nullable(),
7228
+ /** Set only when the job itself broke (not a per-candidate failure). */
7229
+ error: string().nullable()
7230
+ });
7231
+ /**
7127
7232
  * Per-camera FUNCTION SWITCHES — the one coherent on/off surface over the
7128
7233
  * pipeline functions an operator thinks in terms of.
7129
7234
  *
@@ -12106,6 +12211,15 @@ method(object({
12106
12211
  }), method(ReleaseInputSchema.extend({ addonId: string() }), _void(), {
12107
12212
  kind: "mutation",
12108
12213
  auth: "admin"
12214
+ }), method(AdoptInputSchema.extend({ addonId: string() }), object({ jobId: string() }), {
12215
+ kind: "mutation",
12216
+ auth: "admin"
12217
+ }), method(object({
12218
+ addonId: string(),
12219
+ integrationId: string().optional()
12220
+ }), array(AdoptionJobSchema).readonly(), { auth: "admin" }), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
12221
+ kind: "mutation",
12222
+ auth: "admin"
12109
12223
  }), method(ResyncInputSchema, ResyncResultSchema, {
12110
12224
  kind: "mutation",
12111
12225
  auth: "admin"
@@ -14021,8 +14135,35 @@ var NcDeliverySchema = _enum([
14021
14135
  "immediate",
14022
14136
  "track-end",
14023
14137
  "device-event",
14024
- "package-event"
14138
+ "package-event",
14139
+ "system-event"
14025
14140
  ]);
14141
+ /**
14142
+ * Stable Notification Center vocabulary over infrastructure/liveness events.
14143
+ * Bus categories are normalized into these intent-level kinds so rules do not
14144
+ * depend on a provider's raw event name or payload shape.
14145
+ */
14146
+ var NcSystemEventKindSchema = _enum([
14147
+ "camera-online",
14148
+ "camera-offline",
14149
+ "stream-online",
14150
+ "stream-offline",
14151
+ "node-online",
14152
+ "node-offline",
14153
+ "addon-update-available",
14154
+ "server-update-available"
14155
+ ]);
14156
+ /**
14157
+ * One coherent system-event condition. `kinds` is the required opt-in safety
14158
+ * gate; the remaining lists are optional narrowing filters relevant to the
14159
+ * selected kinds.
14160
+ */
14161
+ var NcSystemEventConditionSchema = object({
14162
+ kinds: array(NcSystemEventKindSchema).min(1),
14163
+ deviceIds: array(number().int()).min(1).optional(),
14164
+ nodeIds: array(string().min(1)).min(1).optional(),
14165
+ packageNames: array(string().min(1)).min(1).optional()
14166
+ });
14026
14167
  /** Weekly schedule — OR of windows; absence on the rule = always active. */
14027
14168
  var NcScheduleSchema = object({
14028
14169
  windows: array(object({
@@ -14327,6 +14468,8 @@ var NcConditionsSchema = object({
14327
14468
  "picked-up",
14328
14469
  "both"
14329
14470
  ]).optional(),
14471
+ /** Infrastructure/liveness/update event matcher (`system-event` delivery). */
14472
+ systemEvent: NcSystemEventConditionSchema.optional(),
14330
14473
  /**
14331
14474
  * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
14332
14475
  * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
@@ -14546,7 +14689,8 @@ var NcTestResultSchema = object({
14546
14689
  "object-event",
14547
14690
  "track",
14548
14691
  "device-event",
14549
- "package-event"
14692
+ "package-event",
14693
+ "system-event"
14550
14694
  ]),
14551
14695
  deviceId: number(),
14552
14696
  timestamp: number(),
@@ -14568,7 +14712,8 @@ var NcConditionDescriptorSchema = object({
14568
14712
  "schedule",
14569
14713
  "device",
14570
14714
  "package",
14571
- "occupancy"
14715
+ "occupancy",
14716
+ "system"
14572
14717
  ]),
14573
14718
  label: string(),
14574
14719
  /** Editor widget the UI renders — never hardcode per-condition forms. */
@@ -14586,7 +14731,8 @@ var NcConditionDescriptorSchema = object({
14586
14731
  "crossingSelect",
14587
14732
  "polygonDraw",
14588
14733
  "occupancy",
14589
- "deviceState"
14734
+ "deviceState",
14735
+ "systemEvent"
14590
14736
  ]),
14591
14737
  operator: _enum([
14592
14738
  "in",
@@ -14645,7 +14791,8 @@ var NcHistoryRecordKindSchema = _enum([
14645
14791
  "object-event",
14646
14792
  "track-end",
14647
14793
  "device-event",
14648
- "package-event"
14794
+ "package-event",
14795
+ "system-event"
14649
14796
  ]);
14650
14797
  /** Subject summary frozen on the row at fire time (survives rule/record edits). */
14651
14798
  var NcHistorySubjectSchema = object({
@@ -14653,7 +14800,14 @@ var NcHistorySubjectSchema = object({
14653
14800
  label: string().optional(),
14654
14801
  confidence: number().optional(),
14655
14802
  zones: array(string()),
14656
- timestamp: number()
14803
+ timestamp: number(),
14804
+ systemEvent: object({
14805
+ kind: NcSystemEventKindSchema,
14806
+ subject: string(),
14807
+ title: string(),
14808
+ body: string(),
14809
+ data: record(string(), unknown())
14810
+ }).optional()
14657
14811
  });
14658
14812
  /**
14659
14813
  * One delivery-history row. This is a read-only VIEW over the durable
@@ -15013,6 +15167,76 @@ object({
15013
15167
  * Each provider returns a static descriptor; the core enumerates them
15014
15168
  * to validate the `integration=` query param and resolve the consent
15015
15169
  * label + the scopes baked into the issued token.
15170
+ *
15171
+ * ## Declaring one
15172
+ *
15173
+ * An OAuth client is integration-specific knowledge — who the client is, what
15174
+ * it may ask for, where it may be sent — so it is declared by the ADDON that
15175
+ * owns the integration, never by the kernel and never as a branch inside
15176
+ * `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
15177
+ * steps, no others:
15178
+ *
15179
+ * 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
15180
+ * manifest entry. This is also what tells the hub, at addon-LOAD time, that
15181
+ * a descriptor is owed — see "the boot window" below.
15182
+ * 2. Return a provider from `onInitialize()`:
15183
+ *
15184
+ * ```ts
15185
+ * const provider: IOauthIntegrationProvider = {
15186
+ * getDescriptor: async () => ({
15187
+ * integrationId: 'my-thing', // the `integration=` query param
15188
+ * displayName: 'My Thing',
15189
+ * requestedScopes: [ … ], // see below
15190
+ * allowedRedirectPrefixes: ['https://callback.example/'],
15191
+ * }),
15192
+ * }
15193
+ * return [{ capability: oauthIntegrationCapability, provider }]
15194
+ * ```
15195
+ *
15196
+ * The descriptor must be **static** — it is read on the authorize path, so
15197
+ * never put an await on network or disk behind it, and never register it
15198
+ * behind one either (a provider is registered only once `onInitialize`
15199
+ * RETURNS, so anything awaited before the return delays linking).
15200
+ * 3. Nothing else. There is no allow-list to join, no id to register with the
15201
+ * core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
15202
+ * `/api/oauth2/integrations` are built from this collection alone.
15203
+ *
15204
+ * **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
15205
+ * NEEDS to function.** Not a blast radius, not a conservative
15206
+ * under-declaration, not a description of some other path the addon happens to
15207
+ * have. Derive it from what the client actually calls **with this token** —
15208
+ * every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
15209
+ * addon HTTP route it posts to — and write the call that justifies each entry
15210
+ * next to it. Two integrations once used this field to mean two different
15211
+ * things; the operator ruled there is one meaning, and any third integration
15212
+ * inherits it (2026-08-09).
15213
+ *
15214
+ * This is not documentation, it is the ENFORCEMENT INPUT. Since
15215
+ * [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
15216
+ * checks an integration token's grant before letting it reach an
15217
+ * `access: 'authenticated'` route, so an **under-declaration is an integration
15218
+ * that stops working** — a missing `addon:` entry means `403 Token scope
15219
+ * mismatch` on every control the client tries to actuate. Widen the descriptor
15220
+ * honestly rather than weakening a check to make a route pass.
15221
+ *
15222
+ * Prefer a narrow `capability:` scope to a `category:` one unless the client
15223
+ * genuinely needs a whole family; a category scope grants every future member
15224
+ * of that category too. `category:system [create]` has been rejected once and
15225
+ * should stay rejected: it hands `addons.installPackage` to an integration.
15226
+ *
15227
+ * Calls the ADDON itself makes over `ctx.api` run as the addon and are not
15228
+ * scope-checked, so they are not what this field describes — but reaching the
15229
+ * addon's route in the first place IS, and that is the entry to declare.
15230
+ *
15231
+ * **The boot window.** An addon registers its provider after its runner forks
15232
+ * and initialises, so between hub start and that moment this collection is
15233
+ * incomplete and an `integrationId` can be legitimately absent. The core does
15234
+ * not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
15235
+ * it compares the manifest declarers against the registered providers and
15236
+ * answers `503 temporarily_unavailable` (with `Retry-After` and the pending
15237
+ * addon ids) instead of `400 unknown integration`, and reports
15238
+ * `complete: false` on `GET /api/oauth2/integrations`. A client should retry
15239
+ * while the list is incomplete rather than conclude the hub cannot do OAuth.
15016
15240
  */
15017
15241
  var OauthIntegrationDescriptorSchema = object({
15018
15242
  /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
@@ -15043,7 +15267,30 @@ var OauthIntegrationDescriptorSchema = object({
15043
15267
  * present, /api/oauth2/authorize bakes THIS into the code instead of the
15044
15268
  * hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
15045
15269
  * the hub's env) drives the claim that its cloud Lambda routes back on. */
15046
- hubUrl: string().optional()
15270
+ hubUrl: string().optional(),
15271
+ /**
15272
+ * How long a REFRESH token issued for this integration lives — seconds, or
15273
+ * `'never'` for a token minted with no `exp` claim at all. Omit to keep the
15274
+ * 30-day default, which is what every link used before this field existed.
15275
+ *
15276
+ * Declared here for the same reason `requestedScopes` is: the integration
15277
+ * knows what it needs. Amazon's account linking and a Home Assistant config
15278
+ * entry are both meant to survive indefinitely, and re-linking is a manual
15279
+ * user action, so a 30-day expiry silently unlinks a working integration.
15280
+ *
15281
+ * **The security posture, stated so it is owned deliberately.** A refresh
15282
+ * token that never expires is permanent access if it leaks. What bounds it is
15283
+ * revocation, not time: `oauthRefresh` re-reads the session on every use and
15284
+ * returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
15285
+ * The one gap is the ACCESS token — it is a plain signed JWT that nothing
15286
+ * re-checks against the session on the `/trpc` and `/addon/*` paths, so
15287
+ * revoking a link takes effect there only after its remaining hour. That hour
15288
+ * is why the access TTL is not configurable.
15289
+ *
15290
+ * The value is baked into the authorization code at `/authorize` and travels
15291
+ * on the tokens, so editing this field changes FUTURE links only.
15292
+ */
15293
+ refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
15047
15294
  });
15048
15295
  method(_void(), OauthIntegrationDescriptorSchema);
15049
15296
  /**
@@ -18180,11 +18427,29 @@ var SsoBridgeClaimsSchema = object({
18180
18427
  codeChallenge: string().optional(),
18181
18428
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
18182
18429
  * tokens so the verify path can check the session is not revoked. */
18183
- sessionId: string().optional()
18430
+ sessionId: string().optional(),
18431
+ /**
18432
+ * The refresh lifetime this LINK was created with, in seconds, or `'never'`.
18433
+ * Baked into the code at `/authorize` from the integration's descriptor and
18434
+ * carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
18435
+ * on the token rather than being re-read from the descriptor on purpose:
18436
+ * editing a descriptor must not retroactively extend or shorten a link the
18437
+ * operator already consented to.
18438
+ */
18439
+ refreshTtl: union([number().int().positive(), literal("never")]).optional()
18184
18440
  });
18185
18441
  method(object({
18186
18442
  claims: SsoBridgeClaimsSchema,
18187
- ttlSec: number().int().positive().optional()
18443
+ /**
18444
+ * Seconds, or `'never'` for a token minted with NO `exp` claim.
18445
+ *
18446
+ * `'never'` is a literal rather than `undefined`/`0` because omitting
18447
+ * this field already means "the 5-minute SSO hand-off default", and
18448
+ * `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
18449
+ * went through the numeric path would fail at mint time and break
18450
+ * linking rather than produce an eternal token.
18451
+ */
18452
+ ttlSec: union([number().int().positive(), literal("never")]).optional()
18188
18453
  }), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
18189
18454
  var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
18190
18455
  providerId: string().min(1),
@@ -20165,6 +20430,37 @@ onColorChanged: { data: object({
20165
20430
  */
20166
20431
  runtimeState: ColorStatusSchema
20167
20432
  };
20433
+ var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
20434
+ object({
20435
+ outcome: literal("validated"),
20436
+ /** Round-trip of the sign-in, when the provider measured it. */
20437
+ latencyMs: number().nonnegative().optional(),
20438
+ /** Optional human detail worth showing next to the tick
20439
+ * ("3 devices visible on this account"). */
20440
+ detail: string().optional()
20441
+ }).strict(),
20442
+ object({
20443
+ outcome: literal("rejected"),
20444
+ error: string()
20445
+ }).strict(),
20446
+ object({
20447
+ outcome: literal("inconclusive"),
20448
+ error: string()
20449
+ }).strict()
20450
+ ]);
20451
+ var ConnectionTestInputSchema = object({
20452
+ /** Candidate integration settings, exactly as the create form collected them. */
20453
+ settings: record(string(), unknown()) });
20454
+ /**
20455
+ * What the provider's test actually DOES, so the UI can say it in words before
20456
+ * the operator presses the button ("Signs in to the Dreo cloud"). Purely
20457
+ * descriptive — it never changes routing.
20458
+ */
20459
+ var ConnectionTestDescriptorSchema = object({ label: string() });
20460
+ method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
20461
+ kind: "mutation",
20462
+ auth: "admin"
20463
+ }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
20168
20464
  /**
20169
20465
  * Upstream-system connectivity sensor — distinct from `device-status`,
20170
20466
  * which is the kernel-managed online/offline flag for the device's
@@ -21526,15 +21822,57 @@ var AvailableIntegrationTypeSchema = object({
21526
21822
  * flow can import (e.g. HA areas). Drives the adopt modal's "import
21527
21823
  * locations" checkbox. Provider-declared in the addon manifest. */
21528
21824
  supportsLocationImport: boolean(),
21825
+ /**
21826
+ * True when this integration DECLARES a pre-creation test (the
21827
+ * `connection-test` cap, or a broker whose settings it stores). Drives the
21828
+ * Test button: an integration that cannot be tested must say so up front
21829
+ * rather than offering a button that always answers the same nonsense.
21830
+ */
21831
+ canTest: boolean(),
21529
21832
  existingInstances: array(object({
21530
21833
  id: string(),
21531
21834
  name: string()
21532
21835
  })),
21533
21836
  canAdd: boolean()
21534
21837
  });
21838
+ /**
21839
+ * Why a test could not be answered as a plain boolean.
21840
+ *
21841
+ * `success` alone collapsed four different situations into one red box, and the
21842
+ * one that mattered most — "nobody ever asked the remote anything" — looked
21843
+ * exactly like "the remote said no". The status is the discriminator:
21844
+ *
21845
+ * - `validated` — a provider-declared test ran and the remote ACCEPTED.
21846
+ * - `rejected` — a provider-declared test ran and the remote REFUSED.
21847
+ * The only status that blocks `integrations.create`.
21848
+ * - `inconclusive` — a test IS declared but could not complete (timeout,
21849
+ * DNS, 5xx). Nothing was observed; not a failure.
21850
+ * - `unsupported` — this integration declares NO test. Nothing was
21851
+ * observed either; not a failure, and not a pass.
21852
+ *
21853
+ * `unsupported` and `inconclusive` both carry `success: false` so an older
21854
+ * client can never read them as a green tick, and both carry an `error` string
21855
+ * that SAYS the test did not run rather than inventing a failure.
21856
+ */
21857
+ var TestConnectionStatusEnum = _enum([
21858
+ "validated",
21859
+ "rejected",
21860
+ "inconclusive",
21861
+ "unsupported"
21862
+ ]);
21535
21863
  var TestConnectionResultSchema$1 = object({
21864
+ /** True ONLY for `validated`. Never true for a test that did not run. */
21536
21865
  success: boolean(),
21537
- error: string().optional()
21866
+ error: string().optional(),
21867
+ /** Optional for wire back-compat with clients built before the tri-state;
21868
+ * the server always sets it. */
21869
+ status: TestConnectionStatusEnum.optional(),
21870
+ /** Addon id whose declared test answered — `null` when none did. Lets the UI
21871
+ * attribute a result instead of blaming "the integration". */
21872
+ testedBy: string().nullable().optional(),
21873
+ latencyMs: number().nonnegative().optional(),
21874
+ /** Human detail from a `validated` result ("3 devices on this account"). */
21875
+ detail: string().optional()
21538
21876
  });
21539
21877
  var CreateIntegrationInputSchema = object({
21540
21878
  addonId: string(),
@@ -25303,7 +25641,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
25303
25641
  hubUrl: string(),
25304
25642
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
25305
25643
  * that carries one can ONLY be exchanged with the matching verifier. */
25306
- codeChallenge: string().optional()
25644
+ codeChallenge: string().optional(),
25645
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
25646
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
25647
+ * code so the link carries its own lifetime; omit for the 30-day
25648
+ * default. */
25649
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
25307
25650
  }), object({ code: string() }), {
25308
25651
  kind: "mutation",
25309
25652
  access: "create"
@@ -27757,6 +28100,18 @@ Object.freeze({
27757
28100
  addonId: null,
27758
28101
  access: "create"
27759
28102
  },
28103
+ "connectionTest.describeTest": {
28104
+ capName: "connection-test",
28105
+ capScope: "system",
28106
+ addonId: null,
28107
+ access: "view"
28108
+ },
28109
+ "connectionTest.testSettings": {
28110
+ capName: "connection-test",
28111
+ capScope: "system",
28112
+ addonId: null,
28113
+ access: "create"
28114
+ },
27760
28115
  "consumables.reset": {
27761
28116
  capName: "consumables",
27762
28117
  capScope: "device",
@@ -28147,6 +28502,12 @@ Object.freeze({
28147
28502
  addonId: null,
28148
28503
  access: "create"
28149
28504
  },
28505
+ "deviceManager.adoptionCancelJob": {
28506
+ capName: "device-manager",
28507
+ capScope: "system",
28508
+ addonId: null,
28509
+ access: "create"
28510
+ },
28150
28511
  "deviceManager.adoptionListCandidateFilters": {
28151
28512
  capName: "device-manager",
28152
28513
  capScope: "system",
@@ -28159,6 +28520,12 @@ Object.freeze({
28159
28520
  addonId: null,
28160
28521
  access: "view"
28161
28522
  },
28523
+ "deviceManager.adoptionListJobs": {
28524
+ capName: "device-manager",
28525
+ capScope: "system",
28526
+ addonId: null,
28527
+ access: "view"
28528
+ },
28162
28529
  "deviceManager.adoptionRefresh": {
28163
28530
  capName: "device-manager",
28164
28531
  capScope: "system",
@@ -28177,6 +28544,12 @@ Object.freeze({
28177
28544
  addonId: null,
28178
28545
  access: "create"
28179
28546
  },
28547
+ "deviceManager.adoptionStartJob": {
28548
+ capName: "device-manager",
28549
+ capScope: "system",
28550
+ addonId: null,
28551
+ access: "create"
28552
+ },
28180
28553
  "deviceManager.allocateDeviceId": {
28181
28554
  capName: "device-manager",
28182
28555
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homematic",
3
- "version": "1.2.11",
3
+ "version": "1.2.12",
4
4
  "description": "Homematic / HomematicIP (CCU3 / RaspberryMatic) device-provider addon for CamStack — wraps the nodehomematic library",
5
5
  "keywords": [
6
6
  "camstack",