@camstack/addon-export-hap 1.2.8 → 1.2.10
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/export-hap.addon.js +1283 -428
- package/dist/export-hap.addon.mjs +1283 -428
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { networkInterfaces } from "node:os";
|
|
|
10
10
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
11
11
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
12
12
|
//#endregion
|
|
13
|
-
//#region ../types/dist/event-category-
|
|
13
|
+
//#region ../types/dist/event-category-41fKf-q9.mjs
|
|
14
14
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
15
15
|
EventCategory["SystemBoot"] = "system.boot";
|
|
16
16
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -70,6 +70,26 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
70
70
|
*/
|
|
71
71
|
EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
|
|
72
72
|
/**
|
|
73
|
+
* A liveness invariant of this node is false — it has no devices, every
|
|
74
|
+
* camera is offline, or a camera that was recording has stopped producing
|
|
75
|
+
* segments. Emitted by the `liveness-monitor` builtin, once per fault (the
|
|
76
|
+
* `findingId` is stable across ticks), only after its boot grace period.
|
|
77
|
+
* AlertCenter raises a persistent operator-visible alert.
|
|
78
|
+
*
|
|
79
|
+
* It exists because on 2026-08-03 the hub ran four hours with zero devices
|
|
80
|
+
* and zero recordings while every surface stayed quiet.
|
|
81
|
+
*
|
|
82
|
+
* Payload: `{ findingId, severity, title, message, deviceId? }`.
|
|
83
|
+
*/
|
|
84
|
+
EventCategory["SystemLivenessFailed"] = "system.liveness-failed";
|
|
85
|
+
/**
|
|
86
|
+
* A previously reported liveness fault is true again. AlertCenter dismisses
|
|
87
|
+
* the matching `SystemLivenessFailed` alert.
|
|
88
|
+
*
|
|
89
|
+
* Payload: `{ findingId }`.
|
|
90
|
+
*/
|
|
91
|
+
EventCategory["SystemLivenessRecovered"] = "system.liveness-recovered";
|
|
92
|
+
/**
|
|
73
93
|
* Monitor is attempting to reload a failed addon NOW. UI uses this
|
|
74
94
|
* to show a spinner during the retry attempt. Same transient nature
|
|
75
95
|
* as AddonRetryScheduled.
|
|
@@ -7320,14 +7340,7 @@ var RecordingConfigSchema = object({
|
|
|
7320
7340
|
* windows only — existing sheets are immutable, and each window's index
|
|
7321
7341
|
* carries its own tile dims so mixed-preset history renders correctly.
|
|
7322
7342
|
*/
|
|
7323
|
-
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7324
|
-
/**
|
|
7325
|
-
* OPT-IN thumbnail-strip generation for this camera: every keyframe of the
|
|
7326
|
-
* low recording saved as a JPEG (the fast-drag scrub depth), a derived
|
|
7327
|
-
* cache that eviction reclaims with the footage. Absent/false = no strips
|
|
7328
|
-
* are written and scrub reads exact keyframes at every velocity.
|
|
7329
|
-
*/
|
|
7330
|
-
stripsEnabled: boolean().optional()
|
|
7343
|
+
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7331
7344
|
}).strict();
|
|
7332
7345
|
/**
|
|
7333
7346
|
* Ops-log — the durable, append-only operations audit shared by the
|
|
@@ -7389,7 +7402,7 @@ var OpsLogQueryInputSchema = object({
|
|
|
7389
7402
|
/**
|
|
7390
7403
|
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7391
7404
|
*
|
|
7392
|
-
* One shape shared by the recorder's `relocateFootage` (segments
|
|
7405
|
+
* One shape shared by the recorder's `relocateFootage` (segments) and
|
|
7393
7406
|
* pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
|
|
7394
7407
|
* page renders both movers with one component. Jobs are in-RAM (a restart
|
|
7395
7408
|
* forgets them — re-running is safe by construction: copy-if-absent, delete
|
|
@@ -7411,7 +7424,7 @@ var RelocateJobSchema = object({
|
|
|
7411
7424
|
toLocationId: string(),
|
|
7412
7425
|
/** Scoped device, or null = every device. */
|
|
7413
7426
|
deviceId: number().nullable(),
|
|
7414
|
-
/** What the job moves (owner-addon specific: segments
|
|
7427
|
+
/** What the job moves (owner-addon specific: segments or media). */
|
|
7415
7428
|
entities: array(string()),
|
|
7416
7429
|
filesMoved: number().int(),
|
|
7417
7430
|
bytesMoved: number().int(),
|
|
@@ -7425,7 +7438,7 @@ var RelocateFootageInputSchema = object({
|
|
|
7425
7438
|
deviceId: number().optional(),
|
|
7426
7439
|
fromLocationId: string(),
|
|
7427
7440
|
toLocationId: string(),
|
|
7428
|
-
entities: array(_enum(["segments"
|
|
7441
|
+
entities: array(_enum(["segments"])).optional(),
|
|
7429
7442
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7430
7443
|
* never allowed to starve live writers. */
|
|
7431
7444
|
throttleMbps: number().min(1).max(1e3).optional()
|
|
@@ -8976,6 +8989,72 @@ AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, Accessor
|
|
|
8976
8989
|
DeviceFeature.BatteryOperated;
|
|
8977
8990
|
new Set(["devices", "classes"]);
|
|
8978
8991
|
/**
|
|
8992
|
+
* Alarm-panel cap. Models HA `alarm_control_panel.*` on
|
|
8993
|
+
* `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
|
|
8994
|
+
* across disarmed / armed_(home|away|night|vacation|custom_bypass) /
|
|
8995
|
+
* arming / pending / triggered / disarming.
|
|
8996
|
+
*
|
|
8997
|
+
* Many panels require a PIN code on arm / disarm — the optional
|
|
8998
|
+
* `code` field on the methods passes it through to the upstream
|
|
8999
|
+
* service; it's NEVER persisted in the runtime slice or any event
|
|
9000
|
+
* payload. The presence of a required code is signalled by
|
|
9001
|
+
* `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
|
|
9002
|
+
* field without a slice fetch.
|
|
9003
|
+
*
|
|
9004
|
+
* `availableModes` mirrors HA's `supported_features`-derived arm
|
|
9005
|
+
* mode list — the UI renders only the buttons the panel accepts.
|
|
9006
|
+
*/
|
|
9007
|
+
var AlarmStateSchema = _enum([
|
|
9008
|
+
"disarmed",
|
|
9009
|
+
"armed_home",
|
|
9010
|
+
"armed_away",
|
|
9011
|
+
"armed_night",
|
|
9012
|
+
"armed_vacation",
|
|
9013
|
+
"armed_custom_bypass",
|
|
9014
|
+
"arming",
|
|
9015
|
+
"disarming",
|
|
9016
|
+
"pending",
|
|
9017
|
+
"triggered"
|
|
9018
|
+
]);
|
|
9019
|
+
var AlarmArmModeSchema = _enum([
|
|
9020
|
+
"home",
|
|
9021
|
+
"away",
|
|
9022
|
+
"night",
|
|
9023
|
+
"vacation",
|
|
9024
|
+
"custom_bypass"
|
|
9025
|
+
]);
|
|
9026
|
+
object({
|
|
9027
|
+
/** Current lifecycle state. */
|
|
9028
|
+
state: AlarmStateSchema,
|
|
9029
|
+
/** Subset of arm modes the panel accepts. UI renders one button per
|
|
9030
|
+
* mode in this list. */
|
|
9031
|
+
availableModes: array(AlarmArmModeSchema),
|
|
9032
|
+
/** Whether the panel requires a PIN on arm / disarm. Mirrors
|
|
9033
|
+
* `DeviceFeature.AlarmPinRequired` for slice consumers. */
|
|
9034
|
+
requiresCode: boolean(),
|
|
9035
|
+
/** Ms epoch when the slice was last updated. */
|
|
9036
|
+
lastChangedAt: number()
|
|
9037
|
+
});
|
|
9038
|
+
DeviceType.AlarmPanel, method(object({
|
|
9039
|
+
deviceId: number().int().nonnegative(),
|
|
9040
|
+
mode: AlarmArmModeSchema,
|
|
9041
|
+
/** Optional PIN code. Required when `requiresCode === true`.
|
|
9042
|
+
* Passed through to the upstream service; never persisted. */
|
|
9043
|
+
code: string().min(1).optional()
|
|
9044
|
+
}), _void(), {
|
|
9045
|
+
kind: "mutation",
|
|
9046
|
+
auth: "admin"
|
|
9047
|
+
}), method(object({
|
|
9048
|
+
deviceId: number().int().nonnegative(),
|
|
9049
|
+
code: string().min(1).optional()
|
|
9050
|
+
}), _void(), {
|
|
9051
|
+
kind: "mutation",
|
|
9052
|
+
auth: "admin"
|
|
9053
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
9054
|
+
kind: "mutation",
|
|
9055
|
+
auth: "admin"
|
|
9056
|
+
});
|
|
9057
|
+
/**
|
|
8979
9058
|
* Shared geometry vocabulary for on-frame shape caps — privacy-mask,
|
|
8980
9059
|
* motion-zones, and the detection zones/lines editor all speak this one
|
|
8981
9060
|
* language so a single drawing-plane editor and the providers stay
|
|
@@ -9037,6 +9116,276 @@ var MaskGridDimsSchema = object({
|
|
|
9037
9116
|
height: number()
|
|
9038
9117
|
});
|
|
9039
9118
|
/**
|
|
9119
|
+
* notification-output — canonical, capability-gated notification delivery.
|
|
9120
|
+
*
|
|
9121
|
+
* Apprise-derived model (see
|
|
9122
|
+
* `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
|
|
9123
|
+
* callers emit ONE canonical `Notification`; each provider declares a
|
|
9124
|
+
* per-kind capability descriptor (`TargetKind`), and the pure degrade
|
|
9125
|
+
* engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
|
|
9126
|
+
* message to what the kind supports — callers never special-case a service.
|
|
9127
|
+
*
|
|
9128
|
+
* DESIGN DECISIONS (locked):
|
|
9129
|
+
* - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
|
|
9130
|
+
* `setTargetEnabled`), each provider persisting via the `settings-store`
|
|
9131
|
+
* cap. Rationale: the admin UI needs one uniform surface across the
|
|
9132
|
+
* notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
|
|
9133
|
+
* alternative would fork the UI per addon and cannot host the
|
|
9134
|
+
* discovery→adopt flow.
|
|
9135
|
+
* - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
|
|
9136
|
+
* the generated cap-mount auto-`concatCollection`-fans them across every
|
|
9137
|
+
* registered provider (notifiers addon + HA addon) so one catalog is
|
|
9138
|
+
* routable. `send` / `testTarget` / CRUD route to ONE provider by the
|
|
9139
|
+
* `addonId` the generated collection router extracts from the call input.
|
|
9140
|
+
* - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
|
|
9141
|
+
* transformer) + UDS MsgPack both round-trip typed arrays — already used by
|
|
9142
|
+
* `storage` / `storage-provider` / `recording` caps over the same path. No
|
|
9143
|
+
* base64 fallback needed.
|
|
9144
|
+
*
|
|
9145
|
+
* TODO (deferred, closed-set change — separate decision): add
|
|
9146
|
+
* `providerKind: 'notify'` so notification providers surface on the unified
|
|
9147
|
+
* admin "Integrations" page.
|
|
9148
|
+
*/
|
|
9149
|
+
/**
|
|
9150
|
+
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
9151
|
+
* adapter picks what it supports and the degrade engine filters the rest.
|
|
9152
|
+
*/
|
|
9153
|
+
var AttachmentMediaTypeSchema = _enum([
|
|
9154
|
+
"image",
|
|
9155
|
+
"video",
|
|
9156
|
+
"gif",
|
|
9157
|
+
"audio",
|
|
9158
|
+
"icon"
|
|
9159
|
+
]);
|
|
9160
|
+
/**
|
|
9161
|
+
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
9162
|
+
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
9163
|
+
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
9164
|
+
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
9165
|
+
*/
|
|
9166
|
+
var AttachmentSchema = object({
|
|
9167
|
+
mediaType: AttachmentMediaTypeSchema,
|
|
9168
|
+
url: string().optional(),
|
|
9169
|
+
bytes: _instanceof(Uint8Array).optional(),
|
|
9170
|
+
mime: string().optional(),
|
|
9171
|
+
name: string().optional()
|
|
9172
|
+
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
9173
|
+
var NotificationFormatSchema = _enum([
|
|
9174
|
+
"text",
|
|
9175
|
+
"markdown",
|
|
9176
|
+
"html"
|
|
9177
|
+
]);
|
|
9178
|
+
/**
|
|
9179
|
+
* The CLOSED icon vocabulary an action button may use.
|
|
9180
|
+
*
|
|
9181
|
+
* A closed set, not a free string, and that is the whole point: an arbitrary
|
|
9182
|
+
* icon name is one that ntfy renders, zentik silently drops, and nobody
|
|
9183
|
+
* notices — the same class of gap as a zone vocabulary nothing produced
|
|
9184
|
+
* ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
|
|
9185
|
+
* declares `actionIcons: false` and the degrade engine strips the field.
|
|
9186
|
+
*
|
|
9187
|
+
* Named by INTENT, never by glyph. "check" would tie the vocabulary to one
|
|
9188
|
+
* renderer's icon set; "acknowledge" survives an adapter that draws it
|
|
9189
|
+
* differently.
|
|
9190
|
+
*/
|
|
9191
|
+
var NotificationActionIconSchema = _enum([
|
|
9192
|
+
"acknowledge",
|
|
9193
|
+
"dismiss",
|
|
9194
|
+
"silence",
|
|
9195
|
+
"view",
|
|
9196
|
+
"play",
|
|
9197
|
+
"open",
|
|
9198
|
+
"close",
|
|
9199
|
+
"lock",
|
|
9200
|
+
"unlock",
|
|
9201
|
+
"arm",
|
|
9202
|
+
"disarm",
|
|
9203
|
+
"light",
|
|
9204
|
+
"alert"
|
|
9205
|
+
]);
|
|
9206
|
+
/** A single tap-through action button. */
|
|
9207
|
+
var NotificationActionSchema = object({
|
|
9208
|
+
id: string(),
|
|
9209
|
+
label: string(),
|
|
9210
|
+
url: string().optional(),
|
|
9211
|
+
/** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
|
|
9212
|
+
icon: NotificationActionIconSchema.optional(),
|
|
9213
|
+
/**
|
|
9214
|
+
* Renders in a warning style where the notifier supports it.
|
|
9215
|
+
*
|
|
9216
|
+
* A HINT, never a gate. The callback's authority is its token and nothing
|
|
9217
|
+
* else — see `notification-center/action-token.ts` for what that does and
|
|
9218
|
+
* does not buy.
|
|
9219
|
+
*/
|
|
9220
|
+
destructive: boolean().optional()
|
|
9221
|
+
});
|
|
9222
|
+
/**
|
|
9223
|
+
* The canonical notification. `body` is the only hard field (Apprise model).
|
|
9224
|
+
* `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
|
|
9225
|
+
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
9226
|
+
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
9227
|
+
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
9228
|
+
* `priority` for that one target.
|
|
9229
|
+
*/
|
|
9230
|
+
var NotificationSchema = object({
|
|
9231
|
+
body: string(),
|
|
9232
|
+
title: string().optional(),
|
|
9233
|
+
format: NotificationFormatSchema.default("text"),
|
|
9234
|
+
priority: number().int().min(1).max(5).default(3),
|
|
9235
|
+
level: string().optional(),
|
|
9236
|
+
attachments: array(AttachmentSchema).optional(),
|
|
9237
|
+
clickUrl: string().optional(),
|
|
9238
|
+
actions: array(NotificationActionSchema).optional(),
|
|
9239
|
+
sound: string().optional(),
|
|
9240
|
+
ttl: number().optional(),
|
|
9241
|
+
tag: string().optional(),
|
|
9242
|
+
deviceId: number().optional(),
|
|
9243
|
+
eventId: string().optional(),
|
|
9244
|
+
metadata: record(string(), unknown()).optional()
|
|
9245
|
+
});
|
|
9246
|
+
/** One declared native severity/priority level for a kind. */
|
|
9247
|
+
var TargetKindLevelSchema = object({
|
|
9248
|
+
id: string(),
|
|
9249
|
+
label: string(),
|
|
9250
|
+
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
9251
|
+
ordinal: number().int().min(1).max(5).nullable(),
|
|
9252
|
+
flags: object({
|
|
9253
|
+
critical: boolean().optional(),
|
|
9254
|
+
silent: boolean().optional(),
|
|
9255
|
+
noPush: boolean().optional()
|
|
9256
|
+
}).optional(),
|
|
9257
|
+
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
9258
|
+
requires: array(string()).optional(),
|
|
9259
|
+
description: string().optional()
|
|
9260
|
+
});
|
|
9261
|
+
/** The full capability block consulted before dispatch. */
|
|
9262
|
+
var TargetKindCapsSchema = object({
|
|
9263
|
+
attachments: object({
|
|
9264
|
+
mediaTypes: array(AttachmentMediaTypeSchema),
|
|
9265
|
+
mode: _enum([
|
|
9266
|
+
"url",
|
|
9267
|
+
"bytes",
|
|
9268
|
+
"both"
|
|
9269
|
+
]),
|
|
9270
|
+
max: number().int().nonnegative(),
|
|
9271
|
+
maxBytes: number().int().positive().optional()
|
|
9272
|
+
}),
|
|
9273
|
+
/** Max action buttons (0 = none). */
|
|
9274
|
+
actions: number().int().nonnegative(),
|
|
9275
|
+
/**
|
|
9276
|
+
* Whether this kind renders a per-action ICON.
|
|
9277
|
+
*
|
|
9278
|
+
* `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
|
|
9279
|
+
* run on the addon cap path — three production failures in one day taught
|
|
9280
|
+
* this repo that once. Absent is read as false by the degrade engine, which
|
|
9281
|
+
* is the safe direction: an icon that is not rendered costs nothing, an icon
|
|
9282
|
+
* assumed and dropped costs the operator's trust in the field.
|
|
9283
|
+
*/
|
|
9284
|
+
actionIcons: boolean().optional(),
|
|
9285
|
+
levels: array(TargetKindLevelSchema),
|
|
9286
|
+
format: array(NotificationFormatSchema),
|
|
9287
|
+
clickUrl: boolean(),
|
|
9288
|
+
sound: boolean(),
|
|
9289
|
+
ttl: boolean(),
|
|
9290
|
+
bodyMaxLen: number().int().positive()
|
|
9291
|
+
});
|
|
9292
|
+
/**
|
|
9293
|
+
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
9294
|
+
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
9295
|
+
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
9296
|
+
* the union is large and not meant for runtime validation here; the exported
|
|
9297
|
+
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
9298
|
+
*/
|
|
9299
|
+
var ConfigSchemaPassthrough$1 = unknown();
|
|
9300
|
+
var TargetKindSchema = object({
|
|
9301
|
+
kind: string(),
|
|
9302
|
+
label: string(),
|
|
9303
|
+
icon: string(),
|
|
9304
|
+
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
9305
|
+
addonId: string(),
|
|
9306
|
+
/**
|
|
9307
|
+
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
9308
|
+
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
9309
|
+
* when the addon bundles no icon for that kind — the client then falls back
|
|
9310
|
+
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
9311
|
+
*
|
|
9312
|
+
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
9313
|
+
* client, and a native client joins it onto its own hub base.
|
|
9314
|
+
*
|
|
9315
|
+
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
9316
|
+
* field that survived only because the runtime cap-router forwards provider
|
|
9317
|
+
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
9318
|
+
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
9319
|
+
* broken silently the moment output validation was tightened anywhere.
|
|
9320
|
+
*/
|
|
9321
|
+
iconUrl: string().optional(),
|
|
9322
|
+
/**
|
|
9323
|
+
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
9324
|
+
*
|
|
9325
|
+
* The server knows this and therefore says it, because the client cannot
|
|
9326
|
+
* safely guess: a React-Native client renders SVG and raster through two
|
|
9327
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
9328
|
+
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
9329
|
+
* placeholder glyph for every vector icon while the web build looked fine.
|
|
9330
|
+
*
|
|
9331
|
+
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
9332
|
+
* not been updated — a client that cannot determine the type should prefer
|
|
9333
|
+
* its raster path, which is the safe default for an unknown image.
|
|
9334
|
+
*/
|
|
9335
|
+
iconMediaType: string().optional(),
|
|
9336
|
+
configSchema: ConfigSchemaPassthrough$1,
|
|
9337
|
+
supportsDiscovery: boolean(),
|
|
9338
|
+
caps: TargetKindCapsSchema
|
|
9339
|
+
});
|
|
9340
|
+
/**
|
|
9341
|
+
* A persisted target. `config` holds secrets; providers REDACT secret fields
|
|
9342
|
+
* (return a presence marker only) when serving `listTargets` — never
|
|
9343
|
+
* round-trip a stored secret to the UI.
|
|
9344
|
+
*/
|
|
9345
|
+
var TargetSchema = object({
|
|
9346
|
+
id: string(),
|
|
9347
|
+
name: string(),
|
|
9348
|
+
kind: string(),
|
|
9349
|
+
addonId: string(),
|
|
9350
|
+
enabled: boolean(),
|
|
9351
|
+
config: record(string(), unknown())
|
|
9352
|
+
});
|
|
9353
|
+
/** A discovery-surfaced candidate (config is partial + non-secret). */
|
|
9354
|
+
var DiscoveredTargetSchema = object({
|
|
9355
|
+
kind: string(),
|
|
9356
|
+
suggestedName: string(),
|
|
9357
|
+
config: record(string(), unknown())
|
|
9358
|
+
});
|
|
9359
|
+
/** The degrade engine's report — what was resolved / dropped / degraded. */
|
|
9360
|
+
var RenderedAsSchema = object({
|
|
9361
|
+
level: string(),
|
|
9362
|
+
format: NotificationFormatSchema,
|
|
9363
|
+
attachmentsSent: number().int().nonnegative(),
|
|
9364
|
+
actionsSent: number().int().nonnegative(),
|
|
9365
|
+
truncated: boolean(),
|
|
9366
|
+
dropped: array(string())
|
|
9367
|
+
});
|
|
9368
|
+
var SendResultSchema = object({
|
|
9369
|
+
success: boolean(),
|
|
9370
|
+
error: string().optional(),
|
|
9371
|
+
renderedAs: RenderedAsSchema.optional()
|
|
9372
|
+
});
|
|
9373
|
+
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
9374
|
+
var TestResultSchema = SendResultSchema;
|
|
9375
|
+
method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
|
|
9376
|
+
kind: string(),
|
|
9377
|
+
config: record(string(), unknown()).optional()
|
|
9378
|
+
}), array(DiscoveredTargetSchema)), method(object({
|
|
9379
|
+
targetId: string(),
|
|
9380
|
+
notification: NotificationSchema
|
|
9381
|
+
}), SendResultSchema, { kind: "mutation" }), method(object({
|
|
9382
|
+
targetId: string(),
|
|
9383
|
+
sample: NotificationSchema.optional()
|
|
9384
|
+
}), TestResultSchema, { kind: "mutation" }), method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }), method(object({ targetId: string() }), _void(), { kind: "mutation" }), method(object({
|
|
9385
|
+
targetId: string(),
|
|
9386
|
+
enabled: boolean()
|
|
9387
|
+
}), _void(), { kind: "mutation" });
|
|
9388
|
+
/**
|
|
9040
9389
|
* notification-rules — the Notification Center rule surface (P1 core).
|
|
9041
9390
|
*
|
|
9042
9391
|
* Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
|
|
@@ -9166,7 +9515,115 @@ var NcZoneConditionSchema = object({
|
|
|
9166
9515
|
* The P1 condition set — a flat AND of groups; absent group = pass;
|
|
9167
9516
|
* membership lists are OR within the list (spec §2.3).
|
|
9168
9517
|
*/
|
|
9518
|
+
/**
|
|
9519
|
+
* What a rule may actuate.
|
|
9520
|
+
*
|
|
9521
|
+
* **No hand-maintained allowlist** (operator decision, and the right one — a
|
|
9522
|
+
* written list of methods is a third parallel map to keep aligned, and this
|
|
9523
|
+
* repo has paid for those). The boundary instead comes from a property the
|
|
9524
|
+
* capabilities already carry: an action may target only a **device-scoped**
|
|
9525
|
+
* capability method.
|
|
9526
|
+
*
|
|
9527
|
+
* That is not decoration. A rule can be authored by a NON-ADMIN — personal
|
|
9528
|
+
* rules are a supported flow — and the executor runs with the addon's
|
|
9529
|
+
* privileges, so an unbounded action is an arbitrary RPC channel with a
|
|
9530
|
+
* privilege escalation attached. Restricting to device scope excludes the
|
|
9531
|
+
* system caps (`device-manager.removeDevice` and friends) by construction,
|
|
9532
|
+
* costs nothing to maintain, and cannot rot: a cap that stops being
|
|
9533
|
+
* device-scoped stops being actuatable in the same change.
|
|
9534
|
+
*
|
|
9535
|
+
* The executor enforces it; {@link NcRuleActionSchema} carries the intent.
|
|
9536
|
+
*/
|
|
9537
|
+
/**
|
|
9538
|
+
* One step of a sequence.
|
|
9539
|
+
*
|
|
9540
|
+
* `wait` is a first-class step rather than a property of the next action: it is
|
|
9541
|
+
* what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
|
|
9542
|
+
* cannot be expressed otherwise.
|
|
9543
|
+
*/
|
|
9544
|
+
var NcRuleActionSchema = discriminatedUnion("kind", [object({
|
|
9545
|
+
kind: literal("wait"),
|
|
9546
|
+
seconds: number().min(0).max(300)
|
|
9547
|
+
}), object({
|
|
9548
|
+
kind: literal("cap"),
|
|
9549
|
+
deviceId: number().int(),
|
|
9550
|
+
/** Capability name, e.g. `alarm-panel`. */
|
|
9551
|
+
cap: string().min(1),
|
|
9552
|
+
/** Method on it. The executor refuses a non-device-scoped cap. */
|
|
9553
|
+
method: string().min(1),
|
|
9554
|
+
/** Method arguments, minus `deviceId` (the executor injects it). */
|
|
9555
|
+
args: record(string(), unknown()).optional()
|
|
9556
|
+
})]);
|
|
9557
|
+
/**
|
|
9558
|
+
* A named, ordered run of steps with its own throttle.
|
|
9559
|
+
*
|
|
9560
|
+
* `minDelaySec` exists because a noisy rule otherwise hammers a physical
|
|
9561
|
+
* actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
|
|
9562
|
+
* different budget from "how often may this gate actually open".
|
|
9563
|
+
*/
|
|
9564
|
+
var NcRuleActionSequenceSchema = object({
|
|
9565
|
+
name: string().min(1).max(120),
|
|
9566
|
+
enabled: boolean(),
|
|
9567
|
+
minDelaySec: number().int().min(0).max(86400).optional(),
|
|
9568
|
+
actions: array(NcRuleActionSchema).min(1)
|
|
9569
|
+
});
|
|
9570
|
+
/**
|
|
9571
|
+
* One button carried by the notification, running a named sequence on tap.
|
|
9572
|
+
*
|
|
9573
|
+
* **Read this before adding a button that does something physical.** The tap
|
|
9574
|
+
* arrives over a link that travelled through third-party infrastructure — ntfy,
|
|
9575
|
+
* a push relay, whatever forwarded the message — and the callback's ONLY
|
|
9576
|
+
* authority is the token in that link: single-use, short-lived, bound to this
|
|
9577
|
+
* one action of this one notification. It does not identify who tapped.
|
|
9578
|
+
* Whoever holds the notification can run the button, once, inside the window.
|
|
9579
|
+
* That is the operator's explicit choice (2026-08-05), and `destructive` is a
|
|
9580
|
+
* rendering hint, not a second gate. [D47](decisions/adr-0047.md).
|
|
9581
|
+
*/
|
|
9582
|
+
var NcRuleNotificationButtonSchema = object({
|
|
9583
|
+
/** Stable id — travels in the callback and identifies the button in logs. */
|
|
9584
|
+
id: string().min(1).max(64),
|
|
9585
|
+
label: string().min(1).max(40),
|
|
9586
|
+
/** Name of a sequence in `onTrigger`. The dispatcher drops a button whose
|
|
9587
|
+
* sequence does not exist rather than minting a token for nothing. */
|
|
9588
|
+
sequence: string().min(1).max(120),
|
|
9589
|
+
icon: NotificationActionIconSchema.optional(),
|
|
9590
|
+
destructive: boolean().optional()
|
|
9591
|
+
});
|
|
9592
|
+
/**
|
|
9593
|
+
* Sequences a rule runs, by hook point.
|
|
9594
|
+
*
|
|
9595
|
+
* ONLY `onTrigger` is here, deliberately. The reference also has activation /
|
|
9596
|
+
* deactivation / reset / post-generation hooks, and they are wanted — but this
|
|
9597
|
+
* repo's expensive failure mode is declaring a surface nothing produces, so a
|
|
9598
|
+
* hook appears here in the same change that produces its edge, never before.
|
|
9599
|
+
*/
|
|
9600
|
+
var NcRuleActionsSchema = object({
|
|
9601
|
+
/** Runs when the rule MATCHES. */
|
|
9602
|
+
onTrigger: array(NcRuleActionSequenceSchema).optional(),
|
|
9603
|
+
/**
|
|
9604
|
+
* Buttons the NOTIFICATION carries, each running one of this rule's
|
|
9605
|
+
* sequences when tapped.
|
|
9606
|
+
*
|
|
9607
|
+
* Deliberately a REFERENCE to a sequence rather than a second place to
|
|
9608
|
+
* author steps. A button that could define its own actions would be a
|
|
9609
|
+
* parallel actuation vocabulary — the executor's device-scope check, the
|
|
9610
|
+
* stop-at-first-failure rule and the per-sequence throttle all live on
|
|
9611
|
+
* sequences, and a second authoring surface would drift from every one of
|
|
9612
|
+
* them.
|
|
9613
|
+
*
|
|
9614
|
+
* A sequence reachable ONLY by a button simply appears in `onTrigger` with
|
|
9615
|
+
* `enabled: false`: it is then authored, throttled and validated like the
|
|
9616
|
+
* rest, and nothing runs it automatically.
|
|
9617
|
+
*/
|
|
9618
|
+
buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
|
|
9619
|
+
});
|
|
9169
9620
|
var NcConditionsSchema = object({
|
|
9621
|
+
/** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
|
|
9622
|
+
deviceState: object({
|
|
9623
|
+
deviceId: number().int(),
|
|
9624
|
+
/** Any of these matches. */
|
|
9625
|
+
states: array(string().min(1)).min(1)
|
|
9626
|
+
}).optional(),
|
|
9170
9627
|
/** Device scope — absent = all devices. */
|
|
9171
9628
|
devices: array(number()).optional(),
|
|
9172
9629
|
/** Detector class names (any overlap with the record's class set). */
|
|
@@ -9367,6 +9824,14 @@ var NcMediaPolicySchema = object({
|
|
|
9367
9824
|
clipPreRollSec: number().int().min(0).max(30).optional(),
|
|
9368
9825
|
clipPostRollSec: number().int().min(0).max(30).optional(),
|
|
9369
9826
|
/**
|
|
9827
|
+
* Playback rate of the attached gif / clip. Absent = 2x.
|
|
9828
|
+
*
|
|
9829
|
+
* A notification clip is GLANCED at on a lock screen, not watched: at real
|
|
9830
|
+
* time an eight-second passage is eight seconds of the recipient's attention
|
|
9831
|
+
* and twice the bytes. 1 is real time for the operator who wants it.
|
|
9832
|
+
*/
|
|
9833
|
+
clipSpeed: number().min(1).max(8).optional(),
|
|
9834
|
+
/**
|
|
9370
9835
|
* Which stream profile the footage is cut from. Absent = the CHEAPEST
|
|
9371
9836
|
* assigned profile: a notification is watched on a phone, so the 4K
|
|
9372
9837
|
* rendition would burn CPU to produce a file the client downscales anyway.
|
|
@@ -9438,7 +9903,30 @@ var NcRuleInputSchema = object({
|
|
|
9438
9903
|
* behaviour, visible to all, read-only in the viewer). Present = personal
|
|
9439
9904
|
* rule owned by this userId. Server-stamped; never trusted from a client.
|
|
9440
9905
|
*/
|
|
9441
|
-
ownerUserId: string().optional()
|
|
9906
|
+
ownerUserId: string().optional(),
|
|
9907
|
+
/**
|
|
9908
|
+
* May a non-admin snooze this rule for EVERYONE, not just themselves?
|
|
9909
|
+
*
|
|
9910
|
+
* A snooze is personal by default — it silences the person who set it. This
|
|
9911
|
+
* opts THIS rule into the "the gardener is here all afternoon" case, where
|
|
9912
|
+
* silencing the camera for the whole household is legitimate. It silences
|
|
9913
|
+
* other people, so it is off unless a rule deliberately allows it.
|
|
9914
|
+
*
|
|
9915
|
+
* `.optional()`, deliberately NOT `.default()`: a Zod default does not run on
|
|
9916
|
+
* the addon cap path (three production failures in one day), so absent is
|
|
9917
|
+
* read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
|
|
9918
|
+
* by this flag — see the scope rules on that function.
|
|
9919
|
+
*/
|
|
9920
|
+
snoozeAllowGlobal: boolean().optional(),
|
|
9921
|
+
/**
|
|
9922
|
+
* Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
|
|
9923
|
+
*
|
|
9924
|
+
* This is what makes the rule set the alarm's trigger set without the alarm
|
|
9925
|
+
* being a special case: arming is
|
|
9926
|
+
* `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
|
|
9927
|
+
* shape as every other actuation.
|
|
9928
|
+
*/
|
|
9929
|
+
actions: NcRuleActionsSchema.optional()
|
|
9442
9930
|
});
|
|
9443
9931
|
/**
|
|
9444
9932
|
* Partial patch for `updateRule` — any subset of the input fields, plus the
|
|
@@ -9509,7 +9997,8 @@ var NcConditionDescriptorSchema = object({
|
|
|
9509
9997
|
"packagePhase",
|
|
9510
9998
|
"crossingSelect",
|
|
9511
9999
|
"polygonDraw",
|
|
9512
|
-
"occupancy"
|
|
10000
|
+
"occupancy",
|
|
10001
|
+
"deviceState"
|
|
9513
10002
|
]),
|
|
9514
10003
|
operator: _enum([
|
|
9515
10004
|
"in",
|
|
@@ -9523,7 +10012,28 @@ var NcConditionDescriptorSchema = object({
|
|
|
9523
10012
|
/** Which delivery kinds the condition applies to. */
|
|
9524
10013
|
appliesTo: array(NcDeliverySchema),
|
|
9525
10014
|
phase: string(),
|
|
9526
|
-
description: string().optional()
|
|
10015
|
+
description: string().optional(),
|
|
10016
|
+
/**
|
|
10017
|
+
* The CHOICES for a single-choice widget (`sourceSelect`, `crossingSelect`,
|
|
10018
|
+
* `packagePhase`, …), served with the descriptor.
|
|
10019
|
+
*
|
|
10020
|
+
* Before this the descriptor said which widget to render and not what to put
|
|
10021
|
+
* in it, so every option list lived in three places: this file's enums, the
|
|
10022
|
+
* admin's `NC_*_OPTIONS` and the viewer's `NC_*_VALUES`. That triple mirror
|
|
10023
|
+
* is the drift that emptied the viewer's rule editor on 2026-08-04 — the app
|
|
10024
|
+
* mirrors the cap by hand, so it is only ever as current as its last build.
|
|
10025
|
+
*
|
|
10026
|
+
* With the options on the wire, a condition of an EXISTING `valueType` costs
|
|
10027
|
+
* zero client changes. Clients keep a local fallback for an older hub that
|
|
10028
|
+
* does not send them; absent here is "use your own list", not "no choices".
|
|
10029
|
+
*/
|
|
10030
|
+
options: array(object({
|
|
10031
|
+
/** Written to the rule verbatim. `''` means the ABSENT state. */
|
|
10032
|
+
value: string(),
|
|
10033
|
+
label: string(),
|
|
10034
|
+
/** What THIS choice matches — shown one at a time, under the control. */
|
|
10035
|
+
hint: string().optional()
|
|
10036
|
+
})).readonly().optional()
|
|
9527
10037
|
});
|
|
9528
10038
|
/**
|
|
9529
10039
|
* The delivery lifecycle status of a history row — a straight read of the
|
|
@@ -9608,6 +10118,135 @@ var NcHistoryFilterSchema = object({
|
|
|
9608
10118
|
until: number().optional(),
|
|
9609
10119
|
limit: number().int().min(1).max(500).default(100)
|
|
9610
10120
|
});
|
|
10121
|
+
/**
|
|
10122
|
+
* What a snooze covers. Broader scopes win when several overlap, so one window
|
|
10123
|
+
* leaves ONE digest rather than a rule snooze and a whole-feed snooze both
|
|
10124
|
+
* summarising the same silence.
|
|
10125
|
+
*/
|
|
10126
|
+
var NcSnoozeScopeSchema = _enum([
|
|
10127
|
+
"rule",
|
|
10128
|
+
"device",
|
|
10129
|
+
"all"
|
|
10130
|
+
]);
|
|
10131
|
+
/**
|
|
10132
|
+
* Client-authored snooze. The server stamps `userId`, `startedAt` and
|
|
10133
|
+
* `expiresAt` — a DURATION is sent rather than an instant so a client with a
|
|
10134
|
+
* skewed clock cannot author a window that is already over, or never ends.
|
|
10135
|
+
*/
|
|
10136
|
+
var NcSnoozeInputSchema = object({
|
|
10137
|
+
scope: NcSnoozeScopeSchema,
|
|
10138
|
+
/** Required when `scope: 'rule'` — a scoped snooze with no id matches
|
|
10139
|
+
* NOTHING rather than degrading to "everything". */
|
|
10140
|
+
ruleId: string().optional(),
|
|
10141
|
+
/** Required when `scope: 'device'`. */
|
|
10142
|
+
deviceId: number().int().optional(),
|
|
10143
|
+
durationMinutes: number().int().min(1).max(1440),
|
|
10144
|
+
/**
|
|
10145
|
+
* Silence this for EVERY recipient, not just the caller. Permission is
|
|
10146
|
+
* checked server-side (the rule's `snoozeAllowGlobal`, or admin for the
|
|
10147
|
+
* broader scopes). Absent = personal.
|
|
10148
|
+
*/
|
|
10149
|
+
global: boolean().optional(),
|
|
10150
|
+
/**
|
|
10151
|
+
* Deliver a summary of what was suppressed when the window ends. Absent =
|
|
10152
|
+
* ON: someone silencing a nuisance camera wants it off, someone silencing a
|
|
10153
|
+
* SECURITY camera wants to know what they missed, and choosing "off" for
|
|
10154
|
+
* everybody is how a snooze becomes an outage. Resolved to a concrete
|
|
10155
|
+
* boolean by the server at create time — never left to a Zod default, which
|
|
10156
|
+
* does not run on the addon cap path.
|
|
10157
|
+
*/
|
|
10158
|
+
summary: boolean().optional()
|
|
10159
|
+
});
|
|
10160
|
+
/** A persisted snooze window. */
|
|
10161
|
+
var NcSnoozeSchema = object({
|
|
10162
|
+
id: string(),
|
|
10163
|
+
/** Who set it. Also who it silences, unless `global`. */
|
|
10164
|
+
userId: string(),
|
|
10165
|
+
scope: NcSnoozeScopeSchema,
|
|
10166
|
+
ruleId: string().optional(),
|
|
10167
|
+
deviceId: number().int().optional(),
|
|
10168
|
+
startedAt: number(),
|
|
10169
|
+
/** Exclusive: at exactly this instant the snooze is over. Expiry is a
|
|
10170
|
+
* COMPARISON, not a job — no sweeper can leave the operator silenced. */
|
|
10171
|
+
expiresAt: number(),
|
|
10172
|
+
global: boolean(),
|
|
10173
|
+
summary: boolean(),
|
|
10174
|
+
/** When the end-of-window digest went out. Absent = not sent (yet, or the
|
|
10175
|
+
* window has not closed, or `summary` is false). */
|
|
10176
|
+
digestSentAt: number().optional()
|
|
10177
|
+
});
|
|
10178
|
+
object({
|
|
10179
|
+
snoozeId: string(),
|
|
10180
|
+
targetId: string(),
|
|
10181
|
+
ruleId: string(),
|
|
10182
|
+
ruleName: string(),
|
|
10183
|
+
deviceId: number().int(),
|
|
10184
|
+
/** How many notifications this snooze hid for that pair. */
|
|
10185
|
+
count: number().int(),
|
|
10186
|
+
firstAt: number(),
|
|
10187
|
+
lastAt: number()
|
|
10188
|
+
});
|
|
10189
|
+
/**
|
|
10190
|
+
* The three durations the panel's state machine runs on, plus who hears about
|
|
10191
|
+
* an arm.
|
|
10192
|
+
*
|
|
10193
|
+
* They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
|
|
10194
|
+
* `alarm-panel` is `deviceNative` and its other provider mirrors somebody
|
|
10195
|
+
* else's panel, which has its own delays and no way to be told these. This is
|
|
10196
|
+
* configuration of the panel CamStack owns, and the Notification Center owns
|
|
10197
|
+
* that panel.
|
|
10198
|
+
*/
|
|
10199
|
+
var NcAlarmSettingsSchema = object({
|
|
10200
|
+
/** Grace period between arming and the mode taking effect. 0 = immediate. */
|
|
10201
|
+
exitDelaySec: number().int().min(0).max(600),
|
|
10202
|
+
/** Grace period between a trigger and the alarm firing. 0 = immediate. */
|
|
10203
|
+
entryDelaySec: number().int().min(0).max(600),
|
|
10204
|
+
/**
|
|
10205
|
+
* How long `triggered` lasts before the panel re-arms itself.
|
|
10206
|
+
* **0 = until an operator disarms it** — the behaviour before this field
|
|
10207
|
+
* existed, and therefore what an untouched install keeps doing.
|
|
10208
|
+
*/
|
|
10209
|
+
triggeredDurationSec: number().int().min(0).max(3600),
|
|
10210
|
+
/** Send a notification when a mode takes effect. */
|
|
10211
|
+
announceArm: boolean(),
|
|
10212
|
+
/**
|
|
10213
|
+
* Where that notification goes. Target ids from `notification-output`.
|
|
10214
|
+
*
|
|
10215
|
+
* Explicit rather than "everyone": an arm announcement is a household
|
|
10216
|
+
* message, and broadcasting it to every configured endpoint (including a
|
|
10217
|
+
* webhook wired to something else) is not a default anybody would choose.
|
|
10218
|
+
* Empty with `announceArm: true` sends nothing, and the server logs that —
|
|
10219
|
+
* silence must be attributable.
|
|
10220
|
+
*/
|
|
10221
|
+
announceTargets: array(string().min(1)).max(16)
|
|
10222
|
+
});
|
|
10223
|
+
/** Every field optional — a tab edits one control at a time. */
|
|
10224
|
+
var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.partial();
|
|
10225
|
+
/**
|
|
10226
|
+
* What one arm mode actually arms, DERIVED from the enabled rules gated on it.
|
|
10227
|
+
* Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
|
|
10228
|
+
* copy would lie the first time a rule is disabled.
|
|
10229
|
+
*/
|
|
10230
|
+
var NcAlarmModeCoverageSchema = object({
|
|
10231
|
+
mode: AlarmArmModeSchema,
|
|
10232
|
+
/** Enabled rules gated on `armed_<mode>`. Zero means the mode does nothing. */
|
|
10233
|
+
ruleCount: number().int().min(0),
|
|
10234
|
+
/** At least one covering rule has no device scope, so the mode covers all. */
|
|
10235
|
+
allDevices: boolean(),
|
|
10236
|
+
/** Ids named by the covering rules. A SUBSET when `allDevices` is true. */
|
|
10237
|
+
deviceIds: array(number().int())
|
|
10238
|
+
});
|
|
10239
|
+
var NcAlarmConfigSchema = object({
|
|
10240
|
+
/**
|
|
10241
|
+
* The panel's device id, or null when this install has no panel (the ensure
|
|
10242
|
+
* step failed, or this is not the hub). Null is the honest answer: an editor
|
|
10243
|
+
* that rendered delays for a panel that does not exist would be a form whose
|
|
10244
|
+
* Save does nothing.
|
|
10245
|
+
*/
|
|
10246
|
+
deviceId: number().int().nullable(),
|
|
10247
|
+
settings: NcAlarmSettingsSchema,
|
|
10248
|
+
coverage: array(NcAlarmModeCoverageSchema)
|
|
10249
|
+
});
|
|
9611
10250
|
method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), method(object({ ruleId: string() }), object({ rule: NcRuleSchema.nullable() }), { auth: "admin" }), method(object({ rule: NcRuleInputSchema }), object({ rule: NcRuleSchema }), {
|
|
9612
10251
|
kind: "mutation",
|
|
9613
10252
|
auth: "admin",
|
|
@@ -9637,7 +10276,16 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
9637
10276
|
}), method(object({}), object({
|
|
9638
10277
|
catalog: array(NcConditionDescriptorSchema),
|
|
9639
10278
|
taxonomy: NcTaxonomySchema.optional()
|
|
9640
|
-
})), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" })
|
|
10279
|
+
})), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
|
|
10280
|
+
kind: "mutation",
|
|
10281
|
+
caller: "required"
|
|
10282
|
+
}), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
|
|
10283
|
+
kind: "mutation",
|
|
10284
|
+
caller: "required"
|
|
10285
|
+
}), method(object({}), NcAlarmConfigSchema, { auth: "admin" }), method(object({ patch: NcAlarmSettingsPatchSchema }), NcAlarmConfigSchema, {
|
|
10286
|
+
kind: "mutation",
|
|
10287
|
+
auth: "admin"
|
|
10288
|
+
});
|
|
9641
10289
|
/**
|
|
9642
10290
|
* TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
|
|
9643
10291
|
*
|
|
@@ -9845,72 +10493,6 @@ object({
|
|
|
9845
10493
|
precision: number().int().min(0).max(10).optional()
|
|
9846
10494
|
});
|
|
9847
10495
|
DeviceType.Sensor;
|
|
9848
|
-
/**
|
|
9849
|
-
* Alarm-panel cap. Models HA `alarm_control_panel.*` on
|
|
9850
|
-
* `DeviceType.AlarmPanel`. State follows HA's canonical lifecycle
|
|
9851
|
-
* across disarmed / armed_(home|away|night|vacation|custom_bypass) /
|
|
9852
|
-
* arming / pending / triggered / disarming.
|
|
9853
|
-
*
|
|
9854
|
-
* Many panels require a PIN code on arm / disarm — the optional
|
|
9855
|
-
* `code` field on the methods passes it through to the upstream
|
|
9856
|
-
* service; it's NEVER persisted in the runtime slice or any event
|
|
9857
|
-
* payload. The presence of a required code is signalled by
|
|
9858
|
-
* `DeviceFeature.AlarmPinRequired` so the UI gates a code-entry
|
|
9859
|
-
* field without a slice fetch.
|
|
9860
|
-
*
|
|
9861
|
-
* `availableModes` mirrors HA's `supported_features`-derived arm
|
|
9862
|
-
* mode list — the UI renders only the buttons the panel accepts.
|
|
9863
|
-
*/
|
|
9864
|
-
var AlarmStateSchema = _enum([
|
|
9865
|
-
"disarmed",
|
|
9866
|
-
"armed_home",
|
|
9867
|
-
"armed_away",
|
|
9868
|
-
"armed_night",
|
|
9869
|
-
"armed_vacation",
|
|
9870
|
-
"armed_custom_bypass",
|
|
9871
|
-
"arming",
|
|
9872
|
-
"disarming",
|
|
9873
|
-
"pending",
|
|
9874
|
-
"triggered"
|
|
9875
|
-
]);
|
|
9876
|
-
var AlarmArmModeSchema = _enum([
|
|
9877
|
-
"home",
|
|
9878
|
-
"away",
|
|
9879
|
-
"night",
|
|
9880
|
-
"vacation",
|
|
9881
|
-
"custom_bypass"
|
|
9882
|
-
]);
|
|
9883
|
-
object({
|
|
9884
|
-
/** Current lifecycle state. */
|
|
9885
|
-
state: AlarmStateSchema,
|
|
9886
|
-
/** Subset of arm modes the panel accepts. UI renders one button per
|
|
9887
|
-
* mode in this list. */
|
|
9888
|
-
availableModes: array(AlarmArmModeSchema),
|
|
9889
|
-
/** Whether the panel requires a PIN on arm / disarm. Mirrors
|
|
9890
|
-
* `DeviceFeature.AlarmPinRequired` for slice consumers. */
|
|
9891
|
-
requiresCode: boolean(),
|
|
9892
|
-
/** Ms epoch when the slice was last updated. */
|
|
9893
|
-
lastChangedAt: number()
|
|
9894
|
-
});
|
|
9895
|
-
DeviceType.AlarmPanel, method(object({
|
|
9896
|
-
deviceId: number().int().nonnegative(),
|
|
9897
|
-
mode: AlarmArmModeSchema,
|
|
9898
|
-
/** Optional PIN code. Required when `requiresCode === true`.
|
|
9899
|
-
* Passed through to the upstream service; never persisted. */
|
|
9900
|
-
code: string().min(1).optional()
|
|
9901
|
-
}), _void(), {
|
|
9902
|
-
kind: "mutation",
|
|
9903
|
-
auth: "admin"
|
|
9904
|
-
}), method(object({
|
|
9905
|
-
deviceId: number().int().nonnegative(),
|
|
9906
|
-
code: string().min(1).optional()
|
|
9907
|
-
}), _void(), {
|
|
9908
|
-
kind: "mutation",
|
|
9909
|
-
auth: "admin"
|
|
9910
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
9911
|
-
kind: "mutation",
|
|
9912
|
-
auth: "admin"
|
|
9913
|
-
});
|
|
9914
10496
|
object({
|
|
9915
10497
|
/** Current illuminance in lux (lx). */
|
|
9916
10498
|
lux: number().min(0),
|
|
@@ -10385,7 +10967,15 @@ method(object({
|
|
|
10385
10967
|
format: _enum(["gif", "mp4"]).default("gif"),
|
|
10386
10968
|
maxWidth: number().int().min(120).max(1920).default(480),
|
|
10387
10969
|
/** GIF only — MP4 keeps the source cadence. */
|
|
10388
|
-
fps: number().int().min(1).max(15).default(5)
|
|
10970
|
+
fps: number().int().min(1).max(15).default(5),
|
|
10971
|
+
/**
|
|
10972
|
+
* Playback rate. A notification clip is GLANCED at on a lock screen,
|
|
10973
|
+
* not watched, so 2x is the default: the recipient sees the whole
|
|
10974
|
+
* passage in half the time and the GIF is half the bytes. `1` is real
|
|
10975
|
+
* time. Applies to MP4 as well — the operator set a speed, not a GIF
|
|
10976
|
+
* speed.
|
|
10977
|
+
*/
|
|
10978
|
+
speed: number().min(1).max(8).default(2)
|
|
10389
10979
|
}), object({
|
|
10390
10980
|
base64: string(),
|
|
10391
10981
|
mime: string(),
|
|
@@ -14967,6 +15557,20 @@ var GetInputSchema = object({ id: string() });
|
|
|
14967
15557
|
var AddInputSchema = object({
|
|
14968
15558
|
kind: string().min(1),
|
|
14969
15559
|
name: string().min(1),
|
|
15560
|
+
/**
|
|
15561
|
+
* ADOPT an existing id instead of minting a new one.
|
|
15562
|
+
*
|
|
15563
|
+
* Written the day this cost an outage. A broker's id is not a detail: HA
|
|
15564
|
+
* devices carry it inside their `stableId` (`ha:ha_004:dev:…`), so a broker
|
|
15565
|
+
* lost from config and re-added as `ha_001` leaves every one of its devices
|
|
15566
|
+
* bound to a broker that no longer exists. Re-entering the password under the
|
|
15567
|
+
* ORIGINAL id turns a multi-step device migration back into re-entering a
|
|
15568
|
+
* password.
|
|
15569
|
+
*
|
|
15570
|
+
* A provider MUST refuse an id that is already in use — adopting a live
|
|
15571
|
+
* broker's id would silently take it over.
|
|
15572
|
+
*/
|
|
15573
|
+
id: string().min(1).optional(),
|
|
14970
15574
|
/** Kind-specific settings (e.g. MQTT `{url,username,password}` or HA
|
|
14971
15575
|
* `{baseUrl,accessToken}`). Validated by the kind-specific provider
|
|
14972
15576
|
* branch on receipt — invalid shape rejects the add. */
|
|
@@ -15130,6 +15734,210 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
|
|
|
15130
15734
|
kind: "mutation",
|
|
15131
15735
|
auth: "admin"
|
|
15132
15736
|
});
|
|
15737
|
+
/**
|
|
15738
|
+
* Query filter for settings-store collections.
|
|
15739
|
+
*/
|
|
15740
|
+
var QueryFilterSchema = object({
|
|
15741
|
+
where: record(string(), unknown()).optional(),
|
|
15742
|
+
whereIn: record(string(), array(unknown())).optional(),
|
|
15743
|
+
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
15744
|
+
orderBy: object({
|
|
15745
|
+
field: string(),
|
|
15746
|
+
direction: _enum(["asc", "desc"])
|
|
15747
|
+
}).optional(),
|
|
15748
|
+
limit: number().optional(),
|
|
15749
|
+
offset: number().optional()
|
|
15750
|
+
});
|
|
15751
|
+
/**
|
|
15752
|
+
* The predicate half of a filter, for BULK MUTATIONS.
|
|
15753
|
+
*
|
|
15754
|
+
* Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
|
|
15755
|
+
* meaning for a statement that rewrites a set, and accepting them would invite
|
|
15756
|
+
* a caller to believe `limit` bounds the damage. Every field is optional here
|
|
15757
|
+
* only so the shape stays composable — the implementation REJECTS a filter
|
|
15758
|
+
* that compiles to no predicate, because that is the whole collection.
|
|
15759
|
+
*/
|
|
15760
|
+
var MutationFilterSchema = object({
|
|
15761
|
+
where: record(string(), unknown()).optional(),
|
|
15762
|
+
whereIn: record(string(), array(unknown())).optional(),
|
|
15763
|
+
whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
|
|
15764
|
+
});
|
|
15765
|
+
/** A single stored record: `{ id, data }`. */
|
|
15766
|
+
var SettingsRecordSchema = object({
|
|
15767
|
+
id: string(),
|
|
15768
|
+
data: record(string(), unknown())
|
|
15769
|
+
});
|
|
15770
|
+
/**
|
|
15771
|
+
* Column declaration for a structured (SQL-backed) collection.
|
|
15772
|
+
*
|
|
15773
|
+
* Logical types — the backend translates each to the matching SQLite
|
|
15774
|
+
* storage class and handles per-type marshaling:
|
|
15775
|
+
* - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
|
|
15776
|
+
* - `JSON` — TEXT under the hood; serialised on write, parsed on read
|
|
15777
|
+
* - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
|
|
15778
|
+
*/
|
|
15779
|
+
var CollectionColumnSchema = object({
|
|
15780
|
+
name: string(),
|
|
15781
|
+
type: _enum([
|
|
15782
|
+
"TEXT",
|
|
15783
|
+
"INTEGER",
|
|
15784
|
+
"REAL",
|
|
15785
|
+
"JSON",
|
|
15786
|
+
"BOOLEAN"
|
|
15787
|
+
]),
|
|
15788
|
+
primaryKey: boolean().optional(),
|
|
15789
|
+
notNull: boolean().optional(),
|
|
15790
|
+
unique: boolean().optional(),
|
|
15791
|
+
/**
|
|
15792
|
+
* Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
|
|
15793
|
+
* columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
|
|
15794
|
+
* collection surface simply cannot express their existing tables — which is
|
|
15795
|
+
* why they were still on the uncapped `table*` API.
|
|
15796
|
+
*/
|
|
15797
|
+
defaultValue: union([
|
|
15798
|
+
string(),
|
|
15799
|
+
number(),
|
|
15800
|
+
boolean()
|
|
15801
|
+
]).optional()
|
|
15802
|
+
});
|
|
15803
|
+
var CollectionIndexSchema = object({
|
|
15804
|
+
name: string(),
|
|
15805
|
+
columns: array(string()).readonly(),
|
|
15806
|
+
unique: boolean().optional()
|
|
15807
|
+
});
|
|
15808
|
+
method(object({
|
|
15809
|
+
namespace: string().optional(),
|
|
15810
|
+
collection: string(),
|
|
15811
|
+
key: string()
|
|
15812
|
+
}), unknown()), method(object({
|
|
15813
|
+
namespace: string().optional(),
|
|
15814
|
+
collection: string(),
|
|
15815
|
+
key: string(),
|
|
15816
|
+
value: unknown()
|
|
15817
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15818
|
+
namespace: string().optional(),
|
|
15819
|
+
collection: string(),
|
|
15820
|
+
filter: QueryFilterSchema.optional()
|
|
15821
|
+
}), array(SettingsRecordSchema).readonly()), method(object({
|
|
15822
|
+
namespace: string().optional(),
|
|
15823
|
+
collection: string(),
|
|
15824
|
+
record: SettingsRecordSchema
|
|
15825
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15826
|
+
namespace: string().optional(),
|
|
15827
|
+
collection: string(),
|
|
15828
|
+
id: string(),
|
|
15829
|
+
data: record(string(), unknown())
|
|
15830
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15831
|
+
namespace: string().optional(),
|
|
15832
|
+
collection: string(),
|
|
15833
|
+
key: string()
|
|
15834
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15835
|
+
namespace: string().optional(),
|
|
15836
|
+
collection: string(),
|
|
15837
|
+
filter: MutationFilterSchema
|
|
15838
|
+
}), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
|
|
15839
|
+
namespace: string().optional(),
|
|
15840
|
+
collection: string(),
|
|
15841
|
+
filter: MutationFilterSchema,
|
|
15842
|
+
data: record(string(), unknown())
|
|
15843
|
+
}), object({ updated: number().int() }), { kind: "mutation" }), method(object({
|
|
15844
|
+
namespace: string().optional(),
|
|
15845
|
+
collection: string(),
|
|
15846
|
+
filter: QueryFilterSchema.optional()
|
|
15847
|
+
}), number()), method(object({
|
|
15848
|
+
namespace: string().optional(),
|
|
15849
|
+
collection: string(),
|
|
15850
|
+
field: string(),
|
|
15851
|
+
bucketSize: number().int().positive(),
|
|
15852
|
+
origin: number().int(),
|
|
15853
|
+
filter: QueryFilterSchema.optional()
|
|
15854
|
+
}), array(object({
|
|
15855
|
+
bucket: number().int(),
|
|
15856
|
+
count: number().int()
|
|
15857
|
+
})).readonly()), method(object({
|
|
15858
|
+
namespace: string().optional(),
|
|
15859
|
+
collection: string()
|
|
15860
|
+
}), boolean()), method(object({
|
|
15861
|
+
namespace: string().optional(),
|
|
15862
|
+
collection: string(),
|
|
15863
|
+
columns: array(CollectionColumnSchema).readonly(),
|
|
15864
|
+
indexes: array(CollectionIndexSchema).readonly().optional()
|
|
15865
|
+
}), _void(), { kind: "mutation" });
|
|
15866
|
+
/**
|
|
15867
|
+
* What one engine says about itself. The orchestrator uses `kind` to pick
|
|
15868
|
+
* a registrant for a collection; `engineId` is what a log line names when
|
|
15869
|
+
* a call is routed or refused.
|
|
15870
|
+
*/
|
|
15871
|
+
var EngineInfoSchema = object({
|
|
15872
|
+
engineId: string(),
|
|
15873
|
+
/**
|
|
15874
|
+
* `relational` — rows, columns, indexes, the surface `settings-store`
|
|
15875
|
+
* has always described. `vector` — an embedding store answering
|
|
15876
|
+
* similarity queries. A registrant declares exactly one; an engine that
|
|
15877
|
+
* does both registers twice, because "both" would make the routing
|
|
15878
|
+
* decision ambiguous at exactly the point it must not be.
|
|
15879
|
+
*/
|
|
15880
|
+
kind: _enum(["relational", "vector"]),
|
|
15881
|
+
displayName: string()
|
|
15882
|
+
});
|
|
15883
|
+
method(_void(), EngineInfoSchema), method(object({
|
|
15884
|
+
namespace: string().optional(),
|
|
15885
|
+
collection: string(),
|
|
15886
|
+
key: string()
|
|
15887
|
+
}), unknown()), method(object({
|
|
15888
|
+
namespace: string().optional(),
|
|
15889
|
+
collection: string(),
|
|
15890
|
+
key: string(),
|
|
15891
|
+
value: unknown()
|
|
15892
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15893
|
+
namespace: string().optional(),
|
|
15894
|
+
collection: string(),
|
|
15895
|
+
filter: QueryFilterSchema.optional()
|
|
15896
|
+
}), array(SettingsRecordSchema).readonly()), method(object({
|
|
15897
|
+
namespace: string().optional(),
|
|
15898
|
+
collection: string(),
|
|
15899
|
+
record: SettingsRecordSchema
|
|
15900
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15901
|
+
namespace: string().optional(),
|
|
15902
|
+
collection: string(),
|
|
15903
|
+
id: string(),
|
|
15904
|
+
data: record(string(), unknown())
|
|
15905
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15906
|
+
namespace: string().optional(),
|
|
15907
|
+
collection: string(),
|
|
15908
|
+
key: string()
|
|
15909
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15910
|
+
namespace: string().optional(),
|
|
15911
|
+
collection: string(),
|
|
15912
|
+
filter: MutationFilterSchema
|
|
15913
|
+
}), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
|
|
15914
|
+
namespace: string().optional(),
|
|
15915
|
+
collection: string(),
|
|
15916
|
+
filter: MutationFilterSchema,
|
|
15917
|
+
data: record(string(), unknown())
|
|
15918
|
+
}), object({ updated: number().int() }), { kind: "mutation" }), method(object({
|
|
15919
|
+
namespace: string().optional(),
|
|
15920
|
+
collection: string(),
|
|
15921
|
+
filter: QueryFilterSchema.optional()
|
|
15922
|
+
}), number()), method(object({
|
|
15923
|
+
namespace: string().optional(),
|
|
15924
|
+
collection: string(),
|
|
15925
|
+
field: string(),
|
|
15926
|
+
bucketSize: number().int().positive(),
|
|
15927
|
+
origin: number().int(),
|
|
15928
|
+
filter: QueryFilterSchema.optional()
|
|
15929
|
+
}), array(object({
|
|
15930
|
+
bucket: number().int(),
|
|
15931
|
+
count: number().int()
|
|
15932
|
+
})).readonly()), method(object({
|
|
15933
|
+
namespace: string().optional(),
|
|
15934
|
+
collection: string()
|
|
15935
|
+
}), boolean()), method(object({
|
|
15936
|
+
namespace: string().optional(),
|
|
15937
|
+
collection: string(),
|
|
15938
|
+
columns: array(CollectionColumnSchema).readonly(),
|
|
15939
|
+
indexes: array(CollectionIndexSchema).readonly().optional()
|
|
15940
|
+
}), _void(), { kind: "mutation" });
|
|
15133
15941
|
DeviceType.Camera;
|
|
15134
15942
|
/**
|
|
15135
15943
|
* `device-adoption` — generic discovery + adoption surface,
|
|
@@ -15833,7 +16641,20 @@ method(object({
|
|
|
15833
16641
|
}), _void(), {
|
|
15834
16642
|
kind: "mutation",
|
|
15835
16643
|
auth: "admin"
|
|
15836
|
-
}), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({
|
|
16644
|
+
}), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({
|
|
16645
|
+
addonId: string().optional(),
|
|
16646
|
+
/**
|
|
16647
|
+
* `slim` omits `config` (returned `{}`), `metadata` (null) and the
|
|
16648
|
+
* `sourceInfo` derived from config — and skips the per-device settings
|
|
16649
|
+
* read that produces them. Everything identifying a device (id, name,
|
|
16650
|
+
* type, online, features, isCamera, parent/link ids) is unchanged.
|
|
16651
|
+
* Do not use it for dispatch routing, which needs `sourceInfo`.
|
|
16652
|
+
*/
|
|
16653
|
+
projection: _enum(["full", "slim"]).optional(),
|
|
16654
|
+
/** Return only camera devices. Filtering server-side instead of
|
|
16655
|
+
* shipping 293 rows to find 12. */
|
|
16656
|
+
isCamera: boolean().optional()
|
|
16657
|
+
}), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
|
|
15837
16658
|
mode: DeviceLinkModeSchema,
|
|
15838
16659
|
devices: array(LinkedDeviceSchema)
|
|
15839
16660
|
})), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
@@ -16272,14 +17093,14 @@ var LlmProfileSchema = object({
|
|
|
16272
17093
|
/** ConfigUISchema tree passed through untyped on the wire (the
|
|
16273
17094
|
* notification-output `ConfigSchemaPassthrough` precedent at
|
|
16274
17095
|
* notification-output.cap.ts:151); the exported TS type re-tightens it. */
|
|
16275
|
-
var ConfigSchemaPassthrough
|
|
17096
|
+
var ConfigSchemaPassthrough = unknown();
|
|
16276
17097
|
var LlmProfileKindDescriptorSchema = object({
|
|
16277
17098
|
kind: LlmProfileKindSchema,
|
|
16278
17099
|
label: string(),
|
|
16279
17100
|
icon: string(),
|
|
16280
17101
|
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
16281
17102
|
addonId: string(),
|
|
16282
|
-
configSchema: ConfigSchemaPassthrough
|
|
17103
|
+
configSchema: ConfigSchemaPassthrough
|
|
16283
17104
|
});
|
|
16284
17105
|
var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
|
|
16285
17106
|
var LlmDefaultSchema = object({
|
|
@@ -16810,227 +17631,102 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
|
|
|
16810
17631
|
});
|
|
16811
17632
|
method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
|
|
16812
17633
|
/**
|
|
16813
|
-
*
|
|
16814
|
-
*
|
|
16815
|
-
*
|
|
16816
|
-
* `docs/superpowers/specs/2026-
|
|
16817
|
-
*
|
|
16818
|
-
*
|
|
16819
|
-
*
|
|
16820
|
-
*
|
|
16821
|
-
*
|
|
16822
|
-
*
|
|
16823
|
-
*
|
|
16824
|
-
*
|
|
16825
|
-
*
|
|
16826
|
-
*
|
|
16827
|
-
*
|
|
16828
|
-
*
|
|
16829
|
-
*
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
*
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
*
|
|
16836
|
-
|
|
16837
|
-
|
|
16838
|
-
|
|
16839
|
-
|
|
16840
|
-
|
|
16841
|
-
* admin "Integrations" page.
|
|
16842
|
-
*/
|
|
16843
|
-
/**
|
|
16844
|
-
* Zentik-derived typed-media enum — the superset across every kind. Each
|
|
16845
|
-
* adapter picks what it supports and the degrade engine filters the rest.
|
|
16846
|
-
*/
|
|
16847
|
-
var AttachmentMediaTypeSchema = _enum([
|
|
16848
|
-
"image",
|
|
16849
|
-
"video",
|
|
16850
|
-
"gif",
|
|
16851
|
-
"audio",
|
|
16852
|
-
"icon"
|
|
16853
|
-
]);
|
|
16854
|
-
/**
|
|
16855
|
-
* A single attachment. Exactly one of `url` (remote source, most adapters
|
|
16856
|
-
* prefer this) or `bytes` (inline source; required for Pushover-style
|
|
16857
|
-
* bytes-only kinds) MUST be present — the degrade engine expresses a
|
|
16858
|
-
* url→bytes fetch as a `needsFetch` directive the adapter executes.
|
|
16859
|
-
*/
|
|
16860
|
-
var AttachmentSchema = object({
|
|
16861
|
-
mediaType: AttachmentMediaTypeSchema,
|
|
16862
|
-
url: string().optional(),
|
|
16863
|
-
bytes: _instanceof(Uint8Array).optional(),
|
|
16864
|
-
mime: string().optional(),
|
|
16865
|
-
name: string().optional()
|
|
16866
|
-
}).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
|
|
16867
|
-
var NotificationFormatSchema = _enum([
|
|
16868
|
-
"text",
|
|
16869
|
-
"markdown",
|
|
16870
|
-
"html"
|
|
17634
|
+
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
17635
|
+
* its own process.
|
|
17636
|
+
*
|
|
17637
|
+
* Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
|
|
17638
|
+
*
|
|
17639
|
+
* The first use is **owning devices without being a device provider**: a block
|
|
17640
|
+
* declares devices under a system or custom integration and drives their state,
|
|
17641
|
+
* with the same `ctx` an addon gets. Automations come later; nothing here
|
|
17642
|
+
* models a trigger.
|
|
17643
|
+
*
|
|
17644
|
+
* **Stated plainly, because it does not change by being true:** a block has an
|
|
17645
|
+
* addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
|
|
17646
|
+
* with no review step. What makes that survivable is not a sandbox, it is
|
|
17647
|
+
* PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
|
|
17648
|
+
* so a block that throws or never returns is marked `failed` and visible
|
|
17649
|
+
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
17650
|
+
* must stay so.
|
|
17651
|
+
*/
|
|
17652
|
+
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
17653
|
+
* agent is the reason placement is not fixed to the hub. */
|
|
17654
|
+
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
17655
|
+
/** What a block's process is doing. Mirrors the addon runner's own lifecycle so
|
|
17656
|
+
* a failing block reads the same way a failing addon does. */
|
|
17657
|
+
var CoreBlockStatusSchema = _enum([
|
|
17658
|
+
"stopped",
|
|
17659
|
+
"starting",
|
|
17660
|
+
"running",
|
|
17661
|
+
"failed"
|
|
16871
17662
|
]);
|
|
16872
|
-
/**
|
|
16873
|
-
var
|
|
16874
|
-
|
|
16875
|
-
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
* NOT a fixed severity enum — each kind declares its own `caps.levels` and
|
|
16882
|
-
* the adapter maps this ordinal onto its native level. `level?` is an
|
|
16883
|
-
* optional kind-native level id (`emergency`, `silent`, …) that overrides
|
|
16884
|
-
* `priority` for that one target.
|
|
16885
|
-
*/
|
|
16886
|
-
var NotificationSchema = object({
|
|
16887
|
-
body: string(),
|
|
16888
|
-
title: string().optional(),
|
|
16889
|
-
format: NotificationFormatSchema.default("text"),
|
|
16890
|
-
priority: number().int().min(1).max(5).default(3),
|
|
16891
|
-
level: string().optional(),
|
|
16892
|
-
attachments: array(AttachmentSchema).optional(),
|
|
16893
|
-
clickUrl: string().optional(),
|
|
16894
|
-
actions: array(NotificationActionSchema).optional(),
|
|
16895
|
-
sound: string().optional(),
|
|
16896
|
-
ttl: number().optional(),
|
|
16897
|
-
tag: string().optional(),
|
|
16898
|
-
deviceId: number().optional(),
|
|
16899
|
-
eventId: string().optional(),
|
|
16900
|
-
metadata: record(string(), unknown()).optional()
|
|
16901
|
-
});
|
|
16902
|
-
/** One declared native severity/priority level for a kind. */
|
|
16903
|
-
var TargetKindLevelSchema = object({
|
|
16904
|
-
id: string(),
|
|
16905
|
-
label: string(),
|
|
16906
|
-
/** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
|
|
16907
|
-
ordinal: number().int().min(1).max(5).nullable(),
|
|
16908
|
-
flags: object({
|
|
16909
|
-
critical: boolean().optional(),
|
|
16910
|
-
silent: boolean().optional(),
|
|
16911
|
-
noPush: boolean().optional()
|
|
16912
|
-
}).optional(),
|
|
16913
|
-
/** e.g. Pushover `emergency` requires `retry` / `expire`. */
|
|
16914
|
-
requires: array(string()).optional(),
|
|
16915
|
-
description: string().optional()
|
|
16916
|
-
});
|
|
16917
|
-
/** The full capability block consulted before dispatch. */
|
|
16918
|
-
var TargetKindCapsSchema = object({
|
|
16919
|
-
attachments: object({
|
|
16920
|
-
mediaTypes: array(AttachmentMediaTypeSchema),
|
|
16921
|
-
mode: _enum([
|
|
16922
|
-
"url",
|
|
16923
|
-
"bytes",
|
|
16924
|
-
"both"
|
|
16925
|
-
]),
|
|
16926
|
-
max: number().int().nonnegative(),
|
|
16927
|
-
maxBytes: number().int().positive().optional()
|
|
16928
|
-
}),
|
|
16929
|
-
/** Max action buttons (0 = none). */
|
|
16930
|
-
actions: number().int().nonnegative(),
|
|
16931
|
-
levels: array(TargetKindLevelSchema),
|
|
16932
|
-
format: array(NotificationFormatSchema),
|
|
16933
|
-
clickUrl: boolean(),
|
|
16934
|
-
sound: boolean(),
|
|
16935
|
-
ttl: boolean(),
|
|
16936
|
-
bodyMaxLen: number().int().positive()
|
|
16937
|
-
});
|
|
16938
|
-
/**
|
|
16939
|
-
* `configSchema` is a `ConfigUISchema` tree passed through to the admin
|
|
16940
|
-
* FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
|
|
16941
|
-
* `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
|
|
16942
|
-
* the union is large and not meant for runtime validation here; the exported
|
|
16943
|
-
* `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
|
|
16944
|
-
*/
|
|
16945
|
-
var ConfigSchemaPassthrough = unknown();
|
|
16946
|
-
var TargetKindSchema = object({
|
|
16947
|
-
kind: string(),
|
|
16948
|
-
label: string(),
|
|
16949
|
-
icon: string(),
|
|
16950
|
-
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
16951
|
-
addonId: string(),
|
|
16952
|
-
/**
|
|
16953
|
-
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
16954
|
-
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
16955
|
-
* when the addon bundles no icon for that kind — the client then falls back
|
|
16956
|
-
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
16957
|
-
*
|
|
16958
|
-
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
16959
|
-
* client, and a native client joins it onto its own hub base.
|
|
16960
|
-
*
|
|
16961
|
-
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
16962
|
-
* field that survived only because the runtime cap-router forwards provider
|
|
16963
|
-
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
16964
|
-
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
16965
|
-
* broken silently the moment output validation was tightened anywhere.
|
|
16966
|
-
*/
|
|
16967
|
-
iconUrl: string().optional(),
|
|
17663
|
+
/** Client-authored fields. */
|
|
17664
|
+
var CoreBlockInputSchema = object({
|
|
17665
|
+
name: string().min(1).max(120),
|
|
17666
|
+
/** TypeScript source. Compiled server-side before it is ever stored — a
|
|
17667
|
+
* block that does not compile is a fork failure the operator would meet
|
|
17668
|
+
* minutes later, in a log, instead of in the editor. */
|
|
17669
|
+
code: string().max(2e5),
|
|
17670
|
+
enabled: boolean(),
|
|
17671
|
+
placement: CoreBlockPlacementSchema,
|
|
16968
17672
|
/**
|
|
16969
|
-
*
|
|
16970
|
-
*
|
|
16971
|
-
* The server knows this and therefore says it, because the client cannot
|
|
16972
|
-
* safely guess: a React-Native client renders SVG and raster through two
|
|
16973
|
-
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
16974
|
-
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
16975
|
-
* placeholder glyph for every vector icon while the web build looked fine.
|
|
16976
|
-
*
|
|
16977
|
-
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
16978
|
-
* not been updated — a client that cannot determine the type should prefer
|
|
16979
|
-
* its raster path, which is the safe default for an unknown image.
|
|
17673
|
+
* Integration the block's devices hang from. Absent = the system integration
|
|
17674
|
+
* blocks share. A block may declare its own instead.
|
|
16980
17675
|
*/
|
|
16981
|
-
|
|
16982
|
-
configSchema: ConfigSchemaPassthrough,
|
|
16983
|
-
supportsDiscovery: boolean(),
|
|
16984
|
-
caps: TargetKindCapsSchema
|
|
17676
|
+
integrationId: string().optional()
|
|
16985
17677
|
});
|
|
16986
|
-
/**
|
|
16987
|
-
|
|
16988
|
-
* (return a presence marker only) when serving `listTargets` — never
|
|
16989
|
-
* round-trip a stored secret to the UI.
|
|
16990
|
-
*/
|
|
16991
|
-
var TargetSchema = object({
|
|
17678
|
+
/** A stored block. */
|
|
17679
|
+
var CoreBlockSchema = CoreBlockInputSchema.extend({
|
|
16992
17680
|
id: string(),
|
|
16993
|
-
|
|
16994
|
-
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
var RenderedAsSchema = object({
|
|
17007
|
-
level: string(),
|
|
17008
|
-
format: NotificationFormatSchema,
|
|
17009
|
-
attachmentsSent: number().int().nonnegative(),
|
|
17010
|
-
actionsSent: number().int().nonnegative(),
|
|
17011
|
-
truncated: boolean(),
|
|
17012
|
-
dropped: array(string())
|
|
17681
|
+
createdAt: number(),
|
|
17682
|
+
updatedAt: number(),
|
|
17683
|
+
/** Server-stamped author. */
|
|
17684
|
+
createdBy: string(),
|
|
17685
|
+
status: CoreBlockStatusSchema,
|
|
17686
|
+
/**
|
|
17687
|
+
* Why the block is not running, when it is not. The operator's ONLY window
|
|
17688
|
+
* into a block that failed at load — a block that is silently absent is the
|
|
17689
|
+
* failure mode this whole feature has to avoid.
|
|
17690
|
+
*/
|
|
17691
|
+
lastError: string().optional(),
|
|
17692
|
+
/** Ms epoch of the last state change. */
|
|
17693
|
+
lastChangedAt: number()
|
|
17013
17694
|
});
|
|
17014
|
-
|
|
17015
|
-
|
|
17695
|
+
/** What a compile attempt produced. */
|
|
17696
|
+
var CoreBlockCompileResultSchema = object({
|
|
17697
|
+
ok: boolean(),
|
|
17698
|
+
/** Present when `ok` is false — the first error, in the author's words. */
|
|
17016
17699
|
error: string().optional(),
|
|
17017
|
-
|
|
17700
|
+
line: number().optional(),
|
|
17701
|
+
column: number().optional()
|
|
17018
17702
|
});
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17022
|
-
|
|
17023
|
-
|
|
17024
|
-
|
|
17025
|
-
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
|
|
17029
|
-
|
|
17030
|
-
}),
|
|
17031
|
-
|
|
17703
|
+
method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }), method(object({ blockId: string() }), object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }), method(object({ block: CoreBlockInputSchema }), object({ block: CoreBlockSchema }), {
|
|
17704
|
+
kind: "mutation",
|
|
17705
|
+
auth: "admin",
|
|
17706
|
+
caller: "required"
|
|
17707
|
+
}), method(object({
|
|
17708
|
+
blockId: string(),
|
|
17709
|
+
block: CoreBlockInputSchema.partial()
|
|
17710
|
+
}), object({ block: CoreBlockSchema }), {
|
|
17711
|
+
kind: "mutation",
|
|
17712
|
+
auth: "admin",
|
|
17713
|
+
caller: "required"
|
|
17714
|
+
}), method(object({ blockId: string() }), object({ success: literal(true) }), {
|
|
17715
|
+
kind: "mutation",
|
|
17716
|
+
auth: "admin"
|
|
17717
|
+
}), method(object({
|
|
17718
|
+
blockId: string(),
|
|
17032
17719
|
enabled: boolean()
|
|
17033
|
-
}),
|
|
17720
|
+
}), object({ block: CoreBlockSchema }), {
|
|
17721
|
+
kind: "mutation",
|
|
17722
|
+
auth: "admin"
|
|
17723
|
+
}), method(object({ code: string() }), CoreBlockCompileResultSchema, {
|
|
17724
|
+
kind: "mutation",
|
|
17725
|
+
auth: "admin"
|
|
17726
|
+
}), method(object({}), object({ libs: array(object({
|
|
17727
|
+
filePath: string(),
|
|
17728
|
+
content: string()
|
|
17729
|
+
})) }), { auth: "admin" });
|
|
17034
17730
|
/**
|
|
17035
17731
|
* Zod schemas for persisted record types.
|
|
17036
17732
|
*
|
|
@@ -17278,6 +17974,11 @@ var EventKindDescriptorSchema = object({
|
|
|
17278
17974
|
deviceId: number()
|
|
17279
17975
|
})
|
|
17280
17976
|
});
|
|
17977
|
+
/** One camera's event vocabulary, as returned by `listEventKindsBatch`. */
|
|
17978
|
+
var EventKindsForDeviceSchema = object({
|
|
17979
|
+
deviceId: number(),
|
|
17980
|
+
kinds: array(EventKindDescriptorSchema).readonly()
|
|
17981
|
+
});
|
|
17281
17982
|
var SensorEventSchema = object({
|
|
17282
17983
|
id: string(),
|
|
17283
17984
|
/** The CAMERA the event is attributed to (a sensor linked to N cameras
|
|
@@ -17534,6 +18235,19 @@ var MediaFileSchema = object({
|
|
|
17534
18235
|
sizeBytes: number(),
|
|
17535
18236
|
timestamp: number()
|
|
17536
18237
|
});
|
|
18238
|
+
/**
|
|
18239
|
+
* One media row WITHOUT its bytes.
|
|
18240
|
+
*
|
|
18241
|
+
* A track's media is 5-8 MB of base64 (measured: 7.67 MB across 23 files for a
|
|
18242
|
+
* 140 s track), and a client that renders tiles from the media data plane needs
|
|
18243
|
+
* to know only WHAT EXISTS — the bytes then arrive per tile, lazily, over HTTP
|
|
18244
|
+
* with an immutable cache, instead of all at once inside a tRPC response that
|
|
18245
|
+
* blocks the whole view.
|
|
18246
|
+
*
|
|
18247
|
+
* `sizeBytes` is carried because it is what lets a client decide between the
|
|
18248
|
+
* stored blob and a `?variant=thumb` rendering without fetching either.
|
|
18249
|
+
*/
|
|
18250
|
+
var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
|
|
17537
18251
|
var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
|
|
17538
18252
|
var MAX_EVENT_QUERY_LIMIT = 5e3;
|
|
17539
18253
|
var DeviceEventQueryInput = object({
|
|
@@ -17683,7 +18397,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17683
18397
|
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
|
|
17684
18398
|
kind: "mutation",
|
|
17685
18399
|
auth: "admin"
|
|
17686
|
-
}), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
|
|
18400
|
+
}), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({ deviceIds: array(number()).min(1).max(200) }), array(EventKindsForDeviceSchema).readonly()), method(object({
|
|
17687
18401
|
deviceId: number(),
|
|
17688
18402
|
since: number().optional(),
|
|
17689
18403
|
until: number().optional(),
|
|
@@ -17757,7 +18471,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17757
18471
|
}), array(MediaFileSchema).readonly()), method(object({
|
|
17758
18472
|
trackId: string(),
|
|
17759
18473
|
kinds: array(MediaFileKindEnum).optional()
|
|
17760
|
-
}), array(MediaFileSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
|
|
18474
|
+
}), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
|
|
17761
18475
|
deviceId: number(),
|
|
17762
18476
|
timestamp: number(),
|
|
17763
18477
|
frameWidth: number(),
|
|
@@ -18387,6 +19101,28 @@ var ServerUpdateStateSchema = _enum([
|
|
|
18387
19101
|
"pending-restart",
|
|
18388
19102
|
"awaiting-confirmation"
|
|
18389
19103
|
]);
|
|
19104
|
+
var ImageContractSchema = object({
|
|
19105
|
+
state: _enum([
|
|
19106
|
+
"in-sync",
|
|
19107
|
+
"behind-patch",
|
|
19108
|
+
"behind-series",
|
|
19109
|
+
"ahead",
|
|
19110
|
+
"unknown"
|
|
19111
|
+
]),
|
|
19112
|
+
/** The baked seed closure version — the image/app-bundle fingerprint. */
|
|
19113
|
+
seedVersion: string().nullable(),
|
|
19114
|
+
/** Best-known version the deployment contract delivers today. */
|
|
19115
|
+
contractVersion: string().nullable(),
|
|
19116
|
+
/**
|
|
19117
|
+
* Where `contractVersion` came from: a real registry check (`registry`), or
|
|
19118
|
+
* the node's own running version (`running` — a node can never run code
|
|
19119
|
+
* newer than the newest release, so `seed < running` proves image staleness
|
|
19120
|
+
* even before any registry check has run).
|
|
19121
|
+
*/
|
|
19122
|
+
contractSource: _enum(["registry", "running"]).nullable(),
|
|
19123
|
+
/** One operator-grade sentence: this node runs image X; the contract says Y. */
|
|
19124
|
+
message: string()
|
|
19125
|
+
});
|
|
18390
19126
|
var ServerRollbackInfoSchema = object({
|
|
18391
19127
|
/** The version that failed (or was manually rolled back). */
|
|
18392
19128
|
fromVersion: string(),
|
|
@@ -18423,7 +19159,12 @@ var ServerPackageStatusSchema = object({
|
|
|
18423
19159
|
* versions are being IGNORED. Surfaced as a warning in the UI.
|
|
18424
19160
|
*/
|
|
18425
19161
|
stateFileCorrupt: boolean(),
|
|
18426
|
-
lastCheckedAtMs: number().nullable()
|
|
19162
|
+
lastCheckedAtMs: number().nullable(),
|
|
19163
|
+
/**
|
|
19164
|
+
* Seed-vs-contract verdict (see {@link ImageContractSchema}). Optional for
|
|
19165
|
+
* version skew: an older provider's payload simply omits it.
|
|
19166
|
+
*/
|
|
19167
|
+
imageContract: ImageContractSchema.optional()
|
|
18427
19168
|
});
|
|
18428
19169
|
var ServerUpdateCheckResultSchema = object({
|
|
18429
19170
|
packageName: string(),
|
|
@@ -18458,101 +19199,6 @@ version: string().optional() }), ServerUpdateActionResultSchema, {
|
|
|
18458
19199
|
auth: "admin"
|
|
18459
19200
|
});
|
|
18460
19201
|
/**
|
|
18461
|
-
* Query filter for settings-store collections.
|
|
18462
|
-
*/
|
|
18463
|
-
var QueryFilterSchema = object({
|
|
18464
|
-
where: record(string(), unknown()).optional(),
|
|
18465
|
-
whereIn: record(string(), array(unknown())).optional(),
|
|
18466
|
-
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
18467
|
-
orderBy: object({
|
|
18468
|
-
field: string(),
|
|
18469
|
-
direction: _enum(["asc", "desc"])
|
|
18470
|
-
}).optional(),
|
|
18471
|
-
limit: number().optional(),
|
|
18472
|
-
offset: number().optional()
|
|
18473
|
-
});
|
|
18474
|
-
/** A single stored record: `{ id, data }`. */
|
|
18475
|
-
var SettingsRecordSchema = object({
|
|
18476
|
-
id: string(),
|
|
18477
|
-
data: record(string(), unknown())
|
|
18478
|
-
});
|
|
18479
|
-
/**
|
|
18480
|
-
* Column declaration for a structured (SQL-backed) collection.
|
|
18481
|
-
*
|
|
18482
|
-
* Logical types — the backend translates each to the matching SQLite
|
|
18483
|
-
* storage class and handles per-type marshaling:
|
|
18484
|
-
* - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
|
|
18485
|
-
* - `JSON` — TEXT under the hood; serialised on write, parsed on read
|
|
18486
|
-
* - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
|
|
18487
|
-
*/
|
|
18488
|
-
var CollectionColumnSchema = object({
|
|
18489
|
-
name: string(),
|
|
18490
|
-
type: _enum([
|
|
18491
|
-
"TEXT",
|
|
18492
|
-
"INTEGER",
|
|
18493
|
-
"REAL",
|
|
18494
|
-
"JSON",
|
|
18495
|
-
"BOOLEAN"
|
|
18496
|
-
]),
|
|
18497
|
-
primaryKey: boolean().optional(),
|
|
18498
|
-
notNull: boolean().optional(),
|
|
18499
|
-
unique: boolean().optional()
|
|
18500
|
-
});
|
|
18501
|
-
var CollectionIndexSchema = object({
|
|
18502
|
-
name: string(),
|
|
18503
|
-
columns: array(string()).readonly(),
|
|
18504
|
-
unique: boolean().optional()
|
|
18505
|
-
});
|
|
18506
|
-
method(object({
|
|
18507
|
-
namespace: string().optional(),
|
|
18508
|
-
collection: string(),
|
|
18509
|
-
key: string()
|
|
18510
|
-
}), unknown()), method(object({
|
|
18511
|
-
namespace: string().optional(),
|
|
18512
|
-
collection: string(),
|
|
18513
|
-
key: string(),
|
|
18514
|
-
value: unknown()
|
|
18515
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
18516
|
-
namespace: string().optional(),
|
|
18517
|
-
collection: string(),
|
|
18518
|
-
filter: QueryFilterSchema.optional()
|
|
18519
|
-
}), array(SettingsRecordSchema).readonly()), method(object({
|
|
18520
|
-
namespace: string().optional(),
|
|
18521
|
-
collection: string(),
|
|
18522
|
-
record: SettingsRecordSchema
|
|
18523
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
18524
|
-
namespace: string().optional(),
|
|
18525
|
-
collection: string(),
|
|
18526
|
-
id: string(),
|
|
18527
|
-
data: record(string(), unknown())
|
|
18528
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
18529
|
-
namespace: string().optional(),
|
|
18530
|
-
collection: string(),
|
|
18531
|
-
key: string()
|
|
18532
|
-
}), _void(), { kind: "mutation" }), method(object({
|
|
18533
|
-
namespace: string().optional(),
|
|
18534
|
-
collection: string(),
|
|
18535
|
-
filter: QueryFilterSchema.optional()
|
|
18536
|
-
}), number()), method(object({
|
|
18537
|
-
namespace: string().optional(),
|
|
18538
|
-
collection: string(),
|
|
18539
|
-
field: string(),
|
|
18540
|
-
bucketSize: number().int().positive(),
|
|
18541
|
-
origin: number().int(),
|
|
18542
|
-
filter: QueryFilterSchema.optional()
|
|
18543
|
-
}), array(object({
|
|
18544
|
-
bucket: number().int(),
|
|
18545
|
-
count: number().int()
|
|
18546
|
-
})).readonly()), method(object({
|
|
18547
|
-
namespace: string().optional(),
|
|
18548
|
-
collection: string()
|
|
18549
|
-
}), boolean()), method(object({
|
|
18550
|
-
namespace: string().optional(),
|
|
18551
|
-
collection: string(),
|
|
18552
|
-
columns: array(CollectionColumnSchema).readonly(),
|
|
18553
|
-
indexes: array(CollectionIndexSchema).readonly().optional()
|
|
18554
|
-
}), _void(), { kind: "mutation" });
|
|
18555
|
-
/**
|
|
18556
19202
|
* `smtp-provider` — pluggable email delivery surface.
|
|
18557
19203
|
*
|
|
18558
19204
|
* Collection cap: a deployment may install multiple SMTP relays (e.g.
|
|
@@ -22415,6 +23061,54 @@ Object.freeze({
|
|
|
22415
23061
|
addonId: null,
|
|
22416
23062
|
access: "create"
|
|
22417
23063
|
},
|
|
23064
|
+
"coreBlocks.compile": {
|
|
23065
|
+
capName: "core-blocks",
|
|
23066
|
+
capScope: "system",
|
|
23067
|
+
addonId: null,
|
|
23068
|
+
access: "create"
|
|
23069
|
+
},
|
|
23070
|
+
"coreBlocks.create": {
|
|
23071
|
+
capName: "core-blocks",
|
|
23072
|
+
capScope: "system",
|
|
23073
|
+
addonId: null,
|
|
23074
|
+
access: "create"
|
|
23075
|
+
},
|
|
23076
|
+
"coreBlocks.delete": {
|
|
23077
|
+
capName: "core-blocks",
|
|
23078
|
+
capScope: "system",
|
|
23079
|
+
addonId: null,
|
|
23080
|
+
access: "delete"
|
|
23081
|
+
},
|
|
23082
|
+
"coreBlocks.get": {
|
|
23083
|
+
capName: "core-blocks",
|
|
23084
|
+
capScope: "system",
|
|
23085
|
+
addonId: null,
|
|
23086
|
+
access: "view"
|
|
23087
|
+
},
|
|
23088
|
+
"coreBlocks.getTypeDefs": {
|
|
23089
|
+
capName: "core-blocks",
|
|
23090
|
+
capScope: "system",
|
|
23091
|
+
addonId: null,
|
|
23092
|
+
access: "view"
|
|
23093
|
+
},
|
|
23094
|
+
"coreBlocks.list": {
|
|
23095
|
+
capName: "core-blocks",
|
|
23096
|
+
capScope: "system",
|
|
23097
|
+
addonId: null,
|
|
23098
|
+
access: "view"
|
|
23099
|
+
},
|
|
23100
|
+
"coreBlocks.setEnabled": {
|
|
23101
|
+
capName: "core-blocks",
|
|
23102
|
+
capScope: "system",
|
|
23103
|
+
addonId: null,
|
|
23104
|
+
access: "create"
|
|
23105
|
+
},
|
|
23106
|
+
"coreBlocks.update": {
|
|
23107
|
+
capName: "core-blocks",
|
|
23108
|
+
capScope: "system",
|
|
23109
|
+
addonId: null,
|
|
23110
|
+
access: "create"
|
|
23111
|
+
},
|
|
22418
23112
|
"cover.close": {
|
|
22419
23113
|
capName: "cover",
|
|
22420
23114
|
capScope: "device",
|
|
@@ -22451,6 +23145,84 @@ Object.freeze({
|
|
|
22451
23145
|
addonId: null,
|
|
22452
23146
|
access: "view"
|
|
22453
23147
|
},
|
|
23148
|
+
"dataStoreProvider.count": {
|
|
23149
|
+
capName: "data-store-provider",
|
|
23150
|
+
capScope: "system",
|
|
23151
|
+
addonId: null,
|
|
23152
|
+
access: "view"
|
|
23153
|
+
},
|
|
23154
|
+
"dataStoreProvider.declareCollection": {
|
|
23155
|
+
capName: "data-store-provider",
|
|
23156
|
+
capScope: "system",
|
|
23157
|
+
addonId: null,
|
|
23158
|
+
access: "create"
|
|
23159
|
+
},
|
|
23160
|
+
"dataStoreProvider.delete": {
|
|
23161
|
+
capName: "data-store-provider",
|
|
23162
|
+
capScope: "system",
|
|
23163
|
+
addonId: null,
|
|
23164
|
+
access: "delete"
|
|
23165
|
+
},
|
|
23166
|
+
"dataStoreProvider.deleteWhere": {
|
|
23167
|
+
capName: "data-store-provider",
|
|
23168
|
+
capScope: "system",
|
|
23169
|
+
addonId: null,
|
|
23170
|
+
access: "delete"
|
|
23171
|
+
},
|
|
23172
|
+
"dataStoreProvider.get": {
|
|
23173
|
+
capName: "data-store-provider",
|
|
23174
|
+
capScope: "system",
|
|
23175
|
+
addonId: null,
|
|
23176
|
+
access: "view"
|
|
23177
|
+
},
|
|
23178
|
+
"dataStoreProvider.getEngineInfo": {
|
|
23179
|
+
capName: "data-store-provider",
|
|
23180
|
+
capScope: "system",
|
|
23181
|
+
addonId: null,
|
|
23182
|
+
access: "view"
|
|
23183
|
+
},
|
|
23184
|
+
"dataStoreProvider.histogram": {
|
|
23185
|
+
capName: "data-store-provider",
|
|
23186
|
+
capScope: "system",
|
|
23187
|
+
addonId: null,
|
|
23188
|
+
access: "view"
|
|
23189
|
+
},
|
|
23190
|
+
"dataStoreProvider.insert": {
|
|
23191
|
+
capName: "data-store-provider",
|
|
23192
|
+
capScope: "system",
|
|
23193
|
+
addonId: null,
|
|
23194
|
+
access: "create"
|
|
23195
|
+
},
|
|
23196
|
+
"dataStoreProvider.isEmpty": {
|
|
23197
|
+
capName: "data-store-provider",
|
|
23198
|
+
capScope: "system",
|
|
23199
|
+
addonId: null,
|
|
23200
|
+
access: "view"
|
|
23201
|
+
},
|
|
23202
|
+
"dataStoreProvider.query": {
|
|
23203
|
+
capName: "data-store-provider",
|
|
23204
|
+
capScope: "system",
|
|
23205
|
+
addonId: null,
|
|
23206
|
+
access: "view"
|
|
23207
|
+
},
|
|
23208
|
+
"dataStoreProvider.set": {
|
|
23209
|
+
capName: "data-store-provider",
|
|
23210
|
+
capScope: "system",
|
|
23211
|
+
addonId: null,
|
|
23212
|
+
access: "create"
|
|
23213
|
+
},
|
|
23214
|
+
"dataStoreProvider.update": {
|
|
23215
|
+
capName: "data-store-provider",
|
|
23216
|
+
capScope: "system",
|
|
23217
|
+
addonId: null,
|
|
23218
|
+
access: "create"
|
|
23219
|
+
},
|
|
23220
|
+
"dataStoreProvider.updateWhere": {
|
|
23221
|
+
capName: "data-store-provider",
|
|
23222
|
+
capScope: "system",
|
|
23223
|
+
addonId: null,
|
|
23224
|
+
access: "create"
|
|
23225
|
+
},
|
|
22454
23226
|
"dayNight.getOptions": {
|
|
22455
23227
|
capName: "day-night",
|
|
22456
23228
|
capScope: "device",
|
|
@@ -24245,18 +25017,36 @@ Object.freeze({
|
|
|
24245
25017
|
addonId: null,
|
|
24246
25018
|
access: "create"
|
|
24247
25019
|
},
|
|
25020
|
+
"notificationRules.cancelSnooze": {
|
|
25021
|
+
capName: "notification-rules",
|
|
25022
|
+
capScope: "system",
|
|
25023
|
+
addonId: null,
|
|
25024
|
+
access: "create"
|
|
25025
|
+
},
|
|
24248
25026
|
"notificationRules.createRule": {
|
|
24249
25027
|
capName: "notification-rules",
|
|
24250
25028
|
capScope: "system",
|
|
24251
25029
|
addonId: null,
|
|
24252
25030
|
access: "create"
|
|
24253
25031
|
},
|
|
25032
|
+
"notificationRules.createSnooze": {
|
|
25033
|
+
capName: "notification-rules",
|
|
25034
|
+
capScope: "system",
|
|
25035
|
+
addonId: null,
|
|
25036
|
+
access: "create"
|
|
25037
|
+
},
|
|
24254
25038
|
"notificationRules.deleteRule": {
|
|
24255
25039
|
capName: "notification-rules",
|
|
24256
25040
|
capScope: "system",
|
|
24257
25041
|
addonId: null,
|
|
24258
25042
|
access: "delete"
|
|
24259
25043
|
},
|
|
25044
|
+
"notificationRules.getAlarmConfig": {
|
|
25045
|
+
capName: "notification-rules",
|
|
25046
|
+
capScope: "system",
|
|
25047
|
+
addonId: null,
|
|
25048
|
+
access: "view"
|
|
25049
|
+
},
|
|
24260
25050
|
"notificationRules.getConditionCatalog": {
|
|
24261
25051
|
capName: "notification-rules",
|
|
24262
25052
|
capScope: "system",
|
|
@@ -24281,6 +25071,18 @@ Object.freeze({
|
|
|
24281
25071
|
addonId: null,
|
|
24282
25072
|
access: "view"
|
|
24283
25073
|
},
|
|
25074
|
+
"notificationRules.listSnoozes": {
|
|
25075
|
+
capName: "notification-rules",
|
|
25076
|
+
capScope: "system",
|
|
25077
|
+
addonId: null,
|
|
25078
|
+
access: "view"
|
|
25079
|
+
},
|
|
25080
|
+
"notificationRules.setAlarmConfig": {
|
|
25081
|
+
capName: "notification-rules",
|
|
25082
|
+
capScope: "system",
|
|
25083
|
+
addonId: null,
|
|
25084
|
+
access: "create"
|
|
25085
|
+
},
|
|
24284
25086
|
"notificationRules.setRuleEnabled": {
|
|
24285
25087
|
capName: "notification-rules",
|
|
24286
25088
|
capScope: "system",
|
|
@@ -24485,6 +25287,12 @@ Object.freeze({
|
|
|
24485
25287
|
addonId: null,
|
|
24486
25288
|
access: "view"
|
|
24487
25289
|
},
|
|
25290
|
+
"pipelineAnalytics.listEventKindsBatch": {
|
|
25291
|
+
capName: "pipeline-analytics",
|
|
25292
|
+
capScope: "device",
|
|
25293
|
+
addonId: null,
|
|
25294
|
+
access: "view"
|
|
25295
|
+
},
|
|
24488
25296
|
"pipelineAnalytics.listOpsLog": {
|
|
24489
25297
|
capName: "pipeline-analytics",
|
|
24490
25298
|
capScope: "device",
|
|
@@ -24497,6 +25305,12 @@ Object.freeze({
|
|
|
24497
25305
|
addonId: null,
|
|
24498
25306
|
access: "view"
|
|
24499
25307
|
},
|
|
25308
|
+
"pipelineAnalytics.listTrackMedia": {
|
|
25309
|
+
capName: "pipeline-analytics",
|
|
25310
|
+
capScope: "device",
|
|
25311
|
+
addonId: null,
|
|
25312
|
+
access: "view"
|
|
25313
|
+
},
|
|
24500
25314
|
"pipelineAnalytics.listTracks": {
|
|
24501
25315
|
capName: "pipeline-analytics",
|
|
24502
25316
|
capScope: "device",
|
|
@@ -25529,6 +26343,12 @@ Object.freeze({
|
|
|
25529
26343
|
addonId: null,
|
|
25530
26344
|
access: "delete"
|
|
25531
26345
|
},
|
|
26346
|
+
"settingsStore.deleteWhere": {
|
|
26347
|
+
capName: "settings-store",
|
|
26348
|
+
capScope: "system",
|
|
26349
|
+
addonId: null,
|
|
26350
|
+
access: "delete"
|
|
26351
|
+
},
|
|
25532
26352
|
"settingsStore.get": {
|
|
25533
26353
|
capName: "settings-store",
|
|
25534
26354
|
capScope: "system",
|
|
@@ -25571,6 +26391,12 @@ Object.freeze({
|
|
|
25571
26391
|
addonId: null,
|
|
25572
26392
|
access: "create"
|
|
25573
26393
|
},
|
|
26394
|
+
"settingsStore.updateWhere": {
|
|
26395
|
+
capName: "settings-store",
|
|
26396
|
+
capScope: "system",
|
|
26397
|
+
addonId: null,
|
|
26398
|
+
access: "create"
|
|
26399
|
+
},
|
|
25574
26400
|
"smtpProvider.getStatus": {
|
|
25575
26401
|
capName: "smtp-provider",
|
|
25576
26402
|
capScope: "system",
|
|
@@ -37338,10 +38164,12 @@ async function prepareStream(request, sessions, bctx) {
|
|
|
37338
38164
|
profile: import_src.ProtectionProfileAes128CmHmacSha1_80
|
|
37339
38165
|
});
|
|
37340
38166
|
let videoOutSrtp;
|
|
38167
|
+
let videoOutSrtcp;
|
|
37341
38168
|
let audioOutSrtp;
|
|
37342
38169
|
let audioOutSrtcp;
|
|
37343
38170
|
try {
|
|
37344
38171
|
videoOutSrtp = makeOutSrtp(request.video.srtp_key, request.video.srtp_salt);
|
|
38172
|
+
videoOutSrtcp = makeOutSrtcp(request.video.srtp_key, request.video.srtp_salt);
|
|
37345
38173
|
audioOutSrtp = makeOutSrtp(request.audio.srtp_key, request.audio.srtp_salt);
|
|
37346
38174
|
audioOutSrtcp = makeOutSrtcp(request.audio.srtp_key, request.audio.srtp_salt);
|
|
37347
38175
|
} catch (err) {
|
|
@@ -37375,6 +38203,12 @@ async function prepareStream(request, sessions, bctx) {
|
|
|
37375
38203
|
videoUdp,
|
|
37376
38204
|
videoLoopUdp,
|
|
37377
38205
|
videoOutSrtp,
|
|
38206
|
+
videoOutSrtcp,
|
|
38207
|
+
videoOutPacketCount: 0,
|
|
38208
|
+
videoOutOctetCount: 0,
|
|
38209
|
+
videoOutLastRtpTimestamp: 0,
|
|
38210
|
+
videoOutLastRtcpAt: 0,
|
|
38211
|
+
videoRtcpIntervalMs: 5e3,
|
|
37378
38212
|
videoSendGate: null,
|
|
37379
38213
|
videoSsrc,
|
|
37380
38214
|
hapAudioPort: request.audio.port,
|
|
@@ -37419,9 +38253,7 @@ async function prepareStream(request, sessions, bctx) {
|
|
|
37419
38253
|
audioLoopUdp.on("message", (rtpPacket) => {
|
|
37420
38254
|
audioPacketsForwarded += 1;
|
|
37421
38255
|
if (audioPacketsForwarded === 1 || audioPacketsForwarded % 100 === 0) tagLog.info("export-hap: audio loopback packets forwarded", { meta: { count: audioPacketsForwarded } });
|
|
37422
|
-
if (audioPacketsForwarded === 1) process.stderr.write(`[AUDIO-LISTENER] first packet — about to call forwardEncryptedRtp len=${rtpPacket.length} typeofFn=${typeof forwardEncryptedRtp}\n`);
|
|
37423
38256
|
forwardEncryptedRtp(session, rtpPacket, "audio", tagLog);
|
|
37424
|
-
if (audioPacketsForwarded === 1) process.stderr.write("[AUDIO-LISTENER] forwardEncryptedRtp returned for first packet\n");
|
|
37425
38257
|
});
|
|
37426
38258
|
audioUdp.on("message", (packet, rinfo) => {
|
|
37427
38259
|
handleIncomingAudioRtp(session, packet, rinfo.address, bctx).catch((err) => {
|
|
@@ -37543,32 +38375,48 @@ function ntpTime() {
|
|
|
37543
38375
|
return secs << 32n | frac;
|
|
37544
38376
|
}
|
|
37545
38377
|
/**
|
|
37546
|
-
* Emit one RTCP Sender Report for
|
|
37547
|
-
*
|
|
37548
|
-
*
|
|
37549
|
-
*
|
|
37550
|
-
*
|
|
37551
|
-
*
|
|
38378
|
+
* Emit one RTCP Sender Report for a downstream leg. Carries the cumulative
|
|
38379
|
+
* packet + octet counts plus the (NTP, RTP) timestamp pair the controller
|
|
38380
|
+
* needs to anchor that leg against wall-clock time. Encrypted via the leg's
|
|
38381
|
+
* SRTCP context (same master key/salt pair as its SRTP session) and sent to
|
|
38382
|
+
* the same `(hapAddress, port)` the SRTP packets go to.
|
|
38383
|
+
*
|
|
38384
|
+
* BOTH legs need this, not just audio. HAP negotiates `rtcp_interval`
|
|
38385
|
+
* separately for video and audio, and a Sender Report is the only thing that
|
|
38386
|
+
* pairs an NTP instant with an RTP timestamp — without it the controller
|
|
38387
|
+
* cannot place the stream on a clock. The audio leg alone was covered for a
|
|
38388
|
+
* year (the speaker went mute without it, which made the omission visible);
|
|
38389
|
+
* the video leg has the same requirement and no equally loud symptom.
|
|
37552
38390
|
*/
|
|
37553
|
-
function
|
|
37554
|
-
|
|
38391
|
+
function sendRtcpSr(session, kind, log) {
|
|
38392
|
+
const srtcp = kind === "video" ? session.videoOutSrtcp : session.audioOutSrtcp;
|
|
38393
|
+
if (!srtcp) return;
|
|
38394
|
+
const sink = kind === "video" ? session.videoUdp : session.audioUdp;
|
|
38395
|
+
const port = kind === "video" ? session.hapVideoPort : session.hapAudioPort;
|
|
37555
38396
|
try {
|
|
37556
38397
|
const sr = new import_src.RtcpSrPacket({
|
|
37557
|
-
ssrc: session.audioOutSsrc,
|
|
38398
|
+
ssrc: kind === "video" ? session.videoSsrc : session.audioOutSsrc,
|
|
37558
38399
|
senderInfo: new import_src.RtcpSenderInfo({
|
|
37559
38400
|
ntpTimestamp: ntpTime(),
|
|
37560
|
-
rtpTimestamp: session.audioOutLastRtpTimestamp,
|
|
37561
|
-
packetCount: session.audioOutPacketCount,
|
|
37562
|
-
octetCount: session.audioOutOctetCount
|
|
38401
|
+
rtpTimestamp: kind === "video" ? session.videoOutLastRtpTimestamp : session.audioOutLastRtpTimestamp,
|
|
38402
|
+
packetCount: kind === "video" ? session.videoOutPacketCount : session.audioOutPacketCount,
|
|
38403
|
+
octetCount: kind === "video" ? session.videoOutOctetCount : session.audioOutOctetCount
|
|
37563
38404
|
})
|
|
37564
38405
|
});
|
|
37565
|
-
const encrypted =
|
|
37566
|
-
|
|
37567
|
-
if (err) log.debug("export-hap:
|
|
38406
|
+
const encrypted = srtcp.encrypt(sr.serialize());
|
|
38407
|
+
sink.send(encrypted, port, session.hapAddress, (err) => {
|
|
38408
|
+
if (err) log.debug("export-hap: RTCP send error", { meta: {
|
|
38409
|
+
kind,
|
|
38410
|
+
error: err.message
|
|
38411
|
+
} });
|
|
37568
38412
|
});
|
|
37569
|
-
session.
|
|
38413
|
+
if (kind === "video") session.videoOutLastRtcpAt = Date.now();
|
|
38414
|
+
else session.audioOutLastRtcpAt = Date.now();
|
|
37570
38415
|
} catch (err) {
|
|
37571
|
-
log.debug("export-hap:
|
|
38416
|
+
log.debug("export-hap: RTCP SR build/encrypt failed", { meta: {
|
|
38417
|
+
kind,
|
|
38418
|
+
error: err instanceof Error ? err.message : String(err)
|
|
38419
|
+
} });
|
|
37572
38420
|
}
|
|
37573
38421
|
}
|
|
37574
38422
|
/**
|
|
@@ -37587,21 +38435,18 @@ function forwardEncryptedRtp(session, rtpPacket, kind, log) {
|
|
|
37587
38435
|
const sink = kind === "video" ? session.videoUdp : session.audioUdp;
|
|
37588
38436
|
const port = kind === "video" ? session.hapVideoPort : session.hapAudioPort;
|
|
37589
38437
|
const gate = kind === "video" ? session.videoSendGate : session.audioSendGate;
|
|
37590
|
-
if (kind === "audio" && session.audioOutPacketCount === 0) {
|
|
37591
|
-
process.stderr.write(`[FORWARD-AUDIO-DIAG] hasSrtp=${!!srtp} hasGate=${!!gate} audioOutSsrc=${session.audioOutSsrc} scale=${session.audioIntervalScale} hasStartParams=${!!session.lastStartParams}\n`);
|
|
37592
|
-
log.info("export-hap: forwardEncryptedRtp audio entry diag", { meta: {
|
|
37593
|
-
hasSrtp: !!srtp,
|
|
37594
|
-
hasGate: !!gate,
|
|
37595
|
-
audioOutSsrc: session.audioOutSsrc,
|
|
37596
|
-
audioIntervalScale: session.audioIntervalScale,
|
|
37597
|
-
hasLastStartParams: !!session.lastStartParams
|
|
37598
|
-
} });
|
|
37599
|
-
}
|
|
37600
38438
|
if (!srtp || !gate) return;
|
|
37601
38439
|
gate.then(() => {
|
|
37602
38440
|
try {
|
|
37603
38441
|
const parsed = import_src.RtpPacket.deSerialize(rtpPacket);
|
|
37604
38442
|
let firstAudio = false;
|
|
38443
|
+
let firstVideo = false;
|
|
38444
|
+
if (kind === "video") {
|
|
38445
|
+
if (session.videoOutPacketCount === 0) firstVideo = true;
|
|
38446
|
+
session.videoOutPacketCount += 1;
|
|
38447
|
+
session.videoOutOctetCount += parsed.payload.length;
|
|
38448
|
+
session.videoOutLastRtpTimestamp = parsed.header.timestamp;
|
|
38449
|
+
}
|
|
37605
38450
|
if (kind === "audio") {
|
|
37606
38451
|
const originalTimestamp = parsed.header.timestamp;
|
|
37607
38452
|
if (session.firstAudioOutTimestamp === null) {
|
|
@@ -37631,9 +38476,10 @@ function forwardEncryptedRtp(session, rtpPacket, kind, log) {
|
|
|
37631
38476
|
error: err.message
|
|
37632
38477
|
} });
|
|
37633
38478
|
});
|
|
37634
|
-
|
|
37635
|
-
|
|
37636
|
-
|
|
38479
|
+
const now = Date.now();
|
|
38480
|
+
if (kind === "video") {
|
|
38481
|
+
if (firstVideo || now > session.videoOutLastRtcpAt + session.videoRtcpIntervalMs) sendRtcpSr(session, "video", log);
|
|
38482
|
+
} else if (firstAudio || now > session.audioOutLastRtcpAt + session.audioRtcpIntervalMs) sendRtcpSr(session, "audio", log);
|
|
37637
38483
|
} catch (err) {
|
|
37638
38484
|
log.debug("export-hap: SRTP encrypt failed", { meta: {
|
|
37639
38485
|
kind,
|
|
@@ -37671,6 +38517,11 @@ async function handleStreamRequest(request, sessions, bctx) {
|
|
|
37671
38517
|
session.audioOutLastRtpTimestamp = 0;
|
|
37672
38518
|
session.audioOutLastRtcpAt = 0;
|
|
37673
38519
|
session.audioRtcpIntervalMs = (request.audio.rtcp_interval > 0 ? request.audio.rtcp_interval : 5) * 1e3;
|
|
38520
|
+
session.videoRtcpIntervalMs = (request.video.rtcp_interval > 0 ? request.video.rtcp_interval : 5) * 1e3;
|
|
38521
|
+
session.videoOutPacketCount = 0;
|
|
38522
|
+
session.videoOutOctetCount = 0;
|
|
38523
|
+
session.videoOutLastRtpTimestamp = 0;
|
|
38524
|
+
session.videoOutLastRtcpAt = 0;
|
|
37674
38525
|
session.lastStartParams = {
|
|
37675
38526
|
pt: request.video.pt,
|
|
37676
38527
|
mtu: request.video.mtu,
|
|
@@ -37702,6 +38553,10 @@ async function handleStreamRequest(request, sessions, bctx) {
|
|
|
37702
38553
|
if (session.ffmpeg) killFfmpeg(session, bctx.ctx, bctx.numericDeviceId);
|
|
37703
38554
|
session.firstAudioOutTimestamp = null;
|
|
37704
38555
|
session.audioOutPacketCount = 0;
|
|
38556
|
+
session.videoOutPacketCount = 0;
|
|
38557
|
+
session.videoOutOctetCount = 0;
|
|
38558
|
+
session.videoOutLastRtpTimestamp = 0;
|
|
38559
|
+
session.videoOutLastRtcpAt = 0;
|
|
37705
38560
|
const startParams = {
|
|
37706
38561
|
width: request.video.width,
|
|
37707
38562
|
height: request.video.height,
|