@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.
- package/dist/addon.js +174 -11
- package/dist/addon.mjs +174 -11
- 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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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",
|