@camstack/addon-provider-petkit 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
|
@@ -1101,7 +1101,7 @@ var Nodepetkit = class {
|
|
|
1101
1101
|
}
|
|
1102
1102
|
};
|
|
1103
1103
|
//#endregion
|
|
1104
|
-
//#region ../types/dist/event-category-
|
|
1104
|
+
//#region ../types/dist/event-category-Cv9dO26A.mjs
|
|
1105
1105
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
1106
1106
|
EventCategory["SystemBoot"] = "system.boot";
|
|
1107
1107
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -1117,6 +1117,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
1117
1117
|
*/
|
|
1118
1118
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
1119
1119
|
/**
|
|
1120
|
+
* A newer addon or server-root package version was found by the
|
|
1121
|
+
* authoritative registry check. Emitted once per
|
|
1122
|
+
* `(target, packageName, currentVersion, latestVersion)` transition; repeated
|
|
1123
|
+
* polling of the same result is deduplicated by the checker.
|
|
1124
|
+
*/
|
|
1125
|
+
EventCategory["UpdateAvailable"] = "update.available";
|
|
1126
|
+
/**
|
|
1120
1127
|
* Readiness transition for a capability provider. Every producer emits
|
|
1121
1128
|
* this event on `onInitialize` completion, `onDestroy`, and
|
|
1122
1129
|
* `$node.reconnect`; every consumer that needs to gate on a cross-process
|
|
@@ -15171,8 +15178,35 @@ var NcDeliverySchema = _enum([
|
|
|
15171
15178
|
"immediate",
|
|
15172
15179
|
"track-end",
|
|
15173
15180
|
"device-event",
|
|
15174
|
-
"package-event"
|
|
15181
|
+
"package-event",
|
|
15182
|
+
"system-event"
|
|
15183
|
+
]);
|
|
15184
|
+
/**
|
|
15185
|
+
* Stable Notification Center vocabulary over infrastructure/liveness events.
|
|
15186
|
+
* Bus categories are normalized into these intent-level kinds so rules do not
|
|
15187
|
+
* depend on a provider's raw event name or payload shape.
|
|
15188
|
+
*/
|
|
15189
|
+
var NcSystemEventKindSchema = _enum([
|
|
15190
|
+
"camera-online",
|
|
15191
|
+
"camera-offline",
|
|
15192
|
+
"stream-online",
|
|
15193
|
+
"stream-offline",
|
|
15194
|
+
"node-online",
|
|
15195
|
+
"node-offline",
|
|
15196
|
+
"addon-update-available",
|
|
15197
|
+
"server-update-available"
|
|
15175
15198
|
]);
|
|
15199
|
+
/**
|
|
15200
|
+
* One coherent system-event condition. `kinds` is the required opt-in safety
|
|
15201
|
+
* gate; the remaining lists are optional narrowing filters relevant to the
|
|
15202
|
+
* selected kinds.
|
|
15203
|
+
*/
|
|
15204
|
+
var NcSystemEventConditionSchema = object({
|
|
15205
|
+
kinds: array(NcSystemEventKindSchema).min(1),
|
|
15206
|
+
deviceIds: array(number().int()).min(1).optional(),
|
|
15207
|
+
nodeIds: array(string().min(1)).min(1).optional(),
|
|
15208
|
+
packageNames: array(string().min(1)).min(1).optional()
|
|
15209
|
+
});
|
|
15176
15210
|
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
15177
15211
|
var NcScheduleSchema = object({
|
|
15178
15212
|
windows: array(object({
|
|
@@ -15477,6 +15511,8 @@ var NcConditionsSchema = object({
|
|
|
15477
15511
|
"picked-up",
|
|
15478
15512
|
"both"
|
|
15479
15513
|
]).optional(),
|
|
15514
|
+
/** Infrastructure/liveness/update event matcher (`system-event` delivery). */
|
|
15515
|
+
systemEvent: NcSystemEventConditionSchema.optional(),
|
|
15480
15516
|
/**
|
|
15481
15517
|
* PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
|
|
15482
15518
|
* (MaskShape vocabulary). A record passes when its bbox overlaps ANY
|
|
@@ -15696,7 +15732,8 @@ var NcTestResultSchema = object({
|
|
|
15696
15732
|
"object-event",
|
|
15697
15733
|
"track",
|
|
15698
15734
|
"device-event",
|
|
15699
|
-
"package-event"
|
|
15735
|
+
"package-event",
|
|
15736
|
+
"system-event"
|
|
15700
15737
|
]),
|
|
15701
15738
|
deviceId: number(),
|
|
15702
15739
|
timestamp: number(),
|
|
@@ -15718,7 +15755,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
15718
15755
|
"schedule",
|
|
15719
15756
|
"device",
|
|
15720
15757
|
"package",
|
|
15721
|
-
"occupancy"
|
|
15758
|
+
"occupancy",
|
|
15759
|
+
"system"
|
|
15722
15760
|
]),
|
|
15723
15761
|
label: string(),
|
|
15724
15762
|
/** Editor widget the UI renders — never hardcode per-condition forms. */
|
|
@@ -15736,7 +15774,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
15736
15774
|
"crossingSelect",
|
|
15737
15775
|
"polygonDraw",
|
|
15738
15776
|
"occupancy",
|
|
15739
|
-
"deviceState"
|
|
15777
|
+
"deviceState",
|
|
15778
|
+
"systemEvent"
|
|
15740
15779
|
]),
|
|
15741
15780
|
operator: _enum([
|
|
15742
15781
|
"in",
|
|
@@ -15795,7 +15834,8 @@ var NcHistoryRecordKindSchema = _enum([
|
|
|
15795
15834
|
"object-event",
|
|
15796
15835
|
"track-end",
|
|
15797
15836
|
"device-event",
|
|
15798
|
-
"package-event"
|
|
15837
|
+
"package-event",
|
|
15838
|
+
"system-event"
|
|
15799
15839
|
]);
|
|
15800
15840
|
/** Subject summary frozen on the row at fire time (survives rule/record edits). */
|
|
15801
15841
|
var NcHistorySubjectSchema = object({
|
|
@@ -15803,7 +15843,14 @@ var NcHistorySubjectSchema = object({
|
|
|
15803
15843
|
label: string().optional(),
|
|
15804
15844
|
confidence: number().optional(),
|
|
15805
15845
|
zones: array(string()),
|
|
15806
|
-
timestamp: number()
|
|
15846
|
+
timestamp: number(),
|
|
15847
|
+
systemEvent: object({
|
|
15848
|
+
kind: NcSystemEventKindSchema,
|
|
15849
|
+
subject: string(),
|
|
15850
|
+
title: string(),
|
|
15851
|
+
body: string(),
|
|
15852
|
+
data: record(string(), unknown())
|
|
15853
|
+
}).optional()
|
|
15807
15854
|
});
|
|
15808
15855
|
/**
|
|
15809
15856
|
* One delivery-history row. This is a read-only VIEW over the durable
|
|
@@ -16163,6 +16210,76 @@ object({
|
|
|
16163
16210
|
* Each provider returns a static descriptor; the core enumerates them
|
|
16164
16211
|
* to validate the `integration=` query param and resolve the consent
|
|
16165
16212
|
* label + the scopes baked into the issued token.
|
|
16213
|
+
*
|
|
16214
|
+
* ## Declaring one
|
|
16215
|
+
*
|
|
16216
|
+
* An OAuth client is integration-specific knowledge — who the client is, what
|
|
16217
|
+
* it may ask for, where it may be sent — so it is declared by the ADDON that
|
|
16218
|
+
* owns the integration, never by the kernel and never as a branch inside
|
|
16219
|
+
* `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
|
|
16220
|
+
* steps, no others:
|
|
16221
|
+
*
|
|
16222
|
+
* 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
|
|
16223
|
+
* manifest entry. This is also what tells the hub, at addon-LOAD time, that
|
|
16224
|
+
* a descriptor is owed — see "the boot window" below.
|
|
16225
|
+
* 2. Return a provider from `onInitialize()`:
|
|
16226
|
+
*
|
|
16227
|
+
* ```ts
|
|
16228
|
+
* const provider: IOauthIntegrationProvider = {
|
|
16229
|
+
* getDescriptor: async () => ({
|
|
16230
|
+
* integrationId: 'my-thing', // the `integration=` query param
|
|
16231
|
+
* displayName: 'My Thing',
|
|
16232
|
+
* requestedScopes: [ … ], // see below
|
|
16233
|
+
* allowedRedirectPrefixes: ['https://callback.example/'],
|
|
16234
|
+
* }),
|
|
16235
|
+
* }
|
|
16236
|
+
* return [{ capability: oauthIntegrationCapability, provider }]
|
|
16237
|
+
* ```
|
|
16238
|
+
*
|
|
16239
|
+
* The descriptor must be **static** — it is read on the authorize path, so
|
|
16240
|
+
* never put an await on network or disk behind it, and never register it
|
|
16241
|
+
* behind one either (a provider is registered only once `onInitialize`
|
|
16242
|
+
* RETURNS, so anything awaited before the return delays linking).
|
|
16243
|
+
* 3. Nothing else. There is no allow-list to join, no id to register with the
|
|
16244
|
+
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
16245
|
+
* `/api/oauth2/integrations` are built from this collection alone.
|
|
16246
|
+
*
|
|
16247
|
+
* **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
|
|
16248
|
+
* NEEDS to function.** Not a blast radius, not a conservative
|
|
16249
|
+
* under-declaration, not a description of some other path the addon happens to
|
|
16250
|
+
* have. Derive it from what the client actually calls **with this token** —
|
|
16251
|
+
* every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
|
|
16252
|
+
* addon HTTP route it posts to — and write the call that justifies each entry
|
|
16253
|
+
* next to it. Two integrations once used this field to mean two different
|
|
16254
|
+
* things; the operator ruled there is one meaning, and any third integration
|
|
16255
|
+
* inherits it (2026-08-09).
|
|
16256
|
+
*
|
|
16257
|
+
* This is not documentation, it is the ENFORCEMENT INPUT. Since
|
|
16258
|
+
* [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
|
|
16259
|
+
* checks an integration token's grant before letting it reach an
|
|
16260
|
+
* `access: 'authenticated'` route, so an **under-declaration is an integration
|
|
16261
|
+
* that stops working** — a missing `addon:` entry means `403 Token scope
|
|
16262
|
+
* mismatch` on every control the client tries to actuate. Widen the descriptor
|
|
16263
|
+
* honestly rather than weakening a check to make a route pass.
|
|
16264
|
+
*
|
|
16265
|
+
* Prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
16266
|
+
* genuinely needs a whole family; a category scope grants every future member
|
|
16267
|
+
* of that category too. `category:system [create]` has been rejected once and
|
|
16268
|
+
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
16269
|
+
*
|
|
16270
|
+
* Calls the ADDON itself makes over `ctx.api` run as the addon and are not
|
|
16271
|
+
* scope-checked, so they are not what this field describes — but reaching the
|
|
16272
|
+
* addon's route in the first place IS, and that is the entry to declare.
|
|
16273
|
+
*
|
|
16274
|
+
* **The boot window.** An addon registers its provider after its runner forks
|
|
16275
|
+
* and initialises, so between hub start and that moment this collection is
|
|
16276
|
+
* incomplete and an `integrationId` can be legitimately absent. The core does
|
|
16277
|
+
* not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
|
|
16278
|
+
* it compares the manifest declarers against the registered providers and
|
|
16279
|
+
* answers `503 temporarily_unavailable` (with `Retry-After` and the pending
|
|
16280
|
+
* addon ids) instead of `400 unknown integration`, and reports
|
|
16281
|
+
* `complete: false` on `GET /api/oauth2/integrations`. A client should retry
|
|
16282
|
+
* while the list is incomplete rather than conclude the hub cannot do OAuth.
|
|
16166
16283
|
*/
|
|
16167
16284
|
var OauthIntegrationDescriptorSchema = object({
|
|
16168
16285
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -16193,7 +16310,30 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
16193
16310
|
* present, /api/oauth2/authorize bakes THIS into the code instead of the
|
|
16194
16311
|
* hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
|
|
16195
16312
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
16196
|
-
hubUrl: string().optional()
|
|
16313
|
+
hubUrl: string().optional(),
|
|
16314
|
+
/**
|
|
16315
|
+
* How long a REFRESH token issued for this integration lives — seconds, or
|
|
16316
|
+
* `'never'` for a token minted with no `exp` claim at all. Omit to keep the
|
|
16317
|
+
* 30-day default, which is what every link used before this field existed.
|
|
16318
|
+
*
|
|
16319
|
+
* Declared here for the same reason `requestedScopes` is: the integration
|
|
16320
|
+
* knows what it needs. Amazon's account linking and a Home Assistant config
|
|
16321
|
+
* entry are both meant to survive indefinitely, and re-linking is a manual
|
|
16322
|
+
* user action, so a 30-day expiry silently unlinks a working integration.
|
|
16323
|
+
*
|
|
16324
|
+
* **The security posture, stated so it is owned deliberately.** A refresh
|
|
16325
|
+
* token that never expires is permanent access if it leaks. What bounds it is
|
|
16326
|
+
* revocation, not time: `oauthRefresh` re-reads the session on every use and
|
|
16327
|
+
* returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
|
|
16328
|
+
* The one gap is the ACCESS token — it is a plain signed JWT that nothing
|
|
16329
|
+
* re-checks against the session on the `/trpc` and `/addon/*` paths, so
|
|
16330
|
+
* revoking a link takes effect there only after its remaining hour. That hour
|
|
16331
|
+
* is why the access TTL is not configurable.
|
|
16332
|
+
*
|
|
16333
|
+
* The value is baked into the authorization code at `/authorize` and travels
|
|
16334
|
+
* on the tokens, so editing this field changes FUTURE links only.
|
|
16335
|
+
*/
|
|
16336
|
+
refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
16197
16337
|
});
|
|
16198
16338
|
method(_void(), OauthIntegrationDescriptorSchema);
|
|
16199
16339
|
/**
|
|
@@ -19330,11 +19470,29 @@ var SsoBridgeClaimsSchema = object({
|
|
|
19330
19470
|
codeChallenge: string().optional(),
|
|
19331
19471
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
19332
19472
|
* tokens so the verify path can check the session is not revoked. */
|
|
19333
|
-
sessionId: string().optional()
|
|
19473
|
+
sessionId: string().optional(),
|
|
19474
|
+
/**
|
|
19475
|
+
* The refresh lifetime this LINK was created with, in seconds, or `'never'`.
|
|
19476
|
+
* Baked into the code at `/authorize` from the integration's descriptor and
|
|
19477
|
+
* carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
|
|
19478
|
+
* on the token rather than being re-read from the descriptor on purpose:
|
|
19479
|
+
* editing a descriptor must not retroactively extend or shorten a link the
|
|
19480
|
+
* operator already consented to.
|
|
19481
|
+
*/
|
|
19482
|
+
refreshTtl: union([number().int().positive(), literal("never")]).optional()
|
|
19334
19483
|
});
|
|
19335
19484
|
method(object({
|
|
19336
19485
|
claims: SsoBridgeClaimsSchema,
|
|
19337
|
-
|
|
19486
|
+
/**
|
|
19487
|
+
* Seconds, or `'never'` for a token minted with NO `exp` claim.
|
|
19488
|
+
*
|
|
19489
|
+
* `'never'` is a literal rather than `undefined`/`0` because omitting
|
|
19490
|
+
* this field already means "the 5-minute SSO hand-off default", and
|
|
19491
|
+
* `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
|
|
19492
|
+
* went through the numeric path would fail at mint time and break
|
|
19493
|
+
* linking rather than produce an eternal token.
|
|
19494
|
+
*/
|
|
19495
|
+
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
19338
19496
|
}), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
|
|
19339
19497
|
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
19340
19498
|
providerId: string().min(1),
|
|
@@ -26534,7 +26692,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
26534
26692
|
hubUrl: string(),
|
|
26535
26693
|
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
26536
26694
|
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
26537
|
-
codeChallenge: string().optional()
|
|
26695
|
+
codeChallenge: string().optional(),
|
|
26696
|
+
/** The integration's declared refresh lifetime — seconds, or `'never'`.
|
|
26697
|
+
* From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
|
|
26698
|
+
* code so the link carries its own lifetime; omit for the 30-day
|
|
26699
|
+
* default. */
|
|
26700
|
+
refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
26538
26701
|
}), object({ code: string() }), {
|
|
26539
26702
|
kind: "mutation",
|
|
26540
26703
|
access: "create"
|
package/dist/addon.mjs
CHANGED
|
@@ -1100,7 +1100,7 @@ var Nodepetkit = class {
|
|
|
1100
1100
|
}
|
|
1101
1101
|
};
|
|
1102
1102
|
//#endregion
|
|
1103
|
-
//#region ../types/dist/event-category-
|
|
1103
|
+
//#region ../types/dist/event-category-Cv9dO26A.mjs
|
|
1104
1104
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
1105
1105
|
EventCategory["SystemBoot"] = "system.boot";
|
|
1106
1106
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -1116,6 +1116,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
1116
1116
|
*/
|
|
1117
1117
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
1118
1118
|
/**
|
|
1119
|
+
* A newer addon or server-root package version was found by the
|
|
1120
|
+
* authoritative registry check. Emitted once per
|
|
1121
|
+
* `(target, packageName, currentVersion, latestVersion)` transition; repeated
|
|
1122
|
+
* polling of the same result is deduplicated by the checker.
|
|
1123
|
+
*/
|
|
1124
|
+
EventCategory["UpdateAvailable"] = "update.available";
|
|
1125
|
+
/**
|
|
1119
1126
|
* Readiness transition for a capability provider. Every producer emits
|
|
1120
1127
|
* this event on `onInitialize` completion, `onDestroy`, and
|
|
1121
1128
|
* `$node.reconnect`; every consumer that needs to gate on a cross-process
|
|
@@ -15170,8 +15177,35 @@ var NcDeliverySchema = _enum([
|
|
|
15170
15177
|
"immediate",
|
|
15171
15178
|
"track-end",
|
|
15172
15179
|
"device-event",
|
|
15173
|
-
"package-event"
|
|
15180
|
+
"package-event",
|
|
15181
|
+
"system-event"
|
|
15182
|
+
]);
|
|
15183
|
+
/**
|
|
15184
|
+
* Stable Notification Center vocabulary over infrastructure/liveness events.
|
|
15185
|
+
* Bus categories are normalized into these intent-level kinds so rules do not
|
|
15186
|
+
* depend on a provider's raw event name or payload shape.
|
|
15187
|
+
*/
|
|
15188
|
+
var NcSystemEventKindSchema = _enum([
|
|
15189
|
+
"camera-online",
|
|
15190
|
+
"camera-offline",
|
|
15191
|
+
"stream-online",
|
|
15192
|
+
"stream-offline",
|
|
15193
|
+
"node-online",
|
|
15194
|
+
"node-offline",
|
|
15195
|
+
"addon-update-available",
|
|
15196
|
+
"server-update-available"
|
|
15174
15197
|
]);
|
|
15198
|
+
/**
|
|
15199
|
+
* One coherent system-event condition. `kinds` is the required opt-in safety
|
|
15200
|
+
* gate; the remaining lists are optional narrowing filters relevant to the
|
|
15201
|
+
* selected kinds.
|
|
15202
|
+
*/
|
|
15203
|
+
var NcSystemEventConditionSchema = object({
|
|
15204
|
+
kinds: array(NcSystemEventKindSchema).min(1),
|
|
15205
|
+
deviceIds: array(number().int()).min(1).optional(),
|
|
15206
|
+
nodeIds: array(string().min(1)).min(1).optional(),
|
|
15207
|
+
packageNames: array(string().min(1)).min(1).optional()
|
|
15208
|
+
});
|
|
15175
15209
|
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
15176
15210
|
var NcScheduleSchema = object({
|
|
15177
15211
|
windows: array(object({
|
|
@@ -15476,6 +15510,8 @@ var NcConditionsSchema = object({
|
|
|
15476
15510
|
"picked-up",
|
|
15477
15511
|
"both"
|
|
15478
15512
|
]).optional(),
|
|
15513
|
+
/** Infrastructure/liveness/update event matcher (`system-event` delivery). */
|
|
15514
|
+
systemEvent: NcSystemEventConditionSchema.optional(),
|
|
15479
15515
|
/**
|
|
15480
15516
|
* PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
|
|
15481
15517
|
* (MaskShape vocabulary). A record passes when its bbox overlaps ANY
|
|
@@ -15695,7 +15731,8 @@ var NcTestResultSchema = object({
|
|
|
15695
15731
|
"object-event",
|
|
15696
15732
|
"track",
|
|
15697
15733
|
"device-event",
|
|
15698
|
-
"package-event"
|
|
15734
|
+
"package-event",
|
|
15735
|
+
"system-event"
|
|
15699
15736
|
]),
|
|
15700
15737
|
deviceId: number(),
|
|
15701
15738
|
timestamp: number(),
|
|
@@ -15717,7 +15754,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
15717
15754
|
"schedule",
|
|
15718
15755
|
"device",
|
|
15719
15756
|
"package",
|
|
15720
|
-
"occupancy"
|
|
15757
|
+
"occupancy",
|
|
15758
|
+
"system"
|
|
15721
15759
|
]),
|
|
15722
15760
|
label: string(),
|
|
15723
15761
|
/** Editor widget the UI renders — never hardcode per-condition forms. */
|
|
@@ -15735,7 +15773,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
15735
15773
|
"crossingSelect",
|
|
15736
15774
|
"polygonDraw",
|
|
15737
15775
|
"occupancy",
|
|
15738
|
-
"deviceState"
|
|
15776
|
+
"deviceState",
|
|
15777
|
+
"systemEvent"
|
|
15739
15778
|
]),
|
|
15740
15779
|
operator: _enum([
|
|
15741
15780
|
"in",
|
|
@@ -15794,7 +15833,8 @@ var NcHistoryRecordKindSchema = _enum([
|
|
|
15794
15833
|
"object-event",
|
|
15795
15834
|
"track-end",
|
|
15796
15835
|
"device-event",
|
|
15797
|
-
"package-event"
|
|
15836
|
+
"package-event",
|
|
15837
|
+
"system-event"
|
|
15798
15838
|
]);
|
|
15799
15839
|
/** Subject summary frozen on the row at fire time (survives rule/record edits). */
|
|
15800
15840
|
var NcHistorySubjectSchema = object({
|
|
@@ -15802,7 +15842,14 @@ var NcHistorySubjectSchema = object({
|
|
|
15802
15842
|
label: string().optional(),
|
|
15803
15843
|
confidence: number().optional(),
|
|
15804
15844
|
zones: array(string()),
|
|
15805
|
-
timestamp: number()
|
|
15845
|
+
timestamp: number(),
|
|
15846
|
+
systemEvent: object({
|
|
15847
|
+
kind: NcSystemEventKindSchema,
|
|
15848
|
+
subject: string(),
|
|
15849
|
+
title: string(),
|
|
15850
|
+
body: string(),
|
|
15851
|
+
data: record(string(), unknown())
|
|
15852
|
+
}).optional()
|
|
15806
15853
|
});
|
|
15807
15854
|
/**
|
|
15808
15855
|
* One delivery-history row. This is a read-only VIEW over the durable
|
|
@@ -16162,6 +16209,76 @@ object({
|
|
|
16162
16209
|
* Each provider returns a static descriptor; the core enumerates them
|
|
16163
16210
|
* to validate the `integration=` query param and resolve the consent
|
|
16164
16211
|
* label + the scopes baked into the issued token.
|
|
16212
|
+
*
|
|
16213
|
+
* ## Declaring one
|
|
16214
|
+
*
|
|
16215
|
+
* An OAuth client is integration-specific knowledge — who the client is, what
|
|
16216
|
+
* it may ask for, where it may be sent — so it is declared by the ADDON that
|
|
16217
|
+
* owns the integration, never by the kernel and never as a branch inside
|
|
16218
|
+
* `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
|
|
16219
|
+
* steps, no others:
|
|
16220
|
+
*
|
|
16221
|
+
* 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
|
|
16222
|
+
* manifest entry. This is also what tells the hub, at addon-LOAD time, that
|
|
16223
|
+
* a descriptor is owed — see "the boot window" below.
|
|
16224
|
+
* 2. Return a provider from `onInitialize()`:
|
|
16225
|
+
*
|
|
16226
|
+
* ```ts
|
|
16227
|
+
* const provider: IOauthIntegrationProvider = {
|
|
16228
|
+
* getDescriptor: async () => ({
|
|
16229
|
+
* integrationId: 'my-thing', // the `integration=` query param
|
|
16230
|
+
* displayName: 'My Thing',
|
|
16231
|
+
* requestedScopes: [ … ], // see below
|
|
16232
|
+
* allowedRedirectPrefixes: ['https://callback.example/'],
|
|
16233
|
+
* }),
|
|
16234
|
+
* }
|
|
16235
|
+
* return [{ capability: oauthIntegrationCapability, provider }]
|
|
16236
|
+
* ```
|
|
16237
|
+
*
|
|
16238
|
+
* The descriptor must be **static** — it is read on the authorize path, so
|
|
16239
|
+
* never put an await on network or disk behind it, and never register it
|
|
16240
|
+
* behind one either (a provider is registered only once `onInitialize`
|
|
16241
|
+
* RETURNS, so anything awaited before the return delays linking).
|
|
16242
|
+
* 3. Nothing else. There is no allow-list to join, no id to register with the
|
|
16243
|
+
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
16244
|
+
* `/api/oauth2/integrations` are built from this collection alone.
|
|
16245
|
+
*
|
|
16246
|
+
* **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
|
|
16247
|
+
* NEEDS to function.** Not a blast radius, not a conservative
|
|
16248
|
+
* under-declaration, not a description of some other path the addon happens to
|
|
16249
|
+
* have. Derive it from what the client actually calls **with this token** —
|
|
16250
|
+
* every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
|
|
16251
|
+
* addon HTTP route it posts to — and write the call that justifies each entry
|
|
16252
|
+
* next to it. Two integrations once used this field to mean two different
|
|
16253
|
+
* things; the operator ruled there is one meaning, and any third integration
|
|
16254
|
+
* inherits it (2026-08-09).
|
|
16255
|
+
*
|
|
16256
|
+
* This is not documentation, it is the ENFORCEMENT INPUT. Since
|
|
16257
|
+
* [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
|
|
16258
|
+
* checks an integration token's grant before letting it reach an
|
|
16259
|
+
* `access: 'authenticated'` route, so an **under-declaration is an integration
|
|
16260
|
+
* that stops working** — a missing `addon:` entry means `403 Token scope
|
|
16261
|
+
* mismatch` on every control the client tries to actuate. Widen the descriptor
|
|
16262
|
+
* honestly rather than weakening a check to make a route pass.
|
|
16263
|
+
*
|
|
16264
|
+
* Prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
16265
|
+
* genuinely needs a whole family; a category scope grants every future member
|
|
16266
|
+
* of that category too. `category:system [create]` has been rejected once and
|
|
16267
|
+
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
16268
|
+
*
|
|
16269
|
+
* Calls the ADDON itself makes over `ctx.api` run as the addon and are not
|
|
16270
|
+
* scope-checked, so they are not what this field describes — but reaching the
|
|
16271
|
+
* addon's route in the first place IS, and that is the entry to declare.
|
|
16272
|
+
*
|
|
16273
|
+
* **The boot window.** An addon registers its provider after its runner forks
|
|
16274
|
+
* and initialises, so between hub start and that moment this collection is
|
|
16275
|
+
* incomplete and an `integrationId` can be legitimately absent. The core does
|
|
16276
|
+
* not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
|
|
16277
|
+
* it compares the manifest declarers against the registered providers and
|
|
16278
|
+
* answers `503 temporarily_unavailable` (with `Retry-After` and the pending
|
|
16279
|
+
* addon ids) instead of `400 unknown integration`, and reports
|
|
16280
|
+
* `complete: false` on `GET /api/oauth2/integrations`. A client should retry
|
|
16281
|
+
* while the list is incomplete rather than conclude the hub cannot do OAuth.
|
|
16165
16282
|
*/
|
|
16166
16283
|
var OauthIntegrationDescriptorSchema = object({
|
|
16167
16284
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -16192,7 +16309,30 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
16192
16309
|
* present, /api/oauth2/authorize bakes THIS into the code instead of the
|
|
16193
16310
|
* hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
|
|
16194
16311
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
16195
|
-
hubUrl: string().optional()
|
|
16312
|
+
hubUrl: string().optional(),
|
|
16313
|
+
/**
|
|
16314
|
+
* How long a REFRESH token issued for this integration lives — seconds, or
|
|
16315
|
+
* `'never'` for a token minted with no `exp` claim at all. Omit to keep the
|
|
16316
|
+
* 30-day default, which is what every link used before this field existed.
|
|
16317
|
+
*
|
|
16318
|
+
* Declared here for the same reason `requestedScopes` is: the integration
|
|
16319
|
+
* knows what it needs. Amazon's account linking and a Home Assistant config
|
|
16320
|
+
* entry are both meant to survive indefinitely, and re-linking is a manual
|
|
16321
|
+
* user action, so a 30-day expiry silently unlinks a working integration.
|
|
16322
|
+
*
|
|
16323
|
+
* **The security posture, stated so it is owned deliberately.** A refresh
|
|
16324
|
+
* token that never expires is permanent access if it leaks. What bounds it is
|
|
16325
|
+
* revocation, not time: `oauthRefresh` re-reads the session on every use and
|
|
16326
|
+
* returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
|
|
16327
|
+
* The one gap is the ACCESS token — it is a plain signed JWT that nothing
|
|
16328
|
+
* re-checks against the session on the `/trpc` and `/addon/*` paths, so
|
|
16329
|
+
* revoking a link takes effect there only after its remaining hour. That hour
|
|
16330
|
+
* is why the access TTL is not configurable.
|
|
16331
|
+
*
|
|
16332
|
+
* The value is baked into the authorization code at `/authorize` and travels
|
|
16333
|
+
* on the tokens, so editing this field changes FUTURE links only.
|
|
16334
|
+
*/
|
|
16335
|
+
refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
16196
16336
|
});
|
|
16197
16337
|
method(_void(), OauthIntegrationDescriptorSchema);
|
|
16198
16338
|
/**
|
|
@@ -19329,11 +19469,29 @@ var SsoBridgeClaimsSchema = object({
|
|
|
19329
19469
|
codeChallenge: string().optional(),
|
|
19330
19470
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
19331
19471
|
* tokens so the verify path can check the session is not revoked. */
|
|
19332
|
-
sessionId: string().optional()
|
|
19472
|
+
sessionId: string().optional(),
|
|
19473
|
+
/**
|
|
19474
|
+
* The refresh lifetime this LINK was created with, in seconds, or `'never'`.
|
|
19475
|
+
* Baked into the code at `/authorize` from the integration's descriptor and
|
|
19476
|
+
* carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
|
|
19477
|
+
* on the token rather than being re-read from the descriptor on purpose:
|
|
19478
|
+
* editing a descriptor must not retroactively extend or shorten a link the
|
|
19479
|
+
* operator already consented to.
|
|
19480
|
+
*/
|
|
19481
|
+
refreshTtl: union([number().int().positive(), literal("never")]).optional()
|
|
19333
19482
|
});
|
|
19334
19483
|
method(object({
|
|
19335
19484
|
claims: SsoBridgeClaimsSchema,
|
|
19336
|
-
|
|
19485
|
+
/**
|
|
19486
|
+
* Seconds, or `'never'` for a token minted with NO `exp` claim.
|
|
19487
|
+
*
|
|
19488
|
+
* `'never'` is a literal rather than `undefined`/`0` because omitting
|
|
19489
|
+
* this field already means "the 5-minute SSO hand-off default", and
|
|
19490
|
+
* `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
|
|
19491
|
+
* went through the numeric path would fail at mint time and break
|
|
19492
|
+
* linking rather than produce an eternal token.
|
|
19493
|
+
*/
|
|
19494
|
+
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
19337
19495
|
}), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
|
|
19338
19496
|
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
19339
19497
|
providerId: string().min(1),
|
|
@@ -26533,7 +26691,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
26533
26691
|
hubUrl: string(),
|
|
26534
26692
|
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
26535
26693
|
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
26536
|
-
codeChallenge: string().optional()
|
|
26694
|
+
codeChallenge: string().optional(),
|
|
26695
|
+
/** The integration's declared refresh lifetime — seconds, or `'never'`.
|
|
26696
|
+
* From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
|
|
26697
|
+
* code so the link carries its own lifetime; omit for the 30-day
|
|
26698
|
+
* default. */
|
|
26699
|
+
refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
26537
26700
|
}), object({ code: string() }), {
|
|
26538
26701
|
kind: "mutation",
|
|
26539
26702
|
access: "create"
|
package/package.json
CHANGED