@camstack/addon-notifiers 1.2.7 → 1.2.8
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/assets/samples/animation.gif +0 -0
- package/assets/samples/clip.mp4 +0 -0
- package/assets/samples/image.jpg +0 -0
- package/assets/samples/tone.mp3 +0 -0
- package/dist/addon.js +541 -86
- package/dist/addon.mjs +518 -86
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -2,7 +2,33 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
14
|
+
key = keys[i];
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
//#endregion
|
|
5
27
|
let node_zlib = require("node:zlib");
|
|
28
|
+
let node_fs = require("node:fs");
|
|
29
|
+
let node_path = require("node:path");
|
|
30
|
+
node_path = __toESM(node_path);
|
|
31
|
+
let node_url = require("node:url");
|
|
6
32
|
//#region ../types/dist/event-category-BLcNejAE.mjs
|
|
7
33
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
8
34
|
EventCategory["SystemBoot"] = "system.boot";
|
|
@@ -7304,35 +7330,22 @@ function buildCapturingReply(envelope) {
|
|
|
7304
7330
|
*/
|
|
7305
7331
|
var RecordingWeekdaySchema = number().int().min(0).max(6);
|
|
7306
7332
|
var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
7307
|
-
var RecordingScheduleSchema = discriminatedUnion("kind", [object({ kind: literal("always") }), object({
|
|
7308
|
-
kind: literal("timeOfDay"),
|
|
7309
|
-
start: string().regex(HHMM),
|
|
7310
|
-
end: string().regex(HHMM),
|
|
7311
|
-
/** Restrict to these weekdays; omit = every day. */
|
|
7312
|
-
days: array(RecordingWeekdaySchema).optional()
|
|
7313
|
-
})]);
|
|
7314
|
-
var RecordingModeSchema = _enum([
|
|
7315
|
-
"continuous",
|
|
7316
|
-
"onMotion",
|
|
7317
|
-
"onAudioThreshold"
|
|
7318
|
-
]);
|
|
7319
7333
|
/**
|
|
7320
|
-
*
|
|
7321
|
-
*
|
|
7322
|
-
* - `off` —
|
|
7323
|
-
* - `events` —
|
|
7324
|
-
*
|
|
7325
|
-
* - `continuous` — record 24/7 within the schedule.
|
|
7334
|
+
* DERIVED per-camera storage summary — the single field cheap consumers read
|
|
7335
|
+
* (the viewer's status dot, the camera list) instead of walking `bands`:
|
|
7336
|
+
* - `off` — no band covers the camera (or it is disabled).
|
|
7337
|
+
* - `events` — every band records around triggers only.
|
|
7338
|
+
* - `continuous` — at least one band records continuously.
|
|
7326
7339
|
*
|
|
7327
|
-
*
|
|
7328
|
-
*
|
|
7340
|
+
* NEVER authored: the recorder stamps it from the authoritative `bands` on
|
|
7341
|
+
* every save (`activeModeForConfig`). Writing it has no effect.
|
|
7329
7342
|
*/
|
|
7330
7343
|
var RecordingStorageModeSchema = _enum([
|
|
7331
7344
|
"off",
|
|
7332
7345
|
"events",
|
|
7333
7346
|
"continuous"
|
|
7334
7347
|
]);
|
|
7335
|
-
/** Which detectors trigger an `events`-mode
|
|
7348
|
+
/** Which detectors trigger an `events`-mode band. */
|
|
7336
7349
|
var RecordingTriggersSchema = object({
|
|
7337
7350
|
motion: boolean().optional(),
|
|
7338
7351
|
audioThresholdDbfs: number().optional()
|
|
@@ -7368,18 +7381,6 @@ var RecordingBandSchema = object({
|
|
|
7368
7381
|
preBufferSec: number().min(0).optional(),
|
|
7369
7382
|
postBufferSec: number().min(0).optional()
|
|
7370
7383
|
});
|
|
7371
|
-
var RecordingRuleSchema = object({
|
|
7372
|
-
schedule: RecordingScheduleSchema,
|
|
7373
|
-
mode: RecordingModeSchema,
|
|
7374
|
-
/** Seconds of footage to retain BEFORE a trigger (applied at keep/discard). */
|
|
7375
|
-
preBufferSec: number().min(0).default(0),
|
|
7376
|
-
/** Keep recording until this many seconds after the last trigger. */
|
|
7377
|
-
postBufferSec: number().min(0).default(0),
|
|
7378
|
-
/** Each new trigger restarts the post-buffer window. */
|
|
7379
|
-
resetTimeoutOnNewEvent: boolean().default(true),
|
|
7380
|
-
/** onAudioThreshold only — dBFS level that counts as a trigger. */
|
|
7381
|
-
thresholdDbfs: number().optional()
|
|
7382
|
-
});
|
|
7383
7384
|
/**
|
|
7384
7385
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7385
7386
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -7413,40 +7414,28 @@ var ScrubThumbnailPresetSchema = _enum([
|
|
|
7413
7414
|
/**
|
|
7414
7415
|
* The full per-camera recording intent — the wire shape of a RecordingTarget.
|
|
7415
7416
|
*
|
|
7416
|
-
* `
|
|
7417
|
-
*
|
|
7418
|
-
*
|
|
7419
|
-
*
|
|
7417
|
+
* `bands` is the ONLY authored recording intent: what to record, when, and on
|
|
7418
|
+
* which trigger. `mode` is a derived summary the recorder stamps on save; every
|
|
7419
|
+
* other field is a storage knob (profiles, segment length, retention, scrub).
|
|
7420
|
+
*
|
|
7421
|
+
* STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
|
|
7422
|
+
* `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
|
|
7423
|
+
* A stale caller must fail loudly — silently stripping its legacy intent would
|
|
7424
|
+
* persist a band-less config, i.e. silently stop recording the camera.
|
|
7420
7425
|
*/
|
|
7421
7426
|
var RecordingConfigSchema = object({
|
|
7422
7427
|
enabled: boolean(),
|
|
7423
|
-
/**
|
|
7424
|
-
*
|
|
7428
|
+
/** DERIVED summary of `bands`, stamped by the recorder on every save.
|
|
7429
|
+
* Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
|
|
7425
7430
|
mode: RecordingStorageModeSchema.optional(),
|
|
7426
7431
|
profiles: array(CamProfileSchema).optional(),
|
|
7427
7432
|
segmentSeconds: number().int().positive().optional(),
|
|
7428
|
-
/** Shared recording time-bands for `events` & `continuous` — record only when
|
|
7429
|
-
* the wall-clock falls inside one of these windows. Omit or empty = always.
|
|
7430
|
-
* `continuous` compiles to one rule per band; `events` to band × trigger. */
|
|
7431
|
-
schedules: array(RecordingScheduleSchema).optional(),
|
|
7432
|
-
/** Legacy single-band predecessor of `schedules`. Read-compat only — it is
|
|
7433
|
-
* normalized into `schedules` on read and never written going forward. (Not
|
|
7434
|
-
* tagged `@deprecated`: the normalization paths must read it cast-free.) */
|
|
7435
|
-
schedule: RecordingScheduleSchema.optional(),
|
|
7436
|
-
/** `events`-mode only — which detectors trigger a recording. */
|
|
7437
|
-
triggers: RecordingTriggersSchema.optional(),
|
|
7438
|
-
/** `events`-mode only — seconds retained before / after a trigger. */
|
|
7439
|
-
preBufferSec: number().min(0).optional(),
|
|
7440
|
-
postBufferSec: number().min(0).optional(),
|
|
7441
|
-
/** DEPRECATED authoring input; retained for migration/transition. */
|
|
7442
|
-
rules: array(RecordingRuleSchema).optional(),
|
|
7443
7433
|
/**
|
|
7444
|
-
* AUTHORITATIVE mode-per-band recording model
|
|
7445
|
-
*
|
|
7446
|
-
*
|
|
7447
|
-
* derived into bands once via `migrateConfigToBands`.
|
|
7434
|
+
* AUTHORITATIVE mode-per-band recording model — the single source of truth
|
|
7435
|
+
* the recorder's band engine consumes. An empty array = record nothing;
|
|
7436
|
+
* "off" is the absence of a covering band, never a band value.
|
|
7448
7437
|
*/
|
|
7449
|
-
bands: array(RecordingBandSchema).
|
|
7438
|
+
bands: array(RecordingBandSchema).default([]),
|
|
7450
7439
|
retention: RecordingRetentionSchema.optional(),
|
|
7451
7440
|
/**
|
|
7452
7441
|
* Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
|
|
@@ -7454,8 +7443,15 @@ var RecordingConfigSchema = object({
|
|
|
7454
7443
|
* windows only — existing sheets are immutable, and each window's index
|
|
7455
7444
|
* carries its own tile dims so mixed-preset history renders correctly.
|
|
7456
7445
|
*/
|
|
7457
|
-
scrubThumbnails: ScrubThumbnailPresetSchema.optional()
|
|
7458
|
-
|
|
7446
|
+
scrubThumbnails: ScrubThumbnailPresetSchema.optional(),
|
|
7447
|
+
/**
|
|
7448
|
+
* OPT-IN thumbnail-strip generation for this camera: every keyframe of the
|
|
7449
|
+
* low recording saved as a JPEG (the fast-drag scrub depth), a derived
|
|
7450
|
+
* cache that eviction reclaims with the footage. Absent/false = no strips
|
|
7451
|
+
* are written and scrub reads exact keyframes at every velocity.
|
|
7452
|
+
*/
|
|
7453
|
+
stripsEnabled: boolean().optional()
|
|
7454
|
+
}).strict();
|
|
7459
7455
|
/**
|
|
7460
7456
|
* Ops-log — the durable, append-only operations audit shared by the
|
|
7461
7457
|
* recordings and events management surfaces.
|
|
@@ -7474,7 +7470,8 @@ var OpsLogOpSchema = _enum([
|
|
|
7474
7470
|
"prune",
|
|
7475
7471
|
"manual-delete",
|
|
7476
7472
|
"rescan",
|
|
7477
|
-
"retention-run"
|
|
7473
|
+
"retention-run",
|
|
7474
|
+
"relocate"
|
|
7478
7475
|
]);
|
|
7479
7476
|
/** Why the operation ran. */
|
|
7480
7477
|
var OpsLogReasonSchema = _enum([
|
|
@@ -7513,6 +7510,55 @@ var OpsLogQueryInputSchema = object({
|
|
|
7513
7510
|
limit: number().int().min(1).max(1e3).optional()
|
|
7514
7511
|
});
|
|
7515
7512
|
/**
|
|
7513
|
+
* Entity-relocation job state (storage entity-routing spec, Phase 4).
|
|
7514
|
+
*
|
|
7515
|
+
* One shape shared by the recorder's `relocateFootage` (segments + strips) and
|
|
7516
|
+
* pipeline-analytics' `relocateMedia` (event media blobs) so the admin Data
|
|
7517
|
+
* page renders both movers with one component. Jobs are in-RAM (a restart
|
|
7518
|
+
* forgets them — re-running is safe by construction: copy-if-absent, delete
|
|
7519
|
+
* after verify) and each completed/failed run also lands one durable ops-log
|
|
7520
|
+
* row on the owning addon's surface.
|
|
7521
|
+
*/
|
|
7522
|
+
var RelocateJobStateSchema = _enum([
|
|
7523
|
+
"running",
|
|
7524
|
+
"done",
|
|
7525
|
+
"failed",
|
|
7526
|
+
"cancelled"
|
|
7527
|
+
]);
|
|
7528
|
+
var RelocateJobSchema = object({
|
|
7529
|
+
jobId: string(),
|
|
7530
|
+
state: RelocateJobStateSchema,
|
|
7531
|
+
/** Source location — for media relocation this is informational ('*': rows
|
|
7532
|
+
* move from wherever they are to the target). */
|
|
7533
|
+
fromLocationId: string(),
|
|
7534
|
+
toLocationId: string(),
|
|
7535
|
+
/** Scoped device, or null = every device. */
|
|
7536
|
+
deviceId: number().nullable(),
|
|
7537
|
+
/** What the job moves (owner-addon specific: segments/strips or media). */
|
|
7538
|
+
entities: array(string()),
|
|
7539
|
+
filesMoved: number().int(),
|
|
7540
|
+
bytesMoved: number().int(),
|
|
7541
|
+
/** Total files discovered up front; null while (or when) unknown. */
|
|
7542
|
+
filesTotal: number().int().nullable(),
|
|
7543
|
+
startedAt: number(),
|
|
7544
|
+
finishedAt: number().nullable(),
|
|
7545
|
+
error: string().nullable()
|
|
7546
|
+
});
|
|
7547
|
+
var RelocateFootageInputSchema = object({
|
|
7548
|
+
deviceId: number().optional(),
|
|
7549
|
+
fromLocationId: string(),
|
|
7550
|
+
toLocationId: string(),
|
|
7551
|
+
entities: array(_enum(["segments", "strips"])).optional(),
|
|
7552
|
+
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7553
|
+
* never allowed to starve live writers. */
|
|
7554
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
7555
|
+
});
|
|
7556
|
+
var RelocateMediaInputSchema = object({
|
|
7557
|
+
deviceId: number().optional(),
|
|
7558
|
+
toLocationId: string(),
|
|
7559
|
+
throttleMbps: number().min(1).max(1e3).optional()
|
|
7560
|
+
});
|
|
7561
|
+
/**
|
|
7516
7562
|
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
7517
7563
|
* storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
|
|
7518
7564
|
* so the persisted record schema and the consumer-facing cap can both consume it
|
|
@@ -7562,6 +7608,13 @@ var StorageLocationSchema = object({
|
|
|
7562
7608
|
nodeId: string().optional(),
|
|
7563
7609
|
isDefault: boolean().default(false),
|
|
7564
7610
|
isSystem: boolean().default(false),
|
|
7611
|
+
/** COMPUTED at read time by the orchestrator (statfs of the backing volume
|
|
7612
|
+
* for node-local locations it can reach) — never persisted, absent when the
|
|
7613
|
+
* volume is remote/unreachable. The single capacity truth every UI reads. */
|
|
7614
|
+
capacity: object({
|
|
7615
|
+
totalBytes: number(),
|
|
7616
|
+
availableBytes: number()
|
|
7617
|
+
}).nullable().optional(),
|
|
7565
7618
|
createdAt: number(),
|
|
7566
7619
|
updatedAt: number()
|
|
7567
7620
|
});
|
|
@@ -8329,7 +8382,8 @@ var NcTaxonomyEntrySchema = object({
|
|
|
8329
8382
|
/** Macro/category parent for grouping ('car' → 'vehicle'); null for a top. */
|
|
8330
8383
|
parentKind: string().nullable()
|
|
8331
8384
|
});
|
|
8332
|
-
|
|
8385
|
+
/** The complete NC picker taxonomy — three grouped buckets. */
|
|
8386
|
+
var NcTaxonomySchema = object({
|
|
8333
8387
|
videoClasses: array(NcTaxonomyEntrySchema),
|
|
8334
8388
|
audioKinds: array(NcTaxonomyEntrySchema),
|
|
8335
8389
|
labels: array(NcTaxonomyEntrySchema)
|
|
@@ -9052,9 +9106,20 @@ var DEGRADE_ORDER = {
|
|
|
9052
9106
|
"markdown"
|
|
9053
9107
|
]
|
|
9054
9108
|
};
|
|
9055
|
-
/**
|
|
9109
|
+
/**
|
|
9110
|
+
* Pick the best target format the kind supports for a given source format.
|
|
9111
|
+
*
|
|
9112
|
+
* TOLERANT of a source outside the union. `NotificationSchema.format` carries
|
|
9113
|
+
* `.default('text')`, but that default only runs where the schema is PARSED —
|
|
9114
|
+
* the tRPC router. A cap call arriving from another addon (`ctx.api`) skips
|
|
9115
|
+
* that parse, so `format` reaches here `undefined`, and the bare
|
|
9116
|
+
* `DEGRADE_ORDER[source]` threw `is not iterable`. That broke the one promise
|
|
9117
|
+
* the degrade path makes ("never throws — degradation is reported, never
|
|
9118
|
+
* surfaced as an error") and dead-lettered every Notification-Center delivery
|
|
9119
|
+
* for a day (2026-07-30). An unknown source degrades like `text`.
|
|
9120
|
+
*/
|
|
9056
9121
|
function resolveFormat(source, supported) {
|
|
9057
|
-
for (const candidate of DEGRADE_ORDER[source]) if (supported.includes(candidate)) return candidate;
|
|
9122
|
+
for (const candidate of DEGRADE_ORDER[source] ?? DEGRADE_ORDER.text) if (supported.includes(candidate)) return candidate;
|
|
9058
9123
|
return supported[0] ?? "text";
|
|
9059
9124
|
}
|
|
9060
9125
|
/** Transcode a body between formats. */
|
|
@@ -9099,7 +9164,7 @@ function resolveLevel(levels, n) {
|
|
|
9099
9164
|
})[0]);
|
|
9100
9165
|
}
|
|
9101
9166
|
function splitBody(body, maxLen) {
|
|
9102
|
-
if (maxLen <= 0 || body.length <= maxLen) return [body];
|
|
9167
|
+
if (!Number.isFinite(maxLen) || maxLen <= 0 || body.length <= maxLen) return [body];
|
|
9103
9168
|
const parts = [];
|
|
9104
9169
|
for (let i = 0; i < body.length; i += maxLen) parts.push(body.slice(i, i + maxLen));
|
|
9105
9170
|
return parts;
|
|
@@ -9182,6 +9247,7 @@ function prepareNotification(caps, n) {
|
|
|
9182
9247
|
renderedAs
|
|
9183
9248
|
};
|
|
9184
9249
|
}
|
|
9250
|
+
new Set(["devices", "classes"]);
|
|
9185
9251
|
/**
|
|
9186
9252
|
* Shared geometry vocabulary for on-frame shape caps — privacy-mask,
|
|
9187
9253
|
* motion-zones, and the detection zones/lines editor all speak this one
|
|
@@ -9498,12 +9564,60 @@ var NcRuleTargetSchema = object({
|
|
|
9498
9564
|
* - `keyFrame` — the clean scene frame (no subject box).
|
|
9499
9565
|
* - `none` — no attachment.
|
|
9500
9566
|
*/
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9567
|
+
/**
|
|
9568
|
+
* WHAT THE PICTURE SHOWS — chosen explicitly, because the implicit ladders
|
|
9569
|
+
* conflate it with the selection strategy and betray the request: asking for
|
|
9570
|
+
* the clean scene frame on an object-event owner used to start at
|
|
9571
|
+
* `fullFrameBoxed`, i.e. the annotated frame (2026-07-30).
|
|
9572
|
+
* - `cropped` — the subject, tight. What "who is at the door" wants.
|
|
9573
|
+
* - `full` — the clean scene, no annotation. What "what is going on" wants.
|
|
9574
|
+
* - `boxed` — the scene WITH the detection boxes drawn, for verifying what
|
|
9575
|
+
* the pipeline actually saw.
|
|
9576
|
+
*/
|
|
9577
|
+
var NcMediaFrameSchema = _enum([
|
|
9578
|
+
"cropped",
|
|
9579
|
+
"full",
|
|
9580
|
+
"boxed"
|
|
9581
|
+
]);
|
|
9582
|
+
var NcMediaPolicySchema = object({
|
|
9583
|
+
attach: _enum([
|
|
9584
|
+
"best",
|
|
9585
|
+
"best-matching",
|
|
9586
|
+
"keyFrame",
|
|
9587
|
+
"none"
|
|
9588
|
+
]).default("best"),
|
|
9589
|
+
/** What the still shows. Absent = `cropped` (the historical `best` shape). */
|
|
9590
|
+
frame: NcMediaFrameSchema.optional(),
|
|
9591
|
+
/** Crop the attached still to the rule's condition-zone bbox (padded) —
|
|
9592
|
+
* "show me the ZONE", not the whole scene or the subject crop. */
|
|
9593
|
+
zoneCrop: boolean().optional(),
|
|
9594
|
+
/**
|
|
9595
|
+
* Also attach a short GIF cut from the stream broker's clip ring around the
|
|
9596
|
+
* event — NOT from the recording, so the camera does not have to be
|
|
9597
|
+
* recording, and the window sits AROUND the moment instead of a segment
|
|
9598
|
+
* behind it. Fail-closed: a window the ring does not cover contributes no
|
|
9599
|
+
* gif, never a failed notification.
|
|
9600
|
+
*/
|
|
9601
|
+
gif: boolean().optional(),
|
|
9602
|
+
/**
|
|
9603
|
+
* Also attach a short MP4 CLIP of the same cut. Same source and the same
|
|
9604
|
+
* fail-closed rule as `gif`. Prefer it where the backend takes video
|
|
9605
|
+
* (telegram, discord, zentik, webhook); backends that do not are handled by
|
|
9606
|
+
* the degrade engine, which drops the video and keeps the still.
|
|
9607
|
+
*/
|
|
9608
|
+
clip: boolean().optional(),
|
|
9609
|
+
/** Seconds of footage BEFORE / AFTER the event instant. Defaults 3 / 7. */
|
|
9610
|
+
clipPreRollSec: number().int().min(0).max(30).optional(),
|
|
9611
|
+
clipPostRollSec: number().int().min(0).max(30).optional(),
|
|
9612
|
+
/**
|
|
9613
|
+
* Which stream profile the footage is cut from. Absent = the CHEAPEST
|
|
9614
|
+
* assigned profile: a notification is watched on a phone, so the 4K
|
|
9615
|
+
* rendition would burn CPU to produce a file the client downscales anyway.
|
|
9616
|
+
* A profile that is not assigned falls back to the cheapest, and the render
|
|
9617
|
+
* reports which one actually ran.
|
|
9618
|
+
*/
|
|
9619
|
+
profile: CamProfileSchema.optional()
|
|
9620
|
+
});
|
|
9507
9621
|
/** Throttle — cooldown survives restarts (rebuilt from the outbox on boot). */
|
|
9508
9622
|
var NcThrottleSchema = object({
|
|
9509
9623
|
cooldownSec: number().int().min(0).max(86400).default(60),
|
|
@@ -9517,7 +9631,19 @@ var NcRuleInputSchema = object({
|
|
|
9517
9631
|
delivery: NcDeliverySchema,
|
|
9518
9632
|
conditions: NcConditionsSchema.default({}),
|
|
9519
9633
|
schedule: NcScheduleSchema.optional(),
|
|
9520
|
-
|
|
9634
|
+
/** May be empty when `targetUsers` addresses at least one user — the
|
|
9635
|
+
* "at least one addressee" invariant is enforced by the provider, because
|
|
9636
|
+
* a cross-field refine here would break `NcRulePatchSchema.partial()`. */
|
|
9637
|
+
targets: array(NcRuleTargetSchema),
|
|
9638
|
+
/**
|
|
9639
|
+
* USERS this rule addresses in addition to `targets` (Phase 4). At fire
|
|
9640
|
+
* time each user fans out to the personal targets they own
|
|
9641
|
+
* (`config.ownerUserId`), filtered by that user's `allowedDevices` for the
|
|
9642
|
+
* firing camera — a user is never notified about a device they cannot open.
|
|
9643
|
+
* Per-user opt-outs (`disabledTargetIds`) still apply to the fanned-out
|
|
9644
|
+
* targets.
|
|
9645
|
+
*/
|
|
9646
|
+
targetUsers: array(string()).optional(),
|
|
9521
9647
|
media: NcMediaPolicySchema.default({ attach: "best" }),
|
|
9522
9648
|
throttle: NcThrottleSchema.default({
|
|
9523
9649
|
cooldownSec: 60,
|
|
@@ -9730,7 +9856,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
9730
9856
|
}), object({ results: array(NcTestResultSchema) }), {
|
|
9731
9857
|
kind: "mutation",
|
|
9732
9858
|
auth: "admin"
|
|
9733
|
-
}), method(object({}), object({
|
|
9859
|
+
}), method(object({}), object({
|
|
9860
|
+
catalog: array(NcConditionDescriptorSchema),
|
|
9861
|
+
taxonomy: NcTaxonomySchema.optional()
|
|
9862
|
+
})), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" });
|
|
9734
9863
|
/**
|
|
9735
9864
|
* TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
|
|
9736
9865
|
*
|
|
@@ -10467,6 +10596,28 @@ method(object({
|
|
|
10467
10596
|
}), object({ success: literal(true) }), {
|
|
10468
10597
|
kind: "mutation",
|
|
10469
10598
|
auth: "admin"
|
|
10599
|
+
}), method(object({
|
|
10600
|
+
deviceId: number(),
|
|
10601
|
+
/** Absent = the LOWEST assigned profile — a notification attachment is
|
|
10602
|
+
* watched on a phone, and the cheap rendition is the right default. */
|
|
10603
|
+
profile: CamProfileSchema.optional(),
|
|
10604
|
+
aroundMs: number(),
|
|
10605
|
+
preRollSec: number().min(0).max(20).default(3),
|
|
10606
|
+
postRollSec: number().min(0).max(20).default(5),
|
|
10607
|
+
format: _enum(["gif", "mp4"]).default("gif"),
|
|
10608
|
+
maxWidth: number().int().min(120).max(1920).default(480),
|
|
10609
|
+
/** GIF only — MP4 keeps the source cadence. */
|
|
10610
|
+
fps: number().int().min(1).max(15).default(5)
|
|
10611
|
+
}), object({
|
|
10612
|
+
base64: string(),
|
|
10613
|
+
mime: string(),
|
|
10614
|
+
bytes: number().int(),
|
|
10615
|
+
/** The profile actually rendered (what the default resolved to). */
|
|
10616
|
+
profile: CamProfileSchema,
|
|
10617
|
+
durationMs: number()
|
|
10618
|
+
}), {
|
|
10619
|
+
kind: "mutation",
|
|
10620
|
+
auth: "admin"
|
|
10470
10621
|
}), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
|
|
10471
10622
|
probed: boolean(),
|
|
10472
10623
|
summary: string()
|
|
@@ -17033,6 +17184,36 @@ var TargetKindSchema = object({
|
|
|
17033
17184
|
icon: string(),
|
|
17034
17185
|
/** Stamped by each provider so the concat-fanned catalog stays routable. */
|
|
17035
17186
|
addonId: string(),
|
|
17187
|
+
/**
|
|
17188
|
+
* URL of the kind's bundled BRAND icon, served by the providing addon over
|
|
17189
|
+
* its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
|
|
17190
|
+
* when the addon bundles no icon for that kind — the client then falls back
|
|
17191
|
+
* to a neutral glyph rather than rendering the raw `icon` NAME as text.
|
|
17192
|
+
*
|
|
17193
|
+
* Root-relative on purpose: it resolves against whatever origin serves a web
|
|
17194
|
+
* client, and a native client joins it onto its own hub base.
|
|
17195
|
+
*
|
|
17196
|
+
* DECLARED here deliberately. It used to travel as an undeclared passthrough
|
|
17197
|
+
* field that survived only because the runtime cap-router forwards provider
|
|
17198
|
+
* output verbatim — so every consumer had to re-declare it by hand to stop
|
|
17199
|
+
* its own Zod parse from stripping it, and the whole arrangement would have
|
|
17200
|
+
* broken silently the moment output validation was tightened anywhere.
|
|
17201
|
+
*/
|
|
17202
|
+
iconUrl: string().optional(),
|
|
17203
|
+
/**
|
|
17204
|
+
* Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
|
|
17205
|
+
*
|
|
17206
|
+
* The server knows this and therefore says it, because the client cannot
|
|
17207
|
+
* safely guess: a React-Native client renders SVG and raster through two
|
|
17208
|
+
* DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
|
|
17209
|
+
* not decode SVG on iOS/Android), so without this it silently fell back to a
|
|
17210
|
+
* placeholder glyph for every vector icon while the web build looked fine.
|
|
17211
|
+
*
|
|
17212
|
+
* Absent when {@link iconUrl} is absent, or for a legacy provider that has
|
|
17213
|
+
* not been updated — a client that cannot determine the type should prefer
|
|
17214
|
+
* its raster path, which is the safe default for an unknown image.
|
|
17215
|
+
*/
|
|
17216
|
+
iconMediaType: string().optional(),
|
|
17036
17217
|
configSchema: ConfigSchemaPassthrough,
|
|
17037
17218
|
supportsDiscovery: boolean(),
|
|
17038
17219
|
caps: TargetKindCapsSchema
|
|
@@ -17778,6 +17959,15 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17778
17959
|
}), method(object({ deviceId: number() }), EventPruneCountsSchema, {
|
|
17779
17960
|
kind: "mutation",
|
|
17780
17961
|
auth: "admin"
|
|
17962
|
+
}), method(RelocateMediaInputSchema, object({ jobId: string() }), {
|
|
17963
|
+
kind: "mutation",
|
|
17964
|
+
auth: "admin"
|
|
17965
|
+
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
17966
|
+
kind: "query",
|
|
17967
|
+
auth: "admin"
|
|
17968
|
+
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
17969
|
+
kind: "mutation",
|
|
17970
|
+
auth: "admin"
|
|
17781
17971
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
17782
17972
|
kind: "query",
|
|
17783
17973
|
auth: "admin"
|
|
@@ -20257,6 +20447,17 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
20257
20447
|
*/
|
|
20258
20448
|
priority: number()
|
|
20259
20449
|
})).readonly() });
|
|
20450
|
+
/**
|
|
20451
|
+
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
20452
|
+
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
20453
|
+
* what AUTO currently picks, so the UI can show the effective value either way.
|
|
20454
|
+
*/
|
|
20455
|
+
var NotificationEndpointSchema = object({
|
|
20456
|
+
/** The operator's explicit choice, or null for AUTO. */
|
|
20457
|
+
baseUrl: string().nullable(),
|
|
20458
|
+
/** What the ranking currently resolves to (null when nothing is reachable). */
|
|
20459
|
+
resolved: string().nullable()
|
|
20460
|
+
});
|
|
20260
20461
|
var AllowedAddressesSchema = object({
|
|
20261
20462
|
/**
|
|
20262
20463
|
* Allowlist of interface addresses operators have explicitly opted
|
|
@@ -20279,7 +20480,7 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
|
|
|
20279
20480
|
* to avoid mixed-content blocks in the browser. The public
|
|
20280
20481
|
* tunnel always emits `https://` regardless. */
|
|
20281
20482
|
scheme: _enum(["http", "https"]).optional()
|
|
20282
|
-
}), GetConnectionEndpointsResultSchema), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
|
|
20483
|
+
}), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
|
|
20283
20484
|
/**
|
|
20284
20485
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
20285
20486
|
*
|
|
@@ -21078,7 +21279,12 @@ var RecordingDeviceUsageSchema = object({
|
|
|
21078
21279
|
var RecordingLocationUsageSchema = object({
|
|
21079
21280
|
/** StorageLocation id; null for the legacy/degraded single-root fallback. */
|
|
21080
21281
|
locationId: string().nullable(),
|
|
21081
|
-
/**
|
|
21282
|
+
/** Every location id sharing this row's PHYSICAL volume (aliases). One row
|
|
21283
|
+
* is emitted per physical disk (2026-07-29): two locations on one root
|
|
21284
|
+
* previously rendered as two identical "disks" with a nonsensical used
|
|
21285
|
+
* split — hydrate attribution across aliases is arbitrary by nature. */
|
|
21286
|
+
locationIds: array(string()).optional(),
|
|
21287
|
+
/** Bytes of recordings stored on this PHYSICAL volume (all aliases). */
|
|
21082
21288
|
usedBytes: number(),
|
|
21083
21289
|
/** Free bytes on the location's volume; null when capacity is unknown (remote). */
|
|
21084
21290
|
availableBytes: number().nullable(),
|
|
@@ -21190,6 +21396,44 @@ method(object({
|
|
|
21190
21396
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
21191
21397
|
kind: "query",
|
|
21192
21398
|
auth: "admin"
|
|
21399
|
+
}), method(object({
|
|
21400
|
+
deviceId: number(),
|
|
21401
|
+
aroundMs: number(),
|
|
21402
|
+
preRollSec: number().min(0).max(30).default(2),
|
|
21403
|
+
postRollSec: number().min(0).max(30).default(5),
|
|
21404
|
+
maxWidth: number().int().min(120).max(1280).default(480),
|
|
21405
|
+
fps: number().int().min(1).max(15).default(5)
|
|
21406
|
+
}), object({
|
|
21407
|
+
gifBase64: string(),
|
|
21408
|
+
fromMs: number(),
|
|
21409
|
+
toMs: number()
|
|
21410
|
+
}), {
|
|
21411
|
+
kind: "mutation",
|
|
21412
|
+
auth: "admin"
|
|
21413
|
+
}), method(object({
|
|
21414
|
+
deviceId: number(),
|
|
21415
|
+
aroundMs: number(),
|
|
21416
|
+
preRollSec: number().min(0).max(30).default(3),
|
|
21417
|
+
postRollSec: number().min(0).max(30).default(7),
|
|
21418
|
+
maxWidth: number().int().min(160).max(1920).default(640)
|
|
21419
|
+
}), object({
|
|
21420
|
+
clipBase64: string(),
|
|
21421
|
+
mime: string(),
|
|
21422
|
+
fromMs: number(),
|
|
21423
|
+
toMs: number(),
|
|
21424
|
+
bytes: number().int()
|
|
21425
|
+
}), {
|
|
21426
|
+
kind: "mutation",
|
|
21427
|
+
auth: "admin"
|
|
21428
|
+
}), method(RelocateFootageInputSchema, object({ jobId: string() }), {
|
|
21429
|
+
kind: "mutation",
|
|
21430
|
+
auth: "admin"
|
|
21431
|
+
}), method(object({}), array(RelocateJobSchema).readonly(), {
|
|
21432
|
+
kind: "query",
|
|
21433
|
+
auth: "admin"
|
|
21434
|
+
}), method(object({ jobId: string() }), object({ cancelled: boolean() }), {
|
|
21435
|
+
kind: "mutation",
|
|
21436
|
+
auth: "admin"
|
|
21193
21437
|
});
|
|
21194
21438
|
/**
|
|
21195
21439
|
* `recordingExport` cap — render a footage time range into a single downloadable
|
|
@@ -23675,6 +23919,12 @@ Object.freeze({
|
|
|
23675
23919
|
addonId: null,
|
|
23676
23920
|
access: "view"
|
|
23677
23921
|
},
|
|
23922
|
+
"localNetwork.getNotificationEndpoint": {
|
|
23923
|
+
capName: "local-network",
|
|
23924
|
+
capScope: "system",
|
|
23925
|
+
addonId: null,
|
|
23926
|
+
access: "view"
|
|
23927
|
+
},
|
|
23678
23928
|
"localNetwork.getPreferred": {
|
|
23679
23929
|
capName: "local-network",
|
|
23680
23930
|
capScope: "system",
|
|
@@ -23699,6 +23949,12 @@ Object.freeze({
|
|
|
23699
23949
|
addonId: null,
|
|
23700
23950
|
access: "create"
|
|
23701
23951
|
},
|
|
23952
|
+
"localNetwork.setNotificationEndpoint": {
|
|
23953
|
+
capName: "local-network",
|
|
23954
|
+
capScope: "system",
|
|
23955
|
+
addonId: null,
|
|
23956
|
+
access: "create"
|
|
23957
|
+
},
|
|
23702
23958
|
"lockControl.lock": {
|
|
23703
23959
|
capName: "lock-control",
|
|
23704
23960
|
capScope: "device",
|
|
@@ -24341,6 +24597,12 @@ Object.freeze({
|
|
|
24341
24597
|
addonId: null,
|
|
24342
24598
|
access: "create"
|
|
24343
24599
|
},
|
|
24600
|
+
"pipelineAnalytics.cancelMediaRelocate": {
|
|
24601
|
+
capName: "pipeline-analytics",
|
|
24602
|
+
capScope: "device",
|
|
24603
|
+
addonId: null,
|
|
24604
|
+
access: "create"
|
|
24605
|
+
},
|
|
24344
24606
|
"pipelineAnalytics.clearTracks": {
|
|
24345
24607
|
capName: "pipeline-analytics",
|
|
24346
24608
|
capScope: "device",
|
|
@@ -24395,6 +24657,12 @@ Object.freeze({
|
|
|
24395
24657
|
addonId: null,
|
|
24396
24658
|
access: "view"
|
|
24397
24659
|
},
|
|
24660
|
+
"pipelineAnalytics.getMediaRelocateStatus": {
|
|
24661
|
+
capName: "pipeline-analytics",
|
|
24662
|
+
capScope: "device",
|
|
24663
|
+
addonId: null,
|
|
24664
|
+
access: "view"
|
|
24665
|
+
},
|
|
24398
24666
|
"pipelineAnalytics.getMotionEvents": {
|
|
24399
24667
|
capName: "pipeline-analytics",
|
|
24400
24668
|
capScope: "device",
|
|
@@ -24467,6 +24735,12 @@ Object.freeze({
|
|
|
24467
24735
|
addonId: null,
|
|
24468
24736
|
access: "create"
|
|
24469
24737
|
},
|
|
24738
|
+
"pipelineAnalytics.relocateMedia": {
|
|
24739
|
+
capName: "pipeline-analytics",
|
|
24740
|
+
capScope: "device",
|
|
24741
|
+
addonId: null,
|
|
24742
|
+
access: "create"
|
|
24743
|
+
},
|
|
24470
24744
|
"pipelineAnalytics.searchObjectEvents": {
|
|
24471
24745
|
capName: "pipeline-analytics",
|
|
24472
24746
|
capScope: "device",
|
|
@@ -25229,6 +25503,12 @@ Object.freeze({
|
|
|
25229
25503
|
addonId: null,
|
|
25230
25504
|
access: "create"
|
|
25231
25505
|
},
|
|
25506
|
+
"recording.cancelRelocate": {
|
|
25507
|
+
capName: "recording",
|
|
25508
|
+
capScope: "system",
|
|
25509
|
+
addonId: null,
|
|
25510
|
+
access: "create"
|
|
25511
|
+
},
|
|
25232
25512
|
"recording.deleteFootprint": {
|
|
25233
25513
|
capName: "recording",
|
|
25234
25514
|
capScope: "system",
|
|
@@ -25259,6 +25539,12 @@ Object.freeze({
|
|
|
25259
25539
|
addonId: null,
|
|
25260
25540
|
access: "view"
|
|
25261
25541
|
},
|
|
25542
|
+
"recording.getRelocateStatus": {
|
|
25543
|
+
capName: "recording",
|
|
25544
|
+
capScope: "system",
|
|
25545
|
+
addonId: null,
|
|
25546
|
+
access: "view"
|
|
25547
|
+
},
|
|
25262
25548
|
"recording.getStorageUsage": {
|
|
25263
25549
|
capName: "recording",
|
|
25264
25550
|
capScope: "system",
|
|
@@ -25289,6 +25575,24 @@ Object.freeze({
|
|
|
25289
25575
|
addonId: null,
|
|
25290
25576
|
access: "view"
|
|
25291
25577
|
},
|
|
25578
|
+
"recording.relocateFootage": {
|
|
25579
|
+
capName: "recording",
|
|
25580
|
+
capScope: "system",
|
|
25581
|
+
addonId: null,
|
|
25582
|
+
access: "create"
|
|
25583
|
+
},
|
|
25584
|
+
"recording.renderClip": {
|
|
25585
|
+
capName: "recording",
|
|
25586
|
+
capScope: "system",
|
|
25587
|
+
addonId: null,
|
|
25588
|
+
access: "create"
|
|
25589
|
+
},
|
|
25590
|
+
"recording.renderGif": {
|
|
25591
|
+
capName: "recording",
|
|
25592
|
+
capScope: "system",
|
|
25593
|
+
addonId: null,
|
|
25594
|
+
access: "create"
|
|
25595
|
+
},
|
|
25292
25596
|
"recording.rescanStorage": {
|
|
25293
25597
|
capName: "recording",
|
|
25294
25598
|
capScope: "system",
|
|
@@ -25883,6 +26187,12 @@ Object.freeze({
|
|
|
25883
26187
|
addonId: null,
|
|
25884
26188
|
access: "create"
|
|
25885
26189
|
},
|
|
26190
|
+
"streamBroker.renderPreBufferClip": {
|
|
26191
|
+
capName: "stream-broker",
|
|
26192
|
+
capScope: "system",
|
|
26193
|
+
addonId: null,
|
|
26194
|
+
access: "create"
|
|
26195
|
+
},
|
|
25886
26196
|
"streamBroker.restartProfile": {
|
|
25887
26197
|
capName: "stream-broker",
|
|
25888
26198
|
capScope: "system",
|
|
@@ -26577,7 +26887,7 @@ var NOTIFIER_ICONS = {
|
|
|
26577
26887
|
/** The addon id — also the `/addon/<id>/…` route prefix. Mirrors `NOTIFIERS_ADDON_ID`. */
|
|
26578
26888
|
var NOTIFIERS_ROUTE_ID = "notifiers";
|
|
26579
26889
|
/** One day; icons are immutable per addon version. */
|
|
26580
|
-
var CACHE_CONTROL = "public, max-age=86400, immutable";
|
|
26890
|
+
var CACHE_CONTROL$1 = "public, max-age=86400, immutable";
|
|
26581
26891
|
var ICON_KINDS = new Set(Object.keys(NOTIFIER_ICONS));
|
|
26582
26892
|
/** `true` when a bundled brand icon exists for `kind`. */
|
|
26583
26893
|
function hasIcon(kind) {
|
|
@@ -26591,6 +26901,15 @@ function hasIcon(kind) {
|
|
|
26591
26901
|
function iconUrlFor(kind) {
|
|
26592
26902
|
return hasIcon(kind) ? `/addon/${NOTIFIERS_ROUTE_ID}/icons/${kind}` : void 0;
|
|
26593
26903
|
}
|
|
26904
|
+
/**
|
|
26905
|
+
* The media type `iconUrlFor(kind)` serves, or `undefined` when no icon is
|
|
26906
|
+
* bundled. The client needs it because SVG and raster render through different
|
|
26907
|
+
* components on React Native, and it cannot be guessed from the URL — the route
|
|
26908
|
+
* path deliberately carries no file extension.
|
|
26909
|
+
*/
|
|
26910
|
+
function iconMediaTypeFor(kind) {
|
|
26911
|
+
return hasIcon(kind) ? NOTIFIER_ICONS[kind].contentType : void 0;
|
|
26912
|
+
}
|
|
26594
26913
|
/** Pre-encode one bundled icon into every content coding. */
|
|
26595
26914
|
function encodeIcon(icon) {
|
|
26596
26915
|
const identity = typeof icon.body === "string" ? Buffer.from(icon.body, "utf8") : Buffer.from(icon.body);
|
|
@@ -26626,15 +26945,143 @@ function iconRoute(kind) {
|
|
|
26626
26945
|
description: `Official brand icon (${encoded.contentType}) for the '${kind}' notifier kind.`,
|
|
26627
26946
|
handler: async (request, reply) => {
|
|
26628
26947
|
const coding = negotiateEncoding(String(request.headers["accept-encoding"] ?? ""));
|
|
26629
|
-
reply.code(200).type(encoded.contentType).header("cache-control", CACHE_CONTROL).header("vary", "accept-encoding");
|
|
26948
|
+
reply.code(200).type(encoded.contentType).header("cache-control", CACHE_CONTROL$1).header("vary", "accept-encoding");
|
|
26630
26949
|
if (coding !== null) reply.header("content-encoding", coding);
|
|
26631
26950
|
reply.send(coding !== null ? encoded[coding] : encoded.identity);
|
|
26632
26951
|
}
|
|
26633
26952
|
};
|
|
26634
26953
|
}
|
|
26635
|
-
/**
|
|
26636
|
-
|
|
26637
|
-
|
|
26954
|
+
/**
|
|
26955
|
+
* The icon routes, for composition into the addon's SINGLE `addon-routes`
|
|
26956
|
+
* provider. Not a provider of its own: a provider owns one route id and matches
|
|
26957
|
+
* paths within it, so the icons and the test samples must share one route set
|
|
26958
|
+
* (see `sample-routes.ts`).
|
|
26959
|
+
*/
|
|
26960
|
+
function iconRoutes() {
|
|
26961
|
+
return Object.keys(NOTIFIER_ICONS).map(iconRoute);
|
|
26962
|
+
}
|
|
26963
|
+
//#endregion
|
|
26964
|
+
//#region src/sample-routes.ts
|
|
26965
|
+
/**
|
|
26966
|
+
* Serves the bundled test-notification sample media over the addon's own HTTP
|
|
26967
|
+
* surface via the `addon-routes` cap. One public GET route per sample:
|
|
26968
|
+
*
|
|
26969
|
+
* GET /addon/notifiers/samples/<name> → the bundled sample bytes
|
|
26970
|
+
*
|
|
26971
|
+
* WHY THIS EXISTS
|
|
26972
|
+
* ───────────────
|
|
26973
|
+
* The test panel used to attach `https://example.com/sample.jpg` — the IANA
|
|
26974
|
+
* reserved documentation domain, which serves no media. Every "does my target
|
|
26975
|
+
* deliver an image / gif / video" test was therefore broken by construction and
|
|
26976
|
+
* had never verified anything. The samples have to live somewhere the addon can
|
|
26977
|
+
* actually fetch, on an install with no internet, so they ship with the addon.
|
|
26978
|
+
*
|
|
26979
|
+
* The bytes are read from `assets/samples/` (not embedded in TS): a 128 KB mp4
|
|
26980
|
+
* as a base64 literal would inflate the bundle by ~170 KB of source. Path
|
|
26981
|
+
* resolution mirrors `addon-benchmark`'s `resolveFixturesDir` — `import.meta.url`
|
|
26982
|
+
* always points at the compiled bundle under `dist/`, so walking up one
|
|
26983
|
+
* directory lands on the package root whether the addon runs from source (tsx)
|
|
26984
|
+
* or from its built bundle.
|
|
26985
|
+
*
|
|
26986
|
+
* CONTENT-ENCODING IS OURS, DELIBERATELY
|
|
26987
|
+
* ──────────────────────────────────────
|
|
26988
|
+
* Same hazard `icon-routes.ts` documents: the hub pipes an addon route's reply
|
|
26989
|
+
* through its global `@fastify/compress`, and on the forked-addon UDS route
|
|
26990
|
+
* bridge that compressor emits an EMPTY brotli stream for a compressible body
|
|
26991
|
+
* over 1 KiB. The compressor skips a response that already declares a
|
|
26992
|
+
* non-identity `content-encoding`, so we negotiate and stamp it ourselves.
|
|
26993
|
+
*
|
|
26994
|
+
* Media types here are all ALREADY compressed (JPEG/GIF/MP4/MP3), so we serve
|
|
26995
|
+
* identity bytes and stamp `content-encoding: identity` — re-compressing them
|
|
26996
|
+
* wastes CPU for no gain, and the explicit header is what keeps the hub's
|
|
26997
|
+
* compressor off a body it would corrupt.
|
|
26998
|
+
*/
|
|
26999
|
+
/** Samples are immutable per addon version. */
|
|
27000
|
+
var CACHE_CONTROL = "public, max-age=86400, immutable";
|
|
27001
|
+
/**
|
|
27002
|
+
* The bundled set. Keyed by the `AttachmentMediaType` each one exercises, so a
|
|
27003
|
+
* caps-driven test panel can ask for "the sample for this media type" without
|
|
27004
|
+
* knowing filenames. `icon` deliberately reuses the image sample — an icon slot
|
|
27005
|
+
* wants a small raster, and shipping a fifth file to say the same thing is
|
|
27006
|
+
* waste.
|
|
27007
|
+
*/
|
|
27008
|
+
var SAMPLES = [
|
|
27009
|
+
{
|
|
27010
|
+
name: "image",
|
|
27011
|
+
file: "image.jpg",
|
|
27012
|
+
contentType: "image/jpeg"
|
|
27013
|
+
},
|
|
27014
|
+
{
|
|
27015
|
+
name: "gif",
|
|
27016
|
+
file: "animation.gif",
|
|
27017
|
+
contentType: "image/gif"
|
|
27018
|
+
},
|
|
27019
|
+
{
|
|
27020
|
+
name: "video",
|
|
27021
|
+
file: "clip.mp4",
|
|
27022
|
+
contentType: "video/mp4"
|
|
27023
|
+
},
|
|
27024
|
+
{
|
|
27025
|
+
name: "audio",
|
|
27026
|
+
file: "tone.mp3",
|
|
27027
|
+
contentType: "audio/mpeg"
|
|
27028
|
+
},
|
|
27029
|
+
{
|
|
27030
|
+
name: "icon",
|
|
27031
|
+
file: "image.jpg",
|
|
27032
|
+
contentType: "image/jpeg"
|
|
27033
|
+
}
|
|
27034
|
+
];
|
|
27035
|
+
new Set(SAMPLES.map((s) => s.name));
|
|
27036
|
+
/** Resolve the addon's bundled samples directory (see the file header). */
|
|
27037
|
+
function resolveSamplesDir() {
|
|
27038
|
+
const here = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
27039
|
+
const pkgRoot = node_path.default.resolve(node_path.default.dirname(here), "..");
|
|
27040
|
+
return node_path.default.join(pkgRoot, "assets", "samples");
|
|
27041
|
+
}
|
|
27042
|
+
/**
|
|
27043
|
+
* Read every sample eagerly so a missing asset fails at BOOT with a clear
|
|
27044
|
+
* error, rather than on the first operator click with a 500 that reads as a
|
|
27045
|
+
* broken notifier.
|
|
27046
|
+
*/
|
|
27047
|
+
function loadSamples() {
|
|
27048
|
+
const dir = resolveSamplesDir();
|
|
27049
|
+
const out = /* @__PURE__ */ new Map();
|
|
27050
|
+
for (const sample of SAMPLES) {
|
|
27051
|
+
const bytes = (0, node_fs.readFileSync)(node_path.default.join(dir, sample.file));
|
|
27052
|
+
out.set(sample.name, {
|
|
27053
|
+
contentType: sample.contentType,
|
|
27054
|
+
bytes
|
|
27055
|
+
});
|
|
27056
|
+
}
|
|
27057
|
+
return out;
|
|
27058
|
+
}
|
|
27059
|
+
/** One static GET route serving a single bundled sample. */
|
|
27060
|
+
function sampleRoute(sample, loaded) {
|
|
27061
|
+
return {
|
|
27062
|
+
method: "GET",
|
|
27063
|
+
path: `/samples/${sample.name}`,
|
|
27064
|
+
access: "public",
|
|
27065
|
+
description: `Bundled ${sample.contentType} test sample for the '${sample.name}' attachment slot.`,
|
|
27066
|
+
handler: async (_request, reply) => {
|
|
27067
|
+
reply.code(200).type(loaded.contentType).header("cache-control", CACHE_CONTROL).header("content-encoding", "identity").header("content-length", String(loaded.bytes.byteLength));
|
|
27068
|
+
reply.send(loaded.bytes);
|
|
27069
|
+
}
|
|
27070
|
+
};
|
|
27071
|
+
}
|
|
27072
|
+
/**
|
|
27073
|
+
* The sample routes, for composition into the addon's SINGLE `addon-routes`
|
|
27074
|
+
* provider. Deliberately not a provider of its own: a provider owns one route
|
|
27075
|
+
* id and matches paths within it, so two providers sharing `notifiers` would
|
|
27076
|
+
* collide — the icons and the samples must travel in one route set.
|
|
27077
|
+
*/
|
|
27078
|
+
function sampleRoutes() {
|
|
27079
|
+
const loaded = loadSamples();
|
|
27080
|
+
return SAMPLES.map((sample) => {
|
|
27081
|
+
const bytes = loaded.get(sample.name);
|
|
27082
|
+
if (bytes === void 0) throw new Error(`notifiers: sample '${sample.name}' failed to load`);
|
|
27083
|
+
return sampleRoute(sample, bytes);
|
|
27084
|
+
});
|
|
26638
27085
|
}
|
|
26639
27086
|
//#endregion
|
|
26640
27087
|
//#region src/nc-target-actions.ts
|
|
@@ -29223,6 +29670,7 @@ function createNotificationOutputProvider(deps) {
|
|
|
29223
29670
|
};
|
|
29224
29671
|
}
|
|
29225
29672
|
const iconUrlFor = deps.iconUrlFor;
|
|
29673
|
+
const iconMediaTypeFor = deps.iconMediaTypeFor;
|
|
29226
29674
|
return {
|
|
29227
29675
|
listTargetKinds: async () => deps.adapters.map((a) => {
|
|
29228
29676
|
const descriptor = {
|
|
@@ -29230,10 +29678,16 @@ function createNotificationOutputProvider(deps) {
|
|
|
29230
29678
|
addonId
|
|
29231
29679
|
};
|
|
29232
29680
|
const iconUrl = iconUrlFor?.(a.descriptor.kind);
|
|
29233
|
-
|
|
29681
|
+
if (iconUrl === void 0) return descriptor;
|
|
29682
|
+
const iconMediaType = iconMediaTypeFor?.(a.descriptor.kind);
|
|
29683
|
+
return iconMediaType !== void 0 ? {
|
|
29684
|
+
...descriptor,
|
|
29685
|
+
iconUrl,
|
|
29686
|
+
iconMediaType
|
|
29687
|
+
} : {
|
|
29234
29688
|
...descriptor,
|
|
29235
29689
|
iconUrl
|
|
29236
|
-
}
|
|
29690
|
+
};
|
|
29237
29691
|
}),
|
|
29238
29692
|
listTargets: async () => {
|
|
29239
29693
|
return (await store.list()).map((target) => {
|
|
@@ -29504,9 +29958,10 @@ var NotifiersAddon = class extends BaseAddon {
|
|
|
29504
29958
|
store,
|
|
29505
29959
|
adapters: ADAPTERS,
|
|
29506
29960
|
logger: this.ctx.logger.child("notification-output"),
|
|
29507
|
-
iconUrlFor
|
|
29961
|
+
iconUrlFor,
|
|
29962
|
+
iconMediaTypeFor
|
|
29508
29963
|
});
|
|
29509
|
-
const
|
|
29964
|
+
const routeProvider = buildAddonRouteProvider(NOTIFIERS_ADDON_ID, [...iconRoutes(), ...sampleRoutes()]);
|
|
29510
29965
|
const ncHandlers = makeNcTargetHandlers({
|
|
29511
29966
|
provider,
|
|
29512
29967
|
logger: this.ctx.logger.child("nc-target-actions")
|
|
@@ -29518,7 +29973,7 @@ var NotifiersAddon = class extends BaseAddon {
|
|
|
29518
29973
|
provider
|
|
29519
29974
|
}, {
|
|
29520
29975
|
capability: addonRoutesCapability,
|
|
29521
|
-
provider:
|
|
29976
|
+
provider: routeProvider
|
|
29522
29977
|
}],
|
|
29523
29978
|
customActions: ncTargetActions,
|
|
29524
29979
|
actionHandlers: ncHandlers
|