@camstack/addon-advanced-notifier 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -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
  const YAMNET_TO_MACRO = {
5227
5243
  mapping: {
@@ -5951,6 +5967,53 @@ const DecodedFrameSchema = object({
5951
5967
  format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5952
5968
  timestamp: number()
5953
5969
  });
5970
+ const FrameHandleSchema = object({
5971
+ shmId: string(),
5972
+ slot: number().int().nonnegative(),
5973
+ seq: number().int().nonnegative(),
5974
+ width: number().int().positive(),
5975
+ height: number().int().positive(),
5976
+ format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5977
+ pts: number(),
5978
+ byteLength: number().int().nonnegative(),
5979
+ nodeId: string(),
5980
+ slotCount: number().int().positive()
5981
+ });
5982
+ const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
5983
+ const SubscribeFramesInputSchema = object({
5984
+ brokerId: string(),
5985
+ format: FrameHandleFormatSchema,
5986
+ /**
5987
+ * Optional reader-side cadence hint in frames per second. The broker does
5988
+ * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
5989
+ * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
5990
+ * the consumer can pace its own `pullFrameHandles` polling.
5991
+ */
5992
+ maxFps: number().positive().optional(),
5993
+ /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
5994
+ tag: string().optional()
5995
+ });
5996
+ const SubscribeFramesResultSchema = object({
5997
+ /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
5998
+ subscriptionId: string(),
5999
+ /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
6000
+ maxFps: number().nonnegative()
6001
+ });
6002
+ const DecodedAudioChunkSchema = object({
6003
+ data: _instanceof(Uint8Array),
6004
+ sampleRate: number().int().positive(),
6005
+ channels: number().int().positive(),
6006
+ timestamp: number()
6007
+ });
6008
+ const SubscribeAudioChunksInputSchema = object({
6009
+ brokerId: string(),
6010
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6011
+ tag: string().optional()
6012
+ });
6013
+ const SubscribeAudioChunksResultSchema = object({
6014
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
6015
+ subscriptionId: string()
6016
+ });
5954
6017
  const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
5955
6018
  const BrokerStatsSchema = object({
5956
6019
  status: BrokerStatusSchema$1,
@@ -6172,9 +6235,76 @@ const RtpSourceSchema = object({
6172
6235
  object({ released: boolean(), refcount: number().int().nonnegative() }),
6173
6236
  { kind: "mutation", auth: "admin" }
6174
6237
  ),
6175
- getBroker: method(
6176
- object({ brokerId: string() }),
6177
- custom()
6238
+ /**
6239
+ * ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
6240
+ *
6241
+ * The serialisable replacement for the live-object `IStreamBroker.
6242
+ * onDecodedAudioChunk` callback path. Unlike the video frame plane,
6243
+ * audio chunks are tiny (a ~500ms PCM window is a few KB) so they ship
6244
+ * their bytes INLINE over tRPC — no shared-memory ring. A consumer:
6245
+ *
6246
+ * 1. `subscribeAudioChunks({ brokerId, tag })` — the broker registers
6247
+ * a per-subscription bounded FIFO queue and returns a
6248
+ * `subscriptionId`.
6249
+ * 2. polls `pullAudioChunks({ subscriptionId, maxCount })` — drains
6250
+ * `DecodedAudioChunk[]` in arrival order (FIFO, no latest-wins
6251
+ * drop: an audio gap is audible / breaks an analysis window). The
6252
+ * queue is generously sized; it only drops its oldest chunk if a
6253
+ * truly stalled consumer lets it overflow.
6254
+ * 3. `unsubscribeAudioChunks({ subscriptionId })` on teardown.
6255
+ */
6256
+ subscribeAudioChunks: method(
6257
+ SubscribeAudioChunksInputSchema,
6258
+ SubscribeAudioChunksResultSchema,
6259
+ { kind: "mutation" }
6260
+ ),
6261
+ pullAudioChunks: method(
6262
+ object({
6263
+ subscriptionId: string(),
6264
+ maxCount: number().int().positive().default(8)
6265
+ }),
6266
+ array(DecodedAudioChunkSchema).readonly()
6267
+ ),
6268
+ unsubscribeAudioChunks: method(
6269
+ object({ subscriptionId: string() }),
6270
+ object({ released: boolean() }),
6271
+ { kind: "mutation" }
6272
+ ),
6273
+ /**
6274
+ * ── Shared-memory frame plane (Phase 5 / D9) ──────────────────────
6275
+ *
6276
+ * The handle-based replacement for the live-object `IStreamBroker.
6277
+ * onDecodedFrame` callback path. A consumer:
6278
+ *
6279
+ * 1. `subscribeFrames({ brokerId, format })` — the broker spins up
6280
+ * (or reuses) a `frameSink: 'shm'` decoder session producing that
6281
+ * `format` and returns a `subscriptionId`.
6282
+ * 2. polls `pullFrameHandles({ subscriptionId, maxCount })` — drains
6283
+ * zero-pixel `FrameHandle[]`; each handle is fed to a
6284
+ * `FrameRingReader` that opens the named shm segment and reads the
6285
+ * pixels back zero-copy.
6286
+ * 3. `unsubscribeFrames({ subscriptionId })` on teardown.
6287
+ *
6288
+ * The broker keeps one shm ring per `(brokerId, format)` actually
6289
+ * requested — no broker-side `sharp` conversion. fps throttling is
6290
+ * implicit (latest-wins ring reads drop frames for a slow consumer).
6291
+ */
6292
+ subscribeFrames: method(
6293
+ SubscribeFramesInputSchema,
6294
+ SubscribeFramesResultSchema,
6295
+ { kind: "mutation" }
6296
+ ),
6297
+ pullFrameHandles: method(
6298
+ object({
6299
+ subscriptionId: string(),
6300
+ maxCount: number().int().positive().default(4)
6301
+ }),
6302
+ array(FrameHandleSchema).readonly()
6303
+ ),
6304
+ unsubscribeFrames: method(
6305
+ object({ subscriptionId: string() }),
6306
+ object({ released: boolean() }),
6307
+ { kind: "mutation" }
6178
6308
  ),
6179
6309
  setPreBufferDuration: method(
6180
6310
  object({ brokerId: string(), seconds: number().min(0).max(30) }),
@@ -7252,6 +7382,34 @@ MotionTriggerStatusSchema.extend({
7252
7382
  }) }
7253
7383
  }
7254
7384
  });
7385
+ object({
7386
+ enabled: boolean(),
7387
+ sensitivity: number(),
7388
+ /** Row-major active-cell grid. Length = gridWidth*gridHeight (see getOptions). */
7389
+ cells: array(boolean()),
7390
+ lastFetchedAt: number()
7391
+ });
7392
+ const MotionZoneOptionsSchema = object({
7393
+ gridWidth: number(),
7394
+ gridHeight: number(),
7395
+ sensitivity: object({ min: number(), max: number(), step: number() })
7396
+ });
7397
+ const MotionZonePatchSchema = object({
7398
+ enabled: boolean().optional(),
7399
+ sensitivity: number().optional(),
7400
+ cells: array(boolean()).optional()
7401
+ });
7402
+ ({
7403
+ deviceTypes: [DeviceType.Camera],
7404
+ methods: {
7405
+ getOptions: method(object({ deviceId: number() }), MotionZoneOptionsSchema),
7406
+ setZone: method(
7407
+ object({ deviceId: number(), patch: MotionZonePatchSchema }),
7408
+ _void(),
7409
+ { kind: "mutation", auth: "admin" }
7410
+ )
7411
+ }
7412
+ });
7255
7413
  const AutotrackTargetTypeSchema = string().describe("Vendor target string (people/vehicle/pet); empty = camera default");
7256
7414
  const PtzAutotrackSettingsSchema = object({
7257
7415
  targetType: AutotrackTargetTypeSchema,
@@ -7340,6 +7498,100 @@ PtzAutotrackStatusSchema.extend({
7340
7498
  }) }
7341
7499
  }
7342
7500
  });
7501
+ const StreamProfileSchema = _enum(["main", "sub", "ext"]);
7502
+ const StreamProfileConfigSchema = object({
7503
+ width: number(),
7504
+ height: number(),
7505
+ codec: _enum(["h264", "h265"]),
7506
+ framerate: number(),
7507
+ bitrate: number(),
7508
+ // kbps
7509
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7510
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7511
+ gop: number().optional(),
7512
+ audio: boolean().optional()
7513
+ });
7514
+ object({
7515
+ /** Per-profile current config. A profile absent = the camera doesn't have it. */
7516
+ main: StreamProfileConfigSchema.optional(),
7517
+ sub: StreamProfileConfigSchema.optional(),
7518
+ ext: StreamProfileConfigSchema.optional(),
7519
+ lastFetchedAt: number()
7520
+ });
7521
+ const StreamProfileOptionsSchema = object({
7522
+ resolutions: array(object({ width: number(), height: number() })),
7523
+ codecs: array(_enum(["h264", "h265"])),
7524
+ framerates: array(number()),
7525
+ /** Allowed bitrate values (kbps). Empty if the camera takes a free range. */
7526
+ bitrates: array(number()),
7527
+ /** Optional [min,max] kbps when the camera accepts a continuous range. */
7528
+ bitrateRange: tuple([number(), number()]).optional(),
7529
+ supportsBitrateMode: boolean(),
7530
+ supportsEncoderProfile: boolean(),
7531
+ supportsGop: boolean(),
7532
+ /** Allowed GOP / keyframe-interval range, in seconds — drives the
7533
+ * I-frame-interval selector. Absent when the camera advertises GOP
7534
+ * support but no concrete range (callers then fall back to a free
7535
+ * numeric input). `{ min, max, step }` per the getOptions convention. */
7536
+ gop: object({ min: number(), max: number(), step: number() }).optional()
7537
+ });
7538
+ const StreamParamsOptionsSchema = object({
7539
+ main: StreamProfileOptionsSchema.optional(),
7540
+ sub: StreamProfileOptionsSchema.optional(),
7541
+ ext: StreamProfileOptionsSchema.optional()
7542
+ });
7543
+ const StreamProfilePatchSchema = object({
7544
+ width: number().optional(),
7545
+ height: number().optional(),
7546
+ codec: _enum(["h264", "h265"]).optional(),
7547
+ framerate: number().optional(),
7548
+ bitrate: number().optional(),
7549
+ bitrateMode: _enum(["vbr", "cbr"]).optional(),
7550
+ encoderProfile: _enum(["high", "main", "baseline"]).optional(),
7551
+ gop: number().optional(),
7552
+ audio: boolean().optional()
7553
+ });
7554
+ ({
7555
+ deviceTypes: [DeviceType.Camera],
7556
+ methods: {
7557
+ getOptions: method(
7558
+ object({ deviceId: number() }),
7559
+ StreamParamsOptionsSchema
7560
+ ),
7561
+ setProfile: method(
7562
+ object({
7563
+ deviceId: number(),
7564
+ profile: StreamProfileSchema,
7565
+ patch: StreamProfilePatchSchema
7566
+ }),
7567
+ _void(),
7568
+ { kind: "mutation", auth: "admin" }
7569
+ ),
7570
+ /**
7571
+ * Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
7572
+ * shape) for this camera's stream-encoder settings — one section per
7573
+ * profile (main / sub / ext) with the resolution / codec / framerate
7574
+ * / bitrate / bitrate-mode / encoder-profile / GOP controls the
7575
+ * firmware actually exposes.
7576
+ *
7577
+ * Driven by `getOptions` (camera-probed availability) + `getStatus`
7578
+ * (current per-profile config); each field's `default` is seeded
7579
+ * from the live config so the form renders the camera state in one
7580
+ * pass. Returns `null` when the camera exposes no configurable
7581
+ * stream property — the renderer then shows the unsupported message.
7582
+ *
7583
+ * Output is `z.unknown().nullable()` — the same convention every
7584
+ * other `ConfigUISchema`-returning cap method uses (`device-ops`,
7585
+ * `device-manager`); `ConfigUISchema` is a TS-only type with no
7586
+ * companion Zod schema, and a concrete object would collapse
7587
+ * unrelated AppRouter branches to `unknown` during codegen.
7588
+ */
7589
+ getConfigSchema: method(
7590
+ object({ deviceId: number() }),
7591
+ unknown().nullable()
7592
+ )
7593
+ }
7594
+ });
7343
7595
  object({
7344
7596
  on: boolean(),
7345
7597
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
@@ -8277,6 +8529,83 @@ const VersionOutputSchema = object({ version: string() });
8277
8529
  getVersion: method(_void(), VersionOutputSchema)
8278
8530
  }
8279
8531
  });
8532
+ const MethodAccessSchema = _enum(["view", "create", "delete"]);
8533
+ const AllowedProviderSchema = union([literal("*"), array(string())]);
8534
+ const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
8535
+ const CapScopeSchema = _enum(["device", "system"]);
8536
+ const TokenScopeSchema = discriminatedUnion("type", [
8537
+ object({
8538
+ type: literal("category"),
8539
+ target: CapScopeSchema,
8540
+ access: array(MethodAccessSchema).min(1)
8541
+ }),
8542
+ object({
8543
+ type: literal("capability"),
8544
+ target: string(),
8545
+ access: array(MethodAccessSchema).min(1)
8546
+ }),
8547
+ object({
8548
+ type: literal("addon"),
8549
+ target: string(),
8550
+ access: array(MethodAccessSchema).min(1)
8551
+ }),
8552
+ object({
8553
+ type: literal("device"),
8554
+ /**
8555
+ * One or more deviceIds (serialised as strings for wire-format
8556
+ * consistency with the rest of the union). Matcher accepts if
8557
+ * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
8558
+ * of one scope-per-device when granting access to a set of cameras.
8559
+ */
8560
+ targets: array(string()).min(1),
8561
+ access: array(MethodAccessSchema).min(1)
8562
+ })
8563
+ ]);
8564
+ object({
8565
+ id: string(),
8566
+ username: string(),
8567
+ passwordHash: string(),
8568
+ /**
8569
+ * Admin bypass. When true, the middleware skips the scope-access
8570
+ * check entirely. There is no other axis of privilege; the legacy
8571
+ * role enum collapsed onto this boolean in v2.
8572
+ */
8573
+ isAdmin: boolean().default(false),
8574
+ allowedProviders: AllowedProviderSchema,
8575
+ allowedDevices: AllowedDevicesSchema,
8576
+ /**
8577
+ * Scopes granted to this user. Admins bypass; their `scopes` is
8578
+ * ignored. Non-admins without scopes are locked out of every
8579
+ * protected call.
8580
+ */
8581
+ scopes: array(TokenScopeSchema).default([]),
8582
+ createdAt: number(),
8583
+ updatedAt: number()
8584
+ });
8585
+ object({
8586
+ id: string(),
8587
+ label: string(),
8588
+ isAdmin: boolean().default(false),
8589
+ allowedProviders: AllowedProviderSchema,
8590
+ allowedDevices: AllowedDevicesSchema,
8591
+ tokenHash: string(),
8592
+ tokenPrefix: string(),
8593
+ createdAt: number(),
8594
+ lastUsedAt: number().optional()
8595
+ });
8596
+ object({
8597
+ id: string(),
8598
+ userId: string(),
8599
+ name: string(),
8600
+ tokenHash: string(),
8601
+ tokenPrefix: string(),
8602
+ scopes: array(TokenScopeSchema),
8603
+ // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
8604
+ // schema accepts both `null` (read from disk) and `undefined` (in-memory).
8605
+ expiresAt: number().nullish(),
8606
+ lastUsedAt: number().nullish(),
8607
+ createdAt: number()
8608
+ });
8280
8609
  const SsoBridgeClaimsSchema = object({
8281
8610
  userId: string(),
8282
8611
  username: string(),
@@ -8292,7 +8621,18 @@ const SsoBridgeClaimsSchema = object({
8292
8621
  * JWT WITHOUT verifying the signature — the hub re-verifies on every
8293
8622
  * inbound call so trust still rests with the signing hub.
8294
8623
  */
8295
- hubUrl: string().optional()
8624
+ hubUrl: string().optional(),
8625
+ /** Permission scopes baked into the token. Set by the OAuth
8626
+ * account-linking grant; absent on ordinary SSO-login tokens. */
8627
+ scopes: array(TokenScopeSchema).optional(),
8628
+ /** OAuth authorization-code binding — set only on `oauth-code` tokens. */
8629
+ redirectUri: string().optional(),
8630
+ integrationId: string().optional(),
8631
+ /** JWT ID — unique per issued code; consumed-set enforces single-use. */
8632
+ jti: string().optional(),
8633
+ /** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
8634
+ * tokens so the verify path can check the session is not revoked. */
8635
+ sessionId: string().optional()
8296
8636
  });
8297
8637
  ({
8298
8638
  methods: {
@@ -8309,6 +8649,23 @@ const SsoBridgeClaimsSchema = object({
8309
8649
  )
8310
8650
  }
8311
8651
  });
8652
+ const OauthIntegrationDescriptorSchema = object({
8653
+ /** Stable id used as the `integration=` query param, e.g. 'export-alexa'. */
8654
+ integrationId: string(),
8655
+ /** Human label rendered on the consent page. */
8656
+ displayName: string(),
8657
+ /** Scopes baked into every token issued for this integration. */
8658
+ requestedScopes: array(TokenScopeSchema),
8659
+ /** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
8660
+ * redirect_uri that does not start with one of these. Required —
8661
+ * an empty list means the integration can never complete linking. */
8662
+ allowedRedirectPrefixes: array(string()).min(1)
8663
+ });
8664
+ ({
8665
+ methods: {
8666
+ getDescriptor: method(_void(), OauthIntegrationDescriptorSchema)
8667
+ }
8668
+ });
8312
8669
  const PasskeySummarySchema = object({
8313
8670
  credentialId: string(),
8314
8671
  label: string(),
@@ -8589,21 +8946,30 @@ const AddonPageDeclarationSchema = object({
8589
8946
  });
8590
8947
  const WidgetHostEnum = _enum(["device-tab", "dashboard", "integration-detail"]);
8591
8948
  const WidgetSizeEnum = _enum(["xs", "sm", "md", "lg", "xl"]);
8949
+ const WidgetRemoteSchema = object({
8950
+ remoteName: string(),
8951
+ exposedModule: string(),
8952
+ componentKey: string().optional()
8953
+ });
8592
8954
  const WidgetMetadataSchema = object({
8593
- /** Stable id within the addon — kebab-case. */
8594
- stableId: string(),
8955
+ // ── UiContribution core (kind:'remote') ──────────────────────────
8956
+ /** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
8957
+ tab: string(),
8958
+ /** Optional sub-tab within `tab`. */
8959
+ subTab: string().optional(),
8595
8960
  /** Operator-facing label. */
8596
8961
  label: string(),
8962
+ /** Ordering within `(tab, subTab)`, ascending. */
8963
+ order: number().optional(),
8964
+ /** Always `'remote'` — a widget is a Module Federation remote. */
8965
+ kind: literal("remote"),
8966
+ /** MF remote descriptor. */
8967
+ remote: WidgetRemoteSchema,
8968
+ // ── Widget-only metadata ─────────────────────────────────────────
8969
+ /** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
8970
+ stableId: string(),
8597
8971
  description: string().optional(),
8598
8972
  icon: string().optional(),
8599
- /**
8600
- * Module Federation remote name — must match the `name` field on the
8601
- * widget addon's `federation()` plugin config. Used by the host's
8602
- * `<WidgetRegistryProvider>` to call `loadRemote('<remoteName>/widgets')`.
8603
- * Conventionally `addon_<addonid>_widgets` (snake_case; MF names
8604
- * cannot contain hyphens).
8605
- */
8606
- remoteName: string(),
8607
8973
  /**
8608
8974
  * Bundle filename inside the addon's `dist/` dir served at
8609
8975
  * `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
@@ -8612,9 +8978,9 @@ const WidgetMetadataSchema = object({
8612
8978
  * cache-buster URL without a separate filesystem stat.
8613
8979
  */
8614
8980
  bundle: string(),
8615
- /** Where the widget makes sense to render. */
8981
+ /** Every host the widget supports. The picker filters on this set. */
8616
8982
  hosts: array(WidgetHostEnum).readonly(),
8617
- /** Required props the host must supply. Validated at <WidgetSlot> mount. */
8983
+ /** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
8618
8984
  requires: object({
8619
8985
  deviceContext: boolean().default(false),
8620
8986
  integrationContext: boolean().default(false)
@@ -8685,6 +9051,16 @@ const InvokeReplyEnvelopeSchema = object({
8685
9051
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
8686
9052
  }
8687
9053
  });
9054
+ const ShmRingStatsSchema = object({
9055
+ sessionId: string(),
9056
+ slotCount: number().int(),
9057
+ slotByteLength: number().int(),
9058
+ segmentBytes: number().int(),
9059
+ budgetMb: number().int(),
9060
+ framesWritten: number().int(),
9061
+ getFrameHits: number().int(),
9062
+ getFrameMisses: number().int()
9063
+ });
8688
9064
  ({
8689
9065
  methods: {
8690
9066
  // ── Discovery ─────────────────────────────────────────────────
@@ -8712,10 +9088,27 @@ const InvokeReplyEnvelopeSchema = object({
8712
9088
  url: string()
8713
9089
  }), _void()),
8714
9090
  // ── Output — polling-based frame retrieval ────────────────────
9091
+ // `pullFrames` drains the pixel `DecodedFrame[]` of a `frameSink:
9092
+ // 'callback'` session. `pullHandles` (Phase 5 / D9) drains the
9093
+ // zero-pixel `FrameHandle[]` of a `frameSink: 'shm'` session — the
9094
+ // broker hands each handle to a `FrameRingReader` that opens the
9095
+ // named segment and reads the pixels back zero-copy. A session is
9096
+ // one mode or the other; the unmatched method returns an empty
9097
+ // array.
8715
9098
  pullFrames: method(object({
8716
9099
  sessionId: string(),
8717
9100
  maxCount: number().default(1)
8718
9101
  }), array(DecodedFrameSchema)),
9102
+ pullHandles: method(object({
9103
+ sessionId: string(),
9104
+ maxCount: number().default(1)
9105
+ }), array(FrameHandleSchema)),
9106
+ // ── Frame fetch (Phase 5 / D9 downstream access) ──────────────
9107
+ // Read the pixels a FrameHandle refers to from this node's shm ring.
9108
+ // Returns null when the slot was already recycled (latest-wins).
9109
+ getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
9110
+ // shm ring usage stats for a session.
9111
+ getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
8719
9112
  // ── Control ───────────────────────────────────────────────────
8720
9113
  updateConfig: method(object({
8721
9114
  sessionId: string(),
@@ -9883,8 +10276,8 @@ const DevicePersistConfigPayloadSchema = object({
9883
10276
  /**
9884
10277
  * Return the addon ids that declared a wrapper provider for `capName`.
9885
10278
  * Backs the device-bindings UI's wrapper-picker dropdown. Entries are
9886
- * sourced from `CapabilityRegistry.wrapperProviders`, populated at
9887
- * `ProviderRegistration.kind === 'wrapper'` time.
10279
+ * sourced from `CapabilityRegistry.wrapperProviders`, populated when
10280
+ * the cap definition declares `kind: 'wrapper'`.
9888
10281
  */
9889
10282
  listWrappersForCap: method(
9890
10283
  object({ capName: string() }),
@@ -10190,51 +10583,6 @@ const AuthResultSchema = object({
10190
10583
  validateToken: method(object({ token: string() }), AuthResultSchema.nullable())
10191
10584
  }
10192
10585
  });
10193
- const AuthProviderInfoSchema = object({
10194
- /** Stable id matching the addon id (used for `getLoginUrl({addonId,…})`). */
10195
- addonId: string(),
10196
- /**
10197
- * Per-instance id when one addon registers multiple "logical"
10198
- * providers (e.g. OIDC with Google + Microsoft + custom). The login
10199
- * URL becomes `/addon/${addonId}/${instanceId}/start` — handler reads
10200
- * `:instanceId` from the route. Empty/unset means the addon is a
10201
- * single-instance provider; the URL is `/addon/${addonId}/start`.
10202
- */
10203
- instanceId: string().optional(),
10204
- /** Display label shown on the login button + admin row. */
10205
- displayName: string(),
10206
- /** Optional iconography hint (lucide-react icon name OR emoji). */
10207
- icon: string().optional(),
10208
- /** When true, the provider exposes a redirect-based login flow
10209
- * (`getLoginUrl` returns a URL the browser navigates to). */
10210
- hasRedirectFlow: boolean(),
10211
- /** When true, the provider exposes a credential-form login flow
10212
- * (`validateCredentials` accepts username + password). */
10213
- hasCredentialFlow: boolean(),
10214
- /** Provider kind, drives admin-UI hint dispatch (oidc / saml / totp / …). */
10215
- kind: string().optional(),
10216
- /** Operator-facing status string (e.g. "Connected to https://login.acme.com"). */
10217
- status: string().optional(),
10218
- /** When false, the provider is registered but disabled by config; the
10219
- * UI surfaces it as inactive without enumerating it for login. */
10220
- enabled: boolean()
10221
- });
10222
- ({
10223
- methods: {
10224
- /** All registered auth providers, both enabled and disabled. */
10225
- listProviders: method(_void(), array(AuthProviderInfoSchema).readonly()),
10226
- /**
10227
- * Toggle a provider's enabled flag. Disabled providers stay
10228
- * registered but aren't surfaced on the login page. The orchestrator
10229
- * persists the state in `addon-settings` so it survives restarts.
10230
- */
10231
- setProviderEnabled: method(
10232
- object({ addonId: string(), enabled: boolean() }),
10233
- object({ success: literal(true) }),
10234
- { kind: "mutation", auth: "admin" }
10235
- )
10236
- }
10237
- });
10238
10586
  const NetworkEndpointSchema = object({
10239
10587
  url: string(),
10240
10588
  hostname: string(),
@@ -10266,55 +10614,13 @@ const NetworkEndpointEntrySchema = NetworkEndpointSchema.extend({
10266
10614
  getEndpoint: method(_void(), NetworkEndpointSchema.nullable()),
10267
10615
  getStatus: method(_void(), NetworkAccessStatusSchema),
10268
10616
  /**
10269
- * Enumerate every active ingress entry. Default implementation (when
10270
- * the provider omits this method) is derived from `getEndpoint()` —
10271
- * see the remote-access orchestrator for the fallback path.
10617
+ * Enumerate every active ingress entry. Providers that expose only a
10618
+ * single endpoint may omit this method; callers fall back to
10619
+ * `getEndpoint()` in that case.
10272
10620
  */
10273
10621
  listEndpoints: method(_void(), array(NetworkEndpointEntrySchema).readonly())
10274
10622
  }
10275
10623
  });
10276
- const RemoteAccessEndpointSchema = object({
10277
- url: string(),
10278
- hostname: string(),
10279
- port: number(),
10280
- protocol: _enum(["http", "https"])
10281
- });
10282
- const RemoteAccessProviderInfoSchema = object({
10283
- /** Stable id matching the addon id. */
10284
- addonId: string(),
10285
- /** Display label shown on the admin row — sourced from the addon manifest. */
10286
- displayName: string(),
10287
- /** When false, the provider is registered but disabled. */
10288
- enabled: boolean(),
10289
- /** True when the underlying tunnel/connection is up. */
10290
- connected: boolean(),
10291
- /** Public-facing endpoint, when connected. Null otherwise. */
10292
- endpoint: RemoteAccessEndpointSchema.nullable(),
10293
- /** Last error message (when connected=false), if available. */
10294
- error: string().optional()
10295
- });
10296
- ({
10297
- methods: {
10298
- /** All registered remote-access providers + their live status. */
10299
- listProviders: method(_void(), array(RemoteAccessProviderInfoSchema).readonly()),
10300
- /**
10301
- * Start a specific provider's tunnel. Per-provider config still
10302
- * lives on the addon's settings panel; this is just the on/off
10303
- * trigger so the admin UI can manage the lifecycle from one place.
10304
- */
10305
- startProvider: method(
10306
- object({ addonId: string() }),
10307
- RemoteAccessEndpointSchema,
10308
- { kind: "mutation", auth: "admin" }
10309
- ),
10310
- /** Stop a specific provider's tunnel (idempotent on already-stopped). */
10311
- stopProvider: method(
10312
- object({ addonId: string() }),
10313
- object({ success: literal(true) }),
10314
- { kind: "mutation", auth: "admin" }
10315
- )
10316
- }
10317
- });
10318
10624
  const TurnServerSchema = object({
10319
10625
  /** Single URL or list of URLs (e.g. "turn:turn.example.com:3478?transport=udp"). */
10320
10626
  urls: union([string(), array(string())]),
@@ -10334,45 +10640,6 @@ const TurnServerSchema = object({
10334
10640
  )
10335
10641
  }
10336
10642
  });
10337
- const TurnProviderInfoSchema = object({
10338
- /** Stable id matching the addon id. */
10339
- addonId: string(),
10340
- /** Display label shown on the admin row — sourced from the addon manifest. */
10341
- displayName: string(),
10342
- /** When false, the provider is registered but disabled. */
10343
- enabled: boolean(),
10344
- /** Number of servers this provider is currently exposing. */
10345
- serverCount: number(),
10346
- /**
10347
- * Flat list of every TURN/STUN URL this provider currently exposes.
10348
- * One row per URL (multi-URL ICE server entries are flattened). The
10349
- * admin UI shows this in a compact per-provider list so operators
10350
- * can verify what's actually being negotiated without having to dig
10351
- * into the combined `getAllServers` output.
10352
- */
10353
- urls: array(string()).readonly(),
10354
- /** Last fetch error (when serverCount=0 due to API failure), if any. */
10355
- error: string().optional()
10356
- });
10357
- ({
10358
- methods: {
10359
- /** All registered TURN providers + per-provider stats. */
10360
- listProviders: method(_void(), array(TurnProviderInfoSchema).readonly()),
10361
- /**
10362
- * Combined list of TURN/STUN servers from all ENABLED providers.
10363
- * Consumed by the WebRTC layer at session-creation time —
10364
- * implementations may fetch fresh short-lived credentials each
10365
- * call (e.g. Cloudflare API), so consumers SHOULD call per-session.
10366
- */
10367
- getAllServers: method(_void(), array(TurnServerSchema).readonly()),
10368
- /** Toggle a provider's enabled flag. */
10369
- setProviderEnabled: method(
10370
- object({ addonId: string(), enabled: boolean() }),
10371
- object({ success: literal(true) }),
10372
- { kind: "mutation", auth: "admin" }
10373
- )
10374
- }
10375
- });
10376
10643
  const SnapshotImageSchema = object({
10377
10644
  base64: string(),
10378
10645
  contentType: string()
@@ -11107,6 +11374,18 @@ const PtzMoveCommandSchema = object({
11107
11374
  zoom: number().optional(),
11108
11375
  speed: number().optional()
11109
11376
  });
11377
+ PtzPositionSchema.extend({ autofocus: boolean() });
11378
+ const PtzOptionsSchema = object({
11379
+ hasPan: boolean(),
11380
+ hasTilt: boolean(),
11381
+ hasZoom: boolean(),
11382
+ supportsPresets: boolean(),
11383
+ /** Max number of named presets the camera supports, when known. */
11384
+ maxPresets: number().optional(),
11385
+ /** Whether the camera exposes a controllable autofocus toggle
11386
+ * (boolean `hasX` per the getOptions availability convention). */
11387
+ hasAutofocus: boolean()
11388
+ });
11110
11389
  ({
11111
11390
  deviceTypes: [DeviceType.Camera],
11112
11391
  methods: {
@@ -11134,6 +11413,20 @@ const PtzMoveCommandSchema = object({
11134
11413
  _void(),
11135
11414
  { kind: "mutation" }
11136
11415
  ),
11416
+ savePreset: method(
11417
+ object({ deviceId: number(), presetId: string(), name: string() }),
11418
+ _void(),
11419
+ { kind: "mutation", auth: "admin" }
11420
+ ),
11421
+ deletePreset: method(
11422
+ object({ deviceId: number(), presetId: string() }),
11423
+ _void(),
11424
+ { kind: "mutation", auth: "admin" }
11425
+ ),
11426
+ getOptions: method(
11427
+ object({ deviceId: number() }),
11428
+ PtzOptionsSchema
11429
+ ),
11137
11430
  goHome: method(
11138
11431
  object({ deviceId: number() }),
11139
11432
  _void(),
@@ -11148,6 +11441,13 @@ const PtzMoveCommandSchema = object({
11148
11441
  getPosition: method(
11149
11442
  object({ deviceId: number() }),
11150
11443
  PtzPositionSchema
11444
+ ),
11445
+ /** Toggle the camera's autofocus. Only meaningful when
11446
+ * `getOptions().hasAutofocus` is true. */
11447
+ setAutofocus: method(
11448
+ object({ deviceId: number(), enabled: boolean() }),
11449
+ _void(),
11450
+ { kind: "mutation" }
11151
11451
  )
11152
11452
  }
11153
11453
  });
@@ -11847,7 +12147,7 @@ const AllowedAddressesSchema = object({
11847
12147
  )
11848
12148
  }
11849
12149
  });
11850
- const MeshEndpointSchema$1 = object({
12150
+ const MeshEndpointSchema = object({
11851
12151
  /** Stable identifier within the provider (e.g. `mesh-ipv4`, `magicdns`, `funnel`). */
11852
12152
  id: string(),
11853
12153
  /** Operator-facing label (e.g. "Mesh IPv4", "MagicDNS"). */
@@ -11920,7 +12220,7 @@ const MeshStatusSchema = object({
11920
12220
  /** Number of peers visible to this host (excluding self). */
11921
12221
  peerCount: number(),
11922
12222
  /** Every endpoint this provider exposes for the current host. */
11923
- endpoints: array(MeshEndpointSchema$1).readonly(),
12223
+ endpoints: array(MeshEndpointSchema).readonly(),
11924
12224
  /** Last error from the daemon, when not joined. */
11925
12225
  error: string().optional(),
11926
12226
  // ── Account / tenant identity (generic across providers) ────────
@@ -12083,182 +12383,6 @@ const MeshStatusSchema = object({
12083
12383
  // tabs driven by this cap.
12084
12384
  }
12085
12385
  });
12086
- const MeshEndpointSchema = object({
12087
- id: string(),
12088
- label: string(),
12089
- scope: _enum(["mesh", "public"]),
12090
- url: string(),
12091
- hostname: string(),
12092
- port: number(),
12093
- protocol: _enum(["http", "https"])
12094
- });
12095
- const MeshProviderInfoSchema = object({
12096
- /** Stable id matching the addon id. */
12097
- addonId: string(),
12098
- /** Display label shown on the admin row — sourced from the addon manifest. */
12099
- displayName: string(),
12100
- /** True when the host is joined to this provider's mesh. */
12101
- joined: boolean(),
12102
- /** Local mesh IP (empty when not joined). */
12103
- meshIp: string(),
12104
- /** MagicDNS / mesh hostname (empty when not configured). */
12105
- magicDnsHostname: string(),
12106
- /** Peer count (excluding self). */
12107
- peerCount: number(),
12108
- /** Active endpoints (mesh IP + MagicDNS + optional public Funnel). */
12109
- endpoints: array(MeshEndpointSchema).readonly(),
12110
- /** Last error reported by the provider. */
12111
- error: string().optional(),
12112
- // ── Generic identity fields mirrored from MeshStatus ─────────────
12113
- /** Tenant / tailnet / network display name. Empty pre-join. */
12114
- tenantName: string(),
12115
- /** Mesh DNS suffix (e.g. tailXXXX.ts.net). Empty when not configured. */
12116
- magicDnsSuffix: string(),
12117
- /** Authenticated user / account login. Null for token-only providers. */
12118
- userLogin: string().nullable(),
12119
- /** Provider control-plane URL. */
12120
- controlPlaneUrl: string(),
12121
- /** Machine-key expiry (epoch ms). Null when keys don't rotate. */
12122
- keyExpiry: number().nullable()
12123
- });
12124
- ({
12125
- methods: {
12126
- /** All registered mesh-network providers + live status. */
12127
- listProviders: method(_void(), array(MeshProviderInfoSchema).readonly()),
12128
- /**
12129
- * Join the mesh of a specific provider. Per-provider config still
12130
- * lives on its settings panel; the orchestrator forwards.
12131
- */
12132
- joinProvider: method(
12133
- object({
12134
- addonId: string(),
12135
- authKey: string().min(8),
12136
- hostname: string().optional()
12137
- }),
12138
- object({ joined: literal(true) }),
12139
- { kind: "mutation" }
12140
- ),
12141
- leaveProvider: method(
12142
- object({ addonId: string() }),
12143
- object({ success: literal(true) }),
12144
- { kind: "mutation" }
12145
- ),
12146
- /**
12147
- * Browser-redirect login flow. Forwards to the named provider's
12148
- * `mesh-network.startLogin` and returns the URL the daemon
12149
- * prints. UI opens it in a new tab, then polls `listProviders`
12150
- * for `joined: true`.
12151
- */
12152
- startLoginProvider: method(
12153
- object({
12154
- addonId: string(),
12155
- hostname: string().optional()
12156
- }),
12157
- object({ loginUrl: string() }),
12158
- { kind: "mutation" }
12159
- ),
12160
- /**
12161
- * Sign out of the provider's account entirely (`mesh-network.logout`).
12162
- * Distinct from `leaveProvider` which only takes the host off-mesh;
12163
- * `logoutProvider` wipes credentials so the next start requires a
12164
- * fresh login.
12165
- */
12166
- logoutProvider: method(
12167
- object({ addonId: string() }),
12168
- object({ loggedOut: literal(true) }),
12169
- { kind: "mutation" }
12170
- ),
12171
- /**
12172
- * Per-provider peer list. Forwards to `mesh-network.listPeers` on
12173
- * the addressed provider. Separate from `listProviders` because
12174
- * peer payloads can be large on a heavily-populated tailnet —
12175
- * fetch only when the operator opens the Peers tab.
12176
- */
12177
- listProviderPeers: method(
12178
- object({ addonId: string() }),
12179
- object({
12180
- peers: array(MeshPeerSchema).readonly()
12181
- })
12182
- )
12183
- }
12184
- });
12185
- const MethodAccessSchema = _enum(["view", "create", "delete"]);
12186
- const AllowedProviderSchema = union([literal("*"), array(string())]);
12187
- const AllowedDevicesSchema = record(string(), union([literal("*"), array(string())]));
12188
- const CapScopeSchema = _enum(["device", "system"]);
12189
- const TokenScopeSchema = discriminatedUnion("type", [
12190
- object({
12191
- type: literal("category"),
12192
- target: CapScopeSchema,
12193
- access: array(MethodAccessSchema).min(1)
12194
- }),
12195
- object({
12196
- type: literal("capability"),
12197
- target: string(),
12198
- access: array(MethodAccessSchema).min(1)
12199
- }),
12200
- object({
12201
- type: literal("addon"),
12202
- target: string(),
12203
- access: array(MethodAccessSchema).min(1)
12204
- }),
12205
- object({
12206
- type: literal("device"),
12207
- /**
12208
- * One or more deviceIds (serialised as strings for wire-format
12209
- * consistency with the rest of the union). Matcher accepts if
12210
- * `input.deviceId` ∈ `targets`. Array shape avoids the row-explosion
12211
- * of one scope-per-device when granting access to a set of cameras.
12212
- */
12213
- targets: array(string()).min(1),
12214
- access: array(MethodAccessSchema).min(1)
12215
- })
12216
- ]);
12217
- object({
12218
- id: string(),
12219
- username: string(),
12220
- passwordHash: string(),
12221
- /**
12222
- * Admin bypass. When true, the middleware skips the scope-access
12223
- * check entirely. There is no other axis of privilege; the legacy
12224
- * role enum collapsed onto this boolean in v2.
12225
- */
12226
- isAdmin: boolean().default(false),
12227
- allowedProviders: AllowedProviderSchema,
12228
- allowedDevices: AllowedDevicesSchema,
12229
- /**
12230
- * Scopes granted to this user. Admins bypass; their `scopes` is
12231
- * ignored. Non-admins without scopes are locked out of every
12232
- * protected call.
12233
- */
12234
- scopes: array(TokenScopeSchema).default([]),
12235
- createdAt: number(),
12236
- updatedAt: number()
12237
- });
12238
- object({
12239
- id: string(),
12240
- label: string(),
12241
- isAdmin: boolean().default(false),
12242
- allowedProviders: AllowedProviderSchema,
12243
- allowedDevices: AllowedDevicesSchema,
12244
- tokenHash: string(),
12245
- tokenPrefix: string(),
12246
- createdAt: number(),
12247
- lastUsedAt: number().optional()
12248
- });
12249
- object({
12250
- id: string(),
12251
- userId: string(),
12252
- name: string(),
12253
- tokenHash: string(),
12254
- tokenPrefix: string(),
12255
- scopes: array(TokenScopeSchema),
12256
- // SQLite/JSON storage round-trips undefined → null. Use `nullish` so the
12257
- // schema accepts both `null` (read from disk) and `undefined` (in-memory).
12258
- expiresAt: number().nullish(),
12259
- lastUsedAt: number().nullish(),
12260
- createdAt: number()
12261
- });
12262
12386
  const UserSummarySchema = object({
12263
12387
  id: string(),
12264
12388
  username: string(),
@@ -12331,6 +12455,16 @@ const CreateScopedTokenResultSchema = object({
12331
12455
  token: string(),
12332
12456
  record: ScopedTokenSummarySchema
12333
12457
  });
12458
+ const OauthSessionSummarySchema = object({
12459
+ id: string(),
12460
+ userId: string(),
12461
+ username: string(),
12462
+ integrationId: string(),
12463
+ scopes: array(TokenScopeSchema),
12464
+ createdAt: number(),
12465
+ lastUsedAt: number(),
12466
+ revokedAt: number().nullable()
12467
+ });
12334
12468
  const TotpSetupResultSchema = object({
12335
12469
  secret: string(),
12336
12470
  otpauthUrl: string()
@@ -12406,6 +12540,66 @@ const TotpStatusSchema = object({
12406
12540
  object({ userId: string(), code: string() }),
12407
12541
  object({ valid: boolean() }),
12408
12542
  { kind: "mutation", access: "view" }
12543
+ ),
12544
+ // ── OAuth account-linking grant ────────────────────────────────
12545
+ //
12546
+ // Core's /oauth2/* endpoints delegate here. Tokens are sso-bridge
12547
+ // JWTs (kinds oauth-code / oauth-access / oauth-refresh) and ALWAYS
12548
+ // carry isAdmin:false — the operator login proves hub control; the
12549
+ // issued token is minimal (device scope only).
12550
+ oauthIssueCode: method(
12551
+ object({
12552
+ integrationId: string(),
12553
+ userId: string(),
12554
+ username: string(),
12555
+ scopes: array(TokenScopeSchema),
12556
+ redirectUri: string(),
12557
+ hubUrl: string()
12558
+ }),
12559
+ object({ code: string() }),
12560
+ { kind: "mutation", access: "create" }
12561
+ ),
12562
+ oauthExchangeCode: method(
12563
+ object({ code: string(), redirectUri: string() }),
12564
+ object({
12565
+ accessToken: string(),
12566
+ refreshToken: string(),
12567
+ expiresIn: number()
12568
+ }).nullable(),
12569
+ { kind: "mutation", access: "view" }
12570
+ ),
12571
+ oauthRefresh: method(
12572
+ object({ refreshToken: string() }),
12573
+ object({
12574
+ accessToken: string(),
12575
+ refreshToken: string(),
12576
+ expiresIn: number()
12577
+ }).nullable(),
12578
+ { kind: "mutation", access: "view" }
12579
+ ),
12580
+ oauthVerifyAccessToken: method(
12581
+ object({ token: string() }),
12582
+ object({
12583
+ userId: string(),
12584
+ username: string(),
12585
+ scopes: array(TokenScopeSchema)
12586
+ }).nullable(),
12587
+ { access: "view" }
12588
+ ),
12589
+ // ── OAuth linked-session management (Phase D) ──────────────────
12590
+ //
12591
+ // The admin UI lists active account-linking sessions and revokes
12592
+ // them; revocation makes the linked integration's tokens fail
12593
+ // verification immediately.
12594
+ listOauthSessions: method(
12595
+ _void(),
12596
+ array(OauthSessionSummarySchema),
12597
+ { auth: "admin" }
12598
+ ),
12599
+ revokeOauthSession: method(
12600
+ object({ id: string() }),
12601
+ object({ success: boolean() }),
12602
+ { kind: "mutation", auth: "admin", access: "delete" }
12409
12603
  )
12410
12604
  }
12411
12605
  });
@@ -12622,6 +12816,19 @@ const RenameNodeResultSchema = object({
12622
12816
  record(string(), ClusterAddonStatusEntrySchema),
12623
12817
  { auth: "admin" }
12624
12818
  ),
12819
+ getCapUsageGraph: method(
12820
+ object({
12821
+ windowSeconds: number().int().positive().max(300).default(60)
12822
+ }),
12823
+ array(object({
12824
+ callerAddonId: string(),
12825
+ providerAddonId: string(),
12826
+ capName: string(),
12827
+ callsPerMin: number(),
12828
+ lastCallAtMs: number()
12829
+ })).readonly(),
12830
+ { auth: "admin" }
12831
+ ),
12625
12832
  /**
12626
12833
  * Direct per-node addon listing — calls `$agent.status` on the target
12627
12834
  * node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
@@ -13051,6 +13258,29 @@ const CustomActionInputSchema = object({
13051
13258
  isActive: boolean()
13052
13259
  })).readonly()
13053
13260
  ),
13261
+ /**
13262
+ * Toggle a single collection-cap provider on/off. Generic write-side
13263
+ * counterpart of `listCapabilityProviders` — drives the per-provider
13264
+ * Enable/Disable affordance in admin pages (TURN servers, etc.)
13265
+ * without needing a bespoke orchestrator cap.
13266
+ *
13267
+ * Reaches the hub's `CapabilityRegistry` directly:
13268
+ * `enableCollectionProvider` / `disableCollectionProvider` flip the
13269
+ * registry-level `disabledProviders` set. `getCollectionEntries`
13270
+ * already filters disabled providers out, so a disabled provider
13271
+ * drops out of every collection aggregate immediately. Only valid
13272
+ * for `mode: 'collection'` caps — the registry no-ops + warns for
13273
+ * singletons.
13274
+ */
13275
+ setCapabilityProviderEnabled: method(
13276
+ object({
13277
+ capName: string().min(1),
13278
+ addonId: string().min(1),
13279
+ enabled: boolean()
13280
+ }),
13281
+ object({ success: literal(true) }),
13282
+ { kind: "mutation", auth: "admin" }
13283
+ ),
13054
13284
  /**
13055
13285
  * Live-update one of the framework packages marked
13056
13286
  * `camstack.system: true` (`@camstack/types|kernel|core|sdk|ui-library`).