@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.
Files changed (3) hide show
  1. package/dist/addon.js +216 -11
  2. package/dist/addon.mjs +216 -11
  3. 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-41fKf-q9.mjs
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
- ttlSec: number().int().positive().optional()
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-41fKf-q9.mjs
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
- ttlSec: number().int().positive().optional()
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-rtsp",
3
- "version": "1.2.10",
3
+ "version": "1.2.12",
4
4
  "description": "Generic RTSP camera device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",