@camstack/addon-provider-rtsp 1.2.10 → 1.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +216 -11
- package/dist/addon.mjs +216 -11
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let node_net = require("node:net");
|
|
25
25
|
node_net = __toESM(node_net);
|
|
26
|
-
//#region ../types/dist/event-category-
|
|
26
|
+
//#region ../types/dist/event-category-Cv9dO26A.mjs
|
|
27
27
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
28
28
|
EventCategory["SystemBoot"] = "system.boot";
|
|
29
29
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -39,6 +39,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
39
39
|
*/
|
|
40
40
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
41
41
|
/**
|
|
42
|
+
* A newer addon or server-root package version was found by the
|
|
43
|
+
* authoritative registry check. Emitted once per
|
|
44
|
+
* `(target, packageName, currentVersion, latestVersion)` transition; repeated
|
|
45
|
+
* polling of the same result is deduplicated by the checker.
|
|
46
|
+
*/
|
|
47
|
+
EventCategory["UpdateAvailable"] = "update.available";
|
|
48
|
+
/**
|
|
42
49
|
* Readiness transition for a capability provider. Every producer emits
|
|
43
50
|
* this event on `onInitialize` completion, `onDestroy`, and
|
|
44
51
|
* `$node.reconnect`; every consumer that needs to gate on a cross-process
|
|
@@ -14108,8 +14115,35 @@ var NcDeliverySchema = _enum([
|
|
|
14108
14115
|
"immediate",
|
|
14109
14116
|
"track-end",
|
|
14110
14117
|
"device-event",
|
|
14111
|
-
"package-event"
|
|
14118
|
+
"package-event",
|
|
14119
|
+
"system-event"
|
|
14120
|
+
]);
|
|
14121
|
+
/**
|
|
14122
|
+
* Stable Notification Center vocabulary over infrastructure/liveness events.
|
|
14123
|
+
* Bus categories are normalized into these intent-level kinds so rules do not
|
|
14124
|
+
* depend on a provider's raw event name or payload shape.
|
|
14125
|
+
*/
|
|
14126
|
+
var NcSystemEventKindSchema = _enum([
|
|
14127
|
+
"camera-online",
|
|
14128
|
+
"camera-offline",
|
|
14129
|
+
"stream-online",
|
|
14130
|
+
"stream-offline",
|
|
14131
|
+
"node-online",
|
|
14132
|
+
"node-offline",
|
|
14133
|
+
"addon-update-available",
|
|
14134
|
+
"server-update-available"
|
|
14112
14135
|
]);
|
|
14136
|
+
/**
|
|
14137
|
+
* One coherent system-event condition. `kinds` is the required opt-in safety
|
|
14138
|
+
* gate; the remaining lists are optional narrowing filters relevant to the
|
|
14139
|
+
* selected kinds.
|
|
14140
|
+
*/
|
|
14141
|
+
var NcSystemEventConditionSchema = object({
|
|
14142
|
+
kinds: array(NcSystemEventKindSchema).min(1),
|
|
14143
|
+
deviceIds: array(number().int()).min(1).optional(),
|
|
14144
|
+
nodeIds: array(string().min(1)).min(1).optional(),
|
|
14145
|
+
packageNames: array(string().min(1)).min(1).optional()
|
|
14146
|
+
});
|
|
14113
14147
|
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
14114
14148
|
var NcScheduleSchema = object({
|
|
14115
14149
|
windows: array(object({
|
|
@@ -14414,6 +14448,8 @@ var NcConditionsSchema = object({
|
|
|
14414
14448
|
"picked-up",
|
|
14415
14449
|
"both"
|
|
14416
14450
|
]).optional(),
|
|
14451
|
+
/** Infrastructure/liveness/update event matcher (`system-event` delivery). */
|
|
14452
|
+
systemEvent: NcSystemEventConditionSchema.optional(),
|
|
14417
14453
|
/**
|
|
14418
14454
|
* PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
|
|
14419
14455
|
* (MaskShape vocabulary). A record passes when its bbox overlaps ANY
|
|
@@ -14633,7 +14669,8 @@ var NcTestResultSchema = object({
|
|
|
14633
14669
|
"object-event",
|
|
14634
14670
|
"track",
|
|
14635
14671
|
"device-event",
|
|
14636
|
-
"package-event"
|
|
14672
|
+
"package-event",
|
|
14673
|
+
"system-event"
|
|
14637
14674
|
]),
|
|
14638
14675
|
deviceId: number(),
|
|
14639
14676
|
timestamp: number(),
|
|
@@ -14655,7 +14692,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
14655
14692
|
"schedule",
|
|
14656
14693
|
"device",
|
|
14657
14694
|
"package",
|
|
14658
|
-
"occupancy"
|
|
14695
|
+
"occupancy",
|
|
14696
|
+
"system"
|
|
14659
14697
|
]),
|
|
14660
14698
|
label: string(),
|
|
14661
14699
|
/** Editor widget the UI renders — never hardcode per-condition forms. */
|
|
@@ -14673,7 +14711,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
14673
14711
|
"crossingSelect",
|
|
14674
14712
|
"polygonDraw",
|
|
14675
14713
|
"occupancy",
|
|
14676
|
-
"deviceState"
|
|
14714
|
+
"deviceState",
|
|
14715
|
+
"systemEvent"
|
|
14677
14716
|
]),
|
|
14678
14717
|
operator: _enum([
|
|
14679
14718
|
"in",
|
|
@@ -14732,7 +14771,8 @@ var NcHistoryRecordKindSchema = _enum([
|
|
|
14732
14771
|
"object-event",
|
|
14733
14772
|
"track-end",
|
|
14734
14773
|
"device-event",
|
|
14735
|
-
"package-event"
|
|
14774
|
+
"package-event",
|
|
14775
|
+
"system-event"
|
|
14736
14776
|
]);
|
|
14737
14777
|
/** Subject summary frozen on the row at fire time (survives rule/record edits). */
|
|
14738
14778
|
var NcHistorySubjectSchema = object({
|
|
@@ -14740,7 +14780,14 @@ var NcHistorySubjectSchema = object({
|
|
|
14740
14780
|
label: string().optional(),
|
|
14741
14781
|
confidence: number().optional(),
|
|
14742
14782
|
zones: array(string()),
|
|
14743
|
-
timestamp: number()
|
|
14783
|
+
timestamp: number(),
|
|
14784
|
+
systemEvent: object({
|
|
14785
|
+
kind: NcSystemEventKindSchema,
|
|
14786
|
+
subject: string(),
|
|
14787
|
+
title: string(),
|
|
14788
|
+
body: string(),
|
|
14789
|
+
data: record(string(), unknown())
|
|
14790
|
+
}).optional()
|
|
14744
14791
|
});
|
|
14745
14792
|
/**
|
|
14746
14793
|
* One delivery-history row. This is a read-only VIEW over the durable
|
|
@@ -15100,6 +15147,76 @@ object({
|
|
|
15100
15147
|
* Each provider returns a static descriptor; the core enumerates them
|
|
15101
15148
|
* to validate the `integration=` query param and resolve the consent
|
|
15102
15149
|
* label + the scopes baked into the issued token.
|
|
15150
|
+
*
|
|
15151
|
+
* ## Declaring one
|
|
15152
|
+
*
|
|
15153
|
+
* An OAuth client is integration-specific knowledge — who the client is, what
|
|
15154
|
+
* it may ask for, where it may be sent — so it is declared by the ADDON that
|
|
15155
|
+
* owns the integration, never by the kernel and never as a branch inside
|
|
15156
|
+
* `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
|
|
15157
|
+
* steps, no others:
|
|
15158
|
+
*
|
|
15159
|
+
* 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
|
|
15160
|
+
* manifest entry. This is also what tells the hub, at addon-LOAD time, that
|
|
15161
|
+
* a descriptor is owed — see "the boot window" below.
|
|
15162
|
+
* 2. Return a provider from `onInitialize()`:
|
|
15163
|
+
*
|
|
15164
|
+
* ```ts
|
|
15165
|
+
* const provider: IOauthIntegrationProvider = {
|
|
15166
|
+
* getDescriptor: async () => ({
|
|
15167
|
+
* integrationId: 'my-thing', // the `integration=` query param
|
|
15168
|
+
* displayName: 'My Thing',
|
|
15169
|
+
* requestedScopes: [ … ], // see below
|
|
15170
|
+
* allowedRedirectPrefixes: ['https://callback.example/'],
|
|
15171
|
+
* }),
|
|
15172
|
+
* }
|
|
15173
|
+
* return [{ capability: oauthIntegrationCapability, provider }]
|
|
15174
|
+
* ```
|
|
15175
|
+
*
|
|
15176
|
+
* The descriptor must be **static** — it is read on the authorize path, so
|
|
15177
|
+
* never put an await on network or disk behind it, and never register it
|
|
15178
|
+
* behind one either (a provider is registered only once `onInitialize`
|
|
15179
|
+
* RETURNS, so anything awaited before the return delays linking).
|
|
15180
|
+
* 3. Nothing else. There is no allow-list to join, no id to register with the
|
|
15181
|
+
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
15182
|
+
* `/api/oauth2/integrations` are built from this collection alone.
|
|
15183
|
+
*
|
|
15184
|
+
* **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
|
|
15185
|
+
* NEEDS to function.** Not a blast radius, not a conservative
|
|
15186
|
+
* under-declaration, not a description of some other path the addon happens to
|
|
15187
|
+
* have. Derive it from what the client actually calls **with this token** —
|
|
15188
|
+
* every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
|
|
15189
|
+
* addon HTTP route it posts to — and write the call that justifies each entry
|
|
15190
|
+
* next to it. Two integrations once used this field to mean two different
|
|
15191
|
+
* things; the operator ruled there is one meaning, and any third integration
|
|
15192
|
+
* inherits it (2026-08-09).
|
|
15193
|
+
*
|
|
15194
|
+
* This is not documentation, it is the ENFORCEMENT INPUT. Since
|
|
15195
|
+
* [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
|
|
15196
|
+
* checks an integration token's grant before letting it reach an
|
|
15197
|
+
* `access: 'authenticated'` route, so an **under-declaration is an integration
|
|
15198
|
+
* that stops working** — a missing `addon:` entry means `403 Token scope
|
|
15199
|
+
* mismatch` on every control the client tries to actuate. Widen the descriptor
|
|
15200
|
+
* honestly rather than weakening a check to make a route pass.
|
|
15201
|
+
*
|
|
15202
|
+
* Prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
15203
|
+
* genuinely needs a whole family; a category scope grants every future member
|
|
15204
|
+
* of that category too. `category:system [create]` has been rejected once and
|
|
15205
|
+
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
15206
|
+
*
|
|
15207
|
+
* Calls the ADDON itself makes over `ctx.api` run as the addon and are not
|
|
15208
|
+
* scope-checked, so they are not what this field describes — but reaching the
|
|
15209
|
+
* addon's route in the first place IS, and that is the entry to declare.
|
|
15210
|
+
*
|
|
15211
|
+
* **The boot window.** An addon registers its provider after its runner forks
|
|
15212
|
+
* and initialises, so between hub start and that moment this collection is
|
|
15213
|
+
* incomplete and an `integrationId` can be legitimately absent. The core does
|
|
15214
|
+
* not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
|
|
15215
|
+
* it compares the manifest declarers against the registered providers and
|
|
15216
|
+
* answers `503 temporarily_unavailable` (with `Retry-After` and the pending
|
|
15217
|
+
* addon ids) instead of `400 unknown integration`, and reports
|
|
15218
|
+
* `complete: false` on `GET /api/oauth2/integrations`. A client should retry
|
|
15219
|
+
* while the list is incomplete rather than conclude the hub cannot do OAuth.
|
|
15103
15220
|
*/
|
|
15104
15221
|
var OauthIntegrationDescriptorSchema = object({
|
|
15105
15222
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -15130,7 +15247,30 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15130
15247
|
* present, /api/oauth2/authorize bakes THIS into the code instead of the
|
|
15131
15248
|
* hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
|
|
15132
15249
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15133
|
-
hubUrl: string().optional()
|
|
15250
|
+
hubUrl: string().optional(),
|
|
15251
|
+
/**
|
|
15252
|
+
* How long a REFRESH token issued for this integration lives — seconds, or
|
|
15253
|
+
* `'never'` for a token minted with no `exp` claim at all. Omit to keep the
|
|
15254
|
+
* 30-day default, which is what every link used before this field existed.
|
|
15255
|
+
*
|
|
15256
|
+
* Declared here for the same reason `requestedScopes` is: the integration
|
|
15257
|
+
* knows what it needs. Amazon's account linking and a Home Assistant config
|
|
15258
|
+
* entry are both meant to survive indefinitely, and re-linking is a manual
|
|
15259
|
+
* user action, so a 30-day expiry silently unlinks a working integration.
|
|
15260
|
+
*
|
|
15261
|
+
* **The security posture, stated so it is owned deliberately.** A refresh
|
|
15262
|
+
* token that never expires is permanent access if it leaks. What bounds it is
|
|
15263
|
+
* revocation, not time: `oauthRefresh` re-reads the session on every use and
|
|
15264
|
+
* returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
|
|
15265
|
+
* The one gap is the ACCESS token — it is a plain signed JWT that nothing
|
|
15266
|
+
* re-checks against the session on the `/trpc` and `/addon/*` paths, so
|
|
15267
|
+
* revoking a link takes effect there only after its remaining hour. That hour
|
|
15268
|
+
* is why the access TTL is not configurable.
|
|
15269
|
+
*
|
|
15270
|
+
* The value is baked into the authorization code at `/authorize` and travels
|
|
15271
|
+
* on the tokens, so editing this field changes FUTURE links only.
|
|
15272
|
+
*/
|
|
15273
|
+
refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
15134
15274
|
});
|
|
15135
15275
|
method(_void(), OauthIntegrationDescriptorSchema);
|
|
15136
15276
|
/**
|
|
@@ -18358,11 +18498,29 @@ var SsoBridgeClaimsSchema = object({
|
|
|
18358
18498
|
codeChallenge: string().optional(),
|
|
18359
18499
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
18360
18500
|
* tokens so the verify path can check the session is not revoked. */
|
|
18361
|
-
sessionId: string().optional()
|
|
18501
|
+
sessionId: string().optional(),
|
|
18502
|
+
/**
|
|
18503
|
+
* The refresh lifetime this LINK was created with, in seconds, or `'never'`.
|
|
18504
|
+
* Baked into the code at `/authorize` from the integration's descriptor and
|
|
18505
|
+
* carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
|
|
18506
|
+
* on the token rather than being re-read from the descriptor on purpose:
|
|
18507
|
+
* editing a descriptor must not retroactively extend or shorten a link the
|
|
18508
|
+
* operator already consented to.
|
|
18509
|
+
*/
|
|
18510
|
+
refreshTtl: union([number().int().positive(), literal("never")]).optional()
|
|
18362
18511
|
});
|
|
18363
18512
|
method(object({
|
|
18364
18513
|
claims: SsoBridgeClaimsSchema,
|
|
18365
|
-
|
|
18514
|
+
/**
|
|
18515
|
+
* Seconds, or `'never'` for a token minted with NO `exp` claim.
|
|
18516
|
+
*
|
|
18517
|
+
* `'never'` is a literal rather than `undefined`/`0` because omitting
|
|
18518
|
+
* this field already means "the 5-minute SSO hand-off default", and
|
|
18519
|
+
* `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
|
|
18520
|
+
* went through the numeric path would fail at mint time and break
|
|
18521
|
+
* linking rather than produce an eternal token.
|
|
18522
|
+
*/
|
|
18523
|
+
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
18366
18524
|
}), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
|
|
18367
18525
|
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
18368
18526
|
providerId: string().min(1),
|
|
@@ -18565,6 +18723,22 @@ var TerminalProfileInfoSchema = object({
|
|
|
18565
18723
|
label: string(),
|
|
18566
18724
|
description: string().optional()
|
|
18567
18725
|
});
|
|
18726
|
+
var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
|
|
18727
|
+
seq: number().int().positive(),
|
|
18728
|
+
kind: literal("data"),
|
|
18729
|
+
data: string()
|
|
18730
|
+
}), object({
|
|
18731
|
+
seq: number().int().positive(),
|
|
18732
|
+
kind: literal("exit"),
|
|
18733
|
+
exitCode: number().int(),
|
|
18734
|
+
signal: number().int().optional()
|
|
18735
|
+
})]);
|
|
18736
|
+
var TerminalOutputBatchSchema = object({
|
|
18737
|
+
cursor: number().int().nonnegative(),
|
|
18738
|
+
reset: boolean(),
|
|
18739
|
+
snapshot: string().optional(),
|
|
18740
|
+
events: array(TerminalOutputEventSchema).readonly()
|
|
18741
|
+
});
|
|
18568
18742
|
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
18569
18743
|
profileId: string(),
|
|
18570
18744
|
cols: number().int().positive(),
|
|
@@ -18579,6 +18753,20 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
18579
18753
|
}), _void(), {
|
|
18580
18754
|
kind: "mutation",
|
|
18581
18755
|
auth: "admin"
|
|
18756
|
+
}), method(object({
|
|
18757
|
+
sessionId: string(),
|
|
18758
|
+
afterSeq: number().int().nonnegative(),
|
|
18759
|
+
waitMs: number().int().min(0).max(2e3).default(0)
|
|
18760
|
+
}), TerminalOutputBatchSchema, {
|
|
18761
|
+
kind: "mutation",
|
|
18762
|
+
auth: "admin",
|
|
18763
|
+
timeoutMs: 15e3
|
|
18764
|
+
}), method(object({
|
|
18765
|
+
sessionId: string(),
|
|
18766
|
+
data: string().max(64 * 1024)
|
|
18767
|
+
}), _void(), {
|
|
18768
|
+
kind: "mutation",
|
|
18769
|
+
auth: "admin"
|
|
18582
18770
|
}), method(object({ sessionId: string() }), _void(), {
|
|
18583
18771
|
kind: "mutation",
|
|
18584
18772
|
auth: "admin"
|
|
@@ -25575,7 +25763,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
25575
25763
|
hubUrl: string(),
|
|
25576
25764
|
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
25577
25765
|
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
25578
|
-
codeChallenge: string().optional()
|
|
25766
|
+
codeChallenge: string().optional(),
|
|
25767
|
+
/** The integration's declared refresh lifetime — seconds, or `'never'`.
|
|
25768
|
+
* From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
|
|
25769
|
+
* code so the link carries its own lifetime; omit for the 30-day
|
|
25770
|
+
* default. */
|
|
25771
|
+
refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
25579
25772
|
}), object({ code: string() }), {
|
|
25580
25773
|
kind: "mutation",
|
|
25581
25774
|
access: "create"
|
|
@@ -32259,12 +32452,24 @@ Object.freeze({
|
|
|
32259
32452
|
addonId: null,
|
|
32260
32453
|
access: "create"
|
|
32261
32454
|
},
|
|
32455
|
+
"terminalSession.pullOutput": {
|
|
32456
|
+
capName: "terminal-session",
|
|
32457
|
+
capScope: "system",
|
|
32458
|
+
addonId: null,
|
|
32459
|
+
access: "create"
|
|
32460
|
+
},
|
|
32262
32461
|
"terminalSession.resize": {
|
|
32263
32462
|
capName: "terminal-session",
|
|
32264
32463
|
capScope: "system",
|
|
32265
32464
|
addonId: null,
|
|
32266
32465
|
access: "create"
|
|
32267
32466
|
},
|
|
32467
|
+
"terminalSession.writeInput": {
|
|
32468
|
+
capName: "terminal-session",
|
|
32469
|
+
capScope: "system",
|
|
32470
|
+
addonId: null,
|
|
32471
|
+
access: "create"
|
|
32472
|
+
},
|
|
32268
32473
|
"toast.onToast": {
|
|
32269
32474
|
capName: "toast",
|
|
32270
32475
|
capScope: "system",
|
package/dist/addon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
-
//#region ../types/dist/event-category-
|
|
2
|
+
//#region ../types/dist/event-category-Cv9dO26A.mjs
|
|
3
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
5
5
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -15,6 +15,13 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
15
15
|
*/
|
|
16
16
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
17
17
|
/**
|
|
18
|
+
* A newer addon or server-root package version was found by the
|
|
19
|
+
* authoritative registry check. Emitted once per
|
|
20
|
+
* `(target, packageName, currentVersion, latestVersion)` transition; repeated
|
|
21
|
+
* polling of the same result is deduplicated by the checker.
|
|
22
|
+
*/
|
|
23
|
+
EventCategory["UpdateAvailable"] = "update.available";
|
|
24
|
+
/**
|
|
18
25
|
* Readiness transition for a capability provider. Every producer emits
|
|
19
26
|
* this event on `onInitialize` completion, `onDestroy`, and
|
|
20
27
|
* `$node.reconnect`; every consumer that needs to gate on a cross-process
|
|
@@ -14084,8 +14091,35 @@ var NcDeliverySchema = _enum([
|
|
|
14084
14091
|
"immediate",
|
|
14085
14092
|
"track-end",
|
|
14086
14093
|
"device-event",
|
|
14087
|
-
"package-event"
|
|
14094
|
+
"package-event",
|
|
14095
|
+
"system-event"
|
|
14096
|
+
]);
|
|
14097
|
+
/**
|
|
14098
|
+
* Stable Notification Center vocabulary over infrastructure/liveness events.
|
|
14099
|
+
* Bus categories are normalized into these intent-level kinds so rules do not
|
|
14100
|
+
* depend on a provider's raw event name or payload shape.
|
|
14101
|
+
*/
|
|
14102
|
+
var NcSystemEventKindSchema = _enum([
|
|
14103
|
+
"camera-online",
|
|
14104
|
+
"camera-offline",
|
|
14105
|
+
"stream-online",
|
|
14106
|
+
"stream-offline",
|
|
14107
|
+
"node-online",
|
|
14108
|
+
"node-offline",
|
|
14109
|
+
"addon-update-available",
|
|
14110
|
+
"server-update-available"
|
|
14088
14111
|
]);
|
|
14112
|
+
/**
|
|
14113
|
+
* One coherent system-event condition. `kinds` is the required opt-in safety
|
|
14114
|
+
* gate; the remaining lists are optional narrowing filters relevant to the
|
|
14115
|
+
* selected kinds.
|
|
14116
|
+
*/
|
|
14117
|
+
var NcSystemEventConditionSchema = object({
|
|
14118
|
+
kinds: array(NcSystemEventKindSchema).min(1),
|
|
14119
|
+
deviceIds: array(number().int()).min(1).optional(),
|
|
14120
|
+
nodeIds: array(string().min(1)).min(1).optional(),
|
|
14121
|
+
packageNames: array(string().min(1)).min(1).optional()
|
|
14122
|
+
});
|
|
14089
14123
|
/** Weekly schedule — OR of windows; absence on the rule = always active. */
|
|
14090
14124
|
var NcScheduleSchema = object({
|
|
14091
14125
|
windows: array(object({
|
|
@@ -14390,6 +14424,8 @@ var NcConditionsSchema = object({
|
|
|
14390
14424
|
"picked-up",
|
|
14391
14425
|
"both"
|
|
14392
14426
|
]).optional(),
|
|
14427
|
+
/** Infrastructure/liveness/update event matcher (`system-event` delivery). */
|
|
14428
|
+
systemEvent: NcSystemEventConditionSchema.optional(),
|
|
14393
14429
|
/**
|
|
14394
14430
|
* PERSONAL-RULE custom zones (viewer-drawn). Inline normalized polygons
|
|
14395
14431
|
* (MaskShape vocabulary). A record passes when its bbox overlaps ANY
|
|
@@ -14609,7 +14645,8 @@ var NcTestResultSchema = object({
|
|
|
14609
14645
|
"object-event",
|
|
14610
14646
|
"track",
|
|
14611
14647
|
"device-event",
|
|
14612
|
-
"package-event"
|
|
14648
|
+
"package-event",
|
|
14649
|
+
"system-event"
|
|
14613
14650
|
]),
|
|
14614
14651
|
deviceId: number(),
|
|
14615
14652
|
timestamp: number(),
|
|
@@ -14631,7 +14668,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
14631
14668
|
"schedule",
|
|
14632
14669
|
"device",
|
|
14633
14670
|
"package",
|
|
14634
|
-
"occupancy"
|
|
14671
|
+
"occupancy",
|
|
14672
|
+
"system"
|
|
14635
14673
|
]),
|
|
14636
14674
|
label: string(),
|
|
14637
14675
|
/** Editor widget the UI renders — never hardcode per-condition forms. */
|
|
@@ -14649,7 +14687,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
14649
14687
|
"crossingSelect",
|
|
14650
14688
|
"polygonDraw",
|
|
14651
14689
|
"occupancy",
|
|
14652
|
-
"deviceState"
|
|
14690
|
+
"deviceState",
|
|
14691
|
+
"systemEvent"
|
|
14653
14692
|
]),
|
|
14654
14693
|
operator: _enum([
|
|
14655
14694
|
"in",
|
|
@@ -14708,7 +14747,8 @@ var NcHistoryRecordKindSchema = _enum([
|
|
|
14708
14747
|
"object-event",
|
|
14709
14748
|
"track-end",
|
|
14710
14749
|
"device-event",
|
|
14711
|
-
"package-event"
|
|
14750
|
+
"package-event",
|
|
14751
|
+
"system-event"
|
|
14712
14752
|
]);
|
|
14713
14753
|
/** Subject summary frozen on the row at fire time (survives rule/record edits). */
|
|
14714
14754
|
var NcHistorySubjectSchema = object({
|
|
@@ -14716,7 +14756,14 @@ var NcHistorySubjectSchema = object({
|
|
|
14716
14756
|
label: string().optional(),
|
|
14717
14757
|
confidence: number().optional(),
|
|
14718
14758
|
zones: array(string()),
|
|
14719
|
-
timestamp: number()
|
|
14759
|
+
timestamp: number(),
|
|
14760
|
+
systemEvent: object({
|
|
14761
|
+
kind: NcSystemEventKindSchema,
|
|
14762
|
+
subject: string(),
|
|
14763
|
+
title: string(),
|
|
14764
|
+
body: string(),
|
|
14765
|
+
data: record(string(), unknown())
|
|
14766
|
+
}).optional()
|
|
14720
14767
|
});
|
|
14721
14768
|
/**
|
|
14722
14769
|
* One delivery-history row. This is a read-only VIEW over the durable
|
|
@@ -15076,6 +15123,76 @@ object({
|
|
|
15076
15123
|
* Each provider returns a static descriptor; the core enumerates them
|
|
15077
15124
|
* to validate the `integration=` query param and resolve the consent
|
|
15078
15125
|
* label + the scopes baked into the issued token.
|
|
15126
|
+
*
|
|
15127
|
+
* ## Declaring one
|
|
15128
|
+
*
|
|
15129
|
+
* An OAuth client is integration-specific knowledge — who the client is, what
|
|
15130
|
+
* it may ask for, where it may be sent — so it is declared by the ADDON that
|
|
15131
|
+
* owns the integration, never by the kernel and never as a branch inside
|
|
15132
|
+
* `oauth2-routes.ts` ([D101](../../../../docs/decisions/adr-0101.md)). Three
|
|
15133
|
+
* steps, no others:
|
|
15134
|
+
*
|
|
15135
|
+
* 1. Add `{ "name": "oauth-integration" }` to the addon's `camstack.addons[]`
|
|
15136
|
+
* manifest entry. This is also what tells the hub, at addon-LOAD time, that
|
|
15137
|
+
* a descriptor is owed — see "the boot window" below.
|
|
15138
|
+
* 2. Return a provider from `onInitialize()`:
|
|
15139
|
+
*
|
|
15140
|
+
* ```ts
|
|
15141
|
+
* const provider: IOauthIntegrationProvider = {
|
|
15142
|
+
* getDescriptor: async () => ({
|
|
15143
|
+
* integrationId: 'my-thing', // the `integration=` query param
|
|
15144
|
+
* displayName: 'My Thing',
|
|
15145
|
+
* requestedScopes: [ … ], // see below
|
|
15146
|
+
* allowedRedirectPrefixes: ['https://callback.example/'],
|
|
15147
|
+
* }),
|
|
15148
|
+
* }
|
|
15149
|
+
* return [{ capability: oauthIntegrationCapability, provider }]
|
|
15150
|
+
* ```
|
|
15151
|
+
*
|
|
15152
|
+
* The descriptor must be **static** — it is read on the authorize path, so
|
|
15153
|
+
* never put an await on network or disk behind it, and never register it
|
|
15154
|
+
* behind one either (a provider is registered only once `onInitialize`
|
|
15155
|
+
* RETURNS, so anything awaited before the return delays linking).
|
|
15156
|
+
* 3. Nothing else. There is no allow-list to join, no id to register with the
|
|
15157
|
+
* core, and no per-integration branch anywhere: `/api/oauth2/authorize` and
|
|
15158
|
+
* `/api/oauth2/integrations` are built from this collection alone.
|
|
15159
|
+
*
|
|
15160
|
+
* **Scopes. `requestedScopes` has exactly ONE meaning: what the integration
|
|
15161
|
+
* NEEDS to function.** Not a blast radius, not a conservative
|
|
15162
|
+
* under-declaration, not a description of some other path the addon happens to
|
|
15163
|
+
* have. Derive it from what the client actually calls **with this token** —
|
|
15164
|
+
* every tRPC path against `METHOD_ACCESS_MAP`, plus an `addon:` grant for every
|
|
15165
|
+
* addon HTTP route it posts to — and write the call that justifies each entry
|
|
15166
|
+
* next to it. Two integrations once used this field to mean two different
|
|
15167
|
+
* things; the operator ruled there is one meaning, and any third integration
|
|
15168
|
+
* inherits it (2026-08-09).
|
|
15169
|
+
*
|
|
15170
|
+
* This is not documentation, it is the ENFORCEMENT INPUT. Since
|
|
15171
|
+
* [D103](../../../../docs/decisions/adr-0103.md) the `/addon/:addonId/*` gate
|
|
15172
|
+
* checks an integration token's grant before letting it reach an
|
|
15173
|
+
* `access: 'authenticated'` route, so an **under-declaration is an integration
|
|
15174
|
+
* that stops working** — a missing `addon:` entry means `403 Token scope
|
|
15175
|
+
* mismatch` on every control the client tries to actuate. Widen the descriptor
|
|
15176
|
+
* honestly rather than weakening a check to make a route pass.
|
|
15177
|
+
*
|
|
15178
|
+
* Prefer a narrow `capability:` scope to a `category:` one unless the client
|
|
15179
|
+
* genuinely needs a whole family; a category scope grants every future member
|
|
15180
|
+
* of that category too. `category:system [create]` has been rejected once and
|
|
15181
|
+
* should stay rejected: it hands `addons.installPackage` to an integration.
|
|
15182
|
+
*
|
|
15183
|
+
* Calls the ADDON itself makes over `ctx.api` run as the addon and are not
|
|
15184
|
+
* scope-checked, so they are not what this field describes — but reaching the
|
|
15185
|
+
* addon's route in the first place IS, and that is the entry to declare.
|
|
15186
|
+
*
|
|
15187
|
+
* **The boot window.** An addon registers its provider after its runner forks
|
|
15188
|
+
* and initialises, so between hub start and that moment this collection is
|
|
15189
|
+
* incomplete and an `integrationId` can be legitimately absent. The core does
|
|
15190
|
+
* not wait, poll or cache around this ([D3](../../../../docs/decisions/adr-0003.md)):
|
|
15191
|
+
* it compares the manifest declarers against the registered providers and
|
|
15192
|
+
* answers `503 temporarily_unavailable` (with `Retry-After` and the pending
|
|
15193
|
+
* addon ids) instead of `400 unknown integration`, and reports
|
|
15194
|
+
* `complete: false` on `GET /api/oauth2/integrations`. A client should retry
|
|
15195
|
+
* while the list is incomplete rather than conclude the hub cannot do OAuth.
|
|
15079
15196
|
*/
|
|
15080
15197
|
var OauthIntegrationDescriptorSchema = object({
|
|
15081
15198
|
/** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
|
|
@@ -15106,7 +15223,30 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
15106
15223
|
* present, /api/oauth2/authorize bakes THIS into the code instead of the
|
|
15107
15224
|
* hub-global `publicHubUrl()`, so a forked exporter addon (which can't set
|
|
15108
15225
|
* the hub's env) drives the claim that its cloud Lambda routes back on. */
|
|
15109
|
-
hubUrl: string().optional()
|
|
15226
|
+
hubUrl: string().optional(),
|
|
15227
|
+
/**
|
|
15228
|
+
* How long a REFRESH token issued for this integration lives — seconds, or
|
|
15229
|
+
* `'never'` for a token minted with no `exp` claim at all. Omit to keep the
|
|
15230
|
+
* 30-day default, which is what every link used before this field existed.
|
|
15231
|
+
*
|
|
15232
|
+
* Declared here for the same reason `requestedScopes` is: the integration
|
|
15233
|
+
* knows what it needs. Amazon's account linking and a Home Assistant config
|
|
15234
|
+
* entry are both meant to survive indefinitely, and re-linking is a manual
|
|
15235
|
+
* user action, so a 30-day expiry silently unlinks a working integration.
|
|
15236
|
+
*
|
|
15237
|
+
* **The security posture, stated so it is owned deliberately.** A refresh
|
|
15238
|
+
* token that never expires is permanent access if it leaks. What bounds it is
|
|
15239
|
+
* revocation, not time: `oauthRefresh` re-reads the session on every use and
|
|
15240
|
+
* returns `null` once `revokedAt` is set, as does `oauthVerifyAccessToken`.
|
|
15241
|
+
* The one gap is the ACCESS token — it is a plain signed JWT that nothing
|
|
15242
|
+
* re-checks against the session on the `/trpc` and `/addon/*` paths, so
|
|
15243
|
+
* revoking a link takes effect there only after its remaining hour. That hour
|
|
15244
|
+
* is why the access TTL is not configurable.
|
|
15245
|
+
*
|
|
15246
|
+
* The value is baked into the authorization code at `/authorize` and travels
|
|
15247
|
+
* on the tokens, so editing this field changes FUTURE links only.
|
|
15248
|
+
*/
|
|
15249
|
+
refreshTokenTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
15110
15250
|
});
|
|
15111
15251
|
method(_void(), OauthIntegrationDescriptorSchema);
|
|
15112
15252
|
/**
|
|
@@ -18334,11 +18474,29 @@ var SsoBridgeClaimsSchema = object({
|
|
|
18334
18474
|
codeChallenge: string().optional(),
|
|
18335
18475
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
18336
18476
|
* tokens so the verify path can check the session is not revoked. */
|
|
18337
|
-
sessionId: string().optional()
|
|
18477
|
+
sessionId: string().optional(),
|
|
18478
|
+
/**
|
|
18479
|
+
* The refresh lifetime this LINK was created with, in seconds, or `'never'`.
|
|
18480
|
+
* Baked into the code at `/authorize` from the integration's descriptor and
|
|
18481
|
+
* carried forward so `oauthRefresh` re-mints with the same lifetime. It rides
|
|
18482
|
+
* on the token rather than being re-read from the descriptor on purpose:
|
|
18483
|
+
* editing a descriptor must not retroactively extend or shorten a link the
|
|
18484
|
+
* operator already consented to.
|
|
18485
|
+
*/
|
|
18486
|
+
refreshTtl: union([number().int().positive(), literal("never")]).optional()
|
|
18338
18487
|
});
|
|
18339
18488
|
method(object({
|
|
18340
18489
|
claims: SsoBridgeClaimsSchema,
|
|
18341
|
-
|
|
18490
|
+
/**
|
|
18491
|
+
* Seconds, or `'never'` for a token minted with NO `exp` claim.
|
|
18492
|
+
*
|
|
18493
|
+
* `'never'` is a literal rather than `undefined`/`0` because omitting
|
|
18494
|
+
* this field already means "the 5-minute SSO hand-off default", and
|
|
18495
|
+
* `jwt.sign` THROWS on `{ expiresIn: undefined }` — a "no expiry" that
|
|
18496
|
+
* went through the numeric path would fail at mint time and break
|
|
18497
|
+
* linking rather than produce an eternal token.
|
|
18498
|
+
*/
|
|
18499
|
+
ttlSec: union([number().int().positive(), literal("never")]).optional()
|
|
18342
18500
|
}), object({ token: string() })), method(object({ token: string() }), SsoBridgeClaimsSchema.nullable());
|
|
18343
18501
|
var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object({
|
|
18344
18502
|
providerId: string().min(1),
|
|
@@ -18541,6 +18699,22 @@ var TerminalProfileInfoSchema = object({
|
|
|
18541
18699
|
label: string(),
|
|
18542
18700
|
description: string().optional()
|
|
18543
18701
|
});
|
|
18702
|
+
var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
|
|
18703
|
+
seq: number().int().positive(),
|
|
18704
|
+
kind: literal("data"),
|
|
18705
|
+
data: string()
|
|
18706
|
+
}), object({
|
|
18707
|
+
seq: number().int().positive(),
|
|
18708
|
+
kind: literal("exit"),
|
|
18709
|
+
exitCode: number().int(),
|
|
18710
|
+
signal: number().int().optional()
|
|
18711
|
+
})]);
|
|
18712
|
+
var TerminalOutputBatchSchema = object({
|
|
18713
|
+
cursor: number().int().nonnegative(),
|
|
18714
|
+
reset: boolean(),
|
|
18715
|
+
snapshot: string().optional(),
|
|
18716
|
+
events: array(TerminalOutputEventSchema).readonly()
|
|
18717
|
+
});
|
|
18544
18718
|
method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
|
|
18545
18719
|
profileId: string(),
|
|
18546
18720
|
cols: number().int().positive(),
|
|
@@ -18555,6 +18729,20 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
|
|
|
18555
18729
|
}), _void(), {
|
|
18556
18730
|
kind: "mutation",
|
|
18557
18731
|
auth: "admin"
|
|
18732
|
+
}), method(object({
|
|
18733
|
+
sessionId: string(),
|
|
18734
|
+
afterSeq: number().int().nonnegative(),
|
|
18735
|
+
waitMs: number().int().min(0).max(2e3).default(0)
|
|
18736
|
+
}), TerminalOutputBatchSchema, {
|
|
18737
|
+
kind: "mutation",
|
|
18738
|
+
auth: "admin",
|
|
18739
|
+
timeoutMs: 15e3
|
|
18740
|
+
}), method(object({
|
|
18741
|
+
sessionId: string(),
|
|
18742
|
+
data: string().max(64 * 1024)
|
|
18743
|
+
}), _void(), {
|
|
18744
|
+
kind: "mutation",
|
|
18745
|
+
auth: "admin"
|
|
18558
18746
|
}), method(object({ sessionId: string() }), _void(), {
|
|
18559
18747
|
kind: "mutation",
|
|
18560
18748
|
auth: "admin"
|
|
@@ -25551,7 +25739,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
25551
25739
|
hubUrl: string(),
|
|
25552
25740
|
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
25553
25741
|
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
25554
|
-
codeChallenge: string().optional()
|
|
25742
|
+
codeChallenge: string().optional(),
|
|
25743
|
+
/** The integration's declared refresh lifetime — seconds, or `'never'`.
|
|
25744
|
+
* From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
|
|
25745
|
+
* code so the link carries its own lifetime; omit for the 30-day
|
|
25746
|
+
* default. */
|
|
25747
|
+
refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
|
|
25555
25748
|
}), object({ code: string() }), {
|
|
25556
25749
|
kind: "mutation",
|
|
25557
25750
|
access: "create"
|
|
@@ -32235,12 +32428,24 @@ Object.freeze({
|
|
|
32235
32428
|
addonId: null,
|
|
32236
32429
|
access: "create"
|
|
32237
32430
|
},
|
|
32431
|
+
"terminalSession.pullOutput": {
|
|
32432
|
+
capName: "terminal-session",
|
|
32433
|
+
capScope: "system",
|
|
32434
|
+
addonId: null,
|
|
32435
|
+
access: "create"
|
|
32436
|
+
},
|
|
32238
32437
|
"terminalSession.resize": {
|
|
32239
32438
|
capName: "terminal-session",
|
|
32240
32439
|
capScope: "system",
|
|
32241
32440
|
addonId: null,
|
|
32242
32441
|
access: "create"
|
|
32243
32442
|
},
|
|
32443
|
+
"terminalSession.writeInput": {
|
|
32444
|
+
capName: "terminal-session",
|
|
32445
|
+
capScope: "system",
|
|
32446
|
+
addonId: null,
|
|
32447
|
+
access: "create"
|
|
32448
|
+
},
|
|
32244
32449
|
"toast.onToast": {
|
|
32245
32450
|
capName: "toast",
|
|
32246
32451
|
capScope: "system",
|