@camstack/addon-provider-petkit 0.2.10 → 0.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
@@ -1101,7 +1101,7 @@ var Nodepetkit = class {
1101
1101
  }
1102
1102
  };
1103
1103
  //#endregion
1104
- //#region ../types/dist/event-category-41fKf-q9.mjs
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
- ttlSec: number().int().positive().optional()
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),
@@ -19537,6 +19695,22 @@ var TerminalProfileInfoSchema = object({
19537
19695
  label: string(),
19538
19696
  description: string().optional()
19539
19697
  });
19698
+ var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
19699
+ seq: number().int().positive(),
19700
+ kind: literal("data"),
19701
+ data: string()
19702
+ }), object({
19703
+ seq: number().int().positive(),
19704
+ kind: literal("exit"),
19705
+ exitCode: number().int(),
19706
+ signal: number().int().optional()
19707
+ })]);
19708
+ var TerminalOutputBatchSchema = object({
19709
+ cursor: number().int().nonnegative(),
19710
+ reset: boolean(),
19711
+ snapshot: string().optional(),
19712
+ events: array(TerminalOutputEventSchema).readonly()
19713
+ });
19540
19714
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19541
19715
  profileId: string(),
19542
19716
  cols: number().int().positive(),
@@ -19551,6 +19725,20 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19551
19725
  }), _void(), {
19552
19726
  kind: "mutation",
19553
19727
  auth: "admin"
19728
+ }), method(object({
19729
+ sessionId: string(),
19730
+ afterSeq: number().int().nonnegative(),
19731
+ waitMs: number().int().min(0).max(2e3).default(0)
19732
+ }), TerminalOutputBatchSchema, {
19733
+ kind: "mutation",
19734
+ auth: "admin",
19735
+ timeoutMs: 15e3
19736
+ }), method(object({
19737
+ sessionId: string(),
19738
+ data: string().max(64 * 1024)
19739
+ }), _void(), {
19740
+ kind: "mutation",
19741
+ auth: "admin"
19554
19742
  }), method(object({ sessionId: string() }), _void(), {
19555
19743
  kind: "mutation",
19556
19744
  auth: "admin"
@@ -26534,7 +26722,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
26534
26722
  hubUrl: string(),
26535
26723
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
26536
26724
  * that carries one can ONLY be exchanged with the matching verifier. */
26537
- codeChallenge: string().optional()
26725
+ codeChallenge: string().optional(),
26726
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
26727
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
26728
+ * code so the link carries its own lifetime; omit for the 30-day
26729
+ * default. */
26730
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
26538
26731
  }), object({ code: string() }), {
26539
26732
  kind: "mutation",
26540
26733
  access: "create"
@@ -33152,12 +33345,24 @@ Object.freeze({
33152
33345
  addonId: null,
33153
33346
  access: "create"
33154
33347
  },
33348
+ "terminalSession.pullOutput": {
33349
+ capName: "terminal-session",
33350
+ capScope: "system",
33351
+ addonId: null,
33352
+ access: "create"
33353
+ },
33155
33354
  "terminalSession.resize": {
33156
33355
  capName: "terminal-session",
33157
33356
  capScope: "system",
33158
33357
  addonId: null,
33159
33358
  access: "create"
33160
33359
  },
33360
+ "terminalSession.writeInput": {
33361
+ capName: "terminal-session",
33362
+ capScope: "system",
33363
+ addonId: null,
33364
+ access: "create"
33365
+ },
33161
33366
  "toast.onToast": {
33162
33367
  capName: "toast",
33163
33368
  capScope: "system",
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-41fKf-q9.mjs
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
- ttlSec: number().int().positive().optional()
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),
@@ -19536,6 +19694,22 @@ var TerminalProfileInfoSchema = object({
19536
19694
  label: string(),
19537
19695
  description: string().optional()
19538
19696
  });
19697
+ var TerminalOutputEventSchema = discriminatedUnion("kind", [object({
19698
+ seq: number().int().positive(),
19699
+ kind: literal("data"),
19700
+ data: string()
19701
+ }), object({
19702
+ seq: number().int().positive(),
19703
+ kind: literal("exit"),
19704
+ exitCode: number().int(),
19705
+ signal: number().int().optional()
19706
+ })]);
19707
+ var TerminalOutputBatchSchema = object({
19708
+ cursor: number().int().nonnegative(),
19709
+ reset: boolean(),
19710
+ snapshot: string().optional(),
19711
+ events: array(TerminalOutputEventSchema).readonly()
19712
+ });
19539
19713
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19540
19714
  profileId: string(),
19541
19715
  cols: number().int().positive(),
@@ -19550,6 +19724,20 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19550
19724
  }), _void(), {
19551
19725
  kind: "mutation",
19552
19726
  auth: "admin"
19727
+ }), method(object({
19728
+ sessionId: string(),
19729
+ afterSeq: number().int().nonnegative(),
19730
+ waitMs: number().int().min(0).max(2e3).default(0)
19731
+ }), TerminalOutputBatchSchema, {
19732
+ kind: "mutation",
19733
+ auth: "admin",
19734
+ timeoutMs: 15e3
19735
+ }), method(object({
19736
+ sessionId: string(),
19737
+ data: string().max(64 * 1024)
19738
+ }), _void(), {
19739
+ kind: "mutation",
19740
+ auth: "admin"
19553
19741
  }), method(object({ sessionId: string() }), _void(), {
19554
19742
  kind: "mutation",
19555
19743
  auth: "admin"
@@ -26533,7 +26721,12 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
26533
26721
  hubUrl: string(),
26534
26722
  /** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
26535
26723
  * that carries one can ONLY be exchanged with the matching verifier. */
26536
- codeChallenge: string().optional()
26724
+ codeChallenge: string().optional(),
26725
+ /** The integration's declared refresh lifetime — seconds, or `'never'`.
26726
+ * From `OauthIntegrationDescriptor.refreshTokenTtlSec`. Baked into the
26727
+ * code so the link carries its own lifetime; omit for the 30-day
26728
+ * default. */
26729
+ refreshTtlSec: union([number().int().positive(), literal("never")]).optional()
26537
26730
  }), object({ code: string() }), {
26538
26731
  kind: "mutation",
26539
26732
  access: "create"
@@ -33151,12 +33344,24 @@ Object.freeze({
33151
33344
  addonId: null,
33152
33345
  access: "create"
33153
33346
  },
33347
+ "terminalSession.pullOutput": {
33348
+ capName: "terminal-session",
33349
+ capScope: "system",
33350
+ addonId: null,
33351
+ access: "create"
33352
+ },
33154
33353
  "terminalSession.resize": {
33155
33354
  capName: "terminal-session",
33156
33355
  capScope: "system",
33157
33356
  addonId: null,
33158
33357
  access: "create"
33159
33358
  },
33359
+ "terminalSession.writeInput": {
33360
+ capName: "terminal-session",
33361
+ capScope: "system",
33362
+ addonId: null,
33363
+ access: "create"
33364
+ },
33160
33365
  "toast.onToast": {
33161
33366
  capName: "toast",
33162
33367
  capScope: "system",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",