@camstack/addon-provider-tuya 0.2.10 → 0.2.11

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 +174 -11
  2. package/dist/addon.mjs +174 -11
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -819,7 +819,7 @@ async function discoverDevices(options = {}, binder = createDgramBinder()) {
819
819
  return [...byId.values()];
820
820
  }
821
821
  //#endregion
822
- //#region ../types/dist/event-category-41fKf-q9.mjs
822
+ //#region ../types/dist/event-category-Cv9dO26A.mjs
823
823
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
824
824
  EventCategory["SystemBoot"] = "system.boot";
825
825
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -835,6 +835,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
835
835
  */
836
836
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
837
837
  /**
838
+ * A newer addon or server-root package version was found by the
839
+ * authoritative registry check. Emitted once per
840
+ * `(target, packageName, currentVersion, latestVersion)` transition; repeated
841
+ * polling of the same result is deduplicated by the checker.
842
+ */
843
+ EventCategory["UpdateAvailable"] = "update.available";
844
+ /**
838
845
  * Readiness transition for a capability provider. Every producer emits
839
846
  * this event on `onInitialize` completion, `onDestroy`, and
840
847
  * `$node.reconnect`; every consumer that needs to gate on a cross-process
@@ -14889,8 +14896,35 @@ var NcDeliverySchema = _enum([
14889
14896
  "immediate",
14890
14897
  "track-end",
14891
14898
  "device-event",
14892
- "package-event"
14899
+ "package-event",
14900
+ "system-event"
14901
+ ]);
14902
+ /**
14903
+ * Stable Notification Center vocabulary over infrastructure/liveness events.
14904
+ * Bus categories are normalized into these intent-level kinds so rules do not
14905
+ * depend on a provider's raw event name or payload shape.
14906
+ */
14907
+ var NcSystemEventKindSchema = _enum([
14908
+ "camera-online",
14909
+ "camera-offline",
14910
+ "stream-online",
14911
+ "stream-offline",
14912
+ "node-online",
14913
+ "node-offline",
14914
+ "addon-update-available",
14915
+ "server-update-available"
14893
14916
  ]);
14917
+ /**
14918
+ * One coherent system-event condition. `kinds` is the required opt-in safety
14919
+ * gate; the remaining lists are optional narrowing filters relevant to the
14920
+ * selected kinds.
14921
+ */
14922
+ var NcSystemEventConditionSchema = object({
14923
+ kinds: array(NcSystemEventKindSchema).min(1),
14924
+ deviceIds: array(number().int()).min(1).optional(),
14925
+ nodeIds: array(string().min(1)).min(1).optional(),
14926
+ packageNames: array(string().min(1)).min(1).optional()
14927
+ });
14894
14928
  /** Weekly schedule — OR of windows; absence on the rule = always active. */
14895
14929
  var NcScheduleSchema = object({
14896
14930
  windows: array(object({
@@ -15195,6 +15229,8 @@ var NcConditionsSchema = object({
15195
15229
  "picked-up",
15196
15230
  "both"
15197
15231
  ]).optional(),
15232
+ /** Infrastructure/liveness/update event matcher (`system-event` delivery). */
15233
+ systemEvent: NcSystemEventConditionSchema.optional(),
15198
15234
  /**
15199
15235
  * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
15200
15236
  * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
@@ -15414,7 +15450,8 @@ var NcTestResultSchema = object({
15414
15450
  "object-event",
15415
15451
  "track",
15416
15452
  "device-event",
15417
- "package-event"
15453
+ "package-event",
15454
+ "system-event"
15418
15455
  ]),
15419
15456
  deviceId: number(),
15420
15457
  timestamp: number(),
@@ -15436,7 +15473,8 @@ var NcConditionDescriptorSchema = object({
15436
15473
  "schedule",
15437
15474
  "device",
15438
15475
  "package",
15439
- "occupancy"
15476
+ "occupancy",
15477
+ "system"
15440
15478
  ]),
15441
15479
  label: string(),
15442
15480
  /** Editor widget the UI renders — never hardcode per-condition forms. */
@@ -15454,7 +15492,8 @@ var NcConditionDescriptorSchema = object({
15454
15492
  "crossingSelect",
15455
15493
  "polygonDraw",
15456
15494
  "occupancy",
15457
- "deviceState"
15495
+ "deviceState",
15496
+ "systemEvent"
15458
15497
  ]),
15459
15498
  operator: _enum([
15460
15499
  "in",
@@ -15513,7 +15552,8 @@ var NcHistoryRecordKindSchema = _enum([
15513
15552
  "object-event",
15514
15553
  "track-end",
15515
15554
  "device-event",
15516
- "package-event"
15555
+ "package-event",
15556
+ "system-event"
15517
15557
  ]);
15518
15558
  /** Subject summary frozen on the row at fire time (survives rule/record edits). */
15519
15559
  var NcHistorySubjectSchema = object({
@@ -15521,7 +15561,14 @@ var NcHistorySubjectSchema = object({
15521
15561
  label: string().optional(),
15522
15562
  confidence: number().optional(),
15523
15563
  zones: array(string()),
15524
- timestamp: number()
15564
+ timestamp: number(),
15565
+ systemEvent: object({
15566
+ kind: NcSystemEventKindSchema,
15567
+ subject: string(),
15568
+ title: string(),
15569
+ body: string(),
15570
+ data: record(string(), unknown())
15571
+ }).optional()
15525
15572
  });
15526
15573
  /**
15527
15574
  * One delivery-history row. This is a read-only VIEW over the durable
@@ -15881,6 +15928,76 @@ object({
15881
15928
  * Each provider returns a static descriptor; the core enumerates them
15882
15929
  * to validate the `integration=` query param and resolve the consent
15883
15930
  * label + the scopes baked into the issued token.
15931
+ *
15932
+ * ## Declaring one
15933
+ *
15934
+ * An OAuth client is integration-specific knowledge — who the client is, what
15935
+ * it may ask for, where it may be sent — so it is declared by the ADDON that
15936
+ * owns the integration, never by the kernel and never as a branch inside
15937
+ * `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
15938
+ * steps, no others:
15939
+ *
15940
+ * 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
15941
+ * manifest entry. This is also what tells the hub, at addon-LOAD time, that
15942
+ * a descriptor is owed — see "the boot window" below.
15943
+ * 2. Return a provider from `onInitialize()`:
15944
+ *
15945
+ * ```ts
15946
+ * const provider: IOauthIntegrationProvider = {
15947
+ * getDescriptor: async () => ({
15948
+ * integrationId: 'my-thing', // the `integration=` query param
15949
+ * displayName: 'My Thing',
15950
+ * requestedScopes: [ … ], // see below
15951
+ * allowedRedirectPrefixes: ['https://callback.example/'],
15952
+ * }),
15953
+ * }
15954
+ * return [{ capability: oauthIntegrationCapability, provider }]
15955
+ * ```
15956
+ *
15957
+ * The descriptor must be **static** — it is read on the authorize path, so
15958
+ * never put an await on network or disk behind it, and never register it
15959
+ * behind one either (a provider is registered only once `onInitialize`
15960
+ * RETURNS, so anything awaited before the return delays linking).
15961
+ * 3. Nothing else. There is no allow-list to join, no id to register with the
15962
+ * core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
15963
+ * `/api/oauth2/integrations` are built from this collection alone.
15964
+ *
15965
+ * **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
15966
+ * NEEDS to function.** Not a blast radius, not a conservative
15967
+ * under-declaration, not a description of some other path the addon happens to
15968
+ * have. Derive it from what the client actually calls **with this token** —
15969
+ * every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
15970
+ * addon HTTP route it posts to — and write the call that justifies each entry
15971
+ * next to it. Two integrations once used this field to mean two different
15972
+ * things; the operator ruled there is one meaning, and any third integration
15973
+ * inherits it (2026-08-09).
15974
+ *
15975
+ * This is not documentation, it is the ENFORCEMENT INPUT. Since
15976
+ * [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
15977
+ * checks an integration token's grant before letting it reach an
15978
+ * `access: 'authenticated'` route, so an **under-declaration is an integration
15979
+ * that stops working** — a missing `addon:` entry means `403 Token scope
15980
+ * mismatch` on every control the client tries to actuate. Widen the descriptor
15981
+ * honestly rather than weakening a check to make a route pass.
15982
+ *
15983
+ * Prefer a narrow `capability:` scope to a `category:` one unless the client
15984
+ * genuinely needs a whole family; a category scope grants every future member
15985
+ * of that category too. `category:system [create]` has been rejected once and
15986
+ * should stay rejected: it hands `addons.installPackage` to an integration.
15987
+ *
15988
+ * Calls the ADDON itself makes over `ctx.api` run as the addon and are not
15989
+ * scope-checked, so they are not what this field describes — but reaching the
15990
+ * addon's route in the first place IS, and that is the entry to declare.
15991
+ *
15992
+ * **The boot window.** An addon registers its provider after its runner forks
15993
+ * and initialises, so between hub start and that moment this collection is
15994
+ * incomplete and an `integrationId` can be legitimately absent. The core does
15995
+ * not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
15996
+ * it compares the manifest declarers against the registered providers and
15997
+ * answers `503 temporarily_unavailable` (with `Retry-After` and the pending
15998
+ * addon ids) instead of `400 unknown integration`, and reports
15999
+ * `complete: false` on `GET /api/oauth2/integrations`. A client should retry
16000
+ * while the list is incomplete rather than conclude the hub cannot do OAuth.
15884
16001
  */
15885
16002
  var OauthIntegrationDescriptorSchema = object({
15886
16003
  /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
@@ -15911,7 +16028,30 @@ var OauthIntegrationDescriptorSchema = object({
15911
16028
  * present, /api/oauth2/authorize bakes THIS into the code instead of the
15912
16029
  * hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
15913
16030
  * the hub's env) drives the claim that its cloud Lambda routes back on. */
15914
- hubUrl: string().optional()
16031
+ hubUrl: string().optional(),
16032
+ /**
16033
+ * How long a REFRESH token issued for this integration lives — seconds, or
16034
+ * `'never'` for a token minted with no `exp` claim at all. Omit to keep the
16035
+ * 30-day default, which is what every link used before this field existed.
16036
+ *
16037
+ * Declared here for the same reason `requestedScopes` is: the integration
16038
+ * knows what it needs. Amazon's account linking and a Home Assistant config
16039
+ * entry are both meant to survive indefinitely, and re-linking is a manual
16040
+ * user action, so a 30-day expiry silently unlinks a working integration.
16041
+ *
16042
+ * **The security posture, stated so it is owned deliberately.** A refresh
16043
+ * token that never expires is permanent access if it leaks. What bounds it is
16044
+ * revocation, not time: `oauthRefresh` re-reads the session on every use and
16045
+ * returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
16046
+ * The one gap is the ACCESS token — it is a plain signed JWT that nothing
16047
+ * re-checks against the session on the `/trpc` and `/addon/*` paths, so
16048
+ * revoking a link takes effect there only after its remaining hour. That hour
16049
+ * is why the access TTL is not configurable.
16050
+ *
16051
+ * The value is baked into the authorization code at `/authorize` and travels
16052
+ * on the tokens, so editing this field changes FUTURE links only.
16053
+ */
16054
+ refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
15915
16055
  });
15916
16056
  method(_void(), OauthIntegrationDescriptorSchema);
15917
16057
  /**
@@ -19048,11 +19188,29 @@ var SsoBridgeClaimsSchema = object({
19048
19188
  codeChallenge: string().optional(),
19049
19189
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
19050
19190
  * tokens so the verify path can check the session is not revoked. */
19051
- sessionId: string().optional()
19191
+ sessionId: string().optional(),
19192
+ /**
19193
+ * The refresh lifetime this LINK was created with, in seconds, or `'never'`.
19194
+ * Baked into the code at `/authorize` from the integration's descriptor and
19195
+ * carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
19196
+ * on the token rather than being re-read from the descriptor on purpose:
19197
+ * editing a descriptor must not retroactively extend or shorten a link the
19198
+ * operator already consented to.
19199
+ */
19200
+ refreshTtl: union([number().int().positive(), literal("never")]).optional()
19052
19201
  });
19053
19202
  method(object({
19054
19203
  claims: SsoBridgeClaimsSchema,
19055
- ttlSec: number().int().positive().optional()
19204
+ /**
19205
+ * Seconds, or `'never'` for a token minted with NO `exp` claim.
19206
+ *
19207
+ * `'never'` is a literal rather than `undefined`/`0` because omitting
19208
+ * this field already means "the 5-minute SSO hand-off default", and
19209
+ * `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
19210
+ * went through the numeric path would fail at mint time and break
19211
+ * linking rather than produce an eternal token.
19212
+ */
19213
+ ttlSec: union([number().int().positive(), literal("never")]).optional()
19056
19214
  }), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
19057
19215
  var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
19058
19216
  providerId: string().min(1),
@@ -26252,7 +26410,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
26252
26410
  hubUrl: string(),
26253
26411
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
26254
26412
  * that carries one can ONLY be exchanged with the matching verifier. */
26255
- codeChallenge: string().optional()
26413
+ codeChallenge: string().optional(),
26414
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
26415
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
26416
+ * code so the link carries its own lifetime; omit for the 30-day
26417
+ * default. */
26418
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
26256
26419
  }), object({ code: string() }), {
26257
26420
  kind: "mutation",
26258
26421
  access: "create"
package/dist/addon.mjs CHANGED
@@ -818,7 +818,7 @@ async function discoverDevices(options = {}, binder = createDgramBinder()) {
818
818
  return [...byId.values()];
819
819
  }
820
820
  //#endregion
821
- //#region ../types/dist/event-category-41fKf-q9.mjs
821
+ //#region ../types/dist/event-category-Cv9dO26A.mjs
822
822
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
823
823
  EventCategory["SystemBoot"] = "system.boot";
824
824
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -834,6 +834,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
834
834
  */
835
835
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
836
836
  /**
837
+ * A newer addon or server-root package version was found by the
838
+ * authoritative registry check. Emitted once per
839
+ * `(target, packageName, currentVersion, latestVersion)` transition; repeated
840
+ * polling of the same result is deduplicated by the checker.
841
+ */
842
+ EventCategory["UpdateAvailable"] = "update.available";
843
+ /**
837
844
  * Readiness transition for a capability provider. Every producer emits
838
845
  * this event on `onInitialize` completion, `onDestroy`, and
839
846
  * `$node.reconnect`; every consumer that needs to gate on a cross-process
@@ -14888,8 +14895,35 @@ var NcDeliverySchema = _enum([
14888
14895
  "immediate",
14889
14896
  "track-end",
14890
14897
  "device-event",
14891
- "package-event"
14898
+ "package-event",
14899
+ "system-event"
14900
+ ]);
14901
+ /**
14902
+ * Stable Notification Center vocabulary over infrastructure/liveness events.
14903
+ * Bus categories are normalized into these intent-level kinds so rules do not
14904
+ * depend on a provider's raw event name or payload shape.
14905
+ */
14906
+ var NcSystemEventKindSchema = _enum([
14907
+ "camera-online",
14908
+ "camera-offline",
14909
+ "stream-online",
14910
+ "stream-offline",
14911
+ "node-online",
14912
+ "node-offline",
14913
+ "addon-update-available",
14914
+ "server-update-available"
14892
14915
  ]);
14916
+ /**
14917
+ * One coherent system-event condition. `kinds` is the required opt-in safety
14918
+ * gate; the remaining lists are optional narrowing filters relevant to the
14919
+ * selected kinds.
14920
+ */
14921
+ var NcSystemEventConditionSchema = object({
14922
+ kinds: array(NcSystemEventKindSchema).min(1),
14923
+ deviceIds: array(number().int()).min(1).optional(),
14924
+ nodeIds: array(string().min(1)).min(1).optional(),
14925
+ packageNames: array(string().min(1)).min(1).optional()
14926
+ });
14893
14927
  /** Weekly schedule — OR of windows; absence on the rule = always active. */
14894
14928
  var NcScheduleSchema = object({
14895
14929
  windows: array(object({
@@ -15194,6 +15228,8 @@ var NcConditionsSchema = object({
15194
15228
  "picked-up",
15195
15229
  "both"
15196
15230
  ]).optional(),
15231
+ /** Infrastructure/liveness/update event matcher (`system-event` delivery). */
15232
+ systemEvent: NcSystemEventConditionSchema.optional(),
15197
15233
  /**
15198
15234
  * PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
15199
15235
  * (MaskShape vocabulary). A record passes when its bbox overlaps ANY
@@ -15413,7 +15449,8 @@ var NcTestResultSchema = object({
15413
15449
  "object-event",
15414
15450
  "track",
15415
15451
  "device-event",
15416
- "package-event"
15452
+ "package-event",
15453
+ "system-event"
15417
15454
  ]),
15418
15455
  deviceId: number(),
15419
15456
  timestamp: number(),
@@ -15435,7 +15472,8 @@ var NcConditionDescriptorSchema = object({
15435
15472
  "schedule",
15436
15473
  "device",
15437
15474
  "package",
15438
- "occupancy"
15475
+ "occupancy",
15476
+ "system"
15439
15477
  ]),
15440
15478
  label: string(),
15441
15479
  /** Editor widget the UI renders — never hardcode per-condition forms. */
@@ -15453,7 +15491,8 @@ var NcConditionDescriptorSchema = object({
15453
15491
  "crossingSelect",
15454
15492
  "polygonDraw",
15455
15493
  "occupancy",
15456
- "deviceState"
15494
+ "deviceState",
15495
+ "systemEvent"
15457
15496
  ]),
15458
15497
  operator: _enum([
15459
15498
  "in",
@@ -15512,7 +15551,8 @@ var NcHistoryRecordKindSchema = _enum([
15512
15551
  "object-event",
15513
15552
  "track-end",
15514
15553
  "device-event",
15515
- "package-event"
15554
+ "package-event",
15555
+ "system-event"
15516
15556
  ]);
15517
15557
  /** Subject summary frozen on the row at fire time (survives rule/record edits). */
15518
15558
  var NcHistorySubjectSchema = object({
@@ -15520,7 +15560,14 @@ var NcHistorySubjectSchema = object({
15520
15560
  label: string().optional(),
15521
15561
  confidence: number().optional(),
15522
15562
  zones: array(string()),
15523
- timestamp: number()
15563
+ timestamp: number(),
15564
+ systemEvent: object({
15565
+ kind: NcSystemEventKindSchema,
15566
+ subject: string(),
15567
+ title: string(),
15568
+ body: string(),
15569
+ data: record(string(), unknown())
15570
+ }).optional()
15524
15571
  });
15525
15572
  /**
15526
15573
  * One delivery-history row. This is a read-only VIEW over the durable
@@ -15880,6 +15927,76 @@ object({
15880
15927
  * Each provider returns a static descriptor; the core enumerates them
15881
15928
  * to validate the `integration=` query param and resolve the consent
15882
15929
  * label + the scopes baked into the issued token.
15930
+ *
15931
+ * ## Declaring one
15932
+ *
15933
+ * An OAuth client is integration-specific knowledge — who the client is, what
15934
+ * it may ask for, where it may be sent — so it is declared by the ADDON that
15935
+ * owns the integration, never by the kernel and never as a branch inside
15936
+ * `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
15937
+ * steps, no others:
15938
+ *
15939
+ * 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
15940
+ * manifest entry. This is also what tells the hub, at addon-LOAD time, that
15941
+ * a descriptor is owed — see "the boot window" below.
15942
+ * 2. Return a provider from `onInitialize()`:
15943
+ *
15944
+ * ```ts
15945
+ * const provider: IOauthIntegrationProvider = {
15946
+ * getDescriptor: async () => ({
15947
+ * integrationId: 'my-thing', // the `integration=` query param
15948
+ * displayName: 'My Thing',
15949
+ * requestedScopes: [ … ], // see below
15950
+ * allowedRedirectPrefixes: ['https://callback.example/'],
15951
+ * }),
15952
+ * }
15953
+ * return [{ capability: oauthIntegrationCapability, provider }]
15954
+ * ```
15955
+ *
15956
+ * The descriptor must be **static** — it is read on the authorize path, so
15957
+ * never put an await on network or disk behind it, and never register it
15958
+ * behind one either (a provider is registered only once `onInitialize`
15959
+ * RETURNS, so anything awaited before the return delays linking).
15960
+ * 3. Nothing else. There is no allow-list to join, no id to register with the
15961
+ * core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
15962
+ * `/api/oauth2/integrations` are built from this collection alone.
15963
+ *
15964
+ * **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
15965
+ * NEEDS to function.** Not a blast radius, not a conservative
15966
+ * under-declaration, not a description of some other path the addon happens to
15967
+ * have. Derive it from what the client actually calls **with this token** —
15968
+ * every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
15969
+ * addon HTTP route it posts to — and write the call that justifies each entry
15970
+ * next to it. Two integrations once used this field to mean two different
15971
+ * things; the operator ruled there is one meaning, and any third integration
15972
+ * inherits it (2026-08-09).
15973
+ *
15974
+ * This is not documentation, it is the ENFORCEMENT INPUT. Since
15975
+ * [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
15976
+ * checks an integration token's grant before letting it reach an
15977
+ * `access: 'authenticated'` route, so an **under-declaration is an integration
15978
+ * that stops working** — a missing `addon:` entry means `403 Token scope
15979
+ * mismatch` on every control the client tries to actuate. Widen the descriptor
15980
+ * honestly rather than weakening a check to make a route pass.
15981
+ *
15982
+ * Prefer a narrow `capability:` scope to a `category:` one unless the client
15983
+ * genuinely needs a whole family; a category scope grants every future member
15984
+ * of that category too. `category:system [create]` has been rejected once and
15985
+ * should stay rejected: it hands `addons.installPackage` to an integration.
15986
+ *
15987
+ * Calls the ADDON itself makes over `ctx.api` run as the addon and are not
15988
+ * scope-checked, so they are not what this field describes — but reaching the
15989
+ * addon's route in the first place IS, and that is the entry to declare.
15990
+ *
15991
+ * **The boot window.** An addon registers its provider after its runner forks
15992
+ * and initialises, so between hub start and that moment this collection is
15993
+ * incomplete and an `integrationId` can be legitimately absent. The core does
15994
+ * not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
15995
+ * it compares the manifest declarers against the registered providers and
15996
+ * answers `503 temporarily_unavailable` (with `Retry-After` and the pending
15997
+ * addon ids) instead of `400 unknown integration`, and reports
15998
+ * `complete: false` on `GET /api/oauth2/integrations`. A client should retry
15999
+ * while the list is incomplete rather than conclude the hub cannot do OAuth.
15883
16000
  */
15884
16001
  var OauthIntegrationDescriptorSchema = object({
15885
16002
  /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
@@ -15910,7 +16027,30 @@ var OauthIntegrationDescriptorSchema = object({
15910
16027
  * present, /api/oauth2/authorize bakes THIS into the code instead of the
15911
16028
  * hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
15912
16029
  * the hub's env) drives the claim that its cloud Lambda routes back on. */
15913
- hubUrl: string().optional()
16030
+ hubUrl: string().optional(),
16031
+ /**
16032
+ * How long a REFRESH token issued for this integration lives — seconds, or
16033
+ * `'never'` for a token minted with no `exp` claim at all. Omit to keep the
16034
+ * 30-day default, which is what every link used before this field existed.
16035
+ *
16036
+ * Declared here for the same reason `requestedScopes` is: the integration
16037
+ * knows what it needs. Amazon's account linking and a Home Assistant config
16038
+ * entry are both meant to survive indefinitely, and re-linking is a manual
16039
+ * user action, so a 30-day expiry silently unlinks a working integration.
16040
+ *
16041
+ * **The security posture, stated so it is owned deliberately.** A refresh
16042
+ * token that never expires is permanent access if it leaks. What bounds it is
16043
+ * revocation, not time: `oauthRefresh` re-reads the session on every use and
16044
+ * returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
16045
+ * The one gap is the ACCESS token — it is a plain signed JWT that nothing
16046
+ * re-checks against the session on the `/trpc` and `/addon/*` paths, so
16047
+ * revoking a link takes effect there only after its remaining hour. That hour
16048
+ * is why the access TTL is not configurable.
16049
+ *
16050
+ * The value is baked into the authorization code at `/authorize` and travels
16051
+ * on the tokens, so editing this field changes FUTURE links only.
16052
+ */
16053
+ refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
15914
16054
  });
15915
16055
  method(_void(), OauthIntegrationDescriptorSchema);
15916
16056
  /**
@@ -19047,11 +19187,29 @@ var SsoBridgeClaimsSchema = object({
19047
19187
  codeChallenge: string().optional(),
19048
19188
  /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
19049
19189
  * tokens so the verify path can check the session is not revoked. */
19050
- sessionId: string().optional()
19190
+ sessionId: string().optional(),
19191
+ /**
19192
+ * The refresh lifetime this LINK was created with, in seconds, or `'never'`.
19193
+ * Baked into the code at `/authorize` from the integration's descriptor and
19194
+ * carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
19195
+ * on the token rather than being re-read from the descriptor on purpose:
19196
+ * editing a descriptor must not retroactively extend or shorten a link the
19197
+ * operator already consented to.
19198
+ */
19199
+ refreshTtl: union([number().int().positive(), literal("never")]).optional()
19051
19200
  });
19052
19201
  method(object({
19053
19202
  claims: SsoBridgeClaimsSchema,
19054
- ttlSec: number().int().positive().optional()
19203
+ /**
19204
+ * Seconds, or `'never'` for a token minted with NO `exp` claim.
19205
+ *
19206
+ * `'never'` is a literal rather than `undefined`/`0` because omitting
19207
+ * this field already means "the 5-minute SSO hand-off default", and
19208
+ * `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
19209
+ * went through the numeric path would fail at mint time and break
19210
+ * linking rather than produce an eternal token.
19211
+ */
19212
+ ttlSec: union([number().int().positive(), literal("never")]).optional()
19055
19213
  }), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
19056
19214
  var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
19057
19215
  providerId: string().min(1),
@@ -26251,7 +26409,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
26251
26409
  hubUrl: string(),
26252
26410
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
26253
26411
  * that carries one can ONLY be exchanged with the matching verifier. */
26254
- codeChallenge: string().optional()
26412
+ codeChallenge: string().optional(),
26413
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
26414
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
26415
+ * code so the link carries its own lifetime; omit for the 30-day
26416
+ * default. */
26417
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
26255
26418
  }), object({ code: string() }), {
26256
26419
  kind: "mutation",
26257
26420
  access: "create"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-tuya",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Tuya / Smart Life device-provider addon for CamStack — account-onboarded (Tuya IoT cloud fetch of device localKeys) + LOCAL DP control via the @apocaliss92/nodetuya encrypted-LAN client, exposing switch / water-heater-family kettle entities",
5
5
  "keywords": [
6
6
  "camstack",