@camstack/addon-smtp-nodemailer 1.2.95 → 1.2.97

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.
@@ -7324,6 +7324,23 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7324
7324
  action: string().min(1),
7325
7325
  input: unknown()
7326
7326
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7327
+ new Set([
7328
+ "track",
7329
+ "summary",
7330
+ "face",
7331
+ "identity",
7332
+ "plate",
7333
+ "vehicle",
7334
+ "scene",
7335
+ "motion",
7336
+ "object",
7337
+ "audio"
7338
+ ]);
7339
+ new Set([
7340
+ "motion",
7341
+ "object",
7342
+ "audio"
7343
+ ]);
7327
7344
  var EncodeProfileSchema = object({
7328
7345
  video: object({
7329
7346
  codec: _enum([
@@ -12422,8 +12439,17 @@ method(object({
12422
12439
  }), array(SettingsRecordSchema).readonly()), method(object({
12423
12440
  namespace: string().optional(),
12424
12441
  collection: string(),
12425
- record: SettingsRecordSchema
12426
- }), _void(), { kind: "mutation" }), method(object({
12442
+ record: object({
12443
+ id: string().optional(),
12444
+ data: record(string(), unknown())
12445
+ })
12446
+ }), object({
12447
+ /**
12448
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12449
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12450
+ * primary key — which is the only place an auto key is knowable.
12451
+ */
12452
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12427
12453
  namespace: string().optional(),
12428
12454
  collection: string(),
12429
12455
  records: array(BulkRecordSchema).readonly()
@@ -12532,8 +12558,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12532
12558
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12533
12559
  namespace: string().optional(),
12534
12560
  collection: string(),
12535
- record: SettingsRecordSchema
12536
- }), _void(), {
12561
+ record: object({
12562
+ id: string().optional(),
12563
+ data: record(string(), unknown())
12564
+ })
12565
+ }), object({
12566
+ /**
12567
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12568
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12569
+ * primary key — which is the only place an auto key is knowable.
12570
+ */
12571
+ id: union([string(), number()]) }), {
12537
12572
  kind: "mutation",
12538
12573
  auth: "admin"
12539
12574
  }), method(object({
@@ -19175,7 +19210,20 @@ var TrackSchema = object({
19175
19210
  ...TrackRetrainFields
19176
19211
  });
19177
19212
  var BaseEventFields = {
19178
- id: string(),
19213
+ /**
19214
+ * A SQLite ROWID, assigned by the database (D474).
19215
+ *
19216
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19217
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19218
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19219
+ * the rowid: the table itself is that B-tree, so the index stops existing
19220
+ * rather than getting smaller. No shorter string does that.
19221
+ *
19222
+ * Defined once here for all three event kinds, which is why they move
19223
+ * together: a per-table migration would have forked this and
19224
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19225
+ */
19226
+ id: number().int(),
19179
19227
  deviceId: number(),
19180
19228
  timestamp: number()
19181
19229
  };
@@ -19194,7 +19242,34 @@ var MotionEventSchema = object({
19194
19242
  /** Omitted in slim projection. */
19195
19243
  frameHeight: number().optional(),
19196
19244
  /** Populated by B5 (recording playback URL for this event). */
19197
- mediaUrl: string().optional()
19245
+ mediaUrl: string().optional(),
19246
+ /**
19247
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19248
+ * episode is still open — a further rising edge extends it in place rather
19249
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19250
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19251
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19252
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19253
+ * motion for an instantaneous trigger).
19254
+ *
19255
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19256
+ * "closed the old way, before this column existed", never "still open".
19257
+ * Nothing in this codebase may read an absent `durationMs` as an open
19258
+ * episode; only `null` means open.
19259
+ */
19260
+ durationMs: number().nullable().optional(),
19261
+ /**
19262
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19263
+ * first entry is always `0`) of every genuine off→on transition the
19264
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19265
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19266
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19267
+ * explicit `false` mid-episode and then resumes before the quiet window
19268
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19269
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19270
+ * which must never be read as "no episode happened here".
19271
+ */
19272
+ edges: array(number()).readonly().optional()
19198
19273
  });
19199
19274
  /**
19200
19275
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19249,6 +19324,23 @@ var ObjectEventSchema = object({
19249
19324
  * includes it (it is light). Absent on rows written before this field.
19250
19325
  */
19251
19326
  frameId: string().optional(),
19327
+ /**
19328
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19329
+ * (D474).
19330
+ *
19331
+ * Only the package detector writes it, and it exists because the event id
19332
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19333
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19334
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19335
+ * assigned by SQLite, so that key had to move off the primary key rather
19336
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19337
+ * every parcel on every boot.
19338
+ *
19339
+ * Absent on every other object event, and on every row written before this
19340
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19341
+ * not per row, and nothing addresses a row by it.
19342
+ */
19343
+ idempotencyKey: string().optional(),
19252
19344
  /** Omitted in slim projection. */
19253
19345
  trackId: string().optional(),
19254
19346
  className: string(),
@@ -20351,6 +20443,14 @@ var NativeCropResultSchema = object({
20351
20443
  * set `encodeJpeg: true`; `bytes` is then absent.
20352
20444
  */
20353
20445
  jpeg: string().optional(),
20446
+ /**
20447
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20448
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20449
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20450
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20451
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20452
+ */
20453
+ jpegBytes: _instanceof(Uint8Array).optional(),
20354
20454
  width: number().int().positive(),
20355
20455
  height: number().int().positive(),
20356
20456
  /**
@@ -20417,7 +20517,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20417
20517
  })]);
20418
20518
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20419
20519
  var ParkedTrackFrameSchema = object({
20420
- jpeg: string(),
20520
+ /**
20521
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20522
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20523
+ * Exactly one of the two is present.
20524
+ */
20525
+ jpeg: string().optional(),
20526
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20527
+ jpegBytes: _instanceof(Uint8Array).optional(),
20421
20528
  width: number().int().positive(),
20422
20529
  height: number().int().positive(),
20423
20530
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21004,6 +21111,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21004
21111
  bbox: NativeCropBboxSchema,
21005
21112
  maxWidth: number().int().positive().optional(),
21006
21113
  /**
21114
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21115
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21116
+ * wire — never assume consent: a pre-D462 caller parses the field as
21117
+ * base64 and bytes would decode to garbage rather than fail.
21118
+ */
21119
+ acceptJpegBytes: boolean().optional(),
21120
+ /**
21007
21121
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21008
21122
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21009
21123
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21071,7 +21185,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21071
21185
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21072
21186
  deviceId: number(),
21073
21187
  trackId: string(),
21074
- kind: ParkedFrameKindSchema
21188
+ kind: ParkedFrameKindSchema,
21189
+ /**
21190
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21191
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21192
+ * wire — never assume consent: a pre-D462 caller parses the field as
21193
+ * base64 and bytes would decode to garbage rather than fail.
21194
+ */
21195
+ acceptJpegBytes: boolean().optional()
21075
21196
  }), ParkedTrackFrameSchema.nullable()), method(object({
21076
21197
  deviceId: number(),
21077
21198
  trackId: string()
@@ -26299,9 +26420,54 @@ object({
26299
26420
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26300
26421
  lastDetectedAt: number().nullable(),
26301
26422
  /**
26302
- * Ms after which `detected` auto-reverts to false if no fresh push
26303
- * arrives. Null means the provider leaves detected state until a
26304
- * native "clear" event.
26423
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26424
+ * `null` while false and on every `Sensor` device (D475) — see that
26425
+ * constant's doc for the one-authority rule.
26426
+ *
26427
+ * ## Reading this field still arms nothing
26428
+ *
26429
+ * It reads like an instruction to the consumer ("revert after N ms if
26430
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26431
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26432
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26433
+ * imported constant, not as a read of `device.state.motion.value`, so this
26434
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26435
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26436
+ * field would add a second falling-edge authority beside whichever one
26437
+ * already owns the device, and two that can disagree are worse than one.
26438
+ * Consumers that need a falling edge SHAPED differently — held open across
26439
+ * a flapping source — debounce on their own side and say so, as
26440
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26441
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26442
+ *
26443
+ * ## Who writes it
26444
+ *
26445
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26446
+ * `cooldown_expired`, which then writes this slice with
26447
+ * `detected: false` (`handlePhaseChanged` in
26448
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26449
+ * matters most for the sources that only ever push a rising one:
26450
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26451
+ * a false.
26452
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26453
+ * provider pushes the false itself, from the upstream system's own
26454
+ * state change. No phase machine is involved.
26455
+ *
26456
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26457
+ *
26458
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26459
+ * phase machine is the sole writer for a camera. It is not.
26460
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26461
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26462
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26463
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26464
+ * two authorities can disagree about one slice, which this repo
26465
+ * forbids, and the doc said otherwise — which is worse than saying
26466
+ * nothing, because it reads as verification.
26467
+ *
26468
+ * This predates D475 and is not fixed there: the fix touches every
26469
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26470
+ * "sole writer" wording without also removing the other writers.
26305
26471
  */
26306
26472
  autoClearAfterMs: number().nullable()
26307
26473
  });
@@ -7322,6 +7322,23 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7322
7322
  action: string().min(1),
7323
7323
  input: unknown()
7324
7324
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7325
+ new Set([
7326
+ "track",
7327
+ "summary",
7328
+ "face",
7329
+ "identity",
7330
+ "plate",
7331
+ "vehicle",
7332
+ "scene",
7333
+ "motion",
7334
+ "object",
7335
+ "audio"
7336
+ ]);
7337
+ new Set([
7338
+ "motion",
7339
+ "object",
7340
+ "audio"
7341
+ ]);
7325
7342
  var EncodeProfileSchema = object({
7326
7343
  video: object({
7327
7344
  codec: _enum([
@@ -12420,8 +12437,17 @@ method(object({
12420
12437
  }), array(SettingsRecordSchema).readonly()), method(object({
12421
12438
  namespace: string().optional(),
12422
12439
  collection: string(),
12423
- record: SettingsRecordSchema
12424
- }), _void(), { kind: "mutation" }), method(object({
12440
+ record: object({
12441
+ id: string().optional(),
12442
+ data: record(string(), unknown())
12443
+ })
12444
+ }), object({
12445
+ /**
12446
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12447
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12448
+ * primary key — which is the only place an auto key is knowable.
12449
+ */
12450
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12425
12451
  namespace: string().optional(),
12426
12452
  collection: string(),
12427
12453
  records: array(BulkRecordSchema).readonly()
@@ -12530,8 +12556,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12530
12556
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12531
12557
  namespace: string().optional(),
12532
12558
  collection: string(),
12533
- record: SettingsRecordSchema
12534
- }), _void(), {
12559
+ record: object({
12560
+ id: string().optional(),
12561
+ data: record(string(), unknown())
12562
+ })
12563
+ }), object({
12564
+ /**
12565
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12566
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12567
+ * primary key — which is the only place an auto key is knowable.
12568
+ */
12569
+ id: union([string(), number()]) }), {
12535
12570
  kind: "mutation",
12536
12571
  auth: "admin"
12537
12572
  }), method(object({
@@ -19173,7 +19208,20 @@ var TrackSchema = object({
19173
19208
  ...TrackRetrainFields
19174
19209
  });
19175
19210
  var BaseEventFields = {
19176
- id: string(),
19211
+ /**
19212
+ * A SQLite ROWID, assigned by the database (D474).
19213
+ *
19214
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19215
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19216
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19217
+ * the rowid: the table itself is that B-tree, so the index stops existing
19218
+ * rather than getting smaller. No shorter string does that.
19219
+ *
19220
+ * Defined once here for all three event kinds, which is why they move
19221
+ * together: a per-table migration would have forked this and
19222
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19223
+ */
19224
+ id: number().int(),
19177
19225
  deviceId: number(),
19178
19226
  timestamp: number()
19179
19227
  };
@@ -19192,7 +19240,34 @@ var MotionEventSchema = object({
19192
19240
  /** Omitted in slim projection. */
19193
19241
  frameHeight: number().optional(),
19194
19242
  /** Populated by B5 (recording playback URL for this event). */
19195
- mediaUrl: string().optional()
19243
+ mediaUrl: string().optional(),
19244
+ /**
19245
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19246
+ * episode is still open — a further rising edge extends it in place rather
19247
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19248
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19249
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19250
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19251
+ * motion for an instantaneous trigger).
19252
+ *
19253
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19254
+ * "closed the old way, before this column existed", never "still open".
19255
+ * Nothing in this codebase may read an absent `durationMs` as an open
19256
+ * episode; only `null` means open.
19257
+ */
19258
+ durationMs: number().nullable().optional(),
19259
+ /**
19260
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19261
+ * first entry is always `0`) of every genuine off→on transition the
19262
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19263
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19264
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19265
+ * explicit `false` mid-episode and then resumes before the quiet window
19266
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19267
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19268
+ * which must never be read as "no episode happened here".
19269
+ */
19270
+ edges: array(number()).readonly().optional()
19196
19271
  });
19197
19272
  /**
19198
19273
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19247,6 +19322,23 @@ var ObjectEventSchema = object({
19247
19322
  * includes it (it is light). Absent on rows written before this field.
19248
19323
  */
19249
19324
  frameId: string().optional(),
19325
+ /**
19326
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19327
+ * (D474).
19328
+ *
19329
+ * Only the package detector writes it, and it exists because the event id
19330
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19331
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19332
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19333
+ * assigned by SQLite, so that key had to move off the primary key rather
19334
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19335
+ * every parcel on every boot.
19336
+ *
19337
+ * Absent on every other object event, and on every row written before this
19338
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19339
+ * not per row, and nothing addresses a row by it.
19340
+ */
19341
+ idempotencyKey: string().optional(),
19250
19342
  /** Omitted in slim projection. */
19251
19343
  trackId: string().optional(),
19252
19344
  className: string(),
@@ -20349,6 +20441,14 @@ var NativeCropResultSchema = object({
20349
20441
  * set `encodeJpeg: true`; `bytes` is then absent.
20350
20442
  */
20351
20443
  jpeg: string().optional(),
20444
+ /**
20445
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20446
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20447
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20448
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20449
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20450
+ */
20451
+ jpegBytes: _instanceof(Uint8Array).optional(),
20352
20452
  width: number().int().positive(),
20353
20453
  height: number().int().positive(),
20354
20454
  /**
@@ -20415,7 +20515,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20415
20515
  })]);
20416
20516
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20417
20517
  var ParkedTrackFrameSchema = object({
20418
- jpeg: string(),
20518
+ /**
20519
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20520
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20521
+ * Exactly one of the two is present.
20522
+ */
20523
+ jpeg: string().optional(),
20524
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20525
+ jpegBytes: _instanceof(Uint8Array).optional(),
20419
20526
  width: number().int().positive(),
20420
20527
  height: number().int().positive(),
20421
20528
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21002,6 +21109,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21002
21109
  bbox: NativeCropBboxSchema,
21003
21110
  maxWidth: number().int().positive().optional(),
21004
21111
  /**
21112
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21113
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21114
+ * wire — never assume consent: a pre-D462 caller parses the field as
21115
+ * base64 and bytes would decode to garbage rather than fail.
21116
+ */
21117
+ acceptJpegBytes: boolean().optional(),
21118
+ /**
21005
21119
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21006
21120
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21007
21121
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21069,7 +21183,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21069
21183
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21070
21184
  deviceId: number(),
21071
21185
  trackId: string(),
21072
- kind: ParkedFrameKindSchema
21186
+ kind: ParkedFrameKindSchema,
21187
+ /**
21188
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21189
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21190
+ * wire — never assume consent: a pre-D462 caller parses the field as
21191
+ * base64 and bytes would decode to garbage rather than fail.
21192
+ */
21193
+ acceptJpegBytes: boolean().optional()
21073
21194
  }), ParkedTrackFrameSchema.nullable()), method(object({
21074
21195
  deviceId: number(),
21075
21196
  trackId: string()
@@ -26297,9 +26418,54 @@ object({
26297
26418
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26298
26419
  lastDetectedAt: number().nullable(),
26299
26420
  /**
26300
- * Ms after which `detected` auto-reverts to false if no fresh push
26301
- * arrives. Null means the provider leaves detected state until a
26302
- * native "clear" event.
26421
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26422
+ * `null` while false and on every `Sensor` device (D475) — see that
26423
+ * constant's doc for the one-authority rule.
26424
+ *
26425
+ * ## Reading this field still arms nothing
26426
+ *
26427
+ * It reads like an instruction to the consumer ("revert after N ms if
26428
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26429
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26430
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26431
+ * imported constant, not as a read of `device.state.motion.value`, so this
26432
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26433
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26434
+ * field would add a second falling-edge authority beside whichever one
26435
+ * already owns the device, and two that can disagree are worse than one.
26436
+ * Consumers that need a falling edge SHAPED differently — held open across
26437
+ * a flapping source — debounce on their own side and say so, as
26438
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26439
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26440
+ *
26441
+ * ## Who writes it
26442
+ *
26443
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26444
+ * `cooldown_expired`, which then writes this slice with
26445
+ * `detected: false` (`handlePhaseChanged` in
26446
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26447
+ * matters most for the sources that only ever push a rising one:
26448
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26449
+ * a false.
26450
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26451
+ * provider pushes the false itself, from the upstream system's own
26452
+ * state change. No phase machine is involved.
26453
+ *
26454
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26455
+ *
26456
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26457
+ * phase machine is the sole writer for a camera. It is not.
26458
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26459
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26460
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26461
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26462
+ * two authorities can disagree about one slice, which this repo
26463
+ * forbids, and the doc said otherwise — which is worse than saying
26464
+ * nothing, because it reads as verification.
26465
+ *
26466
+ * This predates D475 and is not fixed there: the fix touches every
26467
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26468
+ * "sole writer" wording without also removing the other writers.
26303
26469
  */
26304
26470
  autoClearAfterMs: number().nullable()
26305
26471
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-smtp-nodemailer",
3
- "version": "1.2.95",
3
+ "version": "1.2.97",
4
4
  "description": "SMTP email provider addon for CamStack — wraps `nodemailer` and registers a `smtp-provider` cap collection entry. Used by magic-link login + notifier addons.",
5
5
  "keywords": [
6
6
  "camstack",