@camstack/system 1.2.117 → 1.2.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.d.ts +1 -1
  4. package/dist/addon-utils.js +1 -1
  5. package/dist/addon-utils.mjs +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/console-logging/index.js +1 -1
  15. package/dist/builtins/console-logging/index.mjs +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  17. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  18. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  19. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  22. package/dist/builtins/hub-forwarder/index.js +1 -1
  23. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  28. package/dist/builtins/local-network/local-network.addon.d.ts +24 -2
  29. package/dist/builtins/local-network/local-network.addon.js +63 -17
  30. package/dist/builtins/local-network/local-network.addon.mjs +62 -18
  31. package/dist/builtins/loki-logging/index.js +1 -1
  32. package/dist/builtins/loki-logging/index.mjs +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  34. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  35. package/dist/builtins/platform-probe/index.js +1 -1
  36. package/dist/builtins/platform-probe/index.mjs +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  38. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  39. package/dist/builtins/snapshot/index.js +1 -1
  40. package/dist/builtins/snapshot/index.mjs +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  43. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +3 -2
  45. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +3 -3
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  47. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  48. package/dist/builtins/system-config/system-config.addon.js +1 -1
  49. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  50. package/dist/builtins/winston-logging/index.js +1 -1
  51. package/dist/builtins/winston-logging/index.mjs +1 -1
  52. package/dist/{dist-DMEGlqg6.js → dist-CX9qXYC4.js} +247 -26
  53. package/dist/{dist-D4Vmj8iw.mjs → dist-DNbsZxNv.mjs} +247 -26
  54. package/dist/download/model-downloader.d.ts +18 -1
  55. package/dist/{file-data-plane-CuE_hBli.mjs → file-data-plane-BhKdxJgf.mjs} +49 -13
  56. package/dist/{file-data-plane-DUHPHa-Y.js → file-data-plane-DO8KbxCe.js} +49 -13
  57. package/dist/index.js +171 -10
  58. package/dist/index.mjs +170 -12
  59. package/dist/kernel/config-manager.d.ts +8 -1
  60. package/dist/kernel/index.d.ts +3 -0
  61. package/dist/kernel/isolated-builtin-phase.d.ts +21 -0
  62. package/dist/kernel/settings-door-view.d.ts +7 -0
  63. package/dist/{manifest-python-deps-CwBbX4Ut.js → manifest-python-deps-76dZLxD0.js} +11 -3
  64. package/dist/{manifest-python-deps-SGdbr9D5.mjs → manifest-python-deps-DZSNlHjU.mjs} +12 -4
  65. package/dist/{retired-settings-keys-DET4YkAU.js → retired-settings-keys-DlIuGG4o.js} +1 -1
  66. package/dist/{retired-settings-keys--aM_vGF9.mjs → retired-settings-keys-Dt0-obcq.mjs} +1 -1
  67. package/package.json +6 -2
@@ -4123,6 +4123,15 @@ var LabelDefinitionSchema = z.object({
4123
4123
  description: z.string().optional(),
4124
4124
  icon: z.string().optional()
4125
4125
  });
4126
+ var ClassMapDefinitionSchema = z.object({
4127
+ mapping: z.record(z.string(), z.enum([
4128
+ "person",
4129
+ "vehicle",
4130
+ "animal",
4131
+ "package"
4132
+ ])),
4133
+ preserveOriginal: z.boolean()
4134
+ });
4126
4135
  var MODEL_FORMATS = [
4127
4136
  "onnx",
4128
4137
  "coreml",
@@ -4206,6 +4215,12 @@ var ModelVariantGroupSchema = z.object({
4206
4215
  */
4207
4216
  resolution: z.number().int().positive().optional()
4208
4217
  });
4218
+ var ModelProviderIdSchema = z.enum([
4219
+ "camstack",
4220
+ "frigate",
4221
+ "scrypted",
4222
+ "custom"
4223
+ ]);
4209
4224
  var ModelCatalogEntrySchema = z.object({
4210
4225
  id: z.string(),
4211
4226
  name: z.string(),
@@ -4301,7 +4316,19 @@ var ModelCatalogEntrySchema = z.object({
4301
4316
  * `id` stays the source of truth for resolution/download/persistence; grouping
4302
4317
  * is a presentation overlay resolved back to an `id`.
4303
4318
  */
4304
- group: ModelVariantGroupSchema.optional()
4319
+ group: ModelVariantGroupSchema.optional(),
4320
+ /**
4321
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
4322
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
4323
+ * persisted before this field existed (`inferModelProvider` fills those).
4324
+ */
4325
+ provider: ModelProviderIdSchema.optional(),
4326
+ /**
4327
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
4328
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
4329
+ * labels already ARE the CamStack macros (Scrypted identity map).
4330
+ */
4331
+ classMap: ClassMapDefinitionSchema.optional()
4305
4332
  });
4306
4333
  var ConvertTargetSchema = z.discriminatedUnion("format", [z.object({
4307
4334
  format: z.literal("openvino"),
@@ -4330,7 +4357,8 @@ var ModelConvertMetadataSchema = z.object({
4330
4357
  "ocr",
4331
4358
  "segmentation"
4332
4359
  ]),
4333
- faceAlignment: z.boolean().optional()
4360
+ faceAlignment: z.boolean().optional(),
4361
+ classMap: ClassMapDefinitionSchema.optional()
4334
4362
  });
4335
4363
  var ConvertArtifactSchema = z.object({
4336
4364
  format: z.enum(MODEL_FORMATS),
@@ -12556,12 +12584,15 @@ var NcOccupancyConditionSchema = z.object({
12556
12584
  * there is no second switch that can disagree with the first and every rule
12557
12585
  * authored before the decision migrates for free (`audioModeOf`):
12558
12586
  *
12559
- * - **LABEL mode — `labels` present.** The rule fires on the FIRST frame the
12560
- * classifier labels with one of them. No window, no percentage:
12561
- * `hitPercent` and `samplingSeconds` are ignored, and the rule's own
12562
- * `throttle` cooldown is the only brake. The per-label confidence floor is
12563
- * the analyzer's (`classificationMinScore`, per device) a label only
12564
- * reaches this condition if the classifier was already confident enough.
12587
+ * - **LABEL mode — `labels` present.** The rule fires when `confirmHits`
12588
+ * labelled frames land inside `confirmWindowSec` (default 2 in 5 s).
12589
+ * `hitPercent` and `samplingSeconds` are still ignored a percentage of
12590
+ * frames is the wrong question for a classifier that labels 1–3 frames
12591
+ * per episode. The count window is the brake that drops a single-frame
12592
+ * false positive; the rule's own `throttle` cooldown is the other. The
12593
+ * per-label confidence floor is the analyzer's (`classificationMinScore`,
12594
+ * per device) — a label only reaches this condition if the classifier was
12595
+ * already confident enough. `confirmHits: 1` restores first-frame fire.
12565
12596
  * - **LEVEL mode — `dbThreshold` present, no labels.** The sampling window IS
12566
12597
  * the condition: at least `hitPercent`% of the samples over
12567
12598
  * `samplingSeconds` must be at or above `dbThreshold` dBFS (see
@@ -12588,14 +12619,22 @@ var NcOccupancyConditionSchema = z.object({
12588
12619
  * an operator who typed `dog` mean the same thing.
12589
12620
  */
12590
12621
  var NcAudioConditionSchema = z.object({
12591
- /** LABEL MODE: audio macro labels. Present ⇒ fires on the first labelled frame. */
12622
+ /** LABEL MODE: audio macro labels. Present ⇒ count-in-window confirm. */
12592
12623
  labels: z.array(z.string().min(1)).min(1).optional(),
12593
12624
  /** LEVEL MODE: floor in dBFS (negative-going, `0` = full scale). */
12594
12625
  dbThreshold: z.number().min(-96).max(0).optional(),
12595
12626
  /** LEVEL MODE ONLY: percentage of the window's samples that must be hits (1–100). */
12596
12627
  hitPercent: z.number().int().min(1).max(100).default(60),
12597
12628
  /** LEVEL MODE ONLY: length of the sampling window in seconds. */
12598
- samplingSeconds: z.number().int().min(1).max(300).default(10)
12629
+ samplingSeconds: z.number().int().min(1).max(300).default(10),
12630
+ /**
12631
+ * LABEL MODE: how many labelled frames must land inside
12632
+ * {@link NcAudioConditionSchema.shape.confirmWindowSec} before dispatch.
12633
+ * Absent ⇒ 2 (the matcher default). `1` is first-frame fire.
12634
+ */
12635
+ confirmHits: z.number().int().min(1).max(20).optional(),
12636
+ /** LABEL MODE: the window those frames must share, in seconds. Absent ⇒ 5. */
12637
+ confirmWindowSec: z.number().int().min(1).max(60).optional()
12599
12638
  });
12600
12639
  /**
12601
12640
  * Which zone-crossing DIRECTION a rule accepts (`ObjectEvent.zoneCrossing`).
@@ -15085,6 +15124,46 @@ var RecentTracksPageSchema = z.object({
15085
15124
  /** Cursor for the next page, or null when this page is the last. */
15086
15125
  nextCursor: z.string().nullable()
15087
15126
  });
15127
+ var LIST_GROUPS_DEFAULT_LIMIT = 40;
15128
+ var LIST_GROUPS_MAX_LIMIT = 100;
15129
+ var AnalyticsGroupRecordSchema = z.object({
15130
+ id: z.string(),
15131
+ deviceId: z.number().int(),
15132
+ openedAt: z.number().int(),
15133
+ closedAt: z.number().int(),
15134
+ timestamp: z.number().int(),
15135
+ memberCount: z.number().int(),
15136
+ memberTrackIds: z.array(z.string()).readonly(),
15137
+ className: z.string(),
15138
+ classes: z.array(z.string()).readonly(),
15139
+ /** Relative event-media path, or null when the group has no picture yet. */
15140
+ mediaUrl: z.string().nullable(),
15141
+ singleton: z.boolean()
15142
+ });
15143
+ var AnalyticsGroupMemberSchema = z.object({
15144
+ trackId: z.string(),
15145
+ deviceId: z.number().int(),
15146
+ className: z.string(),
15147
+ firstSeen: z.number().int(),
15148
+ lastSeen: z.number().int(),
15149
+ mediaUrl: z.string().nullable()
15150
+ });
15151
+ var AnalyticsGroupDetailSchema = AnalyticsGroupRecordSchema.extend({ members: z.array(AnalyticsGroupMemberSchema).readonly() });
15152
+ var ListGroupsQueryInput = z.object({
15153
+ /** Devices to merge. An empty array yields `{ groups: [], nextCursor: null }`. */
15154
+ deviceIds: z.array(z.number()),
15155
+ /** Window lower bound on `closedAt` (inclusive). */
15156
+ since: z.number().optional(),
15157
+ /** Window upper bound on `openedAt` (inclusive). */
15158
+ until: z.number().optional(),
15159
+ limit: z.number().int().min(1).max(LIST_GROUPS_MAX_LIMIT).default(LIST_GROUPS_DEFAULT_LIMIT),
15160
+ /** Opaque continuation cursor from a previous page's `nextCursor`. */
15161
+ cursor: z.string().optional()
15162
+ });
15163
+ var ListGroupsPageSchema = z.object({
15164
+ groups: z.array(AnalyticsGroupRecordSchema).readonly(),
15165
+ nextCursor: z.string().nullable()
15166
+ });
15088
15167
  var KeyEventQueryInput = z.object({
15089
15168
  deviceId: z.number(),
15090
15169
  /** Window lower bound (track firstSeen ≥ since). */
@@ -15160,7 +15239,9 @@ var TrackCascadeCountsSchema = z.object({
15160
15239
  /** Unassigned plate reads removed (best-effort; plate leg deferred to plate-intelligence). */
15161
15240
  plates: z.number().int(),
15162
15241
  /** Per-track CLIP search vectors removed (best-effort). */
15163
- embeddings: z.number().int()
15242
+ embeddings: z.number().int(),
15243
+ /** Group membership + group rows removed with their last member (best-effort). */
15244
+ groups: z.number().int()
15164
15245
  });
15165
15246
  /** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
15166
15247
  var DiskReconcileCountsSchema = z.object({
@@ -15332,6 +15413,16 @@ var pipelineAnalyticsCapability = {
15332
15413
  * are not included (same contract as `listTracks`).
15333
15414
  */
15334
15415
  listRecentTracks: method(RecentTracksQueryInput, RecentTracksPageSchema),
15416
+ /**
15417
+ * Batched co-moving group listing — the Groups feed. Same merge/cursor
15418
+ * contract as {@link listRecentTracks}. A group is a sealed partition of
15419
+ * one session; `getGroup` is the detail with members.
15420
+ */
15421
+ listGroups: method(ListGroupsQueryInput, ListGroupsPageSchema),
15422
+ getGroup: method(z.object({
15423
+ deviceId: z.number(),
15424
+ groupId: z.string().min(1)
15425
+ }), AnalyticsGroupDetailSchema.nullable()),
15335
15426
  clearTracks: method(z.object({ deviceId: z.number() }), z.void(), {
15336
15427
  kind: "mutation",
15337
15428
  auth: "admin"
@@ -15925,6 +16016,33 @@ var NativeCropRefSchema = z.object({
15925
16016
  h: z.number()
15926
16017
  })
15927
16018
  });
16019
+ z.object({
16020
+ crop: z.object({
16021
+ left: z.number(),
16022
+ top: z.number(),
16023
+ width: z.number().positive(),
16024
+ height: z.number().positive()
16025
+ }).optional(),
16026
+ content: z.object({
16027
+ width: z.number().int().positive(),
16028
+ height: z.number().int().positive()
16029
+ }),
16030
+ fit: z.enum(["stretch", "contain"]),
16031
+ format: z.enum([
16032
+ "rgb",
16033
+ "gray",
16034
+ "jpeg"
16035
+ ])
16036
+ });
16037
+ var FrameRefSchema = z.object({
16038
+ registryId: z.string().min(1),
16039
+ id: z.string().min(1),
16040
+ width: z.number().int().positive(),
16041
+ height: z.number().int().positive(),
16042
+ format: z.enum(["rgb", "gray"]),
16043
+ timestamp: z.number(),
16044
+ capturedAt: z.number().optional()
16045
+ });
15928
16046
  var ModelFormatSchema$1 = z.enum([
15929
16047
  "onnx",
15930
16048
  "coreml",
@@ -15991,7 +16109,8 @@ var PipelineModelOptionSchema = z.object({
15991
16109
  sizeMB: z.number()
15992
16110
  })),
15993
16111
  group: ModelVariantGroupSchema.optional(),
15994
- legacy: z.boolean().optional()
16112
+ legacy: z.boolean().optional(),
16113
+ provider: ModelProviderIdSchema.optional()
15995
16114
  });
15996
16115
  var ConfigFieldBridge = z.custom();
15997
16116
  var PipelineAddonSchemaSchema = z.object({
@@ -16246,7 +16365,7 @@ var pipelineExecutorCapability = {
16246
16365
  * legacy call shape used by existing benchmark code; once all
16247
16366
  * callers pass it explicitly we make it required.
16248
16367
  *
16249
- * Exactly one of `frame`, `frameHandle`, `imageBase64`,
16368
+ * Exactly one of `frame`, `frameRef`, `frameHandle`, `imageBase64`,
16250
16369
  * `referenceImage` must be provided:
16251
16370
  * - `frame`: runtime dispatch path (runner → decoded broker frame).
16252
16371
  * Carries the raw buffer, dimensions, and format; the executor
@@ -16268,6 +16387,12 @@ var pipelineExecutorCapability = {
16268
16387
  steps: z.array(PipelineStepInputSchema).min(1),
16269
16388
  frame: FrameInputSchema.optional(),
16270
16389
  /**
16390
+ * Process-local lazy frame. Valid only when caller and provider resolve
16391
+ * in the same execution-group process; split/cross-node callers use
16392
+ * `frame`/`image` inline compatibility instead.
16393
+ */
16394
+ frameRef: FrameRefSchema.optional(),
16395
+ /**
16271
16396
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
16272
16397
  * the decoded pixels live in. One more member of the one-of
16273
16398
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -16617,7 +16742,10 @@ var NativeCropResultSchema = z.object({
16617
16742
  * Which source served this crop, so a quality-sensitive consumer (the native
16618
16743
  * `keyFrame`) can reject a degraded fallback:
16619
16744
  * - `native` — cut from the decode worker's retained NATIVE surface (the
16620
- * quality path).
16745
+ * quality path). A subject-tile serve is also native-resolution and stays
16746
+ * `native` here: the public enum cannot name `tile` without a breaking cap
16747
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
16748
+ * internal crop result (`nativeHits` vs `tileHits`).
16621
16749
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
16622
16750
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
16623
16751
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -17108,12 +17236,41 @@ var RunnerLocalLoadSchema = z.object({
17108
17236
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
17109
17237
  * working unchanged when they switch to reading from the runner cap.
17110
17238
  */
17239
+ var FrameLazyCountersSchema = z.object({
17240
+ framesDecoded: z.number(),
17241
+ framesAdmitted: z.number(),
17242
+ framesDroppedPixelFree: z.number(),
17243
+ viewsMaterialized: z.number(),
17244
+ viewsSkipped: z.number(),
17245
+ workerToRunnerBytes: z.number(),
17246
+ runnerToPoolRawBytes: z.number(),
17247
+ runnerToPoolJpegBytes: z.number(),
17248
+ onDemandFullFrameRequests: z.number(),
17249
+ onDemandCropRequests: z.number(),
17250
+ nativeHits: z.number(),
17251
+ nativeMisses: z.number(),
17252
+ tileHits: z.number(),
17253
+ tileMisses: z.number(),
17254
+ fallbackHits: z.number(),
17255
+ fallbackMisses: z.number(),
17256
+ retainedWritesAvoided: z.number(),
17257
+ residentRefs: z.number(),
17258
+ residentBytes: z.number(),
17259
+ releases: z.number(),
17260
+ evictions: z.number(),
17261
+ staleMisses: z.number()
17262
+ });
17263
+ var FrameLazyMetricsSchema = z.object({
17264
+ node: FrameLazyCountersSchema,
17265
+ cameras: z.array(FrameLazyCountersSchema.extend({ deviceId: z.number() }))
17266
+ });
17111
17267
  var RunnerLocalMetricsSchema = z.object({
17112
17268
  nodeId: z.string(),
17113
17269
  activeCameras: z.number(),
17114
17270
  throttledCameras: z.number(),
17115
17271
  avgInferenceTimeMs: z.number(),
17116
- queueDepth: z.number()
17272
+ queueDepth: z.number(),
17273
+ frameLazy: FrameLazyMetricsSchema.optional()
17117
17274
  });
17118
17275
  /**
17119
17276
  * Pipeline Runner capability — runtime detection workhorse.
@@ -23947,6 +24104,17 @@ var NotificationEndpointSchema = z.object({
23947
24104
  /** What the ranking currently resolves to (null when nothing is reachable). */
23948
24105
  resolved: z.string().nullable()
23949
24106
  });
24107
+ /**
24108
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
24109
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
24110
+ * currently expands to, so the UI can show the effective set either way.
24111
+ */
24112
+ var ViewerEndpointsSchema = z.object({
24113
+ /** The operator's explicit race set, or empty for AUTO. */
24114
+ baseUrls: z.array(z.string()).readonly(),
24115
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
24116
+ resolved: z.array(z.string()).readonly()
24117
+ });
23950
24118
  var AllowedAddressesSchema = z.object({
23951
24119
  /**
23952
24120
  * Allowlist of interface addresses operators have explicitly opted
@@ -23988,13 +24156,13 @@ var localNetworkCapability = {
23988
24156
  */
23989
24157
  getPreferred: method(z.void(), PreferredSchema),
23990
24158
  /**
23991
- * Ordered candidate base URLs the SDK should try on connect.
23992
- * Includes LAN IPs (one per non-internal interface), the public
23993
- * tunnel hostname (when active), and loopback as a last-resort
23994
- * fallback. Filterable by `includeLoopback` / `ipv4Only`.
23995
- * Honours `getAllowedAddresses()` when set addresses outside
23996
- * the allowlist are dropped (the public tunnel + loopback are
23997
- * always included as escape hatches).
24159
+ * Ordered candidate base URLs (the palette the Network tab shows).
24160
+ * Includes LAN IPv4, stable LAN IPv6, the public tunnel, and mesh when
24161
+ * joined. Loopback is off by default. The SDK races the subset from
24162
+ * `getViewerEndpoints`, not this full list — IPv6 stays here because
24163
+ * WebRTC ICE gathers dual-stack regardless of the HTTP race. Honours
24164
+ * `getAllowedAddresses()` when set addresses outside the allowlist
24165
+ * are dropped (the public tunnel is still included as an escape hatch).
23998
24166
  *
23999
24167
  * **The port is the hub's, not the caller's** (D62 — a function's fact
24000
24168
  * belongs to whoever already owns it). This method used to take a `port`
@@ -24025,10 +24193,11 @@ var localNetworkCapability = {
24025
24193
  */
24026
24194
  port: z.number().int().min(1).max(65535).optional(),
24027
24195
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
24028
- * candidate. Default `true`. */
24196
+ * candidate. Default `false` — loopback is not a client route. */
24029
24197
  includeLoopback: z.boolean().optional(),
24030
- /** Skip IPv6 entries. Some legacy clients can't parse them.
24031
- * Default `false`. */
24198
+ /** Skip IPv6 entries. Default `false` the palette includes stable
24199
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
24200
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
24032
24201
  ipv4Only: z.boolean().optional(),
24033
24202
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
24034
24203
  * Pass `'https'` when the caller is itself loaded over HTTPS
@@ -24057,6 +24226,19 @@ var localNetworkCapability = {
24057
24226
  */
24058
24227
  setNotificationEndpoint: method(z.object({ baseUrl: z.string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }),
24059
24228
  /**
24229
+ * The endpoints the SDK / viewer races for API access. Empty `baseUrls`
24230
+ * means AUTO: every LAN IPv4 address plus the public tunnel, never IPv6,
24231
+ * never mesh, never loopback. `resolved` is that set (or the operator's
24232
+ * explicit subset) as it stands right now.
24233
+ */
24234
+ getViewerEndpoints: method(z.void(), ViewerEndpointsSchema),
24235
+ /**
24236
+ * Replace the viewer race set. Empty `baseUrls` restores AUTO. Stored
24237
+ * verbatim (not indices) so a temporarily-down tunnel is not silently
24238
+ * dropped from the operator's choice.
24239
+ */
24240
+ setViewerEndpoints: method(z.object({ baseUrls: z.array(z.string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }),
24241
+ /**
24060
24242
  * Read the operator's allowlist. Empty = "auto" (no filter). Used
24061
24243
  * by the admin UI's address selector to seed its checkbox state.
24062
24244
  */
@@ -25895,7 +26077,12 @@ var PlateInfoSchema = z.object({
25895
26077
  plateBbox: BoundingBoxSchema.optional(),
25896
26078
  /** keyFrame parity: MediaStore key of the track's native-resolution key frame. */
25897
26079
  keyFrameMediaKey: z.string().optional(),
25898
- base64: z.string().optional()
26080
+ base64: z.string().optional(),
26081
+ /**
26082
+ * Same crop as a data-plane URL. `getPlateByTrack` returns this and
26083
+ * never inlines JPEG; `listPlates` still inlines for the admin-ui.
26084
+ */
26085
+ cropUrl: z.string().optional()
25899
26086
  });
25900
26087
  var MediaFileLiteSchema = z.object({
25901
26088
  key: z.string(),
@@ -32591,6 +32778,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
32591
32778
  addonId: null,
32592
32779
  access: "view"
32593
32780
  },
32781
+ "localNetwork.getViewerEndpoints": {
32782
+ capName: "local-network",
32783
+ capScope: "system",
32784
+ addonId: null,
32785
+ access: "view"
32786
+ },
32594
32787
  "localNetwork.list": {
32595
32788
  capName: "local-network",
32596
32789
  capScope: "system",
@@ -32627,6 +32820,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
32627
32820
  addonId: null,
32628
32821
  access: "create"
32629
32822
  },
32823
+ "localNetwork.setViewerEndpoints": {
32824
+ capName: "local-network",
32825
+ capScope: "system",
32826
+ addonId: null,
32827
+ access: "create"
32828
+ },
32630
32829
  "localNetwork.uploadCertificate": {
32631
32830
  capName: "local-network",
32632
32831
  capScope: "system",
@@ -33431,6 +33630,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33431
33630
  addonId: null,
33432
33631
  access: "view"
33433
33632
  },
33633
+ "pipelineAnalytics.getGroup": {
33634
+ capName: "pipeline-analytics",
33635
+ capScope: "device",
33636
+ addonId: null,
33637
+ access: "view"
33638
+ },
33434
33639
  "pipelineAnalytics.getKeyEvents": {
33435
33640
  capName: "pipeline-analytics",
33436
33641
  capScope: "device",
@@ -33515,6 +33720,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
33515
33720
  addonId: null,
33516
33721
  access: "view"
33517
33722
  },
33723
+ "pipelineAnalytics.listGroups": {
33724
+ capName: "pipeline-analytics",
33725
+ capScope: "device",
33726
+ addonId: null,
33727
+ access: "view"
33728
+ },
33518
33729
  "pipelineAnalytics.listOpsLog": {
33519
33730
  capName: "pipeline-analytics",
33520
33731
  capScope: "device",
@@ -36934,6 +37145,11 @@ Object.freeze({
36934
37145
  form: "single",
36935
37146
  optional: false
36936
37147
  }],
37148
+ "pipelineAnalytics.getGroup": [{
37149
+ name: "deviceId",
37150
+ form: "single",
37151
+ optional: false
37152
+ }],
36937
37153
  "pipelineAnalytics.getKeyEvents": [{
36938
37154
  name: "deviceId",
36939
37155
  form: "single",
@@ -36989,6 +37205,11 @@ Object.freeze({
36989
37205
  form: "array",
36990
37206
  optional: false
36991
37207
  }],
37208
+ "pipelineAnalytics.listGroups": [{
37209
+ name: "deviceIds",
37210
+ form: "array",
37211
+ optional: false
37212
+ }],
36992
37213
  "pipelineAnalytics.listOpsLog": [{
36993
37214
  name: "deviceId",
36994
37215
  form: "single",
@@ -23,13 +23,30 @@ export declare function siblingUrl(mainUrl: string, sibling: string): string;
23
23
  * agree on the on-disk layout without re-deriving it.
24
24
  */
25
25
  export declare function collectModelFiles(entry: ModelCatalogEntry, format: ModelFormat): string[];
26
+ export type DownloadRedirectPolicy = (url: URL, hop: number) => void;
27
+ export interface DownloadFileOptions {
28
+ readonly onProgress?: (downloaded: number, total: number) => void;
29
+ /**
30
+ * When set, redirects are followed manually and THIS policy is applied to
31
+ * the initial URL and every Location hop before the next fetch. Absent ⇒
32
+ * native `redirect: 'follow'` (backward compatible).
33
+ */
34
+ readonly redirectPolicy?: DownloadRedirectPolicy;
35
+ readonly maxRedirects?: number;
36
+ readonly timeoutMs?: number;
37
+ readonly maxBytes?: number;
38
+ readonly fetchImpl?: typeof fetch;
39
+ }
26
40
  /**
27
41
  * Download a single file from a URL to a destination path.
28
- * Uses native fetch() (Node 22+) which handles redirects natively.
29
42
  * Streams to disk with optional progress callback.
30
43
  * Returns the destination path. Skips download if file already exists.
44
+ *
45
+ * The third argument remains a progress callback for existing callers. Pass
46
+ * an options object to supply a per-hop redirect policy, timeout, or byte cap.
31
47
  */
32
48
  export declare function downloadFile(url: string, destPath: string, onProgress?: (downloaded: number, total: number) => void): Promise<string>;
49
+ export declare function downloadFile(url: string, destPath: string, options: DownloadFileOptions): Promise<string>;
33
50
  /**
34
51
  * Fetch JSON from a URL using native fetch().
35
52
  */
@@ -50,21 +50,56 @@ function buildHeaders(url) {
50
50
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
51
51
  return headers;
52
52
  }
53
- /**
54
- * Download a single file from a URL to a destination path.
55
- * Uses native fetch() (Node 22+) which handles redirects natively.
56
- * Streams to disk with optional progress callback.
57
- * Returns the destination path. Skips download if file already exists.
58
- */
59
- async function downloadFile(url, destPath, onProgress) {
53
+ var DEFAULT_MAX_REDIRECTS = 5;
54
+ function normalizeDownloadOptions(third) {
55
+ if (typeof third === "function") return { onProgress: third };
56
+ return third ?? {};
57
+ }
58
+ function isRedirectStatus(status) {
59
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
60
+ }
61
+ function resolveRedirectUrl(current, location) {
62
+ return new URL(location, current);
63
+ }
64
+ async function downloadFile(url, destPath, onProgressOrOptions) {
60
65
  if (fs.existsSync(destPath)) return destPath;
66
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
67
+ const fetchImpl = opts.fetchImpl ?? fetch;
68
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
61
69
  fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
62
70
  const tmpPath = destPath + ".downloading";
63
71
  try {
64
- const response = await fetch(url, {
65
- redirect: "follow",
66
- headers: buildHeaders(url)
67
- });
72
+ let current = url;
73
+ const seen = /* @__PURE__ */ new Set();
74
+ let response;
75
+ const manual = opts.redirectPolicy !== void 0;
76
+ for (let hop = 0; hop <= maxRedirects; hop++) {
77
+ const parsed = new URL(current);
78
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
79
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
80
+ seen.add(parsed.href);
81
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
82
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
83
+ try {
84
+ response = await fetchImpl(current, {
85
+ redirect: manual ? "manual" : "follow",
86
+ headers: buildHeaders(current),
87
+ ...controller ? { signal: controller.signal } : {}
88
+ });
89
+ } finally {
90
+ if (timer) clearTimeout(timer);
91
+ }
92
+ if (manual && isRedirectStatus(response.status)) {
93
+ const location = response.headers.get("location");
94
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
95
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
96
+ current = resolveRedirectUrl(current, location).href;
97
+ continue;
98
+ }
99
+ break;
100
+ }
101
+ if (!response) throw new Error(`No response downloading ${url}`);
102
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
68
103
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
69
104
  if (!response.body) throw new Error(`No response body from ${url}`);
70
105
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -75,9 +110,10 @@ async function downloadFile(url, destPath, onProgress) {
75
110
  for (;;) {
76
111
  const { done, value } = await reader.read();
77
112
  if (done || !value) break;
78
- fileStream.write(value);
79
113
  downloaded += value.length;
80
- onProgress?.(downloaded, total);
114
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
115
+ fileStream.write(value);
116
+ opts.onProgress?.(downloaded, total);
81
117
  }
82
118
  } finally {
83
119
  fileStream.end();
@@ -51,21 +51,56 @@ function buildHeaders(url) {
51
51
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
52
52
  return headers;
53
53
  }
54
- /**
55
- * Download a single file from a URL to a destination path.
56
- * Uses native fetch() (Node 22+) which handles redirects natively.
57
- * Streams to disk with optional progress callback.
58
- * Returns the destination path. Skips download if file already exists.
59
- */
60
- async function downloadFile(url, destPath, onProgress) {
54
+ var DEFAULT_MAX_REDIRECTS = 5;
55
+ function normalizeDownloadOptions(third) {
56
+ if (typeof third === "function") return { onProgress: third };
57
+ return third ?? {};
58
+ }
59
+ function isRedirectStatus(status) {
60
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
61
+ }
62
+ function resolveRedirectUrl(current, location) {
63
+ return new URL(location, current);
64
+ }
65
+ async function downloadFile(url, destPath, onProgressOrOptions) {
61
66
  if (node_fs.existsSync(destPath)) return destPath;
67
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
68
+ const fetchImpl = opts.fetchImpl ?? fetch;
69
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
62
70
  node_fs.mkdirSync(node_path.dirname(destPath), { recursive: true });
63
71
  const tmpPath = destPath + ".downloading";
64
72
  try {
65
- const response = await fetch(url, {
66
- redirect: "follow",
67
- headers: buildHeaders(url)
68
- });
73
+ let current = url;
74
+ const seen = /* @__PURE__ */ new Set();
75
+ let response;
76
+ const manual = opts.redirectPolicy !== void 0;
77
+ for (let hop = 0; hop <= maxRedirects; hop++) {
78
+ const parsed = new URL(current);
79
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
80
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
81
+ seen.add(parsed.href);
82
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
83
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
84
+ try {
85
+ response = await fetchImpl(current, {
86
+ redirect: manual ? "manual" : "follow",
87
+ headers: buildHeaders(current),
88
+ ...controller ? { signal: controller.signal } : {}
89
+ });
90
+ } finally {
91
+ if (timer) clearTimeout(timer);
92
+ }
93
+ if (manual && isRedirectStatus(response.status)) {
94
+ const location = response.headers.get("location");
95
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
96
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
97
+ current = resolveRedirectUrl(current, location).href;
98
+ continue;
99
+ }
100
+ break;
101
+ }
102
+ if (!response) throw new Error(`No response downloading ${url}`);
103
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
69
104
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
70
105
  if (!response.body) throw new Error(`No response body from ${url}`);
71
106
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -76,9 +111,10 @@ async function downloadFile(url, destPath, onProgress) {
76
111
  for (;;) {
77
112
  const { done, value } = await reader.read();
78
113
  if (done || !value) break;
79
- fileStream.write(value);
80
114
  downloaded += value.length;
81
- onProgress?.(downloaded, total);
115
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
116
+ fileStream.write(value);
117
+ opts.onProgress?.(downloaded, total);
82
118
  }
83
119
  } finally {
84
120
  fileStream.end();