@camstack/addon-post-analysis 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 (48) hide show
  1. package/dist/embedding-encoder/index.js +1 -1
  2. package/dist/embedding-encoder/index.mjs +1 -1
  3. package/dist/enrichment-engine/index.js +75 -7
  4. package/dist/enrichment-engine/index.js.map +1 -1
  5. package/dist/enrichment-engine/index.mjs +75 -7
  6. package/dist/enrichment-engine/index.mjs.map +1 -1
  7. package/dist/{index-DnAXaymw.mjs → index-CGAj-pkn.mjs} +559 -327
  8. package/dist/index-CGAj-pkn.mjs.map +1 -0
  9. package/dist/{index-DjIIGJS2.js → index-Dbx13pc7.js} +559 -327
  10. package/dist/index-Dbx13pc7.js.map +1 -0
  11. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/LiveStatsTab.d.ts +5 -0
  12. package/dist/pipeline-analytics/@mf-types/compiled-types/pipeline-analytics/widgets/index.d.ts +2 -0
  13. package/dist/pipeline-analytics/@mf-types.zip +0 -0
  14. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-d8PmLbO2.mjs +19 -0
  15. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BcWYbuKp.mjs +18 -0
  16. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-DuO9h7li.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs-D-USVuHq.mjs} +4 -2
  17. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-CmqNjq44.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.mjs_commonjs-proxy-qQCPW8pT.mjs} +1 -1
  18. package/dist/pipeline-analytics/{__mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-CA8cCIEl.mjs → __mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_2_dom__loadShare__.mjs_commonjs-proxy-Bv9bYz9E.mjs} +1 -1
  19. package/dist/pipeline-analytics/_stub.js +497 -431
  20. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BDtqUbh1.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DErNFTYO.mjs} +6 -6
  21. package/dist/pipeline-analytics/{client-DdXDZxzK.mjs → client-DHmQcIWy.mjs} +2990 -3217
  22. package/dist/pipeline-analytics/{hostInit-CtfhvKwy.mjs → hostInit-DmzGJewr.mjs} +12 -12
  23. package/dist/pipeline-analytics/{index-BgDjUxfg.mjs → index-BA65ZJOW.mjs} +1 -1
  24. package/dist/pipeline-analytics/{index-COebxMhm.mjs → index-BCEx31Mh.mjs} +4032 -3582
  25. package/dist/pipeline-analytics/{index-kIgjN-uq.mjs → index-CHnXxMRA.mjs} +1 -1
  26. package/dist/pipeline-analytics/index-CWkKuNLr.mjs +232 -0
  27. package/dist/pipeline-analytics/{index-B4OKsa9p.mjs → index-Crs1D0Uu.mjs} +1 -1
  28. package/dist/pipeline-analytics/{index-k0CA0h_r.mjs → index-DicaGC31.mjs} +1 -1
  29. package/dist/pipeline-analytics/index-gbflFMEY.mjs +36403 -0
  30. package/dist/pipeline-analytics/{index-DyYvUfc7.mjs → index-gpelkpEE.mjs} +1 -1
  31. package/dist/pipeline-analytics/index.js +73 -22
  32. package/dist/pipeline-analytics/index.js.map +1 -1
  33. package/dist/pipeline-analytics/index.mjs +73 -22
  34. package/dist/pipeline-analytics/index.mjs.map +1 -1
  35. package/dist/pipeline-analytics/{jsx-runtime-4ro1c69i.mjs → jsx-runtime-Wcfyyyt4.mjs} +1 -1
  36. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  37. package/dist/recording/index.js +2 -2
  38. package/dist/recording/index.mjs +2 -2
  39. package/dist/{recording-coordinator-CRJ4yA9t.mjs → recording-coordinator-BjWd7HjD.mjs} +2 -2
  40. package/dist/{recording-coordinator-CRJ4yA9t.mjs.map → recording-coordinator-BjWd7HjD.mjs.map} +1 -1
  41. package/dist/{recording-coordinator-UGTDbTdE.js → recording-coordinator-b-7Ast8s.js} +2 -2
  42. package/dist/{recording-coordinator-UGTDbTdE.js.map → recording-coordinator-b-7Ast8s.js.map} +1 -1
  43. package/package.json +5 -1
  44. package/dist/index-DjIIGJS2.js.map +0 -1
  45. package/dist/index-DnAXaymw.mjs.map +0 -1
  46. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-p-Z3JTk9.mjs +0 -19
  47. package/dist/pipeline-analytics/__mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BfmtcT51.mjs +0 -15
  48. package/dist/pipeline-analytics/index-CzwXKSYE.mjs +0 -20852
@@ -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;
@@ -5961,6 +5977,53 @@ const DecodedFrameSchema = object({
5961
5977
  format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5962
5978
  timestamp: number()
5963
5979
  });
5980
+ const FrameHandleSchema = object({
5981
+ shmId: string(),
5982
+ slot: number().int().nonnegative(),
5983
+ seq: number().int().nonnegative(),
5984
+ width: number().int().positive(),
5985
+ height: number().int().positive(),
5986
+ format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5987
+ pts: number(),
5988
+ byteLength: number().int().nonnegative(),
5989
+ nodeId: string(),
5990
+ slotCount: number().int().positive()
5991
+ });
5992
+ const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
5993
+ const SubscribeFramesInputSchema = object({
5994
+ brokerId: string(),
5995
+ format: FrameHandleFormatSchema,
5996
+ /**
5997
+ * Optional reader-side cadence hint in frames per second. The broker does
5998
+ * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
5999
+ * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
6000
+ * the consumer can pace its own `pullFrameHandles` polling.
6001
+ */
6002
+ maxFps: number().positive().optional(),
6003
+ /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
6004
+ tag: string().optional()
6005
+ });
6006
+ const SubscribeFramesResultSchema = object({
6007
+ /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
6008
+ subscriptionId: string(),
6009
+ /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
6010
+ maxFps: number().nonnegative()
6011
+ });
6012
+ const DecodedAudioChunkSchema = object({
6013
+ data: _instanceof(Uint8Array),
6014
+ sampleRate: number().int().positive(),
6015
+ channels: number().int().positive(),
6016
+ timestamp: number()
6017
+ });
6018
+ const SubscribeAudioChunksInputSchema = object({
6019
+ brokerId: string(),
6020
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6021
+ tag: string().optional()
6022
+ });
6023
+ const SubscribeAudioChunksResultSchema = object({
6024
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
6025
+ subscriptionId: string()
6026
+ });
5964
6027
  const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
5965
6028
  const BrokerStatsSchema = object({
5966
6029
  status: BrokerStatusSchema$1,
@@ -6182,9 +6245,76 @@ const RtpSourceSchema = object({
6182
6245
  object({ released: boolean(), refcount: number().int().nonnegative() }),
6183
6246
  { kind: "mutation", auth: "admin" }
6184
6247
  ),
6185
- getBroker: method(
6186
- object({ brokerId: string() }),
6187
- custom()
6248
+ /**
6249
+ * ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
6250
+ *
6251
+ * The serialisable replacement for the live-object `IStreamBroker.
6252
+ * onDecodedAudioChunk` callback path. Unlike the video frame plane,
6253
+ * audio chunks are tiny (a ~500ms PCM window is a few KB) so they ship
6254
+ * their bytes INLINE over tRPC — no shared-memory ring. A consumer:
6255
+ *
6256
+ * 1. `subscribeAudioChunks({ brokerId, tag })` — the broker registers
6257
+ * a per-subscription bounded FIFO queue and returns a
6258
+ * `subscriptionId`.
6259
+ * 2. polls `pullAudioChunks({ subscriptionId, maxCount })` — drains
6260
+ * `DecodedAudioChunk[]` in arrival order (FIFO, no latest-wins
6261
+ * drop: an audio gap is audible / breaks an analysis window). The
6262
+ * queue is generously sized; it only drops its oldest chunk if a
6263
+ * truly stalled consumer lets it overflow.
6264
+ * 3. `unsubscribeAudioChunks({ subscriptionId })` on teardown.
6265
+ */
6266
+ subscribeAudioChunks: method(
6267
+ SubscribeAudioChunksInputSchema,
6268
+ SubscribeAudioChunksResultSchema,
6269
+ { kind: "mutation" }
6270
+ ),
6271
+ pullAudioChunks: method(
6272
+ object({
6273
+ subscriptionId: string(),
6274
+ maxCount: number().int().positive().default(8)
6275
+ }),
6276
+ array(DecodedAudioChunkSchema).readonly()
6277
+ ),
6278
+ unsubscribeAudioChunks: method(
6279
+ object({ subscriptionId: string() }),
6280
+ object({ released: boolean() }),
6281
+ { kind: "mutation" }
6282
+ ),
6283
+ /**
6284
+ * ── Shared-memory frame plane (Phase 5 / D9) ──────────────────────
6285
+ *
6286
+ * The handle-based replacement for the live-object `IStreamBroker.
6287
+ * onDecodedFrame` callback path. A consumer:
6288
+ *
6289
+ * 1. `subscribeFrames({ brokerId, format })` — the broker spins up
6290
+ * (or reuses) a `frameSink: 'shm'` decoder session producing that
6291
+ * `format` and returns a `subscriptionId`.
6292
+ * 2. polls `pullFrameHandles({ subscriptionId, maxCount })` — drains
6293
+ * zero-pixel `FrameHandle[]`; each handle is fed to a
6294
+ * `FrameRingReader` that opens the named shm segment and reads the
6295
+ * pixels back zero-copy.
6296
+ * 3. `unsubscribeFrames({ subscriptionId })` on teardown.
6297
+ *
6298
+ * The broker keeps one shm ring per `(brokerId, format)` actually
6299
+ * requested — no broker-side `sharp` conversion. fps throttling is
6300
+ * implicit (latest-wins ring reads drop frames for a slow consumer).
6301
+ */
6302
+ subscribeFrames: method(
6303
+ SubscribeFramesInputSchema,
6304
+ SubscribeFramesResultSchema,
6305
+ { kind: "mutation" }
6306
+ ),
6307
+ pullFrameHandles: method(
6308
+ object({
6309
+ subscriptionId: string(),
6310
+ maxCount: number().int().positive().default(4)
6311
+ }),
6312
+ array(FrameHandleSchema).readonly()
6313
+ ),
6314
+ unsubscribeFrames: method(
6315
+ object({ subscriptionId: string() }),
6316
+ object({ released: boolean() }),
6317
+ { kind: "mutation" }
6188
6318
  ),
6189
6319
  setPreBufferDuration: method(
6190
6320
  object({ brokerId: string(), seconds: number().min(0).max(30) }),
@@ -7262,6 +7392,34 @@ MotionTriggerStatusSchema.extend({
7262
7392
  }) }
7263
7393
  }
7264
7394
  });
7395
+ object({
7396
+ enabled: boolean(),
7397
+ sensitivity: number(),
7398
+ /** Row-major active-cell grid. Length = gridWidth*gridHeight (see getOptions). */
7399
+ cells: array(boolean()),
7400
+ lastFetchedAt: number()
7401
+ });
7402
+ const MotionZoneOptionsSchema = object({
7403
+ gridWidth: number(),
7404
+ gridHeight: number(),
7405
+ sensitivity: object({ min: number(), max: number(), step: number() })
7406
+ });
7407
+ const MotionZonePatchSchema = object({
7408
+ enabled: boolean().optional(),
7409
+ sensitivity: number().optional(),
7410
+ cells: array(boolean()).optional()
7411
+ });
7412
+ ({
7413
+ deviceTypes: [DeviceType.Camera],
7414
+ methods: {
7415
+ getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
7416
+ setZone: method(
7417
+ object({ deviceId: number(), patch: MotionZonePatchSchema }),
7418
+ _void(),
7419
+ { kind: "mutation", auth: "admin" }
7420
+ )
7421
+ }
7422
+ });
7265
7423
  const AutotrackTargetTypeSchema = string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
7266
7424
  const PtzAutotrackSettingsSchema = object({
7267
7425
  targetType: AutotrackTargetTypeSchema,
@@ -7350,6 +7508,100 @@ PtzAutotrackStatusSchema.extend({
7350
7508
  }) }
7351
7509
  }
7352
7510
  });
7511
+ const StreamProfileSchema = _enum(["main", "sub", "ext"]);
7512
+ const StreamProfileConfigSchema = object({
7513
+ width: number(),
7514
+ height: number(),
7515
+ codec: _enum(["h264", "h265"]),
7516
+ framerate: number(),
7517
+ bitrate: number(),
7518
+ // kbps
7519
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7520
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7521
+ gop: number().optional(),
7522
+ audio: boolean().optional()
7523
+ });
7524
+ object({
7525
+ /** Per-profile current config. A profile absent = the camera doesn't have it. */
7526
+ main: StreamProfileConfigSchema.optional(),
7527
+ sub: StreamProfileConfigSchema.optional(),
7528
+ ext: StreamProfileConfigSchema.optional(),
7529
+ lastFetchedAt: number()
7530
+ });
7531
+ const StreamProfileOptionsSchema = object({
7532
+ resolutions: array(object({ width: number(), height: number() })),
7533
+ codecs: array(_enum(["h264", "h265"])),
7534
+ framerates: array(number()),
7535
+ /** Allowed bitrate values (kbps). Empty if the camera takes a free range. */
7536
+ bitrates: array(number()),
7537
+ /** Optional [min,max] kbps when the camera accepts a continuous range. */
7538
+ bitrateRange: tuple([number(), number()]).optional(),
7539
+ supportsBitrateMode: boolean(),
7540
+ supportsEncoderProfile: boolean(),
7541
+ supportsGop: boolean(),
7542
+ /** Allowed GOP / keyframe-interval range, in seconds — drives the
7543
+ * I-frame-interval selector. Absent when the camera advertises GOP
7544
+ * support but no concrete range (callers then fall back to a free
7545
+ * numeric input). `{ min, max, step }` per the getOptions convention. */
7546
+ gop: object({ min: number(), max: number(), step: number() }).optional()
7547
+ });
7548
+ const StreamParamsOptionsSchema = object({
7549
+ main: StreamProfileOptionsSchema.optional(),
7550
+ sub: StreamProfileOptionsSchema.optional(),
7551
+ ext: StreamProfileOptionsSchema.optional()
7552
+ });
7553
+ const StreamProfilePatchSchema = object({
7554
+ width: number().optional(),
7555
+ height: number().optional(),
7556
+ codec: _enum(["h264", "h265"]).optional(),
7557
+ framerate: number().optional(),
7558
+ bitrate: number().optional(),
7559
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7560
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7561
+ gop: number().optional(),
7562
+ audio: boolean().optional()
7563
+ });
7564
+ ({
7565
+ deviceTypes: [DeviceType.Camera],
7566
+ methods: {
7567
+ getOptions: method(
7568
+ object({ deviceId: number() }),
7569
+ StreamParamsOptionsSchema
7570
+ ),
7571
+ setProfile: method(
7572
+ object({
7573
+ deviceId: number(),
7574
+ profile: StreamProfileSchema,
7575
+ patch: StreamProfilePatchSchema
7576
+ }),
7577
+ _void(),
7578
+ { kind: "mutation", auth: "admin" }
7579
+ ),
7580
+ /**
7581
+ * Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
7582
+ * shape) for this camera's stream-encoder settings — one section per
7583
+ * profile (main / sub / ext) with the resolution / codec / framerate
7584
+ * / bitrate / bitrate-mode / encoder-profile / GOP controls the
7585
+ * firmware actually exposes.
7586
+ *
7587
+ * Driven by `getOptions` (camera-probed availability) + `getStatus`
7588
+ * (current per-profile config); each field's `default` is seeded
7589
+ * from the live config so the form renders the camera state in one
7590
+ * pass. Returns `null` when the camera exposes no configurable
7591
+ * stream property — the renderer then shows the unsupported message.
7592
+ *
7593
+ * Output is `z.unknown().nullable()` — the same convention every
7594
+ * other `ConfigUISchema`-returning cap method uses (`device-ops`,
7595
+ * `device-manager`); `ConfigUISchema` is a TS-only type with no
7596
+ * companion Zod schema, and a concrete object would collapse
7597
+ * unrelated AppRouter branches to `unknown` during codegen.
7598
+ */
7599
+ getConfigSchema: method(
7600
+ object({ deviceId: number() }),
7601
+ unknown().nullable()
7602
+ )
7603
+ }
7604
+ });
7353
7605
  object({
7354
7606
  on: boolean(),
7355
7607
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
@@ -8298,6 +8550,83 @@ const VersionOutputSchema = object({ version: string() });
8298
8550
  getVersion: method(_void(), VersionOutputSchema)
8299
8551
  }
8300
8552
  });
8553
+ const MethodAccessSchema = _enum(["view", "create", "delete"]);
8554
+ const AllowedProviderSchema = union([literal("*"), array(string())]);
8555
+ const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
8556
+ const CapScopeSchema = _enum(["device", "system"]);
8557
+ const TokenScopeSchema = discriminatedUnion("type", [
8558
+ object({
8559
+ type: literal("category"),
8560
+ target: CapScopeSchema,
8561
+ access: array(MethodAccessSchema).min(1)
8562
+ }),
8563
+ object({
8564
+ type: literal("capability"),
8565
+ target: string(),
8566
+ access: array(MethodAccessSchema).min(1)
8567
+ }),
8568
+ object({
8569
+ type: literal("addon"),
8570
+ target: string(),
8571
+ access: array(MethodAccessSchema).min(1)
8572
+ }),
8573
+ object({
8574
+ type: literal("device"),
8575
+ /**
8576
+ * One or more deviceIds (serialised as strings for wire-format
8577
+ * consistency with the rest of the union). Matcher accepts if
8578
+ * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
8579
+ * of one scope-per-device when granting access to a set of cameras.
8580
+ */
8581
+ targets: array(string()).min(1),
8582
+ access: array(MethodAccessSchema).min(1)
8583
+ })
8584
+ ]);
8585
+ object({
8586
+ id: string(),
8587
+ username: string(),
8588
+ passwordHash: string(),
8589
+ /**
8590
+ * Admin bypass. When true, the middleware skips the scope-access
8591
+ * check entirely. There is no other axis of privilege; the legacy
8592
+ * role enum collapsed onto this boolean in v2.
8593
+ */
8594
+ isAdmin: boolean().default(false),
8595
+ allowedProviders: AllowedProviderSchema,
8596
+ allowedDevices: AllowedDevicesSchema,
8597
+ /**
8598
+ * Scopes granted to this user. Admins bypass; their `scopes` is
8599
+ * ignored. Non-admins without scopes are locked out of every
8600
+ * protected call.
8601
+ */
8602
+ scopes: array(TokenScopeSchema).default([]),
8603
+ createdAt: number(),
8604
+ updatedAt: number()
8605
+ });
8606
+ object({
8607
+ id: string(),
8608
+ label: string(),
8609
+ isAdmin: boolean().default(false),
8610
+ allowedProviders: AllowedProviderSchema,
8611
+ allowedDevices: AllowedDevicesSchema,
8612
+ tokenHash: string(),
8613
+ tokenPrefix: string(),
8614
+ createdAt: number(),
8615
+ lastUsedAt: number().optional()
8616
+ });
8617
+ object({
8618
+ id: string(),
8619
+ userId: string(),
8620
+ name: string(),
8621
+ tokenHash: string(),
8622
+ tokenPrefix: string(),
8623
+ scopes: array(TokenScopeSchema),
8624
+ // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
8625
+ // schema accepts both `null` (read from disk) and `undefined` (in-memory).
8626
+ expiresAt: number().nullish(),
8627
+ lastUsedAt: number().nullish(),
8628
+ createdAt: number()
8629
+ });
8301
8630
  const SsoBridgeClaimsSchema = object({
8302
8631
  userId: string(),
8303
8632
  username: string(),
@@ -8313,7 +8642,18 @@ const SsoBridgeClaimsSchema = object({
8313
8642
  * JWT WITHOUT verifying the signature — the hub re-verifies on every
8314
8643
  * inbound call so trust still rests with the signing hub.
8315
8644
  */
8316
- hubUrl: string().optional()
8645
+ hubUrl: string().optional(),
8646
+ /** Permission scopes baked into the token. Set by the OAuth
8647
+ * account-linking grant; absent on ordinary SSO-login tokens. */
8648
+ scopes: array(TokenScopeSchema).optional(),
8649
+ /** OAuth authorization-code binding — set only on `oauth-code` tokens. */
8650
+ redirectUri: string().optional(),
8651
+ integrationId: string().optional(),
8652
+ /** JWT ID — unique per issued code; consumed-set enforces single-use. */
8653
+ jti: string().optional(),
8654
+ /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
8655
+ * tokens so the verify path can check the session is not revoked. */
8656
+ sessionId: string().optional()
8317
8657
  });
8318
8658
  ({
8319
8659
  methods: {
@@ -8330,6 +8670,23 @@ const SsoBridgeClaimsSchema = object({
8330
8670
  )
8331
8671
  }
8332
8672
  });
8673
+ const OauthIntegrationDescriptorSchema = object({
8674
+ /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
8675
+ integrationId: string(),
8676
+ /** Human label rendered on the consent page. */
8677
+ displayName: string(),
8678
+ /** Scopes baked into every token issued for this integration. */
8679
+ requestedScopes: array(TokenScopeSchema),
8680
+ /** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
8681
+ * redirect_uri that does not start with one of these. Required —
8682
+ * an empty list means the integration can never complete linking. */
8683
+ allowedRedirectPrefixes: array(string()).min(1)
8684
+ });
8685
+ ({
8686
+ methods: {
8687
+ getDescriptor: method(_void(), OauthIntegrationDescriptorSchema)
8688
+ }
8689
+ });
8333
8690
  const PasskeySummarySchema = object({
8334
8691
  credentialId: string(),
8335
8692
  label: string(),
@@ -8610,21 +8967,30 @@ const AddonPageDeclarationSchema = object({
8610
8967
  });
8611
8968
  const WidgetHostEnum = _enum(["device-tab", "dashboard", "integration-detail"]);
8612
8969
  const WidgetSizeEnum = _enum(["xs", "sm", "md", "lg", "xl"]);
8970
+ const WidgetRemoteSchema = object({
8971
+ remoteName: string(),
8972
+ exposedModule: string(),
8973
+ componentKey: string().optional()
8974
+ });
8613
8975
  const WidgetMetadataSchema = object({
8614
- /** Stable id within the addon — kebab-case. */
8615
- stableId: string(),
8976
+ // ── UiContribution core (kind:'remote') ──────────────────────────
8977
+ /** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
8978
+ tab: string(),
8979
+ /** Optional sub-tab within `tab`. */
8980
+ subTab: string().optional(),
8616
8981
  /** Operator-facing label. */
8617
8982
  label: string(),
8983
+ /** Ordering within `(tab, subTab)`, ascending. */
8984
+ order: number().optional(),
8985
+ /** Always `'remote'` — a widget is a Module Federation remote. */
8986
+ kind: literal("remote"),
8987
+ /** MF remote descriptor. */
8988
+ remote: WidgetRemoteSchema,
8989
+ // ── Widget-only metadata ─────────────────────────────────────────
8990
+ /** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
8991
+ stableId: string(),
8618
8992
  description: string().optional(),
8619
8993
  icon: string().optional(),
8620
- /**
8621
- * Module Federation remote name — must match the `name` field on the
8622
- * widget addon's `federation()` plugin config. Used by the host's
8623
- * `<WidgetRegistryProvider>` to call `loadRemote('<remoteName>/widgets')`.
8624
- * Conventionally `addon_<addonid>_widgets` (snake_case; MF names
8625
- * cannot contain hyphens).
8626
- */
8627
- remoteName: string(),
8628
8994
  /**
8629
8995
  * Bundle filename inside the addon's `dist/` dir served at
8630
8996
  * `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
@@ -8633,9 +8999,9 @@ const WidgetMetadataSchema = object({
8633
8999
  * cache-buster URL without a separate filesystem stat.
8634
9000
  */
8635
9001
  bundle: string(),
8636
- /** Where the widget makes sense to render. */
9002
+ /** Every host the widget supports. The picker filters on this set. */
8637
9003
  hosts: array(WidgetHostEnum).readonly(),
8638
- /** Required props the host must supply. Validated at <WidgetSlot> mount. */
9004
+ /** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
8639
9005
  requires: object({
8640
9006
  deviceContext: boolean().default(false),
8641
9007
  integrationContext: boolean().default(false)
@@ -8710,6 +9076,16 @@ const InvokeReplyEnvelopeSchema = object({
8710
9076
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
8711
9077
  }
8712
9078
  });
9079
+ const ShmRingStatsSchema = object({
9080
+ sessionId: string(),
9081
+ slotCount: number().int(),
9082
+ slotByteLength: number().int(),
9083
+ segmentBytes: number().int(),
9084
+ budgetMb: number().int(),
9085
+ framesWritten: number().int(),
9086
+ getFrameHits: number().int(),
9087
+ getFrameMisses: number().int()
9088
+ });
8713
9089
  ({
8714
9090
  methods: {
8715
9091
  // ── Discovery ─────────────────────────────────────────────────
@@ -8737,10 +9113,27 @@ const InvokeReplyEnvelopeSchema = object({
8737
9113
  url: string()
8738
9114
  }), _void()),
8739
9115
  // ── Output — polling-based frame retrieval ────────────────────
9116
+ // `pullFrames` drains the pixel `DecodedFrame[]` of a `frameSink:
9117
+ // 'callback'` session. `pullHandles` (Phase 5 / D9) drains the
9118
+ // zero-pixel `FrameHandle[]` of a `frameSink: 'shm'` session — the
9119
+ // broker hands each handle to a `FrameRingReader` that opens the
9120
+ // named segment and reads the pixels back zero-copy. A session is
9121
+ // one mode or the other; the unmatched method returns an empty
9122
+ // array.
8740
9123
  pullFrames: method(object({
8741
9124
  sessionId: string(),
8742
9125
  maxCount: number().default(1)
8743
9126
  }), array(DecodedFrameSchema)),
9127
+ pullHandles: method(object({
9128
+ sessionId: string(),
9129
+ maxCount: number().default(1)
9130
+ }), array(FrameHandleSchema)),
9131
+ // ── Frame fetch (Phase 5 / D9 downstream access) ──────────────
9132
+ // Read the pixels a FrameHandle refers to from this node's shm ring.
9133
+ // Returns null when the slot was already recycled (latest-wins).
9134
+ getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
9135
+ // shm ring usage stats for a session.
9136
+ getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
8744
9137
  // ── Control ───────────────────────────────────────────────────
8745
9138
  updateConfig: method(object({
8746
9139
  sessionId: string(),
@@ -9912,8 +10305,8 @@ const DevicePersistConfigPayloadSchema = object({
9912
10305
  /**
9913
10306
  * Return the addon ids that declared a wrapper provider for `capName`.
9914
10307
  * Backs the device-bindings UI's wrapper-picker dropdown. Entries are
9915
- * sourced from `CapabilityRegistry.wrapperProviders`, populated at
9916
- * `ProviderRegistration.kind === 'wrapper'` time.
10308
+ * sourced from `CapabilityRegistry.wrapperProviders`, populated when
10309
+ * the cap definition declares `kind: 'wrapper'`.
9917
10310
  */
9918
10311
  listWrappersForCap: method(
9919
10312
  object({ capName: string() }),
@@ -10219,51 +10612,6 @@ const AuthResultSchema = object({
10219
10612
  validateToken: method(object({ token: string() }), AuthResultSchema.nullable())
10220
10613
  }
10221
10614
  });
10222
- const AuthProviderInfoSchema = object({
10223
- /** Stable id matching the addon id (used for `getLoginUrl({addonId,…})`). */
10224
- addonId: string(),
10225
- /**
10226
- * Per-instance id when one addon registers multiple "logical"
10227
- * providers (e.g. OIDC with Google + Microsoft + custom). The login
10228
- * URL becomes `/addon/${addonId}/${instanceId}/start` — handler reads
10229
- * `:instanceId` from the route. Empty/unset means the addon is a
10230
- * single-instance provider; the URL is `/addon/${addonId}/start`.
10231
- */
10232
- instanceId: string().optional(),
10233
- /** Display label shown on the login button + admin row. */
10234
- displayName: string(),
10235
- /** Optional iconography hint (lucide-react icon name OR emoji). */
10236
- icon: string().optional(),
10237
- /** When true, the provider exposes a redirect-based login flow
10238
- * (`getLoginUrl` returns a URL the browser navigates to). */
10239
- hasRedirectFlow: boolean(),
10240
- /** When true, the provider exposes a credential-form login flow
10241
- * (`validateCredentials` accepts username + password). */
10242
- hasCredentialFlow: boolean(),
10243
- /** Provider kind, drives admin-UI hint dispatch (oidc / saml / totp / …). */
10244
- kind: string().optional(),
10245
- /** Operator-facing status string (e.g. "Connected to https://login.acme.com"). */
10246
- status: string().optional(),
10247
- /** When false, the provider is registered but disabled by config; the
10248
- * UI surfaces it as inactive without enumerating it for login. */
10249
- enabled: boolean()
10250
- });
10251
- ({
10252
- methods: {
10253
- /** All registered auth providers, both enabled and disabled. */
10254
- listProviders: method(_void(), array(AuthProviderInfoSchema).readonly()),
10255
- /**
10256
- * Toggle a provider's enabled flag. Disabled providers stay
10257
- * registered but aren't surfaced on the login page. The orchestrator
10258
- * persists the state in `addon-settings` so it survives restarts.
10259
- */
10260
- setProviderEnabled: method(
10261
- object({ addonId: string(), enabled: boolean() }),
10262
- object({ success: literal(true) }),
10263
- { kind: "mutation", auth: "admin" }
10264
- )
10265
- }
10266
- });
10267
10615
  const NetworkEndpointSchema = object({
10268
10616
  url: string(),
10269
10617
  hostname: string(),
@@ -10295,55 +10643,13 @@ const NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
10295
10643
  getEndpoint: method(_void(), NetworkEndpointSchema.nullable()),
10296
10644
  getStatus: method(_void(), NetworkAccessStatusSchema),
10297
10645
  /**
10298
- * Enumerate every active ingress entry. Default implementation (when
10299
- * the provider omits this method) is derived from `getEndpoint()` —
10300
- * see the remote-access orchestrator for the fallback path.
10646
+ * Enumerate every active ingress entry. Providers that expose only a
10647
+ * single endpoint may omit this method; callers fall back to
10648
+ * `getEndpoint()` in that case.
10301
10649
  */
10302
10650
  listEndpoints: method(_void(), array(NetworkEndpointEntrySchema).readonly())
10303
10651
  }
10304
10652
  });
10305
- const RemoteAccessEndpointSchema = object({
10306
- url: string(),
10307
- hostname: string(),
10308
- port: number(),
10309
- protocol: _enum(["http", "https"])
10310
- });
10311
- const RemoteAccessProviderInfoSchema = object({
10312
- /** Stable id matching the addon id. */
10313
- addonId: string(),
10314
- /** Display label shown on the admin row — sourced from the addon manifest. */
10315
- displayName: string(),
10316
- /** When false, the provider is registered but disabled. */
10317
- enabled: boolean(),
10318
- /** True when the underlying tunnel/connection is up. */
10319
- connected: boolean(),
10320
- /** Public-facing endpoint, when connected. Null otherwise. */
10321
- endpoint: RemoteAccessEndpointSchema.nullable(),
10322
- /** Last error message (when connected=false), if available. */
10323
- error: string().optional()
10324
- });
10325
- ({
10326
- methods: {
10327
- /** All registered remote-access providers + their live status. */
10328
- listProviders: method(_void(), array(RemoteAccessProviderInfoSchema).readonly()),
10329
- /**
10330
- * Start a specific provider's tunnel. Per-provider config still
10331
- * lives on the addon's settings panel; this is just the on/off
10332
- * trigger so the admin UI can manage the lifecycle from one place.
10333
- */
10334
- startProvider: method(
10335
- object({ addonId: string() }),
10336
- RemoteAccessEndpointSchema,
10337
- { kind: "mutation", auth: "admin" }
10338
- ),
10339
- /** Stop a specific provider's tunnel (idempotent on already-stopped). */
10340
- stopProvider: method(
10341
- object({ addonId: string() }),
10342
- object({ success: literal(true) }),
10343
- { kind: "mutation", auth: "admin" }
10344
- )
10345
- }
10346
- });
10347
10653
  const TurnServerSchema = object({
10348
10654
  /** Single URL or list of URLs (e.g. "turn:turn.example.com:3478?transport=udp"). */
10349
10655
  urls: union([string(), array(string())]),
@@ -10363,45 +10669,6 @@ const TurnServerSchema = object({
10363
10669
  )
10364
10670
  }
10365
10671
  });
10366
- const TurnProviderInfoSchema = object({
10367
- /** Stable id matching the addon id. */
10368
- addonId: string(),
10369
- /** Display label shown on the admin row — sourced from the addon manifest. */
10370
- displayName: string(),
10371
- /** When false, the provider is registered but disabled. */
10372
- enabled: boolean(),
10373
- /** Number of servers this provider is currently exposing. */
10374
- serverCount: number(),
10375
- /**
10376
- * Flat list of every TURN/STUN URL this provider currently exposes.
10377
- * One row per URL (multi-URL ICE server entries are flattened). The
10378
- * admin UI shows this in a compact per-provider list so operators
10379
- * can verify what's actually being negotiated without having to dig
10380
- * into the combined `getAllServers` output.
10381
- */
10382
- urls: array(string()).readonly(),
10383
- /** Last fetch error (when serverCount=0 due to API failure), if any. */
10384
- error: string().optional()
10385
- });
10386
- ({
10387
- methods: {
10388
- /** All registered TURN providers + per-provider stats. */
10389
- listProviders: method(_void(), array(TurnProviderInfoSchema).readonly()),
10390
- /**
10391
- * Combined list of TURN/STUN servers from all ENABLED providers.
10392
- * Consumed by the WebRTC layer at session-creation time —
10393
- * implementations may fetch fresh short-lived credentials each
10394
- * call (e.g. Cloudflare API), so consumers SHOULD call per-session.
10395
- */
10396
- getAllServers: method(_void(), array(TurnServerSchema).readonly()),
10397
- /** Toggle a provider's enabled flag. */
10398
- setProviderEnabled: method(
10399
- object({ addonId: string(), enabled: boolean() }),
10400
- object({ success: literal(true) }),
10401
- { kind: "mutation", auth: "admin" }
10402
- )
10403
- }
10404
- });
10405
10672
  const SnapshotImageSchema = object({
10406
10673
  base64: string(),
10407
10674
  contentType: string()
@@ -10850,6 +11117,8 @@ const pipelineAnalyticsCapability = {
10850
11117
  name: "pipeline-analytics",
10851
11118
  scope: "device",
10852
11119
  mode: "singleton",
11120
+ kind: "wrapper",
11121
+ defaultActive: true,
10853
11122
  deviceTypes: [DeviceType.Camera],
10854
11123
  exposesDeviceSettings: true,
10855
11124
  methods: {
@@ -11136,6 +11405,18 @@ const PtzMoveCommandSchema = object({
11136
11405
  zoom: number().optional(),
11137
11406
  speed: number().optional()
11138
11407
  });
11408
+ PtzPositionSchema.extend({ autofocus: boolean() });
11409
+ const PtzOptionsSchema = object({
11410
+ hasPan: boolean(),
11411
+ hasTilt: boolean(),
11412
+ hasZoom: boolean(),
11413
+ supportsPresets: boolean(),
11414
+ /** Max number of named presets the camera supports, when known. */
11415
+ maxPresets: number().optional(),
11416
+ /** Whether the camera exposes a controllable autofocus toggle
11417
+ * (boolean `hasX` per the getOptions availability convention). */
11418
+ hasAutofocus: boolean()
11419
+ });
11139
11420
  ({
11140
11421
  deviceTypes: [DeviceType.Camera],
11141
11422
  methods: {
@@ -11163,6 +11444,20 @@ const PtzMoveCommandSchema = object({
11163
11444
  _void(),
11164
11445
  { kind: "mutation" }
11165
11446
  ),
11447
+ savePreset: method(
11448
+ object({ deviceId: number(), presetId: string(), name: string() }),
11449
+ _void(),
11450
+ { kind: "mutation", auth: "admin" }
11451
+ ),
11452
+ deletePreset: method(
11453
+ object({ deviceId: number(), presetId: string() }),
11454
+ _void(),
11455
+ { kind: "mutation", auth: "admin" }
11456
+ ),
11457
+ getOptions: method(
11458
+ object({ deviceId: number() }),
11459
+ PtzOptionsSchema
11460
+ ),
11166
11461
  goHome: method(
11167
11462
  object({ deviceId: number() }),
11168
11463
  _void(),
@@ -11177,6 +11472,13 @@ const PtzMoveCommandSchema = object({
11177
11472
  getPosition: method(
11178
11473
  object({ deviceId: number() }),
11179
11474
  PtzPositionSchema
11475
+ ),
11476
+ /** Toggle the camera's autofocus. Only meaningful when
11477
+ * `getOptions().hasAutofocus` is true. */
11478
+ setAutofocus: method(
11479
+ object({ deviceId: number(), enabled: boolean() }),
11480
+ _void(),
11481
+ { kind: "mutation" }
11180
11482
  )
11181
11483
  }
11182
11484
  });
@@ -11876,7 +12178,7 @@ const AllowedAddressesSchema = object({
11876
12178
  )
11877
12179
  }
11878
12180
  });
11879
- const MeshEndpointSchema$1 = object({
12181
+ const MeshEndpointSchema = object({
11880
12182
  /** Stable identifier within the provider (e.g. `mesh-ipv4`, `magicdns`, `funnel`). */
11881
12183
  id: string(),
11882
12184
  /** Operator-facing label (e.g. "Mesh IPv4", "MagicDNS"). */
@@ -11949,7 +12251,7 @@ const MeshStatusSchema = object({
11949
12251
  /** Number of peers visible to this host (excluding self). */
11950
12252
  peerCount: number(),
11951
12253
  /** Every endpoint this provider exposes for the current host. */
11952
- endpoints: array(MeshEndpointSchema$1).readonly(),
12254
+ endpoints: array(MeshEndpointSchema).readonly(),
11953
12255
  /** Last error from the daemon, when not joined. */
11954
12256
  error: string().optional(),
11955
12257
  // ── Account / tenant identity (generic across providers) ────────
@@ -12112,182 +12414,6 @@ const MeshStatusSchema = object({
12112
12414
  // tabs driven by this cap.
12113
12415
  }
12114
12416
  });
12115
- const MeshEndpointSchema = object({
12116
- id: string(),
12117
- label: string(),
12118
- scope: _enum(["mesh", "public"]),
12119
- url: string(),
12120
- hostname: string(),
12121
- port: number(),
12122
- protocol: _enum(["http", "https"])
12123
- });
12124
- const MeshProviderInfoSchema = object({
12125
- /** Stable id matching the addon id. */
12126
- addonId: string(),
12127
- /** Display label shown on the admin row — sourced from the addon manifest. */
12128
- displayName: string(),
12129
- /** True when the host is joined to this provider's mesh. */
12130
- joined: boolean(),
12131
- /** Local mesh IP (empty when not joined). */
12132
- meshIp: string(),
12133
- /** MagicDNS / mesh hostname (empty when not configured). */
12134
- magicDnsHostname: string(),
12135
- /** Peer count (excluding self). */
12136
- peerCount: number(),
12137
- /** Active endpoints (mesh IP + MagicDNS + optional public Funnel). */
12138
- endpoints: array(MeshEndpointSchema).readonly(),
12139
- /** Last error reported by the provider. */
12140
- error: string().optional(),
12141
- // ── Generic identity fields mirrored from MeshStatus ─────────────
12142
- /** Tenant / tailnet / network display name. Empty pre-join. */
12143
- tenantName: string(),
12144
- /** Mesh DNS suffix (e.g. tailXXXX.ts.net). Empty when not configured. */
12145
- magicDnsSuffix: string(),
12146
- /** Authenticated user / account login. Null for token-only providers. */
12147
- userLogin: string().nullable(),
12148
- /** Provider control-plane URL. */
12149
- controlPlaneUrl: string(),
12150
- /** Machine-key expiry (epoch ms). Null when keys don't rotate. */
12151
- keyExpiry: number().nullable()
12152
- });
12153
- ({
12154
- methods: {
12155
- /** All registered mesh-network providers + live status. */
12156
- listProviders: method(_void(), array(MeshProviderInfoSchema).readonly()),
12157
- /**
12158
- * Join the mesh of a specific provider. Per-provider config still
12159
- * lives on its settings panel; the orchestrator forwards.
12160
- */
12161
- joinProvider: method(
12162
- object({
12163
- addonId: string(),
12164
- authKey: string().min(8),
12165
- hostname: string().optional()
12166
- }),
12167
- object({ joined: literal(true) }),
12168
- { kind: "mutation" }
12169
- ),
12170
- leaveProvider: method(
12171
- object({ addonId: string() }),
12172
- object({ success: literal(true) }),
12173
- { kind: "mutation" }
12174
- ),
12175
- /**
12176
- * Browser-redirect login flow. Forwards to the named provider's
12177
- * `mesh-network.startLogin` and returns the URL the daemon
12178
- * prints. UI opens it in a new tab, then polls `listProviders`
12179
- * for `joined: true`.
12180
- */
12181
- startLoginProvider: method(
12182
- object({
12183
- addonId: string(),
12184
- hostname: string().optional()
12185
- }),
12186
- object({ loginUrl: string() }),
12187
- { kind: "mutation" }
12188
- ),
12189
- /**
12190
- * Sign out of the provider's account entirely (`mesh-network.logout`).
12191
- * Distinct from `leaveProvider` which only takes the host off-mesh;
12192
- * `logoutProvider` wipes credentials so the next start requires a
12193
- * fresh login.
12194
- */
12195
- logoutProvider: method(
12196
- object({ addonId: string() }),
12197
- object({ loggedOut: literal(true) }),
12198
- { kind: "mutation" }
12199
- ),
12200
- /**
12201
- * Per-provider peer list. Forwards to `mesh-network.listPeers` on
12202
- * the addressed provider. Separate from `listProviders` because
12203
- * peer payloads can be large on a heavily-populated tailnet —
12204
- * fetch only when the operator opens the Peers tab.
12205
- */
12206
- listProviderPeers: method(
12207
- object({ addonId: string() }),
12208
- object({
12209
- peers: array(MeshPeerSchema).readonly()
12210
- })
12211
- )
12212
- }
12213
- });
12214
- const MethodAccessSchema = _enum(["view", "create", "delete"]);
12215
- const AllowedProviderSchema = union([literal("*"), array(string())]);
12216
- const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
12217
- const CapScopeSchema = _enum(["device", "system"]);
12218
- const TokenScopeSchema = discriminatedUnion("type", [
12219
- object({
12220
- type: literal("category"),
12221
- target: CapScopeSchema,
12222
- access: array(MethodAccessSchema).min(1)
12223
- }),
12224
- object({
12225
- type: literal("capability"),
12226
- target: string(),
12227
- access: array(MethodAccessSchema).min(1)
12228
- }),
12229
- object({
12230
- type: literal("addon"),
12231
- target: string(),
12232
- access: array(MethodAccessSchema).min(1)
12233
- }),
12234
- object({
12235
- type: literal("device"),
12236
- /**
12237
- * One or more deviceIds (serialised as strings for wire-format
12238
- * consistency with the rest of the union). Matcher accepts if
12239
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
12240
- * of one scope-per-device when granting access to a set of cameras.
12241
- */
12242
- targets: array(string()).min(1),
12243
- access: array(MethodAccessSchema).min(1)
12244
- })
12245
- ]);
12246
- object({
12247
- id: string(),
12248
- username: string(),
12249
- passwordHash: string(),
12250
- /**
12251
- * Admin bypass. When true, the middleware skips the scope-access
12252
- * check entirely. There is no other axis of privilege; the legacy
12253
- * role enum collapsed onto this boolean in v2.
12254
- */
12255
- isAdmin: boolean().default(false),
12256
- allowedProviders: AllowedProviderSchema,
12257
- allowedDevices: AllowedDevicesSchema,
12258
- /**
12259
- * Scopes granted to this user. Admins bypass; their `scopes` is
12260
- * ignored. Non-admins without scopes are locked out of every
12261
- * protected call.
12262
- */
12263
- scopes: array(TokenScopeSchema).default([]),
12264
- createdAt: number(),
12265
- updatedAt: number()
12266
- });
12267
- object({
12268
- id: string(),
12269
- label: string(),
12270
- isAdmin: boolean().default(false),
12271
- allowedProviders: AllowedProviderSchema,
12272
- allowedDevices: AllowedDevicesSchema,
12273
- tokenHash: string(),
12274
- tokenPrefix: string(),
12275
- createdAt: number(),
12276
- lastUsedAt: number().optional()
12277
- });
12278
- object({
12279
- id: string(),
12280
- userId: string(),
12281
- name: string(),
12282
- tokenHash: string(),
12283
- tokenPrefix: string(),
12284
- scopes: array(TokenScopeSchema),
12285
- // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
12286
- // schema accepts both `null` (read from disk) and `undefined` (in-memory).
12287
- expiresAt: number().nullish(),
12288
- lastUsedAt: number().nullish(),
12289
- createdAt: number()
12290
- });
12291
12417
  const UserSummarySchema = object({
12292
12418
  id: string(),
12293
12419
  username: string(),
@@ -12360,6 +12486,16 @@ const CreateScopedTokenResultSchema = object({
12360
12486
  token: string(),
12361
12487
  record: ScopedTokenSummarySchema
12362
12488
  });
12489
+ const OauthSessionSummarySchema = object({
12490
+ id: string(),
12491
+ userId: string(),
12492
+ username: string(),
12493
+ integrationId: string(),
12494
+ scopes: array(TokenScopeSchema),
12495
+ createdAt: number(),
12496
+ lastUsedAt: number(),
12497
+ revokedAt: number().nullable()
12498
+ });
12363
12499
  const TotpSetupResultSchema = object({
12364
12500
  secret: string(),
12365
12501
  otpauthUrl: string()
@@ -12435,6 +12571,66 @@ const TotpStatusSchema = object({
12435
12571
  object({ userId: string(), code: string() }),
12436
12572
  object({ valid: boolean() }),
12437
12573
  { kind: "mutation", access: "view" }
12574
+ ),
12575
+ // ── OAuth account-linking grant ────────────────────────────────
12576
+ //
12577
+ // Core's /oauth2/* endpoints delegate here. Tokens are sso-bridge
12578
+ // JWTs (kinds oauth-code / oauth-access / oauth-refresh) and ALWAYS
12579
+ // carry isAdmin:false — the operator login proves hub control; the
12580
+ // issued token is minimal (device scope only).
12581
+ oauthIssueCode: method(
12582
+ object({
12583
+ integrationId: string(),
12584
+ userId: string(),
12585
+ username: string(),
12586
+ scopes: array(TokenScopeSchema),
12587
+ redirectUri: string(),
12588
+ hubUrl: string()
12589
+ }),
12590
+ object({ code: string() }),
12591
+ { kind: "mutation", access: "create" }
12592
+ ),
12593
+ oauthExchangeCode: method(
12594
+ object({ code: string(), redirectUri: string() }),
12595
+ object({
12596
+ accessToken: string(),
12597
+ refreshToken: string(),
12598
+ expiresIn: number()
12599
+ }).nullable(),
12600
+ { kind: "mutation", access: "view" }
12601
+ ),
12602
+ oauthRefresh: method(
12603
+ object({ refreshToken: string() }),
12604
+ object({
12605
+ accessToken: string(),
12606
+ refreshToken: string(),
12607
+ expiresIn: number()
12608
+ }).nullable(),
12609
+ { kind: "mutation", access: "view" }
12610
+ ),
12611
+ oauthVerifyAccessToken: method(
12612
+ object({ token: string() }),
12613
+ object({
12614
+ userId: string(),
12615
+ username: string(),
12616
+ scopes: array(TokenScopeSchema)
12617
+ }).nullable(),
12618
+ { access: "view" }
12619
+ ),
12620
+ // ── OAuth linked-session management (Phase D) ──────────────────
12621
+ //
12622
+ // The admin UI lists active account-linking sessions and revokes
12623
+ // them; revocation makes the linked integration's tokens fail
12624
+ // verification immediately.
12625
+ listOauthSessions: method(
12626
+ _void(),
12627
+ array(OauthSessionSummarySchema),
12628
+ { auth: "admin" }
12629
+ ),
12630
+ revokeOauthSession: method(
12631
+ object({ id: string() }),
12632
+ object({ success: boolean() }),
12633
+ { kind: "mutation", auth: "admin", access: "delete" }
12438
12634
  )
12439
12635
  }
12440
12636
  });
@@ -12651,6 +12847,19 @@ const RenameNodeResultSchema = object({
12651
12847
  record(string(), ClusterAddonStatusEntrySchema),
12652
12848
  { auth: "admin" }
12653
12849
  ),
12850
+ getCapUsageGraph: method(
12851
+ object({
12852
+ windowSeconds: number().int().positive().max(300).default(60)
12853
+ }),
12854
+ array(object({
12855
+ callerAddonId: string(),
12856
+ providerAddonId: string(),
12857
+ capName: string(),
12858
+ callsPerMin: number(),
12859
+ lastCallAtMs: number()
12860
+ })).readonly(),
12861
+ { auth: "admin" }
12862
+ ),
12654
12863
  /**
12655
12864
  * Direct per-node addon listing — calls `$agent.status` on the target
12656
12865
  * node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
@@ -13080,6 +13289,29 @@ const CustomActionInputSchema = object({
13080
13289
  isActive: boolean()
13081
13290
  })).readonly()
13082
13291
  ),
13292
+ /**
13293
+ * Toggle a single collection-cap provider on/off. Generic write-side
13294
+ * counterpart of `listCapabilityProviders` — drives the per-provider
13295
+ * Enable/Disable affordance in admin pages (TURN servers, etc.)
13296
+ * without needing a bespoke orchestrator cap.
13297
+ *
13298
+ * Reaches the hub's `CapabilityRegistry` directly:
13299
+ * `enableCollectionProvider` / `disableCollectionProvider` flip the
13300
+ * registry-level `disabledProviders` set. `getCollectionEntries`
13301
+ * already filters disabled providers out, so a disabled provider
13302
+ * drops out of every collection aggregate immediately. Only valid
13303
+ * for `mode: 'collection'` caps — the registry no-ops + warns for
13304
+ * singletons.
13305
+ */
13306
+ setCapabilityProviderEnabled: method(
13307
+ object({
13308
+ capName: string().min(1),
13309
+ addonId: string().min(1),
13310
+ enabled: boolean()
13311
+ }),
13312
+ object({ success: literal(true) }),
13313
+ { kind: "mutation", auth: "admin" }
13314
+ ),
13083
13315
  /**
13084
13316
  * Live-update one of the framework packages marked
13085
13317
  * `camstack.system: true` (`@camstack/types|kernel|core|sdk|ui-library`).
@@ -13725,4 +13957,4 @@ exports.recordingEngineCapability = recordingEngineCapability;
13725
13957
  exports.string = string;
13726
13958
  exports.tuple = tuple;
13727
13959
  exports.zoneAnalyticsCapability = zoneAnalyticsCapability;
13728
- //# sourceMappingURL=index-DjIIGJS2.js.map
13960
+ //# sourceMappingURL=index-Dbx13pc7.js.map