@camstack/addon-advanced-notifier 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -4632,7 +4632,7 @@ function _instanceof(cls, params = {}) {
4632
4632
  return inst;
4633
4633
  }
4634
4634
  //#endregion
4635
- //#region ../types/dist/sleep-C2M2zF7x.mjs
4635
+ //#region ../types/dist/sleep-MHm--th-.mjs
4636
4636
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4637
4637
  EventCategory["SystemBoot"] = "system.boot";
4638
4638
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5894,6 +5894,7 @@ var CamStreamKindSchema = _enum([
5894
5894
  "pull-rtsp",
5895
5895
  "pull-rtmp",
5896
5896
  "pull-http",
5897
+ "pull-flv",
5897
5898
  "pull-rfc4571",
5898
5899
  "push-annexb",
5899
5900
  "derived"
@@ -7037,7 +7038,21 @@ var StorageLocationDeclarationSchema = object({
7037
7038
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
7038
7039
  * configure the primary location.
7039
7040
  */
7040
- defaultsTo: string().optional()
7041
+ defaultsTo: string().optional(),
7042
+ /**
7043
+ * Which node root the seeded `<id>:default` instance is placed under on a
7044
+ * FRESH install:
7045
+ * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
7046
+ * the appData volume. Right for small/durable data (backups, logs, models).
7047
+ * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
7048
+ * env is set, else falls back to the data root. Right for bulky, hot media
7049
+ * (recordings, event media) that should stay off the appData disk.
7050
+ *
7051
+ * Only affects the seeded default's `basePath`; operators can repoint any
7052
+ * location afterwards, and a `defaultsTo` slot inherits its parent's root
7053
+ * regardless of this field. Absent (the common case) is treated as `'data'`.
7054
+ */
7055
+ defaultRoot: _enum(["data", "media"]).optional()
7041
7056
  });
7042
7057
  var DecoderStatsSchema = object({
7043
7058
  inputFps: number(),
@@ -7898,6 +7913,10 @@ var RtspRestreamEntrySchema = object({
7898
7913
  var BrokerRtspClientSchema = object({
7899
7914
  sessionId: string(),
7900
7915
  remoteAddr: string(),
7916
+ /** Client `User-Agent` (e.g. `recorder` for the recorder's RTSP pull), or
7917
+ * null/absent when the client sent none. Lets the UI label a consumer by
7918
+ * purpose. Optional so a client built against an older schema stays valid. */
7919
+ userAgent: string().nullish(),
7901
7920
  playing: boolean(),
7902
7921
  muted: boolean(),
7903
7922
  connectedAt: number(),
@@ -13199,7 +13218,7 @@ var AddBrokerInputSchema = object({
13199
13218
  });
13200
13219
  var AddBrokerResultSchema = object({ id: string() });
13201
13220
  var IdInputSchema = object({ id: string() });
13202
- var TestResultSchema = discriminatedUnion("ok", [object({
13221
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
13203
13222
  ok: literal(true),
13204
13223
  latencyMs: number()
13205
13224
  }), object({
@@ -13222,7 +13241,7 @@ var StatusSchema = object({
13222
13241
  brokerCount: number(),
13223
13242
  embeddedRunning: boolean()
13224
13243
  });
13225
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
13244
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
13226
13245
  var NetworkEndpointSchema = object({
13227
13246
  url: string(),
13228
13247
  hostname: string(),
@@ -13256,29 +13275,210 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
13256
13275
  sourcePort: number().optional()
13257
13276
  });
13258
13277
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
13278
+ /**
13279
+ * notification-output — canonical, capability-gated notification delivery.
13280
+ *
13281
+ * Apprise-derived model (see
13282
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13283
+ * callers emit ONE canonical `Notification`; each provider declares a
13284
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
13285
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13286
+ * message to what the kind supports — callers never special-case a service.
13287
+ *
13288
+ * DESIGN DECISIONS (locked):
13289
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13290
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
13291
+ * cap. Rationale: the admin UI needs one uniform surface across the
13292
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13293
+ * alternative would fork the UI per addon and cannot host the
13294
+ * discovery→adopt flow.
13295
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13296
+ * the generated cap-mount auto-`concatCollection`-fans them across every
13297
+ * registered provider (notifiers addon + HA addon) so one catalog is
13298
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13299
+ * `addonId` the generated collection router extracts from the call input.
13300
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13301
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13302
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
13303
+ * base64 fallback needed.
13304
+ *
13305
+ * TODO (deferred, closed-set change — separate decision): add
13306
+ * `providerKind: 'notify'` so notification providers surface on the unified
13307
+ * admin "Integrations" page.
13308
+ */
13309
+ /**
13310
+ * Zentik-derived typed-media enum — the superset across every kind. Each
13311
+ * adapter picks what it supports and the degrade engine filters the rest.
13312
+ */
13313
+ var AttachmentMediaTypeSchema = _enum([
13314
+ "image",
13315
+ "video",
13316
+ "gif",
13317
+ "audio",
13318
+ "icon"
13319
+ ]);
13320
+ /**
13321
+ * A single attachment. Exactly one of `url` (remote source, most adapters
13322
+ * prefer this) or `bytes` (inline source; required for Pushover-style
13323
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
13324
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
13325
+ */
13326
+ var AttachmentSchema = object({
13327
+ mediaType: AttachmentMediaTypeSchema,
13328
+ url: string().optional(),
13329
+ bytes: _instanceof(Uint8Array).optional(),
13330
+ mime: string().optional(),
13331
+ name: string().optional()
13332
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13333
+ var NotificationFormatSchema = _enum([
13334
+ "text",
13335
+ "markdown",
13336
+ "html"
13337
+ ]);
13338
+ /** A single tap-through action button. */
13339
+ var NotificationActionSchema = object({
13340
+ id: string(),
13341
+ label: string(),
13342
+ url: string().optional()
13343
+ });
13344
+ /**
13345
+ * The canonical notification. `body` is the only hard field (Apprise model).
13346
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
13347
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
13348
+ * the adapter maps this ordinal onto its native level. `level?` is an
13349
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
13350
+ * `priority` for that one target.
13351
+ */
13352
+ var NotificationSchema = object({
13353
+ body: string(),
13354
+ title: string().optional(),
13355
+ format: NotificationFormatSchema.default("text"),
13356
+ priority: number().int().min(1).max(5).default(3),
13357
+ level: string().optional(),
13358
+ attachments: array(AttachmentSchema).optional(),
13359
+ clickUrl: string().optional(),
13360
+ actions: array(NotificationActionSchema).optional(),
13361
+ sound: string().optional(),
13362
+ ttl: number().optional(),
13363
+ tag: string().optional(),
13364
+ deviceId: number().optional(),
13365
+ eventId: string().optional(),
13366
+ metadata: record(string(), unknown()).optional()
13367
+ });
13368
+ /** One declared native severity/priority level for a kind. */
13369
+ var TargetKindLevelSchema = object({
13370
+ id: string(),
13371
+ label: string(),
13372
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
13373
+ ordinal: number().int().min(1).max(5).nullable(),
13374
+ flags: object({
13375
+ critical: boolean().optional(),
13376
+ silent: boolean().optional(),
13377
+ noPush: boolean().optional()
13378
+ }).optional(),
13379
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
13380
+ requires: array(string()).optional(),
13381
+ description: string().optional()
13382
+ });
13383
+ /** The full capability block consulted before dispatch. */
13384
+ var TargetKindCapsSchema = object({
13385
+ attachments: object({
13386
+ mediaTypes: array(AttachmentMediaTypeSchema),
13387
+ mode: _enum([
13388
+ "url",
13389
+ "bytes",
13390
+ "both"
13391
+ ]),
13392
+ max: number().int().nonnegative(),
13393
+ maxBytes: number().int().positive().optional()
13394
+ }),
13395
+ /** Max action buttons (0 = none). */
13396
+ actions: number().int().nonnegative(),
13397
+ levels: array(TargetKindLevelSchema),
13398
+ format: array(NotificationFormatSchema),
13399
+ clickUrl: boolean(),
13400
+ sound: boolean(),
13401
+ ttl: boolean(),
13402
+ bodyMaxLen: number().int().positive()
13403
+ });
13404
+ /**
13405
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
13406
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
13407
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
13408
+ * the union is large and not meant for runtime validation here; the exported
13409
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
13410
+ */
13411
+ var ConfigSchemaPassthrough = unknown();
13412
+ var TargetKindSchema = object({
13413
+ kind: string(),
13414
+ label: string(),
13415
+ icon: string(),
13416
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
13417
+ addonId: string(),
13418
+ configSchema: ConfigSchemaPassthrough,
13419
+ supportsDiscovery: boolean(),
13420
+ caps: TargetKindCapsSchema
13421
+ });
13422
+ /**
13423
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
13424
+ * (return a presence marker only) when serving `listTargets` — never
13425
+ * round-trip a stored secret to the UI.
13426
+ */
13427
+ var TargetSchema = object({
13428
+ id: string(),
13429
+ name: string(),
13430
+ kind: string(),
13431
+ addonId: string(),
13432
+ enabled: boolean(),
13433
+ config: record(string(), unknown())
13434
+ });
13435
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
13436
+ var DiscoveredTargetSchema = object({
13437
+ kind: string(),
13438
+ suggestedName: string(),
13439
+ config: record(string(), unknown())
13440
+ });
13441
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
13442
+ var RenderedAsSchema = object({
13443
+ level: string(),
13444
+ format: NotificationFormatSchema,
13445
+ attachmentsSent: number().int().nonnegative(),
13446
+ actionsSent: number().int().nonnegative(),
13447
+ truncated: boolean(),
13448
+ dropped: array(string())
13449
+ });
13450
+ var SendResultSchema = object({
13451
+ success: boolean(),
13452
+ error: string().optional(),
13453
+ renderedAs: RenderedAsSchema.optional()
13454
+ });
13455
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
13456
+ var TestResultSchema = SendResultSchema;
13259
13457
  var notificationOutputCapability = {
13260
13458
  name: "notification-output",
13261
13459
  scope: "system",
13262
13460
  mode: "collection",
13263
13461
  methods: {
13462
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
13463
+ listTargets: method(object({}), array(TargetSchema)),
13464
+ discoverTargets: method(object({
13465
+ kind: string(),
13466
+ config: record(string(), unknown()).optional()
13467
+ }), array(DiscoveredTargetSchema)),
13264
13468
  send: method(object({
13265
- title: string(),
13266
- body: string(),
13267
- imageUrl: string().optional(),
13268
- deviceId: number().optional(),
13269
- eventId: string().optional(),
13270
- priority: _enum([
13271
- "low",
13272
- "normal",
13273
- "high",
13274
- "critical"
13275
- ]).default("normal"),
13276
- metadata: record(string(), unknown()).optional()
13277
- }), _void(), { kind: "mutation" }),
13278
- sendTest: method(_void(), object({
13279
- success: boolean(),
13280
- error: string().optional()
13281
- }), { kind: "mutation" })
13469
+ targetId: string(),
13470
+ notification: NotificationSchema
13471
+ }), SendResultSchema, { kind: "mutation" }),
13472
+ testTarget: method(object({
13473
+ targetId: string(),
13474
+ sample: NotificationSchema.optional()
13475
+ }), TestResultSchema, { kind: "mutation" }),
13476
+ upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
13477
+ deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
13478
+ setTargetEnabled: method(object({
13479
+ targetId: string(),
13480
+ enabled: boolean()
13481
+ }), _void(), { kind: "mutation" })
13282
13482
  }
13283
13483
  };
13284
13484
  /**
@@ -19275,13 +19475,49 @@ Object.freeze({
19275
19475
  addonId: null,
19276
19476
  access: "create"
19277
19477
  },
19478
+ "notificationOutput.deleteTarget": {
19479
+ capName: "notification-output",
19480
+ capScope: "system",
19481
+ addonId: null,
19482
+ access: "delete"
19483
+ },
19484
+ "notificationOutput.discoverTargets": {
19485
+ capName: "notification-output",
19486
+ capScope: "system",
19487
+ addonId: null,
19488
+ access: "view"
19489
+ },
19490
+ "notificationOutput.listTargetKinds": {
19491
+ capName: "notification-output",
19492
+ capScope: "system",
19493
+ addonId: null,
19494
+ access: "view"
19495
+ },
19496
+ "notificationOutput.listTargets": {
19497
+ capName: "notification-output",
19498
+ capScope: "system",
19499
+ addonId: null,
19500
+ access: "view"
19501
+ },
19278
19502
  "notificationOutput.send": {
19279
19503
  capName: "notification-output",
19280
19504
  capScope: "system",
19281
19505
  addonId: null,
19282
19506
  access: "create"
19283
19507
  },
19284
- "notificationOutput.sendTest": {
19508
+ "notificationOutput.setTargetEnabled": {
19509
+ capName: "notification-output",
19510
+ capScope: "system",
19511
+ addonId: null,
19512
+ access: "create"
19513
+ },
19514
+ "notificationOutput.testTarget": {
19515
+ capName: "notification-output",
19516
+ capScope: "system",
19517
+ addonId: null,
19518
+ access: "create"
19519
+ },
19520
+ "notificationOutput.upsertTarget": {
19285
19521
  capName: "notification-output",
19286
19522
  capScope: "system",
19287
19523
  addonId: null,
@@ -21409,15 +21645,14 @@ var ConsoleOutput = class {
21409
21645
  this.logger = logger;
21410
21646
  }
21411
21647
  async send(notification) {
21412
- this.logger.info(`[${notification.severity}] ${notification.title}: ${notification.message}`, notification.deviceId !== void 0 ? { tags: { deviceId: notification.deviceId } } : void 0);
21648
+ this.logger.info(`[p${notification.priority}] ${notification.title ?? ""}: ${notification.body}`, notification.deviceId !== void 0 ? { tags: { deviceId: notification.deviceId } } : void 0);
21413
21649
  }
21414
21650
  async sendTest() {
21415
21651
  await this.send({
21416
21652
  title: "Test",
21417
- message: "Console output working",
21418
- severity: "info",
21419
- category: "test",
21420
- timestamp: Date.now()
21653
+ body: "Console output working",
21654
+ format: "text",
21655
+ priority: 3
21421
21656
  });
21422
21657
  return { success: true };
21423
21658
  }
@@ -21685,12 +21920,11 @@ var AdvancedNotifierAddon = class extends BaseAddon {
21685
21920
  const deviceId = Number.isFinite(deviceIdNum) ? deviceIdNum : void 0;
21686
21921
  const notification = {
21687
21922
  title,
21688
- message,
21689
- severity: this.priorityToSeverity(rule.priority),
21690
- category: event.category,
21923
+ body: message,
21924
+ format: "text",
21925
+ priority: this.priorityToOrdinal(rule.priority),
21691
21926
  deviceId,
21692
- data: event.data,
21693
- timestamp: event.timestamp.getTime()
21927
+ metadata: event.data
21694
21928
  };
21695
21929
  let success = true;
21696
21930
  let dispatchError;
@@ -21735,10 +21969,18 @@ var AdvancedNotifierAddon = class extends BaseAddon {
21735
21969
  timestamp: event.timestamp.toISOString()
21736
21970
  };
21737
21971
  }
21738
- priorityToSeverity(priority) {
21739
- if (priority === "critical") return "critical";
21740
- if (priority === "high") return "warning";
21741
- return "info";
21972
+ /**
21973
+ * Map a rule's qualitative priority onto the canonical 1..5 ordinal.
21974
+ * Callers now pass ordinal priority; each `notification-output` provider
21975
+ * maps the ordinal onto its declared native level.
21976
+ */
21977
+ priorityToOrdinal(priority) {
21978
+ switch (priority) {
21979
+ case "critical": return 5;
21980
+ case "high": return 4;
21981
+ case "low": return 2;
21982
+ default: return 3;
21983
+ }
21742
21984
  }
21743
21985
  };
21744
21986
  //#endregion
package/dist/addon.mjs CHANGED
@@ -4628,7 +4628,7 @@ function _instanceof(cls, params = {}) {
4628
4628
  return inst;
4629
4629
  }
4630
4630
  //#endregion
4631
- //#region ../types/dist/sleep-C2M2zF7x.mjs
4631
+ //#region ../types/dist/sleep-MHm--th-.mjs
4632
4632
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4633
4633
  EventCategory["SystemBoot"] = "system.boot";
4634
4634
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -5890,6 +5890,7 @@ var CamStreamKindSchema = _enum([
5890
5890
  "pull-rtsp",
5891
5891
  "pull-rtmp",
5892
5892
  "pull-http",
5893
+ "pull-flv",
5893
5894
  "pull-rfc4571",
5894
5895
  "push-annexb",
5895
5896
  "derived"
@@ -7033,7 +7034,21 @@ var StorageLocationDeclarationSchema = object({
7033
7034
  * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
7034
7035
  * configure the primary location.
7035
7036
  */
7036
- defaultsTo: string().optional()
7037
+ defaultsTo: string().optional(),
7038
+ /**
7039
+ * Which node root the seeded `<id>:default` instance is placed under on a
7040
+ * FRESH install:
7041
+ * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
7042
+ * the appData volume. Right for small/durable data (backups, logs, models).
7043
+ * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
7044
+ * env is set, else falls back to the data root. Right for bulky, hot media
7045
+ * (recordings, event media) that should stay off the appData disk.
7046
+ *
7047
+ * Only affects the seeded default's `basePath`; operators can repoint any
7048
+ * location afterwards, and a `defaultsTo` slot inherits its parent's root
7049
+ * regardless of this field. Absent (the common case) is treated as `'data'`.
7050
+ */
7051
+ defaultRoot: _enum(["data", "media"]).optional()
7037
7052
  });
7038
7053
  var DecoderStatsSchema = object({
7039
7054
  inputFps: number(),
@@ -7894,6 +7909,10 @@ var RtspRestreamEntrySchema = object({
7894
7909
  var BrokerRtspClientSchema = object({
7895
7910
  sessionId: string(),
7896
7911
  remoteAddr: string(),
7912
+ /** Client `User-Agent` (e.g. `recorder` for the recorder's RTSP pull), or
7913
+ * null/absent when the client sent none. Lets the UI label a consumer by
7914
+ * purpose. Optional so a client built against an older schema stays valid. */
7915
+ userAgent: string().nullish(),
7897
7916
  playing: boolean(),
7898
7917
  muted: boolean(),
7899
7918
  connectedAt: number(),
@@ -13195,7 +13214,7 @@ var AddBrokerInputSchema = object({
13195
13214
  });
13196
13215
  var AddBrokerResultSchema = object({ id: string() });
13197
13216
  var IdInputSchema = object({ id: string() });
13198
- var TestResultSchema = discriminatedUnion("ok", [object({
13217
+ var TestResultSchema$1 = discriminatedUnion("ok", [object({
13199
13218
  ok: literal(true),
13200
13219
  latencyMs: number()
13201
13220
  }), object({
@@ -13218,7 +13237,7 @@ var StatusSchema = object({
13218
13237
  brokerCount: number(),
13219
13238
  embeddedRunning: boolean()
13220
13239
  });
13221
- method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
13240
+ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema), method(AddBrokerInputSchema, AddBrokerResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(IdInputSchema, TestResultSchema$1, { kind: "mutation" }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, { kind: "mutation" }), method(IdInputSchema, _void(), { kind: "mutation" }), method(_void(), StatusSchema);
13222
13241
  var NetworkEndpointSchema = object({
13223
13242
  url: string(),
13224
13243
  hostname: string(),
@@ -13252,29 +13271,210 @@ var NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
13252
13271
  sourcePort: number().optional()
13253
13272
  });
13254
13273
  method(_void(), NetworkEndpointSchema, { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), NetworkEndpointSchema.nullable()), method(_void(), NetworkAccessStatusSchema), method(_void(), array(NetworkEndpointEntrySchema).readonly());
13274
+ /**
13275
+ * notification-output — canonical, capability-gated notification delivery.
13276
+ *
13277
+ * Apprise-derived model (see
13278
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13279
+ * callers emit ONE canonical `Notification`; each provider declares a
13280
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
13281
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13282
+ * message to what the kind supports — callers never special-case a service.
13283
+ *
13284
+ * DESIGN DECISIONS (locked):
13285
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13286
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
13287
+ * cap. Rationale: the admin UI needs one uniform surface across the
13288
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13289
+ * alternative would fork the UI per addon and cannot host the
13290
+ * discovery→adopt flow.
13291
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13292
+ * the generated cap-mount auto-`concatCollection`-fans them across every
13293
+ * registered provider (notifiers addon + HA addon) so one catalog is
13294
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13295
+ * `addonId` the generated collection router extracts from the call input.
13296
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13297
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13298
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
13299
+ * base64 fallback needed.
13300
+ *
13301
+ * TODO (deferred, closed-set change — separate decision): add
13302
+ * `providerKind: 'notify'` so notification providers surface on the unified
13303
+ * admin "Integrations" page.
13304
+ */
13305
+ /**
13306
+ * Zentik-derived typed-media enum — the superset across every kind. Each
13307
+ * adapter picks what it supports and the degrade engine filters the rest.
13308
+ */
13309
+ var AttachmentMediaTypeSchema = _enum([
13310
+ "image",
13311
+ "video",
13312
+ "gif",
13313
+ "audio",
13314
+ "icon"
13315
+ ]);
13316
+ /**
13317
+ * A single attachment. Exactly one of `url` (remote source, most adapters
13318
+ * prefer this) or `bytes` (inline source; required for Pushover-style
13319
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
13320
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
13321
+ */
13322
+ var AttachmentSchema = object({
13323
+ mediaType: AttachmentMediaTypeSchema,
13324
+ url: string().optional(),
13325
+ bytes: _instanceof(Uint8Array).optional(),
13326
+ mime: string().optional(),
13327
+ name: string().optional()
13328
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13329
+ var NotificationFormatSchema = _enum([
13330
+ "text",
13331
+ "markdown",
13332
+ "html"
13333
+ ]);
13334
+ /** A single tap-through action button. */
13335
+ var NotificationActionSchema = object({
13336
+ id: string(),
13337
+ label: string(),
13338
+ url: string().optional()
13339
+ });
13340
+ /**
13341
+ * The canonical notification. `body` is the only hard field (Apprise model).
13342
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
13343
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
13344
+ * the adapter maps this ordinal onto its native level. `level?` is an
13345
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
13346
+ * `priority` for that one target.
13347
+ */
13348
+ var NotificationSchema = object({
13349
+ body: string(),
13350
+ title: string().optional(),
13351
+ format: NotificationFormatSchema.default("text"),
13352
+ priority: number().int().min(1).max(5).default(3),
13353
+ level: string().optional(),
13354
+ attachments: array(AttachmentSchema).optional(),
13355
+ clickUrl: string().optional(),
13356
+ actions: array(NotificationActionSchema).optional(),
13357
+ sound: string().optional(),
13358
+ ttl: number().optional(),
13359
+ tag: string().optional(),
13360
+ deviceId: number().optional(),
13361
+ eventId: string().optional(),
13362
+ metadata: record(string(), unknown()).optional()
13363
+ });
13364
+ /** One declared native severity/priority level for a kind. */
13365
+ var TargetKindLevelSchema = object({
13366
+ id: string(),
13367
+ label: string(),
13368
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
13369
+ ordinal: number().int().min(1).max(5).nullable(),
13370
+ flags: object({
13371
+ critical: boolean().optional(),
13372
+ silent: boolean().optional(),
13373
+ noPush: boolean().optional()
13374
+ }).optional(),
13375
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
13376
+ requires: array(string()).optional(),
13377
+ description: string().optional()
13378
+ });
13379
+ /** The full capability block consulted before dispatch. */
13380
+ var TargetKindCapsSchema = object({
13381
+ attachments: object({
13382
+ mediaTypes: array(AttachmentMediaTypeSchema),
13383
+ mode: _enum([
13384
+ "url",
13385
+ "bytes",
13386
+ "both"
13387
+ ]),
13388
+ max: number().int().nonnegative(),
13389
+ maxBytes: number().int().positive().optional()
13390
+ }),
13391
+ /** Max action buttons (0 = none). */
13392
+ actions: number().int().nonnegative(),
13393
+ levels: array(TargetKindLevelSchema),
13394
+ format: array(NotificationFormatSchema),
13395
+ clickUrl: boolean(),
13396
+ sound: boolean(),
13397
+ ttl: boolean(),
13398
+ bodyMaxLen: number().int().positive()
13399
+ });
13400
+ /**
13401
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
13402
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
13403
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
13404
+ * the union is large and not meant for runtime validation here; the exported
13405
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
13406
+ */
13407
+ var ConfigSchemaPassthrough = unknown();
13408
+ var TargetKindSchema = object({
13409
+ kind: string(),
13410
+ label: string(),
13411
+ icon: string(),
13412
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
13413
+ addonId: string(),
13414
+ configSchema: ConfigSchemaPassthrough,
13415
+ supportsDiscovery: boolean(),
13416
+ caps: TargetKindCapsSchema
13417
+ });
13418
+ /**
13419
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
13420
+ * (return a presence marker only) when serving `listTargets` — never
13421
+ * round-trip a stored secret to the UI.
13422
+ */
13423
+ var TargetSchema = object({
13424
+ id: string(),
13425
+ name: string(),
13426
+ kind: string(),
13427
+ addonId: string(),
13428
+ enabled: boolean(),
13429
+ config: record(string(), unknown())
13430
+ });
13431
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
13432
+ var DiscoveredTargetSchema = object({
13433
+ kind: string(),
13434
+ suggestedName: string(),
13435
+ config: record(string(), unknown())
13436
+ });
13437
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
13438
+ var RenderedAsSchema = object({
13439
+ level: string(),
13440
+ format: NotificationFormatSchema,
13441
+ attachmentsSent: number().int().nonnegative(),
13442
+ actionsSent: number().int().nonnegative(),
13443
+ truncated: boolean(),
13444
+ dropped: array(string())
13445
+ });
13446
+ var SendResultSchema = object({
13447
+ success: boolean(),
13448
+ error: string().optional(),
13449
+ renderedAs: RenderedAsSchema.optional()
13450
+ });
13451
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
13452
+ var TestResultSchema = SendResultSchema;
13255
13453
  var notificationOutputCapability = {
13256
13454
  name: "notification-output",
13257
13455
  scope: "system",
13258
13456
  mode: "collection",
13259
13457
  methods: {
13458
+ listTargetKinds: method(object({}), array(TargetKindSchema)),
13459
+ listTargets: method(object({}), array(TargetSchema)),
13460
+ discoverTargets: method(object({
13461
+ kind: string(),
13462
+ config: record(string(), unknown()).optional()
13463
+ }), array(DiscoveredTargetSchema)),
13260
13464
  send: method(object({
13261
- title: string(),
13262
- body: string(),
13263
- imageUrl: string().optional(),
13264
- deviceId: number().optional(),
13265
- eventId: string().optional(),
13266
- priority: _enum([
13267
- "low",
13268
- "normal",
13269
- "high",
13270
- "critical"
13271
- ]).default("normal"),
13272
- metadata: record(string(), unknown()).optional()
13273
- }), _void(), { kind: "mutation" }),
13274
- sendTest: method(_void(), object({
13275
- success: boolean(),
13276
- error: string().optional()
13277
- }), { kind: "mutation" })
13465
+ targetId: string(),
13466
+ notification: NotificationSchema
13467
+ }), SendResultSchema, { kind: "mutation" }),
13468
+ testTarget: method(object({
13469
+ targetId: string(),
13470
+ sample: NotificationSchema.optional()
13471
+ }), TestResultSchema, { kind: "mutation" }),
13472
+ upsertTarget: method(object({ target: TargetSchema }), TargetSchema, { kind: "mutation" }),
13473
+ deleteTarget: method(object({ targetId: string() }), _void(), { kind: "mutation" }),
13474
+ setTargetEnabled: method(object({
13475
+ targetId: string(),
13476
+ enabled: boolean()
13477
+ }), _void(), { kind: "mutation" })
13278
13478
  }
13279
13479
  };
13280
13480
  /**
@@ -19271,13 +19471,49 @@ Object.freeze({
19271
19471
  addonId: null,
19272
19472
  access: "create"
19273
19473
  },
19474
+ "notificationOutput.deleteTarget": {
19475
+ capName: "notification-output",
19476
+ capScope: "system",
19477
+ addonId: null,
19478
+ access: "delete"
19479
+ },
19480
+ "notificationOutput.discoverTargets": {
19481
+ capName: "notification-output",
19482
+ capScope: "system",
19483
+ addonId: null,
19484
+ access: "view"
19485
+ },
19486
+ "notificationOutput.listTargetKinds": {
19487
+ capName: "notification-output",
19488
+ capScope: "system",
19489
+ addonId: null,
19490
+ access: "view"
19491
+ },
19492
+ "notificationOutput.listTargets": {
19493
+ capName: "notification-output",
19494
+ capScope: "system",
19495
+ addonId: null,
19496
+ access: "view"
19497
+ },
19274
19498
  "notificationOutput.send": {
19275
19499
  capName: "notification-output",
19276
19500
  capScope: "system",
19277
19501
  addonId: null,
19278
19502
  access: "create"
19279
19503
  },
19280
- "notificationOutput.sendTest": {
19504
+ "notificationOutput.setTargetEnabled": {
19505
+ capName: "notification-output",
19506
+ capScope: "system",
19507
+ addonId: null,
19508
+ access: "create"
19509
+ },
19510
+ "notificationOutput.testTarget": {
19511
+ capName: "notification-output",
19512
+ capScope: "system",
19513
+ addonId: null,
19514
+ access: "create"
19515
+ },
19516
+ "notificationOutput.upsertTarget": {
19281
19517
  capName: "notification-output",
19282
19518
  capScope: "system",
19283
19519
  addonId: null,
@@ -21405,15 +21641,14 @@ var ConsoleOutput = class {
21405
21641
  this.logger = logger;
21406
21642
  }
21407
21643
  async send(notification) {
21408
- this.logger.info(`[${notification.severity}] ${notification.title}: ${notification.message}`, notification.deviceId !== void 0 ? { tags: { deviceId: notification.deviceId } } : void 0);
21644
+ this.logger.info(`[p${notification.priority}] ${notification.title ?? ""}: ${notification.body}`, notification.deviceId !== void 0 ? { tags: { deviceId: notification.deviceId } } : void 0);
21409
21645
  }
21410
21646
  async sendTest() {
21411
21647
  await this.send({
21412
21648
  title: "Test",
21413
- message: "Console output working",
21414
- severity: "info",
21415
- category: "test",
21416
- timestamp: Date.now()
21649
+ body: "Console output working",
21650
+ format: "text",
21651
+ priority: 3
21417
21652
  });
21418
21653
  return { success: true };
21419
21654
  }
@@ -21681,12 +21916,11 @@ var AdvancedNotifierAddon = class extends BaseAddon {
21681
21916
  const deviceId = Number.isFinite(deviceIdNum) ? deviceIdNum : void 0;
21682
21917
  const notification = {
21683
21918
  title,
21684
- message,
21685
- severity: this.priorityToSeverity(rule.priority),
21686
- category: event.category,
21919
+ body: message,
21920
+ format: "text",
21921
+ priority: this.priorityToOrdinal(rule.priority),
21687
21922
  deviceId,
21688
- data: event.data,
21689
- timestamp: event.timestamp.getTime()
21923
+ metadata: event.data
21690
21924
  };
21691
21925
  let success = true;
21692
21926
  let dispatchError;
@@ -21731,10 +21965,18 @@ var AdvancedNotifierAddon = class extends BaseAddon {
21731
21965
  timestamp: event.timestamp.toISOString()
21732
21966
  };
21733
21967
  }
21734
- priorityToSeverity(priority) {
21735
- if (priority === "critical") return "critical";
21736
- if (priority === "high") return "warning";
21737
- return "info";
21968
+ /**
21969
+ * Map a rule's qualitative priority onto the canonical 1..5 ordinal.
21970
+ * Callers now pass ordinal priority; each `notification-output` provider
21971
+ * maps the ordinal onto its declared native level.
21972
+ */
21973
+ priorityToOrdinal(priority) {
21974
+ switch (priority) {
21975
+ case "critical": return 5;
21976
+ case "high": return 4;
21977
+ case "low": return 2;
21978
+ default: return 3;
21979
+ }
21738
21980
  }
21739
21981
  };
21740
21982
  //#endregion
package/dist/index.js CHANGED
@@ -26,6 +26,11 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_OUTPUT_TIMEOUT_MS
26
26
  }
27
27
  //#endregion
28
28
  //#region src/outputs/webhook.ts
29
+ /**
30
+ * @deprecated Superseded by the `addon-notifiers` generic-webhook adapter
31
+ * (the `notification-output` cap collection provider). Kept only for
32
+ * compile-compat; not registered as a live output.
33
+ */
29
34
  var WebhookOutput = class {
30
35
  id = "webhook";
31
36
  name = "Webhook";
@@ -45,10 +50,9 @@ var WebhookOutput = class {
45
50
  try {
46
51
  await this.send({
47
52
  title: "Test Notification",
48
- message: "This is a test from CamStack Advanced Notifier",
49
- severity: "info",
50
- category: "test",
51
- timestamp: Date.now()
53
+ body: "This is a test from CamStack Advanced Notifier",
54
+ format: "text",
55
+ priority: 3
52
56
  });
53
57
  return { success: true };
54
58
  } catch (err) {
@@ -61,6 +65,12 @@ var WebhookOutput = class {
61
65
  };
62
66
  //#endregion
63
67
  //#region src/outputs/home-assistant.ts
68
+ /**
69
+ * @deprecated Superseded by the `homeassistant` kind in
70
+ * `addon-provider-homeassistant` (the `notification-output` cap collection
71
+ * provider, reusing the HA broker connection). Kept only for compile-compat;
72
+ * not registered as a live output.
73
+ */
64
74
  var HomeAssistantOutput = class {
65
75
  id = "home-assistant";
66
76
  name = "Home Assistant";
@@ -80,11 +90,10 @@ var HomeAssistantOutput = class {
80
90
  },
81
91
  body: JSON.stringify({
82
92
  title: notification.title,
83
- message: notification.message,
84
- severity: notification.severity,
85
- category: notification.category,
93
+ message: notification.body,
94
+ priority: notification.priority,
86
95
  device_id: notification.deviceId,
87
- data: notification.data
96
+ data: notification.metadata
88
97
  })
89
98
  });
90
99
  }
@@ -92,10 +101,9 @@ var HomeAssistantOutput = class {
92
101
  try {
93
102
  await this.send({
94
103
  title: "CamStack Test",
95
- message: "Connection verified",
96
- severity: "info",
97
- category: "test",
98
- timestamp: Date.now()
104
+ body: "Connection verified",
105
+ format: "text",
106
+ priority: 3
99
107
  });
100
108
  return { success: true };
101
109
  } catch (err) {
package/dist/index.mjs CHANGED
@@ -25,6 +25,11 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_OUTPUT_TIMEOUT_MS
25
25
  }
26
26
  //#endregion
27
27
  //#region src/outputs/webhook.ts
28
+ /**
29
+ * @deprecated Superseded by the `addon-notifiers` generic-webhook adapter
30
+ * (the `notification-output` cap collection provider). Kept only for
31
+ * compile-compat; not registered as a live output.
32
+ */
28
33
  var WebhookOutput = class {
29
34
  id = "webhook";
30
35
  name = "Webhook";
@@ -44,10 +49,9 @@ var WebhookOutput = class {
44
49
  try {
45
50
  await this.send({
46
51
  title: "Test Notification",
47
- message: "This is a test from CamStack Advanced Notifier",
48
- severity: "info",
49
- category: "test",
50
- timestamp: Date.now()
52
+ body: "This is a test from CamStack Advanced Notifier",
53
+ format: "text",
54
+ priority: 3
51
55
  });
52
56
  return { success: true };
53
57
  } catch (err) {
@@ -60,6 +64,12 @@ var WebhookOutput = class {
60
64
  };
61
65
  //#endregion
62
66
  //#region src/outputs/home-assistant.ts
67
+ /**
68
+ * @deprecated Superseded by the `homeassistant` kind in
69
+ * `addon-provider-homeassistant` (the `notification-output` cap collection
70
+ * provider, reusing the HA broker connection). Kept only for compile-compat;
71
+ * not registered as a live output.
72
+ */
63
73
  var HomeAssistantOutput = class {
64
74
  id = "home-assistant";
65
75
  name = "Home Assistant";
@@ -79,11 +89,10 @@ var HomeAssistantOutput = class {
79
89
  },
80
90
  body: JSON.stringify({
81
91
  title: notification.title,
82
- message: notification.message,
83
- severity: notification.severity,
84
- category: notification.category,
92
+ message: notification.body,
93
+ priority: notification.priority,
85
94
  device_id: notification.deviceId,
86
- data: notification.data
95
+ data: notification.metadata
87
96
  })
88
97
  });
89
98
  }
@@ -91,10 +100,9 @@ var HomeAssistantOutput = class {
91
100
  try {
92
101
  await this.send({
93
102
  title: "CamStack Test",
94
- message: "Connection verified",
95
- severity: "info",
96
- category: "test",
97
- timestamp: Date.now()
103
+ body: "Connection verified",
104
+ format: "text",
105
+ priority: 3
98
106
  });
99
107
  return { success: true };
100
108
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-advanced-notifier",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "description": "Rules-based notification engine for CamStack",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",