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