@camstack/addon-notifiers 1.2.11 → 1.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +1226 -486
  2. package/dist/addon.mjs +1226 -486
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -29,7 +29,7 @@ let node_fs = require("node:fs");
29
29
  let node_path = require("node:path");
30
30
  node_path = __toESM(node_path);
31
31
  let node_url = require("node:url");
32
- //#region ../types/dist/event-category-Bz24uP1U.mjs
32
+ //#region ../types/dist/event-category-41fKf-q9.mjs
33
33
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
34
34
  EventCategory["SystemBoot"] = "system.boot";
35
35
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -89,6 +89,26 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
89
89
  */
90
90
  EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
91
91
  /**
92
+ * A liveness invariant of this node is false — it has no devices, every
93
+ * camera is offline, or a camera that was recording has stopped producing
94
+ * segments. Emitted by the `liveness-monitor` builtin, once per fault (the
95
+ * `findingId` is stable across ticks), only after its boot grace period.
96
+ * AlertCenter raises a persistent operator-visible alert.
97
+ *
98
+ * It exists because on 2026-08-03 the hub ran four hours with zero devices
99
+ * and zero recordings while every surface stayed quiet.
100
+ *
101
+ * Payload: `{ findingId, severity, title, message, deviceId? }`.
102
+ */
103
+ EventCategory["SystemLivenessFailed"] = "system.liveness-failed";
104
+ /**
105
+ * A previously reported liveness fault is true again. AlertCenter dismisses
106
+ * the matching `SystemLivenessFailed` alert.
107
+ *
108
+ * Payload: `{ findingId }`.
109
+ */
110
+ EventCategory["SystemLivenessRecovered"] = "system.liveness-recovered";
111
+ /**
92
112
  * Monitor is attempting to reload a failed addon NOW. UI uses this
93
113
  * to show a spinner during the retry attempt. Same transient nature
94
114
  * as AddonRetryScheduled.
@@ -7456,14 +7476,7 @@ var RecordingConfigSchema = object({
7456
7476
  * windows only — existing sheets are immutable, and each window's index
7457
7477
  * carries its own tile dims so mixed-preset history renders correctly.
7458
7478
  */
7459
- scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
7460
- /**
7461
- * OPT-IN thumbnail-strip generation for this camera: every keyframe of the
7462
- * low recording saved as a JPEG (the fast-drag scrub depth), a derived
7463
- * cache that eviction reclaims with the footage. Absent/false = no strips
7464
- * are written and scrub reads exact keyframes at every velocity.
7465
- */
7466
- stripsEnabled: boolean().optional()
7479
+ scrubThumbnails: ScrubThumbnailPresetSchema.optional()
7467
7480
  }).strict();
7468
7481
  /**
7469
7482
  * Ops-log — the durable, append-only operations audit shared by the
@@ -7525,7 +7538,7 @@ var OpsLogQueryInputSchema = object({
7525
7538
  /**
7526
7539
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
7527
7540
  *
7528
- * One shape shared by the recorder's `relocateFootage` (segments + strips) and
7541
+ * One shape shared by the recorder's `relocateFootage` (segments) and
7529
7542
  * pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
7530
7543
  * page renders both movers with one component. Jobs are in-RAM (a restart
7531
7544
  * forgets them — re-running is safe by construction: copy-if-absent, delete
@@ -7547,7 +7560,7 @@ var RelocateJobSchema = object({
7547
7560
  toLocationId: string(),
7548
7561
  /** Scoped device, or null = every device. */
7549
7562
  deviceId: number().nullable(),
7550
- /** What the job moves (owner-addon specific: segments/strips or media). */
7563
+ /** What the job moves (owner-addon specific: segments or media). */
7551
7564
  entities: array(string()),
7552
7565
  filesMoved: number().int(),
7553
7566
  bytesMoved: number().int(),
@@ -7561,7 +7574,7 @@ var RelocateFootageInputSchema = object({
7561
7574
  deviceId: number().optional(),
7562
7575
  fromLocationId: string(),
7563
7576
  toLocationId: string(),
7564
- entities: array(_enum(["segments", "strips"])).optional(),
7577
+ entities: array(_enum(["segments"])).optional(),
7565
7578
  /** Copy throttle in MB/s (default 40) — the drain is a background chore,
7566
7579
  * never allowed to starve live writers. */
7567
7580
  throttleMbps: number().min(1).max(1e3).optional()
@@ -9223,8 +9236,11 @@ function prepareNotification(caps, n) {
9223
9236
  });
9224
9237
  }
9225
9238
  const inActions = n.actions ?? [];
9226
- const actions = inActions.slice(0, Math.max(0, caps.actions));
9227
- if (inActions.length > actions.length) dropped.push("actions");
9239
+ const kept = inActions.slice(0, Math.max(0, caps.actions));
9240
+ if (inActions.length > kept.length) dropped.push("actions");
9241
+ const iconsSupported = caps.actionIcons === true;
9242
+ if (!iconsSupported && kept.some((a) => a.icon !== void 0)) dropped.push("actionIcons");
9243
+ const actions = iconsSupported ? kept : kept.map(({ icon: _icon, ...rest }) => rest);
9228
9244
  let clickUrl = null;
9229
9245
  if (n.clickUrl !== void 0) if (caps.clickUrl) clickUrl = n.clickUrl;
9230
9246
  else dropped.push("clickUrl");
@@ -9323,160 +9339,552 @@ var MaskGridDimsSchema = object({
9323
9339
  height: number()
9324
9340
  });
9325
9341
  /**
9326
- * notification-rulesthe Notification Center rule surface (P1 core).
9327
- *
9328
- * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
9329
- * (operator decisions D-1/D-2/D-3 are binding):
9342
+ * notification-outputcanonical, capability-gated notification delivery.
9330
9343
  *
9331
- * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
9332
- * `notification-center` module), hooked on the durable persistence
9333
- * moments (object-event insert, TrackCloser.closeExpired) with a
9334
- * persisted outbox + retry never the lossy telemetry bus (D8).
9335
- * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
9336
- * FIRST persisted detection matching the conditions (per-track dedup,
9337
- * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
9338
- * `delivery: 'track-end'` evaluates the finalized track record at close.
9339
- * - DISPATCH stays behind `notification-output` (rules reference targets
9340
- * by id; per-backend params are a passthrough blob capped by the
9341
- * target kind's own caps/degrade engine).
9344
+ * Apprise-derived model (see
9345
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
9346
+ * callers emit ONE canonical `Notification`; each provider declares a
9347
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
9348
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
9349
+ * message to what the kind supports — callers never special-case a service.
9342
9350
  *
9343
- * P1 scope: admin-authored rules only (`createdBy` stamped from the
9344
- * server-injected caller identity the first `caller: 'required'`
9345
- * adopter). The P1 condition subset is: devices, classes(+exclude),
9346
- * minConfidence, admin zones (any/all + exclude), weekly schedule
9347
- * windows, and the optional label/identity/plate matchers. User rules,
9348
- * private zones, per-recipient fan-out and the wider condition table are
9349
- * P2+ (see spec §7).
9351
+ * DESIGN DECISIONS (locked):
9352
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
9353
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
9354
+ * cap. Rationale: the admin UI needs one uniform surface across the
9355
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
9356
+ * alternative would fork the UI per addon and cannot host the
9357
+ * discovery→adopt flow.
9358
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
9359
+ * the generated cap-mount auto-`concatCollection`-fans them across every
9360
+ * registered provider (notifiers addon + HA addon) so one catalog is
9361
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
9362
+ * `addonId` the generated collection router extracts from the call input.
9363
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
9364
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
9365
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
9366
+ * base64 fallback needed.
9350
9367
  *
9351
- * All schemas here are the single source of truth `NcRule` etc. are
9352
- * `z.infer` exports; no duplicate interfaces (the advanced-notifier
9353
- * schema/interface drift is explicitly not repeated).
9368
+ * TODO (deferred, closed-set changeseparate decision): add
9369
+ * `providerKind: 'notify'` so notification providers surface on the unified
9370
+ * admin "Integrations" page.
9354
9371
  */
9355
9372
  /**
9356
- * D-3: the trigger/urgency of a rule which persistence moment evaluates it.
9357
- * The value maps 1:1 onto the evaluated record kind:
9358
- * - `immediate` ↔ object-event persist (lowest-latency detection burst)
9359
- * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
9360
- * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
9361
- * change of a LINKED device, one row per linked camera)
9362
- * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
9363
- * delivery / pick-up)
9364
- *
9365
- * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
9366
- * `package-event` are pure trigger kinds (no urgency dimension). Extending
9367
- * this one field keeps the schema additive — a rule still declares exactly
9368
- * one trigger.
9373
+ * Zentik-derived typed-media enumthe superset across every kind. Each
9374
+ * adapter picks what it supports and the degrade engine filters the rest.
9369
9375
  */
9370
- var NcDeliverySchema = _enum([
9371
- "immediate",
9372
- "track-end",
9373
- "device-event",
9374
- "package-event"
9376
+ var AttachmentMediaTypeSchema = _enum([
9377
+ "image",
9378
+ "video",
9379
+ "gif",
9380
+ "audio",
9381
+ "icon"
9375
9382
  ]);
9376
- /** Weekly schedule — OR of windows; absence on the rule = always active. */
9377
- var NcScheduleSchema = object({
9378
- windows: array(object({
9379
- /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
9380
- days: array(number().int().min(0).max(6)).min(1),
9381
- startMinute: number().int().min(0).max(1439),
9382
- endMinute: number().int().min(0).max(1439)
9383
- })).min(1),
9384
- /** IANA timezone; default = hub host timezone. */
9385
- timezone: string().optional(),
9386
- /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
9387
- invert: boolean().optional()
9388
- });
9389
- /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
9390
- var NcPlateMatcherSchema = object({
9391
- values: array(string().min(1)).min(1),
9392
- /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
9393
- maxDistance: number().int().min(0).max(3).default(1)
9394
- });
9395
9383
  /**
9396
- * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
9397
- * occupancy edge for a device optionally narrowed to a single admin
9398
- * `zoneId` and/or object `className`. `op` selects the edge/threshold:
9399
- * - `became-occupied` (default) count crossed 0 `count`
9400
- * - `became-free` — count crossed ≥ `count` → below it
9401
- * - `>=` / `<=` — count is at/over or at/under `count`
9402
- * `sustainSeconds` requires the condition hold continuously that long
9403
- * before firing (debounces flicker; 0 = fire on the first matching edge).
9404
- * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
9405
- * the condition never matches. Confirmed edge-state survives addon restarts
9406
- * (declared SQLite collection, reseeded on boot).
9384
+ * A single attachment. Exactly one of `url` (remote source, most adapters
9385
+ * prefer this) or `bytes` (inline source; required for Pushover-style
9386
+ * bytes-only kinds) MUST be present the degrade engine expresses a
9387
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
9407
9388
  */
9408
- var NcOccupancyConditionSchema = object({
9409
- /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
9410
- zoneId: string().optional(),
9411
- /** Object class to count; absent = any class. */
9412
- className: string().optional(),
9413
- op: _enum([
9414
- "became-occupied",
9415
- "became-free",
9416
- ">=",
9417
- "<="
9418
- ]).default("became-occupied"),
9419
- count: number().int().min(0).default(1),
9420
- sustainSeconds: number().int().min(0).max(3600).default(15)
9421
- });
9389
+ var AttachmentSchema = object({
9390
+ mediaType: AttachmentMediaTypeSchema,
9391
+ url: string().optional(),
9392
+ bytes: _instanceof(Uint8Array).optional(),
9393
+ mime: string().optional(),
9394
+ name: string().optional()
9395
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
9396
+ var NotificationFormatSchema = _enum([
9397
+ "text",
9398
+ "markdown",
9399
+ "html"
9400
+ ]);
9422
9401
  /**
9423
- * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
9402
+ * The CLOSED icon vocabulary an action button may use.
9424
9403
  *
9425
- * The values are not symmetric, and deliberately so the absent value has to
9426
- * mean exactly what every rule authored before this condition existed already
9427
- * does:
9428
- * - `enter` entries and every NON-crossing record (movement state,
9429
- * package, sensor). Exits are rejected. **This is the absent behaviour**:
9430
- * an operator who never asked for exits must not start receiving them.
9431
- * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
9432
- * fails closed, because "the car left the drive" is a question about a
9433
- * boundary, not about a detection.
9434
- * - `any` — no direction filter; entries, exits and non-crossings alike.
9404
+ * A closed set, not a free string, and that is the whole point: an arbitrary
9405
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
9406
+ * notices — the same class of gap as a zone vocabulary nothing produced
9407
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
9408
+ * declares `actionIcons: false` and the degrade engine strips the field.
9435
9409
  *
9436
- * A rule asking for a direction should normally also scope `zones`, which the
9437
- * engine evaluates against the crossed zone as well as the current membership
9438
- * (an exit's membership no longer contains the zone it just left).
9410
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
9411
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
9412
+ * differently.
9439
9413
  */
9440
- var NcCrossingSchema = _enum([
9441
- "enter",
9442
- "exit",
9443
- "any"
9414
+ var NotificationActionIconSchema = _enum([
9415
+ "acknowledge",
9416
+ "dismiss",
9417
+ "silence",
9418
+ "view",
9419
+ "play",
9420
+ "open",
9421
+ "close",
9422
+ "lock",
9423
+ "unlock",
9424
+ "arm",
9425
+ "disarm",
9426
+ "light",
9427
+ "alert"
9444
9428
  ]);
9445
- /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9446
- var NcZoneConditionSchema = object({
9447
- ids: array(string().min(1)).min(1),
9448
- /** Quantifier over `ids` — at least one / every one visited. */
9449
- match: _enum(["any", "all"]).default("any")
9429
+ /** A single tap-through action button. */
9430
+ var NotificationActionSchema = object({
9431
+ id: string(),
9432
+ label: string(),
9433
+ url: string().optional(),
9434
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
9435
+ icon: NotificationActionIconSchema.optional(),
9436
+ /**
9437
+ * Renders in a warning style where the notifier supports it.
9438
+ *
9439
+ * A HINT, never a gate. The callback's authority is its token and nothing
9440
+ * else — see `notification-center/action-token.ts` for what that does and
9441
+ * does not buy.
9442
+ */
9443
+ destructive: boolean().optional()
9450
9444
  });
9451
9445
  /**
9452
- * The P1 condition set a flat AND of groups; absent group = pass;
9453
- * membership lists are OR within the list (spec §2.3).
9446
+ * The canonical notification. `body` is the only hard field (Apprise model).
9447
+ * `priority` is a 5-level ORDINAL (1=lowest 3=normal(default) … 5=urgent),
9448
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
9449
+ * the adapter maps this ordinal onto its native level. `level?` is an
9450
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
9451
+ * `priority` for that one target.
9454
9452
  */
9455
- var NcConditionsSchema = object({
9456
- /** Device scope — absent = all devices. */
9457
- devices: array(number()).optional(),
9458
- /** Detector class names (any overlap with the record's class set). */
9459
- classes: array(string().min(1)).optional(),
9460
- /** Veto classes — any overlap fails the rule. */
9461
- classesExclude: array(string().min(1)).optional(),
9462
- /** Minimum detection confidence 0–1 (fails when the record has none). */
9463
- minConfidence: number().min(0).max(1).optional(),
9464
- /** Admin zone membership over event `zones` / track `zonesVisited`. */
9465
- zones: NcZoneConditionSchema.optional(),
9466
- /** Veto zones — any hit fails the rule. */
9467
- zonesExclude: array(string().min(1)).optional(),
9468
- /**
9469
- * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
9470
- * and a closed track carries none, so a `track-end` rule asking for one
9471
- * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
9472
- * which is exactly today's behaviour. See {@link NcCrossingSchema}.
9473
- */
9474
- crossing: NcCrossingSchema.optional(),
9453
+ var NotificationSchema = object({
9454
+ body: string(),
9455
+ title: string().optional(),
9456
+ format: NotificationFormatSchema.default("text"),
9457
+ priority: number().int().min(1).max(5).default(3),
9458
+ level: string().optional(),
9459
+ attachments: array(AttachmentSchema).optional(),
9460
+ clickUrl: string().optional(),
9461
+ actions: array(NotificationActionSchema).optional(),
9462
+ sound: string().optional(),
9463
+ ttl: number().optional(),
9464
+ tag: string().optional(),
9465
+ deviceId: number().optional(),
9466
+ eventId: string().optional(),
9467
+ metadata: record(string(), unknown()).optional()
9468
+ });
9469
+ /** One declared native severity/priority level for a kind. */
9470
+ var TargetKindLevelSchema = object({
9471
+ id: string(),
9472
+ label: string(),
9473
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
9474
+ ordinal: number().int().min(1).max(5).nullable(),
9475
+ flags: object({
9476
+ critical: boolean().optional(),
9477
+ silent: boolean().optional(),
9478
+ noPush: boolean().optional()
9479
+ }).optional(),
9480
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
9481
+ requires: array(string()).optional(),
9482
+ description: string().optional()
9483
+ });
9484
+ /** The full capability block consulted before dispatch. */
9485
+ var TargetKindCapsSchema = object({
9486
+ attachments: object({
9487
+ mediaTypes: array(AttachmentMediaTypeSchema),
9488
+ mode: _enum([
9489
+ "url",
9490
+ "bytes",
9491
+ "both"
9492
+ ]),
9493
+ max: number().int().nonnegative(),
9494
+ maxBytes: number().int().positive().optional()
9495
+ }),
9496
+ /** Max action buttons (0 = none). */
9497
+ actions: number().int().nonnegative(),
9475
9498
  /**
9476
- * Exact (case-insensitive) match on the record's collapsed `label`
9477
- * (identity name / plate text / subclass).
9499
+ * Whether this kind renders a per-action ICON.
9500
+ *
9501
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
9502
+ * run on the addon cap path — three production failures in one day taught
9503
+ * this repo that once. Absent is read as false by the degrade engine, which
9504
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
9505
+ * assumed and dropped costs the operator's trust in the field.
9478
9506
  */
9479
- labelEquals: array(string().min(1)).optional(),
9507
+ actionIcons: boolean().optional(),
9508
+ levels: array(TargetKindLevelSchema),
9509
+ format: array(NotificationFormatSchema),
9510
+ clickUrl: boolean(),
9511
+ sound: boolean(),
9512
+ ttl: boolean(),
9513
+ bodyMaxLen: number().int().positive()
9514
+ });
9515
+ /**
9516
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
9517
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
9518
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
9519
+ * the union is large and not meant for runtime validation here; the exported
9520
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
9521
+ */
9522
+ var ConfigSchemaPassthrough$1 = unknown();
9523
+ var TargetKindSchema = object({
9524
+ kind: string(),
9525
+ label: string(),
9526
+ icon: string(),
9527
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
9528
+ addonId: string(),
9529
+ /**
9530
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
9531
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
9532
+ * when the addon bundles no icon for that kind — the client then falls back
9533
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
9534
+ *
9535
+ * Root-relative on purpose: it resolves against whatever origin serves a web
9536
+ * client, and a native client joins it onto its own hub base.
9537
+ *
9538
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
9539
+ * field that survived only because the runtime cap-router forwards provider
9540
+ * output verbatim — so every consumer had to re-declare it by hand to stop
9541
+ * its own Zod parse from stripping it, and the whole arrangement would have
9542
+ * broken silently the moment output validation was tightened anywhere.
9543
+ */
9544
+ iconUrl: string().optional(),
9545
+ /**
9546
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
9547
+ *
9548
+ * The server knows this and therefore says it, because the client cannot
9549
+ * safely guess: a React-Native client renders SVG and raster through two
9550
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
9551
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
9552
+ * placeholder glyph for every vector icon while the web build looked fine.
9553
+ *
9554
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
9555
+ * not been updated — a client that cannot determine the type should prefer
9556
+ * its raster path, which is the safe default for an unknown image.
9557
+ */
9558
+ iconMediaType: string().optional(),
9559
+ configSchema: ConfigSchemaPassthrough$1,
9560
+ supportsDiscovery: boolean(),
9561
+ caps: TargetKindCapsSchema
9562
+ });
9563
+ /**
9564
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
9565
+ * (return a presence marker only) when serving `listTargets` — never
9566
+ * round-trip a stored secret to the UI.
9567
+ */
9568
+ var TargetSchema = object({
9569
+ id: string(),
9570
+ name: string(),
9571
+ kind: string(),
9572
+ addonId: string(),
9573
+ enabled: boolean(),
9574
+ config: record(string(), unknown())
9575
+ });
9576
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
9577
+ var DiscoveredTargetSchema = object({
9578
+ kind: string(),
9579
+ suggestedName: string(),
9580
+ config: record(string(), unknown())
9581
+ });
9582
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
9583
+ var RenderedAsSchema = object({
9584
+ level: string(),
9585
+ format: NotificationFormatSchema,
9586
+ attachmentsSent: number().int().nonnegative(),
9587
+ actionsSent: number().int().nonnegative(),
9588
+ truncated: boolean(),
9589
+ dropped: array(string())
9590
+ });
9591
+ var SendResultSchema = object({
9592
+ success: boolean(),
9593
+ error: string().optional(),
9594
+ renderedAs: RenderedAsSchema.optional()
9595
+ });
9596
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
9597
+ var TestResultSchema = SendResultSchema;
9598
+ var notificationOutputCapability = {
9599
+ name: "notification-output",
9600
+ scope: "system",
9601
+ mode: "collection",
9602
+ methods: {
9603
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
9604
+ listTargets: method(object({}), array(TargetSchema)),
9605
+ discoverTargets: method(object({
9606
+ kind: string(),
9607
+ config: record(string(), unknown()).optional()
9608
+ }), array(DiscoveredTargetSchema)),
9609
+ send: method(object({
9610
+ targetId: string(),
9611
+ notification: NotificationSchema
9612
+ }), SendResultSchema, { kind: "mutation" }),
9613
+ testTarget: method(object({
9614
+ targetId: string(),
9615
+ sample: NotificationSchema.optional()
9616
+ }), TestResultSchema, { kind: "mutation" }),
9617
+ upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
9618
+ deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
9619
+ setTargetEnabled: method(object({
9620
+ targetId: string(),
9621
+ enabled: boolean()
9622
+ }), _void(), { kind: "mutation" })
9623
+ }
9624
+ };
9625
+ /**
9626
+ * notification-rules — the Notification Center rule surface (P1 core).
9627
+ *
9628
+ * Spec: `docs/superpowers/specs/2026-07-22-notification-center-requirements.md`
9629
+ * (operator decisions D-1/D-2/D-3 are binding):
9630
+ *
9631
+ * - D-2: rule EVALUATION lives in `addon-post-analysis` (the
9632
+ * `notification-center` module), hooked on the durable persistence
9633
+ * moments (object-event insert, TrackCloser.closeExpired) with a
9634
+ * persisted outbox + retry — never the lossy telemetry bus (D8).
9635
+ * - D-3: urgency belongs to the RULE. `delivery: 'immediate'` fires on the
9636
+ * FIRST persisted detection matching the conditions (per-track dedup,
9637
+ * `maxPerTrack` fixed at 1 — see {@link NC_MAX_PER_TRACK_IMMEDIATE});
9638
+ * `delivery: 'track-end'` evaluates the finalized track record at close.
9639
+ * - DISPATCH stays behind `notification-output` (rules reference targets
9640
+ * by id; per-backend params are a passthrough blob capped by the
9641
+ * target kind's own caps/degrade engine).
9642
+ *
9643
+ * P1 scope: admin-authored rules only (`createdBy` stamped from the
9644
+ * server-injected caller identity — the first `caller: 'required'`
9645
+ * adopter). The P1 condition subset is: devices, classes(+exclude),
9646
+ * minConfidence, admin zones (any/all + exclude), weekly schedule
9647
+ * windows, and the optional label/identity/plate matchers. User rules,
9648
+ * private zones, per-recipient fan-out and the wider condition table are
9649
+ * P2+ (see spec §7).
9650
+ *
9651
+ * All schemas here are the single source of truth — `NcRule` etc. are
9652
+ * `z.infer` exports; no duplicate interfaces (the advanced-notifier
9653
+ * schema/interface drift is explicitly not repeated).
9654
+ */
9655
+ /**
9656
+ * D-3: the trigger/urgency of a rule — which persistence moment evaluates it.
9657
+ * The value maps 1:1 onto the evaluated record kind:
9658
+ * - `immediate` ↔ object-event persist (lowest-latency detection burst)
9659
+ * - `track-end` ↔ TrackCloser.closeExpired (finalized track record)
9660
+ * - `device-event` ↔ SensorEventStore insert (doorbell press / sensor state
9661
+ * change of a LINKED device, one row per linked camera)
9662
+ * - `package-event` ↔ PackageDropDetector object-event insert (a `package`
9663
+ * delivery / pick-up)
9664
+ *
9665
+ * `immediate`/`track-end` carry the D-3 urgency semantics; `device-event`/
9666
+ * `package-event` are pure trigger kinds (no urgency dimension). Extending
9667
+ * this one field keeps the schema additive — a rule still declares exactly
9668
+ * one trigger.
9669
+ */
9670
+ var NcDeliverySchema = _enum([
9671
+ "immediate",
9672
+ "track-end",
9673
+ "device-event",
9674
+ "package-event"
9675
+ ]);
9676
+ /** Weekly schedule — OR of windows; absence on the rule = always active. */
9677
+ var NcScheduleSchema = object({
9678
+ windows: array(object({
9679
+ /** Days of week the window STARTS on (0 = Sunday … 6 = Saturday). */
9680
+ days: array(number().int().min(0).max(6)).min(1),
9681
+ startMinute: number().int().min(0).max(1439),
9682
+ endMinute: number().int().min(0).max(1439)
9683
+ })).min(1),
9684
+ /** IANA timezone; default = hub host timezone. */
9685
+ timezone: string().optional(),
9686
+ /** Active OUTSIDE the windows (e.g. "only outside business hours"). */
9687
+ invert: boolean().optional()
9688
+ });
9689
+ /** Fuzzy plate matcher — OCR noise makes exact match useless (spec row 12/13). */
9690
+ var NcPlateMatcherSchema = object({
9691
+ values: array(string().min(1)).min(1),
9692
+ /** Max Levenshtein distance after normalization (uppercase alphanumeric). */
9693
+ maxDistance: number().int().min(0).max(3).default(1)
9694
+ });
9695
+ /**
9696
+ * Occupancy condition (DEVICE-EVENT trigger). Fires on a ZoneAnalytics
9697
+ * occupancy edge for a device — optionally narrowed to a single admin
9698
+ * `zoneId` and/or object `className`. `op` selects the edge/threshold:
9699
+ * - `became-occupied` (default) — count crossed 0 → ≥ `count`
9700
+ * - `became-free` — count crossed ≥ `count` → below it
9701
+ * - `>=` / `<=` — count is at/over or at/under `count`
9702
+ * `sustainSeconds` requires the condition hold continuously that long
9703
+ * before firing (debounces flicker; 0 = fire on the first matching edge).
9704
+ * Fail-closed: no ZoneAnalytics snapshot / missing zone / null snapshot ⇒
9705
+ * the condition never matches. Confirmed edge-state survives addon restarts
9706
+ * (declared SQLite collection, reseeded on boot).
9707
+ */
9708
+ var NcOccupancyConditionSchema = object({
9709
+ /** Admin zone id to scope the count to; absent = whole-frame occupancy. */
9710
+ zoneId: string().optional(),
9711
+ /** Object class to count; absent = any class. */
9712
+ className: string().optional(),
9713
+ op: _enum([
9714
+ "became-occupied",
9715
+ "became-free",
9716
+ ">=",
9717
+ "<="
9718
+ ]).default("became-occupied"),
9719
+ count: number().int().min(0).default(1),
9720
+ sustainSeconds: number().int().min(0).max(3600).default(15)
9721
+ });
9722
+ /**
9723
+ * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
9724
+ *
9725
+ * The values are not symmetric, and deliberately so — the absent value has to
9726
+ * mean exactly what every rule authored before this condition existed already
9727
+ * does:
9728
+ * - `enter` — entries and every NON-crossing record (movement state,
9729
+ * package, sensor). Exits are rejected. **This is the absent behaviour**:
9730
+ * an operator who never asked for exits must not start receiving them.
9731
+ * - `exit` — ONLY an exit crossing. A record that is not a crossing at all
9732
+ * fails closed, because "the car left the drive" is a question about a
9733
+ * boundary, not about a detection.
9734
+ * - `any` — no direction filter; entries, exits and non-crossings alike.
9735
+ *
9736
+ * A rule asking for a direction should normally also scope `zones`, which the
9737
+ * engine evaluates against the crossed zone as well as the current membership
9738
+ * (an exit's membership no longer contains the zone it just left).
9739
+ */
9740
+ var NcCrossingSchema = _enum([
9741
+ "enter",
9742
+ "exit",
9743
+ "any"
9744
+ ]);
9745
+ /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
9746
+ var NcZoneConditionSchema = object({
9747
+ ids: array(string().min(1)).min(1),
9748
+ /** Quantifier over `ids` — at least one / every one visited. */
9749
+ match: _enum(["any", "all"]).default("any")
9750
+ });
9751
+ /**
9752
+ * The P1 condition set — a flat AND of groups; absent group = pass;
9753
+ * membership lists are OR within the list (spec §2.3).
9754
+ */
9755
+ /**
9756
+ * What a rule may actuate.
9757
+ *
9758
+ * **No hand-maintained allowlist** (operator decision, and the right one — a
9759
+ * written list of methods is a third parallel map to keep aligned, and this
9760
+ * repo has paid for those). The boundary instead comes from a property the
9761
+ * capabilities already carry: an action may target only a **device-scoped**
9762
+ * capability method.
9763
+ *
9764
+ * That is not decoration. A rule can be authored by a NON-ADMIN — personal
9765
+ * rules are a supported flow — and the executor runs with the addon's
9766
+ * privileges, so an unbounded action is an arbitrary RPC channel with a
9767
+ * privilege escalation attached. Restricting to device scope excludes the
9768
+ * system caps (`device-manager.removeDevice` and friends) by construction,
9769
+ * costs nothing to maintain, and cannot rot: a cap that stops being
9770
+ * device-scoped stops being actuatable in the same change.
9771
+ *
9772
+ * The executor enforces it; {@link NcRuleActionSchema} carries the intent.
9773
+ */
9774
+ /**
9775
+ * One step of a sequence.
9776
+ *
9777
+ * `wait` is a first-class step rather than a property of the next action: it is
9778
+ * what makes a sequence a SEQUENCE and not a list — "unlock, wait 5s, open"
9779
+ * cannot be expressed otherwise.
9780
+ */
9781
+ var NcRuleActionSchema = discriminatedUnion("kind", [object({
9782
+ kind: literal("wait"),
9783
+ seconds: number().min(0).max(300)
9784
+ }), object({
9785
+ kind: literal("cap"),
9786
+ deviceId: number().int(),
9787
+ /** Capability name, e.g. `alarm-panel`. */
9788
+ cap: string().min(1),
9789
+ /** Method on it. The executor refuses a non-device-scoped cap. */
9790
+ method: string().min(1),
9791
+ /** Method arguments, minus `deviceId` (the executor injects it). */
9792
+ args: record(string(), unknown()).optional()
9793
+ })]);
9794
+ /**
9795
+ * A named, ordered run of steps with its own throttle.
9796
+ *
9797
+ * `minDelaySec` exists because a noisy rule otherwise hammers a physical
9798
+ * actuator — the rule's own cooldown governs NOTIFICATIONS, which is a
9799
+ * different budget from "how often may this gate actually open".
9800
+ */
9801
+ var NcRuleActionSequenceSchema = object({
9802
+ name: string().min(1).max(120),
9803
+ enabled: boolean(),
9804
+ minDelaySec: number().int().min(0).max(86400).optional(),
9805
+ actions: array(NcRuleActionSchema).min(1)
9806
+ });
9807
+ /**
9808
+ * One button carried by the notification, running a named sequence on tap.
9809
+ *
9810
+ * **Read this before adding a button that does something physical.** The tap
9811
+ * arrives over a link that travelled through third-party infrastructure — ntfy,
9812
+ * a push relay, whatever forwarded the message — and the callback's ONLY
9813
+ * authority is the token in that link: single-use, short-lived, bound to this
9814
+ * one action of this one notification. It does not identify who tapped.
9815
+ * Whoever holds the notification can run the button, once, inside the window.
9816
+ * That is the operator's explicit choice (2026-08-05), and `destructive` is a
9817
+ * rendering hint, not a second gate. [D47](decisions/adr-0047.md).
9818
+ */
9819
+ var NcRuleNotificationButtonSchema = object({
9820
+ /** Stable id — travels in the callback and identifies the button in logs. */
9821
+ id: string().min(1).max(64),
9822
+ label: string().min(1).max(40),
9823
+ /** Name of a sequence in `onTrigger`. The dispatcher drops a button whose
9824
+ * sequence does not exist rather than minting a token for nothing. */
9825
+ sequence: string().min(1).max(120),
9826
+ icon: NotificationActionIconSchema.optional(),
9827
+ destructive: boolean().optional()
9828
+ });
9829
+ /**
9830
+ * Sequences a rule runs, by hook point.
9831
+ *
9832
+ * ONLY `onTrigger` is here, deliberately. The reference also has activation /
9833
+ * deactivation / reset / post-generation hooks, and they are wanted — but this
9834
+ * repo's expensive failure mode is declaring a surface nothing produces, so a
9835
+ * hook appears here in the same change that produces its edge, never before.
9836
+ */
9837
+ var NcRuleActionsSchema = object({
9838
+ /** Runs when the rule MATCHES. */
9839
+ onTrigger: array(NcRuleActionSequenceSchema).optional(),
9840
+ /**
9841
+ * Buttons the NOTIFICATION carries, each running one of this rule's
9842
+ * sequences when tapped.
9843
+ *
9844
+ * Deliberately a REFERENCE to a sequence rather than a second place to
9845
+ * author steps. A button that could define its own actions would be a
9846
+ * parallel actuation vocabulary — the executor's device-scope check, the
9847
+ * stop-at-first-failure rule and the per-sequence throttle all live on
9848
+ * sequences, and a second authoring surface would drift from every one of
9849
+ * them.
9850
+ *
9851
+ * A sequence reachable ONLY by a button simply appears in `onTrigger` with
9852
+ * `enabled: false`: it is then authored, throttled and validated like the
9853
+ * rest, and nothing runs it automatically.
9854
+ */
9855
+ buttons: array(NcRuleNotificationButtonSchema).max(8).optional()
9856
+ });
9857
+ var NcConditionsSchema = object({
9858
+ /** Gate on ANOTHER device's current state (the alarm armed, a switch on). */
9859
+ deviceState: object({
9860
+ deviceId: number().int(),
9861
+ /** Any of these matches. */
9862
+ states: array(string().min(1)).min(1)
9863
+ }).optional(),
9864
+ /** Device scope — absent = all devices. */
9865
+ devices: array(number()).optional(),
9866
+ /** Detector class names (any overlap with the record's class set). */
9867
+ classes: array(string().min(1)).optional(),
9868
+ /** Veto classes — any overlap fails the rule. */
9869
+ classesExclude: array(string().min(1)).optional(),
9870
+ /** Minimum detection confidence 0–1 (fails when the record has none). */
9871
+ minConfidence: number().min(0).max(1).optional(),
9872
+ /** Admin zone membership over event `zones` / track `zonesVisited`. */
9873
+ zones: NcZoneConditionSchema.optional(),
9874
+ /** Veto zones — any hit fails the rule. */
9875
+ zonesExclude: array(string().min(1)).optional(),
9876
+ /**
9877
+ * Zone-crossing direction. IMMEDIATE only — a crossing is a per-event fact
9878
+ * and a closed track carries none, so a `track-end` rule asking for one
9879
+ * fails closed (use `zones`, which tests `zonesVisited`). ABSENT = `enter`,
9880
+ * which is exactly today's behaviour. See {@link NcCrossingSchema}.
9881
+ */
9882
+ crossing: NcCrossingSchema.optional(),
9883
+ /**
9884
+ * Exact (case-insensitive) match on the record's collapsed `label`
9885
+ * (identity name / plate text / subclass).
9886
+ */
9887
+ labelEquals: array(string().min(1)).optional(),
9480
9888
  /**
9481
9889
  * Identity matcher. P1 boundary: matched against the record's collapsed
9482
9890
  * `label` (the identity display name propagated by the face pipeline) —
@@ -9653,6 +10061,14 @@ var NcMediaPolicySchema = object({
9653
10061
  clipPreRollSec: number().int().min(0).max(30).optional(),
9654
10062
  clipPostRollSec: number().int().min(0).max(30).optional(),
9655
10063
  /**
10064
+ * Playback rate of the attached gif / clip. Absent = 2x.
10065
+ *
10066
+ * A notification clip is GLANCED at on a lock screen, not watched: at real
10067
+ * time an eight-second passage is eight seconds of the recipient's attention
10068
+ * and twice the bytes. 1 is real time for the operator who wants it.
10069
+ */
10070
+ clipSpeed: number().min(1).max(8).optional(),
10071
+ /**
9656
10072
  * Which stream profile the footage is cut from. Absent = the CHEAPEST
9657
10073
  * assigned profile: a notification is watched on a phone, so the 4K
9658
10074
  * rendition would burn CPU to produce a file the client downscales anyway.
@@ -9724,7 +10140,30 @@ var NcRuleInputSchema = object({
9724
10140
  * behaviour, visible to all, read-only in the viewer). Present = personal
9725
10141
  * rule owned by this userId. Server-stamped; never trusted from a client.
9726
10142
  */
9727
- ownerUserId: string().optional()
10143
+ ownerUserId: string().optional(),
10144
+ /**
10145
+ * May a non-admin snooze this rule for EVERYONE, not just themselves?
10146
+ *
10147
+ * A snooze is personal by default — it silences the person who set it. This
10148
+ * opts THIS rule into the "the gardener is here all afternoon" case, where
10149
+ * silencing the camera for the whole household is legitimate. It silences
10150
+ * other people, so it is off unless a rule deliberately allows it.
10151
+ *
10152
+ * `.optional()`, deliberately NOT `.default()`: a Zod default does not run on
10153
+ * the addon cap path (three production failures in one day), so absent is
10154
+ * read as `false` by {@link canSetGlobal} in the engine. Admins are not bound
10155
+ * by this flag — see the scope rules on that function.
10156
+ */
10157
+ snoozeAllowGlobal: boolean().optional(),
10158
+ /**
10159
+ * Devices this rule ACTUATES — arm the alarm, open a gate, turn on a light.
10160
+ *
10161
+ * This is what makes the rule set the alarm's trigger set without the alarm
10162
+ * being a special case: arming is
10163
+ * `{ cap: 'alarm-panel', method: 'arm', args: { mode: 'away' } }`, the same
10164
+ * shape as every other actuation.
10165
+ */
10166
+ actions: NcRuleActionsSchema.optional()
9728
10167
  });
9729
10168
  /**
9730
10169
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -9795,7 +10234,8 @@ var NcConditionDescriptorSchema = object({
9795
10234
  "packagePhase",
9796
10235
  "crossingSelect",
9797
10236
  "polygonDraw",
9798
- "occupancy"
10237
+ "occupancy",
10238
+ "deviceState"
9799
10239
  ]),
9800
10240
  operator: _enum([
9801
10241
  "in",
@@ -9809,7 +10249,28 @@ var NcConditionDescriptorSchema = object({
9809
10249
  /** Which delivery kinds the condition applies to. */
9810
10250
  appliesTo: array(NcDeliverySchema),
9811
10251
  phase: string(),
9812
- description: string().optional()
10252
+ description: string().optional(),
10253
+ /**
10254
+ * The CHOICES for a single-choice widget (`sourceSelect`, `crossingSelect`,
10255
+ * `packagePhase`, …), served with the descriptor.
10256
+ *
10257
+ * Before this the descriptor said which widget to render and not what to put
10258
+ * in it, so every option list lived in three places: this file's enums, the
10259
+ * admin's `NC_*_OPTIONS` and the viewer's `NC_*_VALUES`. That triple mirror
10260
+ * is the drift that emptied the viewer's rule editor on 2026-08-04 — the app
10261
+ * mirrors the cap by hand, so it is only ever as current as its last build.
10262
+ *
10263
+ * With the options on the wire, a condition of an EXISTING `valueType` costs
10264
+ * zero client changes. Clients keep a local fallback for an older hub that
10265
+ * does not send them; absent here is "use your own list", not "no choices".
10266
+ */
10267
+ options: array(object({
10268
+ /** Written to the rule verbatim. `''` means the ABSENT state. */
10269
+ value: string(),
10270
+ label: string(),
10271
+ /** What THIS choice matches — shown one at a time, under the control. */
10272
+ hint: string().optional()
10273
+ })).readonly().optional()
9813
10274
  });
9814
10275
  /**
9815
10276
  * The delivery lifecycle status of a history row — a straight read of the
@@ -9894,6 +10355,74 @@ var NcHistoryFilterSchema = object({
9894
10355
  until: number().optional(),
9895
10356
  limit: number().int().min(1).max(500).default(100)
9896
10357
  });
10358
+ /**
10359
+ * What a snooze covers. Broader scopes win when several overlap, so one window
10360
+ * leaves ONE digest rather than a rule snooze and a whole-feed snooze both
10361
+ * summarising the same silence.
10362
+ */
10363
+ var NcSnoozeScopeSchema = _enum([
10364
+ "rule",
10365
+ "device",
10366
+ "all"
10367
+ ]);
10368
+ /**
10369
+ * Client-authored snooze. The server stamps `userId`, `startedAt` and
10370
+ * `expiresAt` — a DURATION is sent rather than an instant so a client with a
10371
+ * skewed clock cannot author a window that is already over, or never ends.
10372
+ */
10373
+ var NcSnoozeInputSchema = object({
10374
+ scope: NcSnoozeScopeSchema,
10375
+ /** Required when `scope: 'rule'` — a scoped snooze with no id matches
10376
+ * NOTHING rather than degrading to "everything". */
10377
+ ruleId: string().optional(),
10378
+ /** Required when `scope: 'device'`. */
10379
+ deviceId: number().int().optional(),
10380
+ durationMinutes: number().int().min(1).max(1440),
10381
+ /**
10382
+ * Silence this for EVERY recipient, not just the caller. Permission is
10383
+ * checked server-side (the rule's `snoozeAllowGlobal`, or admin for the
10384
+ * broader scopes). Absent = personal.
10385
+ */
10386
+ global: boolean().optional(),
10387
+ /**
10388
+ * Deliver a summary of what was suppressed when the window ends. Absent =
10389
+ * ON: someone silencing a nuisance camera wants it off, someone silencing a
10390
+ * SECURITY camera wants to know what they missed, and choosing "off" for
10391
+ * everybody is how a snooze becomes an outage. Resolved to a concrete
10392
+ * boolean by the server at create time — never left to a Zod default, which
10393
+ * does not run on the addon cap path.
10394
+ */
10395
+ summary: boolean().optional()
10396
+ });
10397
+ /** A persisted snooze window. */
10398
+ var NcSnoozeSchema = object({
10399
+ id: string(),
10400
+ /** Who set it. Also who it silences, unless `global`. */
10401
+ userId: string(),
10402
+ scope: NcSnoozeScopeSchema,
10403
+ ruleId: string().optional(),
10404
+ deviceId: number().int().optional(),
10405
+ startedAt: number(),
10406
+ /** Exclusive: at exactly this instant the snooze is over. Expiry is a
10407
+ * COMPARISON, not a job — no sweeper can leave the operator silenced. */
10408
+ expiresAt: number(),
10409
+ global: boolean(),
10410
+ summary: boolean(),
10411
+ /** When the end-of-window digest went out. Absent = not sent (yet, or the
10412
+ * window has not closed, or `summary` is false). */
10413
+ digestSentAt: number().optional()
10414
+ });
10415
+ object({
10416
+ snoozeId: string(),
10417
+ targetId: string(),
10418
+ ruleId: string(),
10419
+ ruleName: string(),
10420
+ deviceId: number().int(),
10421
+ /** How many notifications this snooze hid for that pair. */
10422
+ count: number().int(),
10423
+ firstAt: number(),
10424
+ lastAt: number()
10425
+ });
9897
10426
  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 }), {
9898
10427
  kind: "mutation",
9899
10428
  auth: "admin",
@@ -9923,7 +10452,13 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
9923
10452
  }), method(object({}), object({
9924
10453
  catalog: array(NcConditionDescriptorSchema),
9925
10454
  taxonomy: NcTaxonomySchema.optional()
9926
- })), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
10455
+ })), 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 }), {
10456
+ kind: "mutation",
10457
+ caller: "required"
10458
+ }), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
10459
+ kind: "mutation",
10460
+ caller: "required"
10461
+ });
9927
10462
  /**
9928
10463
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
9929
10464
  *
@@ -10671,7 +11206,15 @@ method(object({
10671
11206
  format: _enum(["gif", "mp4"]).default("gif"),
10672
11207
  maxWidth: number().int().min(120).max(1920).default(480),
10673
11208
  /** GIF only — MP4 keeps the source cadence. */
10674
- fps: number().int().min(1).max(15).default(5)
11209
+ fps: number().int().min(1).max(15).default(5),
11210
+ /**
11211
+ * Playback rate. A notification clip is GLANCED at on a lock screen,
11212
+ * not watched, so 2x is the default: the recipient sees the whole
11213
+ * passage in half the time and the GIF is half the bytes. `1` is real
11214
+ * time. Applies to MP4 as well — the operator set a speed, not a GIF
11215
+ * speed.
11216
+ */
11217
+ speed: number().min(1).max(8).default(2)
10675
11218
  }), object({
10676
11219
  base64: string(),
10677
11220
  mime: string(),
@@ -15457,6 +16000,210 @@ method(object({ codec: string() }), boolean()), method(_void(), object({
15457
16000
  kind: "mutation",
15458
16001
  auth: "admin"
15459
16002
  });
16003
+ /**
16004
+ * Query filter for settings-store collections.
16005
+ */
16006
+ var QueryFilterSchema = object({
16007
+ where: record(string(), unknown()).optional(),
16008
+ whereIn: record(string(), array(unknown())).optional(),
16009
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
16010
+ orderBy: object({
16011
+ field: string(),
16012
+ direction: _enum(["asc", "desc"])
16013
+ }).optional(),
16014
+ limit: number().optional(),
16015
+ offset: number().optional()
16016
+ });
16017
+ /**
16018
+ * The predicate half of a filter, for BULK MUTATIONS.
16019
+ *
16020
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
16021
+ * meaning for a statement that rewrites a set, and accepting them would invite
16022
+ * a caller to believe `limit` bounds the damage. Every field is optional here
16023
+ * only so the shape stays composable — the implementation REJECTS a filter
16024
+ * that compiles to no predicate, because that is the whole collection.
16025
+ */
16026
+ var MutationFilterSchema = object({
16027
+ where: record(string(), unknown()).optional(),
16028
+ whereIn: record(string(), array(unknown())).optional(),
16029
+ whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
16030
+ });
16031
+ /** A single stored record: `{ id, data }`. */
16032
+ var SettingsRecordSchema = object({
16033
+ id: string(),
16034
+ data: record(string(), unknown())
16035
+ });
16036
+ /**
16037
+ * Column declaration for a structured (SQL-backed) collection.
16038
+ *
16039
+ * Logical types — the backend translates each to the matching SQLite
16040
+ * storage class and handles per-type marshaling:
16041
+ * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
16042
+ * - `JSON` — TEXT under the hood; serialised on write, parsed on read
16043
+ * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
16044
+ */
16045
+ var CollectionColumnSchema = object({
16046
+ name: string(),
16047
+ type: _enum([
16048
+ "TEXT",
16049
+ "INTEGER",
16050
+ "REAL",
16051
+ "JSON",
16052
+ "BOOLEAN"
16053
+ ]),
16054
+ primaryKey: boolean().optional(),
16055
+ notNull: boolean().optional(),
16056
+ unique: boolean().optional(),
16057
+ /**
16058
+ * Column DEFAULT. Required for B2: the four `ensureTable` consumers declare
16059
+ * columns like `enabled INTEGER NOT NULL DEFAULT 1`, and without this the
16060
+ * collection surface simply cannot express their existing tables — which is
16061
+ * why they were still on the uncapped `table*` API.
16062
+ */
16063
+ defaultValue: union([
16064
+ string(),
16065
+ number(),
16066
+ boolean()
16067
+ ]).optional()
16068
+ });
16069
+ var CollectionIndexSchema = object({
16070
+ name: string(),
16071
+ columns: array(string()).readonly(),
16072
+ unique: boolean().optional()
16073
+ });
16074
+ method(object({
16075
+ namespace: string().optional(),
16076
+ collection: string(),
16077
+ key: string()
16078
+ }), unknown()), method(object({
16079
+ namespace: string().optional(),
16080
+ collection: string(),
16081
+ key: string(),
16082
+ value: unknown()
16083
+ }), _void(), { kind: "mutation" }), method(object({
16084
+ namespace: string().optional(),
16085
+ collection: string(),
16086
+ filter: QueryFilterSchema.optional()
16087
+ }), array(SettingsRecordSchema).readonly()), method(object({
16088
+ namespace: string().optional(),
16089
+ collection: string(),
16090
+ record: SettingsRecordSchema
16091
+ }), _void(), { kind: "mutation" }), method(object({
16092
+ namespace: string().optional(),
16093
+ collection: string(),
16094
+ id: string(),
16095
+ data: record(string(), unknown())
16096
+ }), _void(), { kind: "mutation" }), method(object({
16097
+ namespace: string().optional(),
16098
+ collection: string(),
16099
+ key: string()
16100
+ }), _void(), { kind: "mutation" }), method(object({
16101
+ namespace: string().optional(),
16102
+ collection: string(),
16103
+ filter: MutationFilterSchema
16104
+ }), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
16105
+ namespace: string().optional(),
16106
+ collection: string(),
16107
+ filter: MutationFilterSchema,
16108
+ data: record(string(), unknown())
16109
+ }), object({ updated: number().int() }), { kind: "mutation" }), method(object({
16110
+ namespace: string().optional(),
16111
+ collection: string(),
16112
+ filter: QueryFilterSchema.optional()
16113
+ }), number()), method(object({
16114
+ namespace: string().optional(),
16115
+ collection: string(),
16116
+ field: string(),
16117
+ bucketSize: number().int().positive(),
16118
+ origin: number().int(),
16119
+ filter: QueryFilterSchema.optional()
16120
+ }), array(object({
16121
+ bucket: number().int(),
16122
+ count: number().int()
16123
+ })).readonly()), method(object({
16124
+ namespace: string().optional(),
16125
+ collection: string()
16126
+ }), boolean()), method(object({
16127
+ namespace: string().optional(),
16128
+ collection: string(),
16129
+ columns: array(CollectionColumnSchema).readonly(),
16130
+ indexes: array(CollectionIndexSchema).readonly().optional()
16131
+ }), _void(), { kind: "mutation" });
16132
+ /**
16133
+ * What one engine says about itself. The orchestrator uses `kind` to pick
16134
+ * a registrant for a collection; `engineId` is what a log line names when
16135
+ * a call is routed or refused.
16136
+ */
16137
+ var EngineInfoSchema = object({
16138
+ engineId: string(),
16139
+ /**
16140
+ * `relational` — rows, columns, indexes, the surface `settings-store`
16141
+ * has always described. `vector` — an embedding store answering
16142
+ * similarity queries. A registrant declares exactly one; an engine that
16143
+ * does both registers twice, because "both" would make the routing
16144
+ * decision ambiguous at exactly the point it must not be.
16145
+ */
16146
+ kind: _enum(["relational", "vector"]),
16147
+ displayName: string()
16148
+ });
16149
+ method(_void(), EngineInfoSchema), method(object({
16150
+ namespace: string().optional(),
16151
+ collection: string(),
16152
+ key: string()
16153
+ }), unknown()), method(object({
16154
+ namespace: string().optional(),
16155
+ collection: string(),
16156
+ key: string(),
16157
+ value: unknown()
16158
+ }), _void(), { kind: "mutation" }), method(object({
16159
+ namespace: string().optional(),
16160
+ collection: string(),
16161
+ filter: QueryFilterSchema.optional()
16162
+ }), array(SettingsRecordSchema).readonly()), method(object({
16163
+ namespace: string().optional(),
16164
+ collection: string(),
16165
+ record: SettingsRecordSchema
16166
+ }), _void(), { kind: "mutation" }), method(object({
16167
+ namespace: string().optional(),
16168
+ collection: string(),
16169
+ id: string(),
16170
+ data: record(string(), unknown())
16171
+ }), _void(), { kind: "mutation" }), method(object({
16172
+ namespace: string().optional(),
16173
+ collection: string(),
16174
+ key: string()
16175
+ }), _void(), { kind: "mutation" }), method(object({
16176
+ namespace: string().optional(),
16177
+ collection: string(),
16178
+ filter: MutationFilterSchema
16179
+ }), object({ deleted: number().int() }), { kind: "mutation" }), method(object({
16180
+ namespace: string().optional(),
16181
+ collection: string(),
16182
+ filter: MutationFilterSchema,
16183
+ data: record(string(), unknown())
16184
+ }), object({ updated: number().int() }), { kind: "mutation" }), method(object({
16185
+ namespace: string().optional(),
16186
+ collection: string(),
16187
+ filter: QueryFilterSchema.optional()
16188
+ }), number()), method(object({
16189
+ namespace: string().optional(),
16190
+ collection: string(),
16191
+ field: string(),
16192
+ bucketSize: number().int().positive(),
16193
+ origin: number().int(),
16194
+ filter: QueryFilterSchema.optional()
16195
+ }), array(object({
16196
+ bucket: number().int(),
16197
+ count: number().int()
16198
+ })).readonly()), method(object({
16199
+ namespace: string().optional(),
16200
+ collection: string()
16201
+ }), boolean()), method(object({
16202
+ namespace: string().optional(),
16203
+ collection: string(),
16204
+ columns: array(CollectionColumnSchema).readonly(),
16205
+ indexes: array(CollectionIndexSchema).readonly().optional()
16206
+ }), _void(), { kind: "mutation" });
15460
16207
  DeviceType.Camera;
15461
16208
  /**
15462
16209
  * `device-adoption` — generic discovery + adoption surface,
@@ -16135,7 +16882,20 @@ method(object({
16135
16882
  }), _void(), {
16136
16883
  kind: "mutation",
16137
16884
  auth: "admin"
16138
- }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({ addonId: string().optional() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
16885
+ }), method(object({ addonId: string() }), array(SavedDeviceRowSchema)), method(object({
16886
+ addonId: string().optional(),
16887
+ /**
16888
+ * `slim` omits `config` (returned `{}`), `metadata` (null) and the
16889
+ * `sourceInfo` derived from config — and skips the per-device settings
16890
+ * read that produces them. Everything identifying a device (id, name,
16891
+ * type, online, features, isCamera, parent/link ids) is unchanged.
16892
+ * Do not use it for dispatch routing, which needs `sourceInfo`.
16893
+ */
16894
+ projection: _enum(["full", "slim"]).optional(),
16895
+ /** Return only camera devices. Filtering server-side instead of
16896
+ * shipping 293 rows to find 12. */
16897
+ isCamera: boolean().optional()
16898
+ }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
16139
16899
  mode: DeviceLinkModeSchema,
16140
16900
  devices: array(LinkedDeviceSchema)
16141
16901
  })), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
@@ -16574,14 +17334,14 @@ var LlmProfileSchema = object({
16574
17334
  /** ConfigUISchema tree passed through untyped on the wire (the
16575
17335
  * notification-output `ConfigSchemaPassthrough` precedent at
16576
17336
  * notification-output.cap.ts:151); the exported TS type re-tightens it. */
16577
- var ConfigSchemaPassthrough$1 = unknown();
17337
+ var ConfigSchemaPassthrough = unknown();
16578
17338
  var LlmProfileKindDescriptorSchema = object({
16579
17339
  kind: LlmProfileKindSchema,
16580
17340
  label: string(),
16581
17341
  icon: string(),
16582
17342
  /** Stamped by each provider so the concat-fanned catalog stays routable. */
16583
17343
  addonId: string(),
16584
- configSchema: ConfigSchemaPassthrough$1
17344
+ configSchema: ConfigSchemaPassthrough
16585
17345
  });
16586
17346
  var LlmDefaultSelectorSchema = union([object({ consumer: string() }), object({ purpose: _enum(["text", "vision"]) })]);
16587
17347
  var LlmDefaultSchema = object({
@@ -17112,241 +17872,102 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
17112
17872
  });
17113
17873
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
17114
17874
  /**
17115
- * notification-outputcanonical, capability-gated notification delivery.
17116
- *
17117
- * Apprise-derived model (see
17118
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
17119
- * callers emit ONE canonical `Notification`; each provider declares a
17120
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
17121
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
17122
- * message to what the kind supports callers never special-case a service.
17123
- *
17124
- * DESIGN DECISIONS (locked):
17125
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
17126
- * `setTargetEnabled`), each provider persisting via the `settings-store`
17127
- * cap. Rationale: the admin UI needs one uniform surface across the
17128
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
17129
- * alternative would fork the UI per addon and cannot host the
17130
- * discovery→adopt flow.
17131
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
17132
- * the generated cap-mount auto-`concatCollection`-fans them across every
17133
- * registered provider (notifiers addon + HA addon) so one catalog is
17134
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
17135
- * `addonId` the generated collection router extracts from the call input.
17136
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
17137
- * transformer) + UDS MsgPack both round-trip typed arrays already used by
17138
- * `storage` / `storage-provider` / `recording` caps over the same path. No
17139
- * base64 fallback needed.
17140
- *
17141
- * TODO (deferred, closed-set change — separate decision): add
17142
- * `providerKind: 'notify'` so notification providers surface on the unified
17143
- * admin "Integrations" page.
17144
- */
17145
- /**
17146
- * Zentik-derived typed-media enum — the superset across every kind. Each
17147
- * adapter picks what it supports and the degrade engine filters the rest.
17148
- */
17149
- var AttachmentMediaTypeSchema = _enum([
17150
- "image",
17151
- "video",
17152
- "gif",
17153
- "audio",
17154
- "icon"
17155
- ]);
17156
- /**
17157
- * A single attachment. Exactly one of `url` (remote source, most adapters
17158
- * prefer this) or `bytes` (inline source; required for Pushover-style
17159
- * bytes-only kinds) MUST be present — the degrade engine expresses a
17160
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
17161
- */
17162
- var AttachmentSchema = object({
17163
- mediaType: AttachmentMediaTypeSchema,
17164
- url: string().optional(),
17165
- bytes: _instanceof(Uint8Array).optional(),
17166
- mime: string().optional(),
17167
- name: string().optional()
17168
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
17169
- var NotificationFormatSchema = _enum([
17170
- "text",
17171
- "markdown",
17172
- "html"
17875
+ * core-blocksuser-authored TypeScript, stored in the kernel and executed in
17876
+ * its own process.
17877
+ *
17878
+ * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
17879
+ *
17880
+ * The first use is **owning devices without being a device provider**: a block
17881
+ * declares devices under a system or custom integration and drives their state,
17882
+ * with the same `ctx` an addon gets. Automations come later; nothing here
17883
+ * models a trigger.
17884
+ *
17885
+ * **Stated plainly, because it does not change by being true:** a block has an
17886
+ * addon's powers devices, storage, the event bus, `ctx.api`. It is a plugin
17887
+ * with no review step. What makes that survivable is not a sandbox, it is
17888
+ * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
17889
+ * so a block that throws or never returns is marked `failed` and visible
17890
+ * instead of taking the hub with it (D6). Every method here is admin-only, and
17891
+ * must stay so.
17892
+ */
17893
+ /** Where a block runs. The operator chooses a block driving a device on an
17894
+ * agent is the reason placement is not fixed to the hub. */
17895
+ var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
17896
+ /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
17897
+ * a failing block reads the same way a failing addon does. */
17898
+ var CoreBlockStatusSchema = _enum([
17899
+ "stopped",
17900
+ "starting",
17901
+ "running",
17902
+ "failed"
17173
17903
  ]);
17174
- /** A single tap-through action button. */
17175
- var NotificationActionSchema = object({
17176
- id: string(),
17177
- label: string(),
17178
- url: string().optional()
17179
- });
17180
- /**
17181
- * The canonical notification. `body` is the only hard field (Apprise model).
17182
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
17183
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
17184
- * the adapter maps this ordinal onto its native level. `level?` is an
17185
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
17186
- * `priority` for that one target.
17187
- */
17188
- var NotificationSchema = object({
17189
- body: string(),
17190
- title: string().optional(),
17191
- format: NotificationFormatSchema.default("text"),
17192
- priority: number().int().min(1).max(5).default(3),
17193
- level: string().optional(),
17194
- attachments: array(AttachmentSchema).optional(),
17195
- clickUrl: string().optional(),
17196
- actions: array(NotificationActionSchema).optional(),
17197
- sound: string().optional(),
17198
- ttl: number().optional(),
17199
- tag: string().optional(),
17200
- deviceId: number().optional(),
17201
- eventId: string().optional(),
17202
- metadata: record(string(), unknown()).optional()
17203
- });
17204
- /** One declared native severity/priority level for a kind. */
17205
- var TargetKindLevelSchema = object({
17206
- id: string(),
17207
- label: string(),
17208
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
17209
- ordinal: number().int().min(1).max(5).nullable(),
17210
- flags: object({
17211
- critical: boolean().optional(),
17212
- silent: boolean().optional(),
17213
- noPush: boolean().optional()
17214
- }).optional(),
17215
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
17216
- requires: array(string()).optional(),
17217
- description: string().optional()
17218
- });
17219
- /** The full capability block consulted before dispatch. */
17220
- var TargetKindCapsSchema = object({
17221
- attachments: object({
17222
- mediaTypes: array(AttachmentMediaTypeSchema),
17223
- mode: _enum([
17224
- "url",
17225
- "bytes",
17226
- "both"
17227
- ]),
17228
- max: number().int().nonnegative(),
17229
- maxBytes: number().int().positive().optional()
17230
- }),
17231
- /** Max action buttons (0 = none). */
17232
- actions: number().int().nonnegative(),
17233
- levels: array(TargetKindLevelSchema),
17234
- format: array(NotificationFormatSchema),
17235
- clickUrl: boolean(),
17236
- sound: boolean(),
17237
- ttl: boolean(),
17238
- bodyMaxLen: number().int().positive()
17239
- });
17240
- /**
17241
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
17242
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
17243
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
17244
- * the union is large and not meant for runtime validation here; the exported
17245
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
17246
- */
17247
- var ConfigSchemaPassthrough = unknown();
17248
- var TargetKindSchema = object({
17249
- kind: string(),
17250
- label: string(),
17251
- icon: string(),
17252
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
17253
- addonId: string(),
17254
- /**
17255
- * URL of the kind's bundled BRAND icon, served by the providing addon over
17256
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
17257
- * when the addon bundles no icon for that kind — the client then falls back
17258
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
17259
- *
17260
- * Root-relative on purpose: it resolves against whatever origin serves a web
17261
- * client, and a native client joins it onto its own hub base.
17262
- *
17263
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
17264
- * field that survived only because the runtime cap-router forwards provider
17265
- * output verbatim — so every consumer had to re-declare it by hand to stop
17266
- * its own Zod parse from stripping it, and the whole arrangement would have
17267
- * broken silently the moment output validation was tightened anywhere.
17268
- */
17269
- iconUrl: string().optional(),
17904
+ /** Client-authored fields. */
17905
+ var CoreBlockInputSchema = object({
17906
+ name: string().min(1).max(120),
17907
+ /** TypeScript source. Compiled server-side before it is ever stored — a
17908
+ * block that does not compile is a fork failure the operator would meet
17909
+ * minutes later, in a log, instead of in the editor. */
17910
+ code: string().max(2e5),
17911
+ enabled: boolean(),
17912
+ placement: CoreBlockPlacementSchema,
17270
17913
  /**
17271
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
17272
- *
17273
- * The server knows this and therefore says it, because the client cannot
17274
- * safely guess: a React-Native client renders SVG and raster through two
17275
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
17276
- * not decode SVG on iOS/Android), so without this it silently fell back to a
17277
- * placeholder glyph for every vector icon while the web build looked fine.
17278
- *
17279
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
17280
- * not been updated — a client that cannot determine the type should prefer
17281
- * its raster path, which is the safe default for an unknown image.
17914
+ * Integration the block's devices hang from. Absent = the system integration
17915
+ * blocks share. A block may declare its own instead.
17282
17916
  */
17283
- iconMediaType: string().optional(),
17284
- configSchema: ConfigSchemaPassthrough,
17285
- supportsDiscovery: boolean(),
17286
- caps: TargetKindCapsSchema
17917
+ integrationId: string().optional()
17287
17918
  });
17288
- /**
17289
- * A persisted target. `config` holds secrets; providers REDACT secret fields
17290
- * (return a presence marker only) when serving `listTargets` — never
17291
- * round-trip a stored secret to the UI.
17292
- */
17293
- var TargetSchema = object({
17919
+ /** A stored block. */
17920
+ var CoreBlockSchema = CoreBlockInputSchema.extend({
17294
17921
  id: string(),
17295
- name: string(),
17296
- kind: string(),
17297
- addonId: string(),
17298
- enabled: boolean(),
17299
- config: record(string(), unknown())
17300
- });
17301
- /** A discovery-surfaced candidate (config is partial + non-secret). */
17302
- var DiscoveredTargetSchema = object({
17303
- kind: string(),
17304
- suggestedName: string(),
17305
- config: record(string(), unknown())
17306
- });
17307
- /** The degrade engine's report — what was resolved / dropped / degraded. */
17308
- var RenderedAsSchema = object({
17309
- level: string(),
17310
- format: NotificationFormatSchema,
17311
- attachmentsSent: number().int().nonnegative(),
17312
- actionsSent: number().int().nonnegative(),
17313
- truncated: boolean(),
17314
- dropped: array(string())
17315
- });
17316
- var SendResultSchema = object({
17317
- success: boolean(),
17318
- error: string().optional(),
17319
- renderedAs: RenderedAsSchema.optional()
17320
- });
17321
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
17322
- var TestResultSchema = SendResultSchema;
17323
- var notificationOutputCapability = {
17324
- name: "notification-output",
17325
- scope: "system",
17326
- mode: "collection",
17327
- methods: {
17328
- listTargetKinds: method(object({}), array(TargetKindSchema)),
17329
- listTargets: method(object({}), array(TargetSchema)),
17330
- discoverTargets: method(object({
17331
- kind: string(),
17332
- config: record(string(), unknown()).optional()
17333
- }), array(DiscoveredTargetSchema)),
17334
- send: method(object({
17335
- targetId: string(),
17336
- notification: NotificationSchema
17337
- }), SendResultSchema, { kind: "mutation" }),
17338
- testTarget: method(object({
17339
- targetId: string(),
17340
- sample: NotificationSchema.optional()
17341
- }), TestResultSchema, { kind: "mutation" }),
17342
- upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
17343
- deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
17344
- setTargetEnabled: method(object({
17345
- targetId: string(),
17346
- enabled: boolean()
17347
- }), _void(), { kind: "mutation" })
17348
- }
17349
- };
17922
+ createdAt: number(),
17923
+ updatedAt: number(),
17924
+ /** Server-stamped author. */
17925
+ createdBy: string(),
17926
+ status: CoreBlockStatusSchema,
17927
+ /**
17928
+ * Why the block is not running, when it is not. The operator's ONLY window
17929
+ * into a block that failed at load — a block that is silently absent is the
17930
+ * failure mode this whole feature has to avoid.
17931
+ */
17932
+ lastError: string().optional(),
17933
+ /** Ms epoch of the last state change. */
17934
+ lastChangedAt: number()
17935
+ });
17936
+ /** What a compile attempt produced. */
17937
+ var CoreBlockCompileResultSchema = object({
17938
+ ok: boolean(),
17939
+ /** Present when `ok` is false — the first error, in the author's words. */
17940
+ error: string().optional(),
17941
+ line: number().optional(),
17942
+ column: number().optional()
17943
+ });
17944
+ 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 }), {
17945
+ kind: "mutation",
17946
+ auth: "admin",
17947
+ caller: "required"
17948
+ }), method(object({
17949
+ blockId: string(),
17950
+ block: CoreBlockInputSchema.partial()
17951
+ }), object({ block: CoreBlockSchema }), {
17952
+ kind: "mutation",
17953
+ auth: "admin",
17954
+ caller: "required"
17955
+ }), method(object({ blockId: string() }), object({ success: literal(true) }), {
17956
+ kind: "mutation",
17957
+ auth: "admin"
17958
+ }), method(object({
17959
+ blockId: string(),
17960
+ enabled: boolean()
17961
+ }), object({ block: CoreBlockSchema }), {
17962
+ kind: "mutation",
17963
+ auth: "admin"
17964
+ }), method(object({ code: string() }), CoreBlockCompileResultSchema, {
17965
+ kind: "mutation",
17966
+ auth: "admin"
17967
+ }), method(object({}), object({ libs: array(object({
17968
+ filePath: string(),
17969
+ content: string()
17970
+ })) }), { auth: "admin" });
17350
17971
  /**
17351
17972
  * Zod schemas for persisted record types.
17352
17973
  *
@@ -17594,6 +18215,11 @@ var EventKindDescriptorSchema = object({
17594
18215
  deviceId: number()
17595
18216
  })
17596
18217
  });
18218
+ /** One camera's event vocabulary, as returned by `listEventKindsBatch`. */
18219
+ var EventKindsForDeviceSchema = object({
18220
+ deviceId: number(),
18221
+ kinds: array(EventKindDescriptorSchema).readonly()
18222
+ });
17597
18223
  var SensorEventSchema = object({
17598
18224
  id: string(),
17599
18225
  /** The CAMERA the event is attributed to (a sensor linked to N cameras
@@ -17850,6 +18476,19 @@ var MediaFileSchema = object({
17850
18476
  sizeBytes: number(),
17851
18477
  timestamp: number()
17852
18478
  });
18479
+ /**
18480
+ * One media row WITHOUT its bytes.
18481
+ *
18482
+ * A track's media is 5-8 MB of base64 (measured: 7.67 MB across 23 files for a
18483
+ * 140 s track), and a client that renders tiles from the media data plane needs
18484
+ * to know only WHAT EXISTS — the bytes then arrive per tile, lazily, over HTTP
18485
+ * with an immutable cache, instead of all at once inside a tRPC response that
18486
+ * blocks the whole view.
18487
+ *
18488
+ * `sizeBytes` is carried because it is what lets a client decide between the
18489
+ * stored blob and a `?variant=thumb` rendering without fetching either.
18490
+ */
18491
+ var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
17853
18492
  var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
17854
18493
  var MAX_EVENT_QUERY_LIMIT = 5e3;
17855
18494
  var DeviceEventQueryInput = object({
@@ -17999,7 +18638,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
17999
18638
  }), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
18000
18639
  kind: "mutation",
18001
18640
  auth: "admin"
18002
- }), method(DeviceEventQueryInput, array(MotionEventSchema).readonly()), method(ObjectEventQueryInput, array(ObjectEventSchema).readonly()), method(DeviceEventQueryInput, array(AudioEventSchema).readonly()), method(object({ deviceId: number() }), array(EventKindDescriptorSchema).readonly()), method(object({
18641
+ }), 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({
18003
18642
  deviceId: number(),
18004
18643
  since: number().optional(),
18005
18644
  until: number().optional(),
@@ -18073,7 +18712,7 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
18073
18712
  }), array(MediaFileSchema).readonly()), method(object({
18074
18713
  trackId: string(),
18075
18714
  kinds: array(MediaFileKindEnum).optional()
18076
- }), array(MediaFileSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
18715
+ }), array(MediaFileSchema).readonly()), method(object({ trackId: string() }), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), object({
18077
18716
  deviceId: number(),
18078
18717
  timestamp: number(),
18079
18718
  frameWidth: number(),
@@ -18703,6 +19342,28 @@ var ServerUpdateStateSchema = _enum([
18703
19342
  "pending-restart",
18704
19343
  "awaiting-confirmation"
18705
19344
  ]);
19345
+ var ImageContractSchema = object({
19346
+ state: _enum([
19347
+ "in-sync",
19348
+ "behind-patch",
19349
+ "behind-series",
19350
+ "ahead",
19351
+ "unknown"
19352
+ ]),
19353
+ /** The baked seed closure version — the image/app-bundle fingerprint. */
19354
+ seedVersion: string().nullable(),
19355
+ /** Best-known version the deployment contract delivers today. */
19356
+ contractVersion: string().nullable(),
19357
+ /**
19358
+ * Where `contractVersion` came from: a real registry check (`registry`), or
19359
+ * the node's own running version (`running` — a node can never run code
19360
+ * newer than the newest release, so `seed < running` proves image staleness
19361
+ * even before any registry check has run).
19362
+ */
19363
+ contractSource: _enum(["registry", "running"]).nullable(),
19364
+ /** One operator-grade sentence: this node runs image X; the contract says Y. */
19365
+ message: string()
19366
+ });
18706
19367
  var ServerRollbackInfoSchema = object({
18707
19368
  /** The version that failed (or was manually rolled back). */
18708
19369
  fromVersion: string(),
@@ -18739,7 +19400,12 @@ var ServerPackageStatusSchema = object({
18739
19400
  * versions are being IGNORED. Surfaced as a warning in the UI.
18740
19401
  */
18741
19402
  stateFileCorrupt: boolean(),
18742
- lastCheckedAtMs: number().nullable()
19403
+ lastCheckedAtMs: number().nullable(),
19404
+ /**
19405
+ * Seed-vs-contract verdict (see {@link ImageContractSchema}). Optional for
19406
+ * version skew: an older provider's payload simply omits it.
19407
+ */
19408
+ imageContract: ImageContractSchema.optional()
18743
19409
  });
18744
19410
  var ServerUpdateCheckResultSchema = object({
18745
19411
  packageName: string(),
@@ -18774,101 +19440,6 @@ version: string().optional() }), ServerUpdateActionResultSchema, {
18774
19440
  auth: "admin"
18775
19441
  });
18776
19442
  /**
18777
- * Query filter for settings-store collections.
18778
- */
18779
- var QueryFilterSchema = object({
18780
- where: record(string(), unknown()).optional(),
18781
- whereIn: record(string(), array(unknown())).optional(),
18782
- whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
18783
- orderBy: object({
18784
- field: string(),
18785
- direction: _enum(["asc", "desc"])
18786
- }).optional(),
18787
- limit: number().optional(),
18788
- offset: number().optional()
18789
- });
18790
- /** A single stored record: `{ id, data }`. */
18791
- var SettingsRecordSchema = object({
18792
- id: string(),
18793
- data: record(string(), unknown())
18794
- });
18795
- /**
18796
- * Column declaration for a structured (SQL-backed) collection.
18797
- *
18798
- * Logical types — the backend translates each to the matching SQLite
18799
- * storage class and handles per-type marshaling:
18800
- * - `TEXT` / `INTEGER` / `REAL` — native SQLite types, pass-through
18801
- * - `JSON` — TEXT under the hood; serialised on write, parsed on read
18802
- * - `BOOLEAN` — INTEGER 0/1 under the hood; coerced both directions
18803
- */
18804
- var CollectionColumnSchema = object({
18805
- name: string(),
18806
- type: _enum([
18807
- "TEXT",
18808
- "INTEGER",
18809
- "REAL",
18810
- "JSON",
18811
- "BOOLEAN"
18812
- ]),
18813
- primaryKey: boolean().optional(),
18814
- notNull: boolean().optional(),
18815
- unique: boolean().optional()
18816
- });
18817
- var CollectionIndexSchema = object({
18818
- name: string(),
18819
- columns: array(string()).readonly(),
18820
- unique: boolean().optional()
18821
- });
18822
- method(object({
18823
- namespace: string().optional(),
18824
- collection: string(),
18825
- key: string()
18826
- }), unknown()), method(object({
18827
- namespace: string().optional(),
18828
- collection: string(),
18829
- key: string(),
18830
- value: unknown()
18831
- }), _void(), { kind: "mutation" }), method(object({
18832
- namespace: string().optional(),
18833
- collection: string(),
18834
- filter: QueryFilterSchema.optional()
18835
- }), array(SettingsRecordSchema).readonly()), method(object({
18836
- namespace: string().optional(),
18837
- collection: string(),
18838
- record: SettingsRecordSchema
18839
- }), _void(), { kind: "mutation" }), method(object({
18840
- namespace: string().optional(),
18841
- collection: string(),
18842
- id: string(),
18843
- data: record(string(), unknown())
18844
- }), _void(), { kind: "mutation" }), method(object({
18845
- namespace: string().optional(),
18846
- collection: string(),
18847
- key: string()
18848
- }), _void(), { kind: "mutation" }), method(object({
18849
- namespace: string().optional(),
18850
- collection: string(),
18851
- filter: QueryFilterSchema.optional()
18852
- }), number()), method(object({
18853
- namespace: string().optional(),
18854
- collection: string(),
18855
- field: string(),
18856
- bucketSize: number().int().positive(),
18857
- origin: number().int(),
18858
- filter: QueryFilterSchema.optional()
18859
- }), array(object({
18860
- bucket: number().int(),
18861
- count: number().int()
18862
- })).readonly()), method(object({
18863
- namespace: string().optional(),
18864
- collection: string()
18865
- }), boolean()), method(object({
18866
- namespace: string().optional(),
18867
- collection: string(),
18868
- columns: array(CollectionColumnSchema).readonly(),
18869
- indexes: array(CollectionIndexSchema).readonly().optional()
18870
- }), _void(), { kind: "mutation" });
18871
- /**
18872
19443
  * `smtp-provider` — pluggable email delivery surface.
18873
19444
  *
18874
19445
  * Collection cap: a deployment may install multiple SMTP relays (e.g.
@@ -22731,6 +23302,54 @@ Object.freeze({
22731
23302
  addonId: null,
22732
23303
  access: "create"
22733
23304
  },
23305
+ "coreBlocks.compile": {
23306
+ capName: "core-blocks",
23307
+ capScope: "system",
23308
+ addonId: null,
23309
+ access: "create"
23310
+ },
23311
+ "coreBlocks.create": {
23312
+ capName: "core-blocks",
23313
+ capScope: "system",
23314
+ addonId: null,
23315
+ access: "create"
23316
+ },
23317
+ "coreBlocks.delete": {
23318
+ capName: "core-blocks",
23319
+ capScope: "system",
23320
+ addonId: null,
23321
+ access: "delete"
23322
+ },
23323
+ "coreBlocks.get": {
23324
+ capName: "core-blocks",
23325
+ capScope: "system",
23326
+ addonId: null,
23327
+ access: "view"
23328
+ },
23329
+ "coreBlocks.getTypeDefs": {
23330
+ capName: "core-blocks",
23331
+ capScope: "system",
23332
+ addonId: null,
23333
+ access: "view"
23334
+ },
23335
+ "coreBlocks.list": {
23336
+ capName: "core-blocks",
23337
+ capScope: "system",
23338
+ addonId: null,
23339
+ access: "view"
23340
+ },
23341
+ "coreBlocks.setEnabled": {
23342
+ capName: "core-blocks",
23343
+ capScope: "system",
23344
+ addonId: null,
23345
+ access: "create"
23346
+ },
23347
+ "coreBlocks.update": {
23348
+ capName: "core-blocks",
23349
+ capScope: "system",
23350
+ addonId: null,
23351
+ access: "create"
23352
+ },
22734
23353
  "cover.close": {
22735
23354
  capName: "cover",
22736
23355
  capScope: "device",
@@ -22767,6 +23386,84 @@ Object.freeze({
22767
23386
  addonId: null,
22768
23387
  access: "view"
22769
23388
  },
23389
+ "dataStoreProvider.count": {
23390
+ capName: "data-store-provider",
23391
+ capScope: "system",
23392
+ addonId: null,
23393
+ access: "view"
23394
+ },
23395
+ "dataStoreProvider.declareCollection": {
23396
+ capName: "data-store-provider",
23397
+ capScope: "system",
23398
+ addonId: null,
23399
+ access: "create"
23400
+ },
23401
+ "dataStoreProvider.delete": {
23402
+ capName: "data-store-provider",
23403
+ capScope: "system",
23404
+ addonId: null,
23405
+ access: "delete"
23406
+ },
23407
+ "dataStoreProvider.deleteWhere": {
23408
+ capName: "data-store-provider",
23409
+ capScope: "system",
23410
+ addonId: null,
23411
+ access: "delete"
23412
+ },
23413
+ "dataStoreProvider.get": {
23414
+ capName: "data-store-provider",
23415
+ capScope: "system",
23416
+ addonId: null,
23417
+ access: "view"
23418
+ },
23419
+ "dataStoreProvider.getEngineInfo": {
23420
+ capName: "data-store-provider",
23421
+ capScope: "system",
23422
+ addonId: null,
23423
+ access: "view"
23424
+ },
23425
+ "dataStoreProvider.histogram": {
23426
+ capName: "data-store-provider",
23427
+ capScope: "system",
23428
+ addonId: null,
23429
+ access: "view"
23430
+ },
23431
+ "dataStoreProvider.insert": {
23432
+ capName: "data-store-provider",
23433
+ capScope: "system",
23434
+ addonId: null,
23435
+ access: "create"
23436
+ },
23437
+ "dataStoreProvider.isEmpty": {
23438
+ capName: "data-store-provider",
23439
+ capScope: "system",
23440
+ addonId: null,
23441
+ access: "view"
23442
+ },
23443
+ "dataStoreProvider.query": {
23444
+ capName: "data-store-provider",
23445
+ capScope: "system",
23446
+ addonId: null,
23447
+ access: "view"
23448
+ },
23449
+ "dataStoreProvider.set": {
23450
+ capName: "data-store-provider",
23451
+ capScope: "system",
23452
+ addonId: null,
23453
+ access: "create"
23454
+ },
23455
+ "dataStoreProvider.update": {
23456
+ capName: "data-store-provider",
23457
+ capScope: "system",
23458
+ addonId: null,
23459
+ access: "create"
23460
+ },
23461
+ "dataStoreProvider.updateWhere": {
23462
+ capName: "data-store-provider",
23463
+ capScope: "system",
23464
+ addonId: null,
23465
+ access: "create"
23466
+ },
22770
23467
  "dayNight.getOptions": {
22771
23468
  capName: "day-night",
22772
23469
  capScope: "device",
@@ -24561,12 +25258,24 @@ Object.freeze({
24561
25258
  addonId: null,
24562
25259
  access: "create"
24563
25260
  },
25261
+ "notificationRules.cancelSnooze": {
25262
+ capName: "notification-rules",
25263
+ capScope: "system",
25264
+ addonId: null,
25265
+ access: "create"
25266
+ },
24564
25267
  "notificationRules.createRule": {
24565
25268
  capName: "notification-rules",
24566
25269
  capScope: "system",
24567
25270
  addonId: null,
24568
25271
  access: "create"
24569
25272
  },
25273
+ "notificationRules.createSnooze": {
25274
+ capName: "notification-rules",
25275
+ capScope: "system",
25276
+ addonId: null,
25277
+ access: "create"
25278
+ },
24570
25279
  "notificationRules.deleteRule": {
24571
25280
  capName: "notification-rules",
24572
25281
  capScope: "system",
@@ -24597,6 +25306,12 @@ Object.freeze({
24597
25306
  addonId: null,
24598
25307
  access: "view"
24599
25308
  },
25309
+ "notificationRules.listSnoozes": {
25310
+ capName: "notification-rules",
25311
+ capScope: "system",
25312
+ addonId: null,
25313
+ access: "view"
25314
+ },
24600
25315
  "notificationRules.setRuleEnabled": {
24601
25316
  capName: "notification-rules",
24602
25317
  capScope: "system",
@@ -24801,6 +25516,12 @@ Object.freeze({
24801
25516
  addonId: null,
24802
25517
  access: "view"
24803
25518
  },
25519
+ "pipelineAnalytics.listEventKindsBatch": {
25520
+ capName: "pipeline-analytics",
25521
+ capScope: "device",
25522
+ addonId: null,
25523
+ access: "view"
25524
+ },
24804
25525
  "pipelineAnalytics.listOpsLog": {
24805
25526
  capName: "pipeline-analytics",
24806
25527
  capScope: "device",
@@ -24813,6 +25534,12 @@ Object.freeze({
24813
25534
  addonId: null,
24814
25535
  access: "view"
24815
25536
  },
25537
+ "pipelineAnalytics.listTrackMedia": {
25538
+ capName: "pipeline-analytics",
25539
+ capScope: "device",
25540
+ addonId: null,
25541
+ access: "view"
25542
+ },
24816
25543
  "pipelineAnalytics.listTracks": {
24817
25544
  capName: "pipeline-analytics",
24818
25545
  capScope: "device",
@@ -25845,6 +26572,12 @@ Object.freeze({
25845
26572
  addonId: null,
25846
26573
  access: "delete"
25847
26574
  },
26575
+ "settingsStore.deleteWhere": {
26576
+ capName: "settings-store",
26577
+ capScope: "system",
26578
+ addonId: null,
26579
+ access: "delete"
26580
+ },
25848
26581
  "settingsStore.get": {
25849
26582
  capName: "settings-store",
25850
26583
  capScope: "system",
@@ -25887,6 +26620,12 @@ Object.freeze({
25887
26620
  addonId: null,
25888
26621
  access: "create"
25889
26622
  },
26623
+ "settingsStore.updateWhere": {
26624
+ capName: "settings-store",
26625
+ capScope: "system",
26626
+ addonId: null,
26627
+ access: "create"
26628
+ },
25890
26629
  "smtpProvider.getStatus": {
25891
26630
  capName: "smtp-provider",
25892
26631
  capScope: "system",
@@ -28613,6 +29352,7 @@ var WEBHOOK_CAPS = {
28613
29352
  max: 99
28614
29353
  },
28615
29354
  actions: 99,
29355
+ actionIcons: true,
28616
29356
  levels: [
28617
29357
  {
28618
29358
  id: "p1",
@@ -29945,7 +30685,7 @@ function createMemorySettingsStorePort() {
29945
30685
  * the store persists whatever it is handed verbatim.
29946
30686
  */
29947
30687
  /** The default collection name (see `settings-store` addonId-prefix note). */
29948
- var TARGETS_COLLECTION = "notification-targets";
30688
+ var TARGETS_COLLECTION = "notifiers:notification-targets";
29949
30689
  /** KV JSON-blob shape — a single `data` column routes the row through the
29950
30690
  * settings backend's canonical key/value path (id TEXT PK, data TEXT). */
29951
30691
  var KV_BLOB_COLUMNS = [{