@camstack/addon-pipeline 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/audio-analyzer/index.js +2 -4
  2. package/dist/audio-analyzer/index.js.map +1 -1
  3. package/dist/audio-analyzer/index.mjs +2 -4
  4. package/dist/audio-analyzer/index.mjs.map +1 -1
  5. package/dist/audio-codec-nodeav/index.js +1 -1
  6. package/dist/audio-codec-nodeav/index.mjs +1 -1
  7. package/dist/decoder-nodeav/index.js +552 -18
  8. package/dist/decoder-nodeav/index.js.map +1 -1
  9. package/dist/decoder-nodeav/index.mjs +553 -19
  10. package/dist/decoder-nodeav/index.mjs.map +1 -1
  11. package/dist/detection-pipeline/index.js +2 -4
  12. package/dist/detection-pipeline/index.js.map +1 -1
  13. package/dist/detection-pipeline/index.mjs +2 -4
  14. package/dist/detection-pipeline/index.mjs.map +1 -1
  15. package/dist/{index-BwLnHesq.mjs → index-CVzLrojg.mjs} +567 -327
  16. package/dist/index-CVzLrojg.mjs.map +1 -0
  17. package/dist/{index-BBpVDiWL.js → index-p-6GfKOg.js} +567 -327
  18. package/dist/index-p-6GfKOg.js.map +1 -0
  19. package/dist/motion-wasm/index.js +2 -4
  20. package/dist/motion-wasm/index.js.map +1 -1
  21. package/dist/motion-wasm/index.mjs +2 -4
  22. package/dist/motion-wasm/index.mjs.map +1 -1
  23. package/dist/pipeline-runner/index.js +133 -54
  24. package/dist/pipeline-runner/index.js.map +1 -1
  25. package/dist/pipeline-runner/index.mjs +133 -54
  26. package/dist/pipeline-runner/index.mjs.map +1 -1
  27. package/dist/stream-broker/@mf-types.zip +0 -0
  28. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-d8PmLbO2.mjs +19 -0
  29. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-B4l8Nb2y.mjs +20 -0
  30. package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DePVYdid.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs-DAssX3h0.mjs} +4 -2
  31. package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CBlCGyx5.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-DFoJJhpt.mjs} +1 -1
  32. package/dist/stream-broker/{__mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-DZchZKbW.mjs → __mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-x7XMEeuJ.mjs} +1 -1
  33. package/dist/stream-broker/_stub.js +2 -2
  34. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B8d_i3jf.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWHjxwIc.mjs} +6 -6
  35. package/dist/stream-broker/{client-BK73l2KT.mjs → client-CZXrddDR.mjs} +2990 -3217
  36. package/dist/stream-broker/{hostInit-oba_vMZE.mjs → hostInit-B86vUcFC.mjs} +12 -12
  37. package/dist/stream-broker/{index-COebxMhm.mjs → index-BCEx31Mh.mjs} +4032 -3582
  38. package/dist/stream-broker/{index-BAZcm437.mjs → index-BvV3RVTZ.mjs} +1 -1
  39. package/dist/stream-broker/{index-IUYKHbxX.mjs → index-C0BzaWmB.mjs} +1 -1
  40. package/dist/stream-broker/index-CWkKuNLr.mjs +232 -0
  41. package/dist/stream-broker/{index-ns1fRD30.mjs → index-CZNxa0ad.mjs} +1 -1
  42. package/dist/stream-broker/index-Kb4xa8FX.mjs +36403 -0
  43. package/dist/stream-broker/{index-BxHaCH3N.mjs → index-KtR7Pp0O.mjs} +1 -1
  44. package/dist/stream-broker/{index-Ss9m7Jum.mjs → index-cYW01SNH.mjs} +1 -1
  45. package/dist/stream-broker/index.js +805 -544
  46. package/dist/stream-broker/index.js.map +1 -1
  47. package/dist/stream-broker/index.mjs +805 -522
  48. package/dist/stream-broker/index.mjs.map +1 -1
  49. package/dist/stream-broker/{jsx-runtime-ZdY5pIZz.mjs → jsx-runtime-B_evVsXl.mjs} +1 -1
  50. package/dist/stream-broker/remoteEntry.js +1 -1
  51. package/package.json +23 -31
  52. package/dist/index-BBpVDiWL.js.map +0 -1
  53. package/dist/index-BwLnHesq.mjs.map +0 -1
  54. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-p-Z3JTk9.mjs +0 -19
  55. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-MYpjp-jd.mjs +0 -20
  56. package/dist/stream-broker/index-BkB3U-Tc.mjs +0 -20852
  57. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  58. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  59. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  60. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  61. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  62. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  63. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  64. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  65. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  66. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  67. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  68. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  69. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  70. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  71. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  72. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  73. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  74. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  75. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  76. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
@@ -5068,6 +5068,7 @@ const WELL_KNOWN_TABS = [
5068
5068
  { id: "osd", label: "OSD", icon: "type", order: 18 },
5069
5069
  { id: "stream-broker", label: "Stream Broker", icon: "radio", order: 20 },
5070
5070
  { id: "streaming", label: "Streaming", icon: "video", order: 35 },
5071
+ { id: "ptz", label: "PTZ", icon: "move", order: 40 },
5071
5072
  { id: "pipeline", label: "Detection Pipeline", icon: "cpu", order: 39 },
5072
5073
  { id: "zones", label: "Detection", icon: "shapes", order: 38 },
5073
5074
  { id: "live-stats", label: "Live Stats", icon: "activity", order: 39 },
@@ -5135,6 +5136,9 @@ function hydrateField(field, values) {
5135
5136
  return { ...field, value: items };
5136
5137
  }
5137
5138
  const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
5139
+ if (field.type === "password") {
5140
+ return { ...field, value: "" };
5141
+ }
5138
5142
  const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
5139
5143
  const hydrated = { ...field, value };
5140
5144
  return hydrated;
@@ -5221,7 +5225,19 @@ const DecoderSessionConfigSchema = object({
5221
5225
  * on every line so `grep tag=broker:5/high` filters one camera
5222
5226
  * profile cleanly.
5223
5227
  */
5224
- tag: string().optional()
5228
+ tag: string().optional(),
5229
+ /**
5230
+ * Where the session delivers decoded frames (Phase 5 / D9):
5231
+ *
5232
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
5233
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
5234
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
5235
+ * into an OS shared-memory ring and drained as zero-pixel
5236
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
5237
+ * other — `pullFrames` returns nothing for an `'shm'` session and
5238
+ * `pullHandles` returns nothing for a `'callback'` session.
5239
+ */
5240
+ frameSink: _enum(["callback", "shm"]).default("callback")
5225
5241
  });
5226
5242
  function errMsg(err) {
5227
5243
  if (err instanceof Error) return err.message;
@@ -5987,6 +6003,53 @@ const DecodedFrameSchema = object({
5987
6003
  format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5988
6004
  timestamp: number()
5989
6005
  });
6006
+ const FrameHandleSchema = object({
6007
+ shmId: string(),
6008
+ slot: number().int().nonnegative(),
6009
+ seq: number().int().nonnegative(),
6010
+ width: number().int().positive(),
6011
+ height: number().int().positive(),
6012
+ format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
6013
+ pts: number(),
6014
+ byteLength: number().int().nonnegative(),
6015
+ nodeId: string(),
6016
+ slotCount: number().int().positive()
6017
+ });
6018
+ const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
6019
+ const SubscribeFramesInputSchema = object({
6020
+ brokerId: string(),
6021
+ format: FrameHandleFormatSchema,
6022
+ /**
6023
+ * Optional reader-side cadence hint in frames per second. The broker does
6024
+ * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
6025
+ * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
6026
+ * the consumer can pace its own `pullFrameHandles` polling.
6027
+ */
6028
+ maxFps: number().positive().optional(),
6029
+ /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
6030
+ tag: string().optional()
6031
+ });
6032
+ const SubscribeFramesResultSchema = object({
6033
+ /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
6034
+ subscriptionId: string(),
6035
+ /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
6036
+ maxFps: number().nonnegative()
6037
+ });
6038
+ const DecodedAudioChunkSchema = object({
6039
+ data: _instanceof(Uint8Array),
6040
+ sampleRate: number().int().positive(),
6041
+ channels: number().int().positive(),
6042
+ timestamp: number()
6043
+ });
6044
+ const SubscribeAudioChunksInputSchema = object({
6045
+ brokerId: string(),
6046
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6047
+ tag: string().optional()
6048
+ });
6049
+ const SubscribeAudioChunksResultSchema = object({
6050
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
6051
+ subscriptionId: string()
6052
+ });
5990
6053
  const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
5991
6054
  const BrokerStatsSchema = object({
5992
6055
  status: BrokerStatusSchema$1,
@@ -6212,9 +6275,76 @@ const streamBrokerCapability = {
6212
6275
  object({ released: boolean(), refcount: number().int().nonnegative() }),
6213
6276
  { kind: "mutation", auth: "admin" }
6214
6277
  ),
6215
- getBroker: method(
6216
- object({ brokerId: string() }),
6217
- custom()
6278
+ /**
6279
+ * ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
6280
+ *
6281
+ * The serialisable replacement for the live-object `IStreamBroker.
6282
+ * onDecodedAudioChunk` callback path. Unlike the video frame plane,
6283
+ * audio chunks are tiny (a ~500ms PCM window is a few KB) so they ship
6284
+ * their bytes INLINE over tRPC — no shared-memory ring. A consumer:
6285
+ *
6286
+ * 1. `subscribeAudioChunks({ brokerId, tag })` — the broker registers
6287
+ * a per-subscription bounded FIFO queue and returns a
6288
+ * `subscriptionId`.
6289
+ * 2. polls `pullAudioChunks({ subscriptionId, maxCount })` — drains
6290
+ * `DecodedAudioChunk[]` in arrival order (FIFO, no latest-wins
6291
+ * drop: an audio gap is audible / breaks an analysis window). The
6292
+ * queue is generously sized; it only drops its oldest chunk if a
6293
+ * truly stalled consumer lets it overflow.
6294
+ * 3. `unsubscribeAudioChunks({ subscriptionId })` on teardown.
6295
+ */
6296
+ subscribeAudioChunks: method(
6297
+ SubscribeAudioChunksInputSchema,
6298
+ SubscribeAudioChunksResultSchema,
6299
+ { kind: "mutation" }
6300
+ ),
6301
+ pullAudioChunks: method(
6302
+ object({
6303
+ subscriptionId: string(),
6304
+ maxCount: number().int().positive().default(8)
6305
+ }),
6306
+ array(DecodedAudioChunkSchema).readonly()
6307
+ ),
6308
+ unsubscribeAudioChunks: method(
6309
+ object({ subscriptionId: string() }),
6310
+ object({ released: boolean() }),
6311
+ { kind: "mutation" }
6312
+ ),
6313
+ /**
6314
+ * ── Shared-memory frame plane (Phase 5 / D9) ──────────────────────
6315
+ *
6316
+ * The handle-based replacement for the live-object `IStreamBroker.
6317
+ * onDecodedFrame` callback path. A consumer:
6318
+ *
6319
+ * 1. `subscribeFrames({ brokerId, format })` — the broker spins up
6320
+ * (or reuses) a `frameSink: 'shm'` decoder session producing that
6321
+ * `format` and returns a `subscriptionId`.
6322
+ * 2. polls `pullFrameHandles({ subscriptionId, maxCount })` — drains
6323
+ * zero-pixel `FrameHandle[]`; each handle is fed to a
6324
+ * `FrameRingReader` that opens the named shm segment and reads the
6325
+ * pixels back zero-copy.
6326
+ * 3. `unsubscribeFrames({ subscriptionId })` on teardown.
6327
+ *
6328
+ * The broker keeps one shm ring per `(brokerId, format)` actually
6329
+ * requested — no broker-side `sharp` conversion. fps throttling is
6330
+ * implicit (latest-wins ring reads drop frames for a slow consumer).
6331
+ */
6332
+ subscribeFrames: method(
6333
+ SubscribeFramesInputSchema,
6334
+ SubscribeFramesResultSchema,
6335
+ { kind: "mutation" }
6336
+ ),
6337
+ pullFrameHandles: method(
6338
+ object({
6339
+ subscriptionId: string(),
6340
+ maxCount: number().int().positive().default(4)
6341
+ }),
6342
+ array(FrameHandleSchema).readonly()
6343
+ ),
6344
+ unsubscribeFrames: method(
6345
+ object({ subscriptionId: string() }),
6346
+ object({ released: boolean() }),
6347
+ { kind: "mutation" }
6218
6348
  ),
6219
6349
  setPreBufferDuration: method(
6220
6350
  object({ brokerId: string(), seconds: number().min(0).max(30) }),
@@ -6270,6 +6400,8 @@ const cameraStreamsCapability = {
6270
6400
  name: "camera-streams",
6271
6401
  scope: "device",
6272
6402
  mode: "singleton",
6403
+ kind: "wrapper",
6404
+ defaultActive: true,
6273
6405
  deviceTypes: [DeviceType.Camera],
6274
6406
  methods: {
6275
6407
  getCameraStreams: method(
@@ -6535,6 +6667,8 @@ const motionDetectionCapability = {
6535
6667
  name: "motion-detection",
6536
6668
  scope: "device",
6537
6669
  mode: "singleton",
6670
+ kind: "wrapper",
6671
+ defaultActive: true,
6538
6672
  exposesDeviceSettings: true,
6539
6673
  methods: {
6540
6674
  analyze: method(
@@ -7305,6 +7439,34 @@ MotionTriggerStatusSchema.extend({
7305
7439
  }) }
7306
7440
  }
7307
7441
  });
7442
+ object({
7443
+ enabled: boolean(),
7444
+ sensitivity: number(),
7445
+ /** Row-major active-cell grid. Length = gridWidth*gridHeight (see getOptions). */
7446
+ cells: array(boolean()),
7447
+ lastFetchedAt: number()
7448
+ });
7449
+ const MotionZoneOptionsSchema = object({
7450
+ gridWidth: number(),
7451
+ gridHeight: number(),
7452
+ sensitivity: object({ min: number(), max: number(), step: number() })
7453
+ });
7454
+ const MotionZonePatchSchema = object({
7455
+ enabled: boolean().optional(),
7456
+ sensitivity: number().optional(),
7457
+ cells: array(boolean()).optional()
7458
+ });
7459
+ ({
7460
+ deviceTypes: [DeviceType.Camera],
7461
+ methods: {
7462
+ getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
7463
+ setZone: method(
7464
+ object({ deviceId: number(), patch: MotionZonePatchSchema }),
7465
+ _void(),
7466
+ { kind: "mutation", auth: "admin" }
7467
+ )
7468
+ }
7469
+ });
7308
7470
  const AutotrackTargetTypeSchema = string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
7309
7471
  const PtzAutotrackSettingsSchema = object({
7310
7472
  targetType: AutotrackTargetTypeSchema,
@@ -7393,6 +7555,100 @@ PtzAutotrackStatusSchema.extend({
7393
7555
  }) }
7394
7556
  }
7395
7557
  });
7558
+ const StreamProfileSchema = _enum(["main", "sub", "ext"]);
7559
+ const StreamProfileConfigSchema = object({
7560
+ width: number(),
7561
+ height: number(),
7562
+ codec: _enum(["h264", "h265"]),
7563
+ framerate: number(),
7564
+ bitrate: number(),
7565
+ // kbps
7566
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7567
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7568
+ gop: number().optional(),
7569
+ audio: boolean().optional()
7570
+ });
7571
+ object({
7572
+ /** Per-profile current config. A profile absent = the camera doesn't have it. */
7573
+ main: StreamProfileConfigSchema.optional(),
7574
+ sub: StreamProfileConfigSchema.optional(),
7575
+ ext: StreamProfileConfigSchema.optional(),
7576
+ lastFetchedAt: number()
7577
+ });
7578
+ const StreamProfileOptionsSchema = object({
7579
+ resolutions: array(object({ width: number(), height: number() })),
7580
+ codecs: array(_enum(["h264", "h265"])),
7581
+ framerates: array(number()),
7582
+ /** Allowed bitrate values (kbps). Empty if the camera takes a free range. */
7583
+ bitrates: array(number()),
7584
+ /** Optional [min,max] kbps when the camera accepts a continuous range. */
7585
+ bitrateRange: tuple([number(), number()]).optional(),
7586
+ supportsBitrateMode: boolean(),
7587
+ supportsEncoderProfile: boolean(),
7588
+ supportsGop: boolean(),
7589
+ /** Allowed GOP / keyframe-interval range, in seconds — drives the
7590
+ * I-frame-interval selector. Absent when the camera advertises GOP
7591
+ * support but no concrete range (callers then fall back to a free
7592
+ * numeric input). `{ min, max, step }` per the getOptions convention. */
7593
+ gop: object({ min: number(), max: number(), step: number() }).optional()
7594
+ });
7595
+ const StreamParamsOptionsSchema = object({
7596
+ main: StreamProfileOptionsSchema.optional(),
7597
+ sub: StreamProfileOptionsSchema.optional(),
7598
+ ext: StreamProfileOptionsSchema.optional()
7599
+ });
7600
+ const StreamProfilePatchSchema = object({
7601
+ width: number().optional(),
7602
+ height: number().optional(),
7603
+ codec: _enum(["h264", "h265"]).optional(),
7604
+ framerate: number().optional(),
7605
+ bitrate: number().optional(),
7606
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7607
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7608
+ gop: number().optional(),
7609
+ audio: boolean().optional()
7610
+ });
7611
+ ({
7612
+ deviceTypes: [DeviceType.Camera],
7613
+ methods: {
7614
+ getOptions: method(
7615
+ object({ deviceId: number() }),
7616
+ StreamParamsOptionsSchema
7617
+ ),
7618
+ setProfile: method(
7619
+ object({
7620
+ deviceId: number(),
7621
+ profile: StreamProfileSchema,
7622
+ patch: StreamProfilePatchSchema
7623
+ }),
7624
+ _void(),
7625
+ { kind: "mutation", auth: "admin" }
7626
+ ),
7627
+ /**
7628
+ * Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
7629
+ * shape) for this camera's stream-encoder settings — one section per
7630
+ * profile (main / sub / ext) with the resolution / codec / framerate
7631
+ * / bitrate / bitrate-mode / encoder-profile / GOP controls the
7632
+ * firmware actually exposes.
7633
+ *
7634
+ * Driven by `getOptions` (camera-probed availability) + `getStatus`
7635
+ * (current per-profile config); each field's `default` is seeded
7636
+ * from the live config so the form renders the camera state in one
7637
+ * pass. Returns `null` when the camera exposes no configurable
7638
+ * stream property — the renderer then shows the unsupported message.
7639
+ *
7640
+ * Output is `z.unknown().nullable()` — the same convention every
7641
+ * other `ConfigUISchema`-returning cap method uses (`device-ops`,
7642
+ * `device-manager`); `ConfigUISchema` is a TS-only type with no
7643
+ * companion Zod schema, and a concrete object would collapse
7644
+ * unrelated AppRouter branches to `unknown` during codegen.
7645
+ */
7646
+ getConfigSchema: method(
7647
+ object({ deviceId: number() }),
7648
+ unknown().nullable()
7649
+ )
7650
+ }
7651
+ });
7396
7652
  object({
7397
7653
  on: boolean(),
7398
7654
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
@@ -8330,6 +8586,83 @@ const VersionOutputSchema = object({ version: string() });
8330
8586
  getVersion: method(_void(), VersionOutputSchema)
8331
8587
  }
8332
8588
  });
8589
+ const MethodAccessSchema = _enum(["view", "create", "delete"]);
8590
+ const AllowedProviderSchema = union([literal("*"), array(string())]);
8591
+ const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
8592
+ const CapScopeSchema = _enum(["device", "system"]);
8593
+ const TokenScopeSchema = discriminatedUnion("type", [
8594
+ object({
8595
+ type: literal("category"),
8596
+ target: CapScopeSchema,
8597
+ access: array(MethodAccessSchema).min(1)
8598
+ }),
8599
+ object({
8600
+ type: literal("capability"),
8601
+ target: string(),
8602
+ access: array(MethodAccessSchema).min(1)
8603
+ }),
8604
+ object({
8605
+ type: literal("addon"),
8606
+ target: string(),
8607
+ access: array(MethodAccessSchema).min(1)
8608
+ }),
8609
+ object({
8610
+ type: literal("device"),
8611
+ /**
8612
+ * One or more deviceIds (serialised as strings for wire-format
8613
+ * consistency with the rest of the union). Matcher accepts if
8614
+ * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
8615
+ * of one scope-per-device when granting access to a set of cameras.
8616
+ */
8617
+ targets: array(string()).min(1),
8618
+ access: array(MethodAccessSchema).min(1)
8619
+ })
8620
+ ]);
8621
+ object({
8622
+ id: string(),
8623
+ username: string(),
8624
+ passwordHash: string(),
8625
+ /**
8626
+ * Admin bypass. When true, the middleware skips the scope-access
8627
+ * check entirely. There is no other axis of privilege; the legacy
8628
+ * role enum collapsed onto this boolean in v2.
8629
+ */
8630
+ isAdmin: boolean().default(false),
8631
+ allowedProviders: AllowedProviderSchema,
8632
+ allowedDevices: AllowedDevicesSchema,
8633
+ /**
8634
+ * Scopes granted to this user. Admins bypass; their `scopes` is
8635
+ * ignored. Non-admins without scopes are locked out of every
8636
+ * protected call.
8637
+ */
8638
+ scopes: array(TokenScopeSchema).default([]),
8639
+ createdAt: number(),
8640
+ updatedAt: number()
8641
+ });
8642
+ object({
8643
+ id: string(),
8644
+ label: string(),
8645
+ isAdmin: boolean().default(false),
8646
+ allowedProviders: AllowedProviderSchema,
8647
+ allowedDevices: AllowedDevicesSchema,
8648
+ tokenHash: string(),
8649
+ tokenPrefix: string(),
8650
+ createdAt: number(),
8651
+ lastUsedAt: number().optional()
8652
+ });
8653
+ object({
8654
+ id: string(),
8655
+ userId: string(),
8656
+ name: string(),
8657
+ tokenHash: string(),
8658
+ tokenPrefix: string(),
8659
+ scopes: array(TokenScopeSchema),
8660
+ // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
8661
+ // schema accepts both `null` (read from disk) and `undefined` (in-memory).
8662
+ expiresAt: number().nullish(),
8663
+ lastUsedAt: number().nullish(),
8664
+ createdAt: number()
8665
+ });
8333
8666
  const SsoBridgeClaimsSchema = object({
8334
8667
  userId: string(),
8335
8668
  username: string(),
@@ -8345,7 +8678,18 @@ const SsoBridgeClaimsSchema = object({
8345
8678
  * JWT WITHOUT verifying the signature — the hub re-verifies on every
8346
8679
  * inbound call so trust still rests with the signing hub.
8347
8680
  */
8348
- hubUrl: string().optional()
8681
+ hubUrl: string().optional(),
8682
+ /** Permission scopes baked into the token. Set by the OAuth
8683
+ * account-linking grant; absent on ordinary SSO-login tokens. */
8684
+ scopes: array(TokenScopeSchema).optional(),
8685
+ /** OAuth authorization-code binding — set only on `oauth-code` tokens. */
8686
+ redirectUri: string().optional(),
8687
+ integrationId: string().optional(),
8688
+ /** JWT ID — unique per issued code; consumed-set enforces single-use. */
8689
+ jti: string().optional(),
8690
+ /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
8691
+ * tokens so the verify path can check the session is not revoked. */
8692
+ sessionId: string().optional()
8349
8693
  });
8350
8694
  ({
8351
8695
  methods: {
@@ -8362,6 +8706,23 @@ const SsoBridgeClaimsSchema = object({
8362
8706
  )
8363
8707
  }
8364
8708
  });
8709
+ const OauthIntegrationDescriptorSchema = object({
8710
+ /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
8711
+ integrationId: string(),
8712
+ /** Human label rendered on the consent page. */
8713
+ displayName: string(),
8714
+ /** Scopes baked into every token issued for this integration. */
8715
+ requestedScopes: array(TokenScopeSchema),
8716
+ /** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
8717
+ * redirect_uri that does not start with one of these. Required —
8718
+ * an empty list means the integration can never complete linking. */
8719
+ allowedRedirectPrefixes: array(string()).min(1)
8720
+ });
8721
+ ({
8722
+ methods: {
8723
+ getDescriptor: method(_void(), OauthIntegrationDescriptorSchema)
8724
+ }
8725
+ });
8365
8726
  const PasskeySummarySchema = object({
8366
8727
  credentialId: string(),
8367
8728
  label: string(),
@@ -8642,21 +9003,30 @@ const AddonPageDeclarationSchema = object({
8642
9003
  });
8643
9004
  const WidgetHostEnum = _enum(["device-tab", "dashboard", "integration-detail"]);
8644
9005
  const WidgetSizeEnum = _enum(["xs", "sm", "md", "lg", "xl"]);
9006
+ const WidgetRemoteSchema = object({
9007
+ remoteName: string(),
9008
+ exposedModule: string(),
9009
+ componentKey: string().optional()
9010
+ });
8645
9011
  const WidgetMetadataSchema = object({
8646
- /** Stable id within the addon — kebab-case. */
8647
- stableId: string(),
9012
+ // ── UiContribution core (kind:'remote') ──────────────────────────
9013
+ /** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
9014
+ tab: string(),
9015
+ /** Optional sub-tab within `tab`. */
9016
+ subTab: string().optional(),
8648
9017
  /** Operator-facing label. */
8649
9018
  label: string(),
9019
+ /** Ordering within `(tab, subTab)`, ascending. */
9020
+ order: number().optional(),
9021
+ /** Always `'remote'` — a widget is a Module Federation remote. */
9022
+ kind: literal("remote"),
9023
+ /** MF remote descriptor. */
9024
+ remote: WidgetRemoteSchema,
9025
+ // ── Widget-only metadata ─────────────────────────────────────────
9026
+ /** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
9027
+ stableId: string(),
8650
9028
  description: string().optional(),
8651
9029
  icon: string().optional(),
8652
- /**
8653
- * Module Federation remote name — must match the `name` field on the
8654
- * widget addon's `federation()` plugin config. Used by the host's
8655
- * `<WidgetRegistryProvider>` to call `loadRemote('<remoteName>/widgets')`.
8656
- * Conventionally `addon_<addonid>_widgets` (snake_case; MF names
8657
- * cannot contain hyphens).
8658
- */
8659
- remoteName: string(),
8660
9030
  /**
8661
9031
  * Bundle filename inside the addon's `dist/` dir served at
8662
9032
  * `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
@@ -8665,9 +9035,9 @@ const WidgetMetadataSchema = object({
8665
9035
  * cache-buster URL without a separate filesystem stat.
8666
9036
  */
8667
9037
  bundle: string(),
8668
- /** Where the widget makes sense to render. */
9038
+ /** Every host the widget supports. The picker filters on this set. */
8669
9039
  hosts: array(WidgetHostEnum).readonly(),
8670
- /** Required props the host must supply. Validated at <WidgetSlot> mount. */
9040
+ /** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
8671
9041
  requires: object({
8672
9042
  deviceContext: boolean().default(false),
8673
9043
  integrationContext: boolean().default(false)
@@ -8760,6 +9130,16 @@ const DEFAULT_DECODER_HWACCEL_CONFIG = {
8760
9130
  hwaccel: "auto",
8761
9131
  probedBestHwaccel: ""
8762
9132
  };
9133
+ const ShmRingStatsSchema = object({
9134
+ sessionId: string(),
9135
+ slotCount: number().int(),
9136
+ slotByteLength: number().int(),
9137
+ segmentBytes: number().int(),
9138
+ budgetMb: number().int(),
9139
+ framesWritten: number().int(),
9140
+ getFrameHits: number().int(),
9141
+ getFrameMisses: number().int()
9142
+ });
8763
9143
  const decoderCapability = {
8764
9144
  name: "decoder",
8765
9145
  scope: "system",
@@ -8796,10 +9176,27 @@ const decoderCapability = {
8796
9176
  url: string()
8797
9177
  }), _void()),
8798
9178
  // ── Output — polling-based frame retrieval ────────────────────
9179
+ // `pullFrames` drains the pixel `DecodedFrame[]` of a `frameSink:
9180
+ // 'callback'` session. `pullHandles` (Phase 5 / D9) drains the
9181
+ // zero-pixel `FrameHandle[]` of a `frameSink: 'shm'` session — the
9182
+ // broker hands each handle to a `FrameRingReader` that opens the
9183
+ // named segment and reads the pixels back zero-copy. A session is
9184
+ // one mode or the other; the unmatched method returns an empty
9185
+ // array.
8799
9186
  pullFrames: method(object({
8800
9187
  sessionId: string(),
8801
9188
  maxCount: number().default(1)
8802
9189
  }), array(DecodedFrameSchema)),
9190
+ pullHandles: method(object({
9191
+ sessionId: string(),
9192
+ maxCount: number().default(1)
9193
+ }), array(FrameHandleSchema)),
9194
+ // ── Frame fetch (Phase 5 / D9 downstream access) ──────────────
9195
+ // Read the pixels a FrameHandle refers to from this node's shm ring.
9196
+ // Returns null when the slot was already recycled (latest-wins).
9197
+ getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
9198
+ // shm ring usage stats for a session.
9199
+ getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
8803
9200
  // ── Control ───────────────────────────────────────────────────
8804
9201
  updateConfig: method(object({
8805
9202
  sessionId: string(),
@@ -8944,6 +9341,8 @@ const webrtcSessionCapability = {
8944
9341
  name: "webrtc-session",
8945
9342
  scope: "device",
8946
9343
  mode: "singleton",
9344
+ kind: "wrapper",
9345
+ defaultActive: true,
8947
9346
  deviceTypes: [DeviceType.Camera],
8948
9347
  methods: {
8949
9348
  /**
@@ -9458,6 +9857,8 @@ const audioAnalysisCapability = {
9458
9857
  name: "audio-analysis",
9459
9858
  scope: "device",
9460
9859
  mode: "singleton",
9860
+ kind: "wrapper",
9861
+ defaultActive: true,
9461
9862
  deviceTypes: [DeviceType.Camera],
9462
9863
  exposesDeviceSettings: true,
9463
9864
  methods: {
@@ -9990,8 +10391,8 @@ const DevicePersistConfigPayloadSchema = object({
9990
10391
  /**
9991
10392
  * Return the addon ids that declared a wrapper provider for `capName`.
9992
10393
  * Backs the device-bindings UI's wrapper-picker dropdown. Entries are
9993
- * sourced from `CapabilityRegistry.wrapperProviders`, populated at
9994
- * `ProviderRegistration.kind === 'wrapper'` time.
10394
+ * sourced from `CapabilityRegistry.wrapperProviders`, populated when
10395
+ * the cap definition declares `kind: 'wrapper'`.
9995
10396
  */
9996
10397
  listWrappersForCap: method(
9997
10398
  object({ capName: string() }),
@@ -10297,51 +10698,6 @@ const AuthResultSchema = object({
10297
10698
  validateToken: method(object({ token: string() }), AuthResultSchema.nullable())
10298
10699
  }
10299
10700
  });
10300
- const AuthProviderInfoSchema = object({
10301
- /** Stable id matching the addon id (used for `getLoginUrl({addonId,…})`). */
10302
- addonId: string(),
10303
- /**
10304
- * Per-instance id when one addon registers multiple "logical"
10305
- * providers (e.g. OIDC with Google + Microsoft + custom). The login
10306
- * URL becomes `/addon/${addonId}/${instanceId}/start` — handler reads
10307
- * `:instanceId` from the route. Empty/unset means the addon is a
10308
- * single-instance provider; the URL is `/addon/${addonId}/start`.
10309
- */
10310
- instanceId: string().optional(),
10311
- /** Display label shown on the login button + admin row. */
10312
- displayName: string(),
10313
- /** Optional iconography hint (lucide-react icon name OR emoji). */
10314
- icon: string().optional(),
10315
- /** When true, the provider exposes a redirect-based login flow
10316
- * (`getLoginUrl` returns a URL the browser navigates to). */
10317
- hasRedirectFlow: boolean(),
10318
- /** When true, the provider exposes a credential-form login flow
10319
- * (`validateCredentials` accepts username + password). */
10320
- hasCredentialFlow: boolean(),
10321
- /** Provider kind, drives admin-UI hint dispatch (oidc / saml / totp / …). */
10322
- kind: string().optional(),
10323
- /** Operator-facing status string (e.g. "Connected to https://login.acme.com"). */
10324
- status: string().optional(),
10325
- /** When false, the provider is registered but disabled by config; the
10326
- * UI surfaces it as inactive without enumerating it for login. */
10327
- enabled: boolean()
10328
- });
10329
- ({
10330
- methods: {
10331
- /** All registered auth providers, both enabled and disabled. */
10332
- listProviders: method(_void(), array(AuthProviderInfoSchema).readonly()),
10333
- /**
10334
- * Toggle a provider's enabled flag. Disabled providers stay
10335
- * registered but aren't surfaced on the login page. The orchestrator
10336
- * persists the state in `addon-settings` so it survives restarts.
10337
- */
10338
- setProviderEnabled: method(
10339
- object({ addonId: string(), enabled: boolean() }),
10340
- object({ success: literal(true) }),
10341
- { kind: "mutation", auth: "admin" }
10342
- )
10343
- }
10344
- });
10345
10701
  const NetworkEndpointSchema = object({
10346
10702
  url: string(),
10347
10703
  hostname: string(),
@@ -10373,55 +10729,13 @@ const NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
10373
10729
  getEndpoint: method(_void(), NetworkEndpointSchema.nullable()),
10374
10730
  getStatus: method(_void(), NetworkAccessStatusSchema),
10375
10731
  /**
10376
- * Enumerate every active ingress entry. Default implementation (when
10377
- * the provider omits this method) is derived from `getEndpoint()` —
10378
- * see the remote-access orchestrator for the fallback path.
10732
+ * Enumerate every active ingress entry. Providers that expose only a
10733
+ * single endpoint may omit this method; callers fall back to
10734
+ * `getEndpoint()` in that case.
10379
10735
  */
10380
10736
  listEndpoints: method(_void(), array(NetworkEndpointEntrySchema).readonly())
10381
10737
  }
10382
10738
  });
10383
- const RemoteAccessEndpointSchema = object({
10384
- url: string(),
10385
- hostname: string(),
10386
- port: number(),
10387
- protocol: _enum(["http", "https"])
10388
- });
10389
- const RemoteAccessProviderInfoSchema = object({
10390
- /** Stable id matching the addon id. */
10391
- addonId: string(),
10392
- /** Display label shown on the admin row — sourced from the addon manifest. */
10393
- displayName: string(),
10394
- /** When false, the provider is registered but disabled. */
10395
- enabled: boolean(),
10396
- /** True when the underlying tunnel/connection is up. */
10397
- connected: boolean(),
10398
- /** Public-facing endpoint, when connected. Null otherwise. */
10399
- endpoint: RemoteAccessEndpointSchema.nullable(),
10400
- /** Last error message (when connected=false), if available. */
10401
- error: string().optional()
10402
- });
10403
- ({
10404
- methods: {
10405
- /** All registered remote-access providers + their live status. */
10406
- listProviders: method(_void(), array(RemoteAccessProviderInfoSchema).readonly()),
10407
- /**
10408
- * Start a specific provider's tunnel. Per-provider config still
10409
- * lives on the addon's settings panel; this is just the on/off
10410
- * trigger so the admin UI can manage the lifecycle from one place.
10411
- */
10412
- startProvider: method(
10413
- object({ addonId: string() }),
10414
- RemoteAccessEndpointSchema,
10415
- { kind: "mutation", auth: "admin" }
10416
- ),
10417
- /** Stop a specific provider's tunnel (idempotent on already-stopped). */
10418
- stopProvider: method(
10419
- object({ addonId: string() }),
10420
- object({ success: literal(true) }),
10421
- { kind: "mutation", auth: "admin" }
10422
- )
10423
- }
10424
- });
10425
10739
  const TurnServerSchema = object({
10426
10740
  /** Single URL or list of URLs (e.g. "turn:turn.example.com:3478?transport=udp"). */
10427
10741
  urls: union([string(), array(string())]),
@@ -10441,45 +10755,6 @@ const TurnServerSchema = object({
10441
10755
  )
10442
10756
  }
10443
10757
  });
10444
- const TurnProviderInfoSchema = object({
10445
- /** Stable id matching the addon id. */
10446
- addonId: string(),
10447
- /** Display label shown on the admin row — sourced from the addon manifest. */
10448
- displayName: string(),
10449
- /** When false, the provider is registered but disabled. */
10450
- enabled: boolean(),
10451
- /** Number of servers this provider is currently exposing. */
10452
- serverCount: number(),
10453
- /**
10454
- * Flat list of every TURN/STUN URL this provider currently exposes.
10455
- * One row per URL (multi-URL ICE server entries are flattened). The
10456
- * admin UI shows this in a compact per-provider list so operators
10457
- * can verify what's actually being negotiated without having to dig
10458
- * into the combined `getAllServers` output.
10459
- */
10460
- urls: array(string()).readonly(),
10461
- /** Last fetch error (when serverCount=0 due to API failure), if any. */
10462
- error: string().optional()
10463
- });
10464
- ({
10465
- methods: {
10466
- /** All registered TURN providers + per-provider stats. */
10467
- listProviders: method(_void(), array(TurnProviderInfoSchema).readonly()),
10468
- /**
10469
- * Combined list of TURN/STUN servers from all ENABLED providers.
10470
- * Consumed by the WebRTC layer at session-creation time —
10471
- * implementations may fetch fresh short-lived credentials each
10472
- * call (e.g. Cloudflare API), so consumers SHOULD call per-session.
10473
- */
10474
- getAllServers: method(_void(), array(TurnServerSchema).readonly()),
10475
- /** Toggle a provider's enabled flag. */
10476
- setProviderEnabled: method(
10477
- object({ addonId: string(), enabled: boolean() }),
10478
- object({ success: literal(true) }),
10479
- { kind: "mutation", auth: "admin" }
10480
- )
10481
- }
10482
- });
10483
10758
  const SnapshotImageSchema = object({
10484
10759
  base64: string(),
10485
10760
  contentType: string()
@@ -10822,6 +11097,8 @@ const detectionPipelineCapability = {
10822
11097
  name: "detection-pipeline",
10823
11098
  scope: "device",
10824
11099
  mode: "singleton",
11100
+ kind: "wrapper",
11101
+ defaultActive: true,
10825
11102
  deviceTypes: [DeviceType.Camera],
10826
11103
  exposesDeviceSettings: true,
10827
11104
  methods: {}
@@ -11212,6 +11489,18 @@ const PtzMoveCommandSchema = object({
11212
11489
  zoom: number().optional(),
11213
11490
  speed: number().optional()
11214
11491
  });
11492
+ PtzPositionSchema.extend({ autofocus: boolean() });
11493
+ const PtzOptionsSchema = object({
11494
+ hasPan: boolean(),
11495
+ hasTilt: boolean(),
11496
+ hasZoom: boolean(),
11497
+ supportsPresets: boolean(),
11498
+ /** Max number of named presets the camera supports, when known. */
11499
+ maxPresets: number().optional(),
11500
+ /** Whether the camera exposes a controllable autofocus toggle
11501
+ * (boolean `hasX` per the getOptions availability convention). */
11502
+ hasAutofocus: boolean()
11503
+ });
11215
11504
  ({
11216
11505
  deviceTypes: [DeviceType.Camera],
11217
11506
  methods: {
@@ -11239,6 +11528,20 @@ const PtzMoveCommandSchema = object({
11239
11528
  _void(),
11240
11529
  { kind: "mutation" }
11241
11530
  ),
11531
+ savePreset: method(
11532
+ object({ deviceId: number(), presetId: string(), name: string() }),
11533
+ _void(),
11534
+ { kind: "mutation", auth: "admin" }
11535
+ ),
11536
+ deletePreset: method(
11537
+ object({ deviceId: number(), presetId: string() }),
11538
+ _void(),
11539
+ { kind: "mutation", auth: "admin" }
11540
+ ),
11541
+ getOptions: method(
11542
+ object({ deviceId: number() }),
11543
+ PtzOptionsSchema
11544
+ ),
11242
11545
  goHome: method(
11243
11546
  object({ deviceId: number() }),
11244
11547
  _void(),
@@ -11253,6 +11556,13 @@ const PtzMoveCommandSchema = object({
11253
11556
  getPosition: method(
11254
11557
  object({ deviceId: number() }),
11255
11558
  PtzPositionSchema
11559
+ ),
11560
+ /** Toggle the camera's autofocus. Only meaningful when
11561
+ * `getOptions().hasAutofocus` is true. */
11562
+ setAutofocus: method(
11563
+ object({ deviceId: number(), enabled: boolean() }),
11564
+ _void(),
11565
+ { kind: "mutation" }
11256
11566
  )
11257
11567
  }
11258
11568
  });
@@ -11952,7 +12262,7 @@ const AllowedAddressesSchema = object({
11952
12262
  )
11953
12263
  }
11954
12264
  });
11955
- const MeshEndpointSchema$1 = object({
12265
+ const MeshEndpointSchema = object({
11956
12266
  /** Stable identifier within the provider (e.g. `mesh-ipv4`, `magicdns`, `funnel`). */
11957
12267
  id: string(),
11958
12268
  /** Operator-facing label (e.g. "Mesh IPv4", "MagicDNS"). */
@@ -12025,7 +12335,7 @@ const MeshStatusSchema = object({
12025
12335
  /** Number of peers visible to this host (excluding self). */
12026
12336
  peerCount: number(),
12027
12337
  /** Every endpoint this provider exposes for the current host. */
12028
- endpoints: array(MeshEndpointSchema$1).readonly(),
12338
+ endpoints: array(MeshEndpointSchema).readonly(),
12029
12339
  /** Last error from the daemon, when not joined. */
12030
12340
  error: string().optional(),
12031
12341
  // ── Account / tenant identity (generic across providers) ────────
@@ -12188,182 +12498,6 @@ const MeshStatusSchema = object({
12188
12498
  // tabs driven by this cap.
12189
12499
  }
12190
12500
  });
12191
- const MeshEndpointSchema = object({
12192
- id: string(),
12193
- label: string(),
12194
- scope: _enum(["mesh", "public"]),
12195
- url: string(),
12196
- hostname: string(),
12197
- port: number(),
12198
- protocol: _enum(["http", "https"])
12199
- });
12200
- const MeshProviderInfoSchema = object({
12201
- /** Stable id matching the addon id. */
12202
- addonId: string(),
12203
- /** Display label shown on the admin row — sourced from the addon manifest. */
12204
- displayName: string(),
12205
- /** True when the host is joined to this provider's mesh. */
12206
- joined: boolean(),
12207
- /** Local mesh IP (empty when not joined). */
12208
- meshIp: string(),
12209
- /** MagicDNS / mesh hostname (empty when not configured). */
12210
- magicDnsHostname: string(),
12211
- /** Peer count (excluding self). */
12212
- peerCount: number(),
12213
- /** Active endpoints (mesh IP + MagicDNS + optional public Funnel). */
12214
- endpoints: array(MeshEndpointSchema).readonly(),
12215
- /** Last error reported by the provider. */
12216
- error: string().optional(),
12217
- // ── Generic identity fields mirrored from MeshStatus ─────────────
12218
- /** Tenant / tailnet / network display name. Empty pre-join. */
12219
- tenantName: string(),
12220
- /** Mesh DNS suffix (e.g. tailXXXX.ts.net). Empty when not configured. */
12221
- magicDnsSuffix: string(),
12222
- /** Authenticated user / account login. Null for token-only providers. */
12223
- userLogin: string().nullable(),
12224
- /** Provider control-plane URL. */
12225
- controlPlaneUrl: string(),
12226
- /** Machine-key expiry (epoch ms). Null when keys don't rotate. */
12227
- keyExpiry: number().nullable()
12228
- });
12229
- ({
12230
- methods: {
12231
- /** All registered mesh-network providers + live status. */
12232
- listProviders: method(_void(), array(MeshProviderInfoSchema).readonly()),
12233
- /**
12234
- * Join the mesh of a specific provider. Per-provider config still
12235
- * lives on its settings panel; the orchestrator forwards.
12236
- */
12237
- joinProvider: method(
12238
- object({
12239
- addonId: string(),
12240
- authKey: string().min(8),
12241
- hostname: string().optional()
12242
- }),
12243
- object({ joined: literal(true) }),
12244
- { kind: "mutation" }
12245
- ),
12246
- leaveProvider: method(
12247
- object({ addonId: string() }),
12248
- object({ success: literal(true) }),
12249
- { kind: "mutation" }
12250
- ),
12251
- /**
12252
- * Browser-redirect login flow. Forwards to the named provider's
12253
- * `mesh-network.startLogin` and returns the URL the daemon
12254
- * prints. UI opens it in a new tab, then polls `listProviders`
12255
- * for `joined: true`.
12256
- */
12257
- startLoginProvider: method(
12258
- object({
12259
- addonId: string(),
12260
- hostname: string().optional()
12261
- }),
12262
- object({ loginUrl: string() }),
12263
- { kind: "mutation" }
12264
- ),
12265
- /**
12266
- * Sign out of the provider's account entirely (`mesh-network.logout`).
12267
- * Distinct from `leaveProvider` which only takes the host off-mesh;
12268
- * `logoutProvider` wipes credentials so the next start requires a
12269
- * fresh login.
12270
- */
12271
- logoutProvider: method(
12272
- object({ addonId: string() }),
12273
- object({ loggedOut: literal(true) }),
12274
- { kind: "mutation" }
12275
- ),
12276
- /**
12277
- * Per-provider peer list. Forwards to `mesh-network.listPeers` on
12278
- * the addressed provider. Separate from `listProviders` because
12279
- * peer payloads can be large on a heavily-populated tailnet —
12280
- * fetch only when the operator opens the Peers tab.
12281
- */
12282
- listProviderPeers: method(
12283
- object({ addonId: string() }),
12284
- object({
12285
- peers: array(MeshPeerSchema).readonly()
12286
- })
12287
- )
12288
- }
12289
- });
12290
- const MethodAccessSchema = _enum(["view", "create", "delete"]);
12291
- const AllowedProviderSchema = union([literal("*"), array(string())]);
12292
- const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
12293
- const CapScopeSchema = _enum(["device", "system"]);
12294
- const TokenScopeSchema = discriminatedUnion("type", [
12295
- object({
12296
- type: literal("category"),
12297
- target: CapScopeSchema,
12298
- access: array(MethodAccessSchema).min(1)
12299
- }),
12300
- object({
12301
- type: literal("capability"),
12302
- target: string(),
12303
- access: array(MethodAccessSchema).min(1)
12304
- }),
12305
- object({
12306
- type: literal("addon"),
12307
- target: string(),
12308
- access: array(MethodAccessSchema).min(1)
12309
- }),
12310
- object({
12311
- type: literal("device"),
12312
- /**
12313
- * One or more deviceIds (serialised as strings for wire-format
12314
- * consistency with the rest of the union). Matcher accepts if
12315
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
12316
- * of one scope-per-device when granting access to a set of cameras.
12317
- */
12318
- targets: array(string()).min(1),
12319
- access: array(MethodAccessSchema).min(1)
12320
- })
12321
- ]);
12322
- object({
12323
- id: string(),
12324
- username: string(),
12325
- passwordHash: string(),
12326
- /**
12327
- * Admin bypass. When true, the middleware skips the scope-access
12328
- * check entirely. There is no other axis of privilege; the legacy
12329
- * role enum collapsed onto this boolean in v2.
12330
- */
12331
- isAdmin: boolean().default(false),
12332
- allowedProviders: AllowedProviderSchema,
12333
- allowedDevices: AllowedDevicesSchema,
12334
- /**
12335
- * Scopes granted to this user. Admins bypass; their `scopes` is
12336
- * ignored. Non-admins without scopes are locked out of every
12337
- * protected call.
12338
- */
12339
- scopes: array(TokenScopeSchema).default([]),
12340
- createdAt: number(),
12341
- updatedAt: number()
12342
- });
12343
- object({
12344
- id: string(),
12345
- label: string(),
12346
- isAdmin: boolean().default(false),
12347
- allowedProviders: AllowedProviderSchema,
12348
- allowedDevices: AllowedDevicesSchema,
12349
- tokenHash: string(),
12350
- tokenPrefix: string(),
12351
- createdAt: number(),
12352
- lastUsedAt: number().optional()
12353
- });
12354
- object({
12355
- id: string(),
12356
- userId: string(),
12357
- name: string(),
12358
- tokenHash: string(),
12359
- tokenPrefix: string(),
12360
- scopes: array(TokenScopeSchema),
12361
- // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
12362
- // schema accepts both `null` (read from disk) and `undefined` (in-memory).
12363
- expiresAt: number().nullish(),
12364
- lastUsedAt: number().nullish(),
12365
- createdAt: number()
12366
- });
12367
12501
  const UserSummarySchema = object({
12368
12502
  id: string(),
12369
12503
  username: string(),
@@ -12436,6 +12570,16 @@ const CreateScopedTokenResultSchema = object({
12436
12570
  token: string(),
12437
12571
  record: ScopedTokenSummarySchema
12438
12572
  });
12573
+ const OauthSessionSummarySchema = object({
12574
+ id: string(),
12575
+ userId: string(),
12576
+ username: string(),
12577
+ integrationId: string(),
12578
+ scopes: array(TokenScopeSchema),
12579
+ createdAt: number(),
12580
+ lastUsedAt: number(),
12581
+ revokedAt: number().nullable()
12582
+ });
12439
12583
  const TotpSetupResultSchema = object({
12440
12584
  secret: string(),
12441
12585
  otpauthUrl: string()
@@ -12511,6 +12655,66 @@ const TotpStatusSchema = object({
12511
12655
  object({ userId: string(), code: string() }),
12512
12656
  object({ valid: boolean() }),
12513
12657
  { kind: "mutation", access: "view" }
12658
+ ),
12659
+ // ── OAuth account-linking grant ────────────────────────────────
12660
+ //
12661
+ // Core's /oauth2/* endpoints delegate here. Tokens are sso-bridge
12662
+ // JWTs (kinds oauth-code / oauth-access / oauth-refresh) and ALWAYS
12663
+ // carry isAdmin:false — the operator login proves hub control; the
12664
+ // issued token is minimal (device scope only).
12665
+ oauthIssueCode: method(
12666
+ object({
12667
+ integrationId: string(),
12668
+ userId: string(),
12669
+ username: string(),
12670
+ scopes: array(TokenScopeSchema),
12671
+ redirectUri: string(),
12672
+ hubUrl: string()
12673
+ }),
12674
+ object({ code: string() }),
12675
+ { kind: "mutation", access: "create" }
12676
+ ),
12677
+ oauthExchangeCode: method(
12678
+ object({ code: string(), redirectUri: string() }),
12679
+ object({
12680
+ accessToken: string(),
12681
+ refreshToken: string(),
12682
+ expiresIn: number()
12683
+ }).nullable(),
12684
+ { kind: "mutation", access: "view" }
12685
+ ),
12686
+ oauthRefresh: method(
12687
+ object({ refreshToken: string() }),
12688
+ object({
12689
+ accessToken: string(),
12690
+ refreshToken: string(),
12691
+ expiresIn: number()
12692
+ }).nullable(),
12693
+ { kind: "mutation", access: "view" }
12694
+ ),
12695
+ oauthVerifyAccessToken: method(
12696
+ object({ token: string() }),
12697
+ object({
12698
+ userId: string(),
12699
+ username: string(),
12700
+ scopes: array(TokenScopeSchema)
12701
+ }).nullable(),
12702
+ { access: "view" }
12703
+ ),
12704
+ // ── OAuth linked-session management (Phase D) ──────────────────
12705
+ //
12706
+ // The admin UI lists active account-linking sessions and revokes
12707
+ // them; revocation makes the linked integration's tokens fail
12708
+ // verification immediately.
12709
+ listOauthSessions: method(
12710
+ _void(),
12711
+ array(OauthSessionSummarySchema),
12712
+ { auth: "admin" }
12713
+ ),
12714
+ revokeOauthSession: method(
12715
+ object({ id: string() }),
12716
+ object({ success: boolean() }),
12717
+ { kind: "mutation", auth: "admin", access: "delete" }
12514
12718
  )
12515
12719
  }
12516
12720
  });
@@ -12727,6 +12931,19 @@ const RenameNodeResultSchema = object({
12727
12931
  record(string(), ClusterAddonStatusEntrySchema),
12728
12932
  { auth: "admin" }
12729
12933
  ),
12934
+ getCapUsageGraph: method(
12935
+ object({
12936
+ windowSeconds: number().int().positive().max(300).default(60)
12937
+ }),
12938
+ array(object({
12939
+ callerAddonId: string(),
12940
+ providerAddonId: string(),
12941
+ capName: string(),
12942
+ callsPerMin: number(),
12943
+ lastCallAtMs: number()
12944
+ })).readonly(),
12945
+ { auth: "admin" }
12946
+ ),
12730
12947
  /**
12731
12948
  * Direct per-node addon listing — calls `$agent.status` on the target
12732
12949
  * node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
@@ -13156,6 +13373,29 @@ const CustomActionInputSchema = object({
13156
13373
  isActive: boolean()
13157
13374
  })).readonly()
13158
13375
  ),
13376
+ /**
13377
+ * Toggle a single collection-cap provider on/off. Generic write-side
13378
+ * counterpart of `listCapabilityProviders` — drives the per-provider
13379
+ * Enable/Disable affordance in admin pages (TURN servers, etc.)
13380
+ * without needing a bespoke orchestrator cap.
13381
+ *
13382
+ * Reaches the hub's `CapabilityRegistry` directly:
13383
+ * `enableCollectionProvider` / `disableCollectionProvider` flip the
13384
+ * registry-level `disabledProviders` set. `getCollectionEntries`
13385
+ * already filters disabled providers out, so a disabled provider
13386
+ * drops out of every collection aggregate immediately. Only valid
13387
+ * for `mode: 'collection'` caps — the registry no-ops + warns for
13388
+ * singletons.
13389
+ */
13390
+ setCapabilityProviderEnabled: method(
13391
+ object({
13392
+ capName: string().min(1),
13393
+ addonId: string().min(1),
13394
+ enabled: boolean()
13395
+ }),
13396
+ object({ success: literal(true) }),
13397
+ { kind: "mutation", auth: "admin" }
13398
+ ),
13159
13399
  /**
13160
13400
  * Live-update one of the framework packages marked
13161
13401
  * `camstack.system: true` (`@camstack/types|kernel|core|sdk|ui-library`).
@@ -14027,4 +14267,4 @@ exports.pipelineRunnerCapability = pipelineRunnerCapability;
14027
14267
  exports.streamBrokerCapability = streamBrokerCapability;
14028
14268
  exports.string = string;
14029
14269
  exports.webrtcSessionCapability = webrtcSessionCapability;
14030
- //# sourceMappingURL=index-BBpVDiWL.js.map
14270
+ //# sourceMappingURL=index-p-6GfKOg.js.map