@camstack/server 1.2.454 → 1.2.456

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.
@@ -30,32 +30,26 @@ under [Non-commercial models](#non-commercial-models).
30
30
 
31
31
  ## Required attributions
32
32
 
33
- - © Ultralytics, AGPL-3.0. Corresponding source: the upstream Ultralytics checkpoint and scripts/build-camstack-models.py in https://github.com/camstack/server. https://github.com/ultralytics/ultralytics
33
+ - © Ultralytics, AGPL-3.0. Corresponding source: the upstream Ultralytics checkpoint and build-camstack-models.py (with convert_lib.py) published at https://huggingface.co/camstack/camstack-models/tree/main/source. https://github.com/ultralytics/ultralytics
34
34
  - Coral Edge TPU models and AIY Vision Birds V1 © Google LLC, Apache-2.0. https://github.com/google-coral/test_data
35
- - © Ultralytics, AGPL-3.0 (Ultralytics YOLOv9 weights). YOLOv9 architecture: Chien-Yao Wang, I-Hau Yeh, Hong-Yuan Mark Liao, GPL-3.0 (https://github.com/WongKinYiu/yolov9). Corresponding source: the upstream Ultralytics checkpoint and scripts/build-camstack-models.py in https://github.com/camstack/server. https://github.com/ultralytics/ultralytics
36
- - © Ultralytics, AGPL-3.0 (YOLOv8n base, fine-tuned by CamStack with Ultralytics). The fine-tuning checkpoint is not published yet. https://github.com/ultralytics/ultralytics
35
+ - © Ultralytics, AGPL-3.0 (Ultralytics YOLOv9 weights). YOLOv9 architecture: Chien-Yao Wang, I-Hau Yeh, Hong-Yuan Mark Liao, GPL-3.0 (https://github.com/WongKinYiu/yolov9). Corresponding source: the upstream Ultralytics checkpoint and build-camstack-models.py (with convert_lib.py) published at https://huggingface.co/camstack/camstack-models/tree/main/source. https://github.com/ultralytics/ultralytics
37
36
  - RF-DETR © Roboflow, Apache-2.0. https://github.com/roboflow/rf-detr
38
- - SCRFD © InsightFace. Pretrained models released for non-commercial research purposes only. https://github.com/deepinsight/insightface/tree/master/detection/scrfd
39
37
  - YuNet weights Copyright (c) 2020 Shiqi Yu, MIT. Training code: ShiqiYu/libfacedetection.train, BSD-3-Clause. https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet
40
38
  - AuraFace v1 by fal.ai, Apache-2.0. https://huggingface.co/fal/AuraFace-v1
41
- - Scrypted plugin models, Koushik Dutta: repository tagged MIT (licenses/models/MIT-scrypted-plugin-models.txt), no per-model terms. https://huggingface.co/scrypted/plugin-models
42
39
  - © Ultralytics, AGPL-3.0 (YOLOv8n base, fine-tuned for plates by an unidentified third party). CamStack holds no training checkpoint; the ONNX file is the only form it has. https://github.com/ultralytics/ultralytics
43
40
  - fast-plate-ocr, MIT License, copyright (c) 2024 ankandrew (full notice: licenses/models/MIT-fast-plate-ocr.txt). https://github.com/ankandrew/fast-plate-ocr
44
- - EasyOCR © JaidedAI, Apache-2.0. https://github.com/JaidedAI/EasyOCR
45
41
  - EfficientNet-Lite0 via timm (Ross Wightman), Apache-2.0. Open Images V7 annotations © Google LLC, CC BY 4.0; images listed by Google as CC BY 2.0, without warranty. https://github.com/huggingface/pytorch-image-models
46
42
  - torchvision MobileNetV3, BSD-3-Clause. Trained on the Vehicle classification dataset by DrBimmer (https://huggingface.co/datasets/DrBimmer/vehicle-classification), CC BY-NC 4.0. https://github.com/pytorch/vision
47
- - Apple Machine Learning Research Model is licensed under the Apple Machine Learning Research Model License Agreement. https://github.com/apple/ml-mobileclip
48
43
  - SigLIP 2 © Google LLC, Apache-2.0. https://huggingface.co/google/siglip2-base-patch16-224
49
44
  - U²-Net © Xuebin Qin et al., Apache-2.0. https://github.com/xuebinqin/U-2-Net
50
45
  - YAMNet © Google LLC, Apache-2.0. AudioSet ontology and labels © Google LLC, CC BY 4.0. https://github.com/tensorflow/models/tree/master/research/audioset/yamnet
51
46
  - Qwen models © Alibaba Cloud, Apache-2.0. https://huggingface.co/Qwen · https://huggingface.co/unsloth · https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF
52
47
  - Built with Llama. Llama 3.2 is licensed under the Llama 3.2 Community License, Copyright © Meta Platforms, Inc. All Rights Reserved. https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF
53
- - Scrypted YOLOv9 ReLU detectors, Koushik Dutta (scrypted/plugin-models, repository tagged MIT); trained with the YOLOv9 code of Chien-Yao Wang, I-Hau Yeh, Hong-Yuan Mark Liao, GPL-3.0 (https://github.com/WongKinYiu/yolov9). https://huggingface.co/scrypted/plugin-models
54
48
 
55
49
  Training data under attribution terms:
56
50
 
57
- - **COCO 2017** (training data of `yolo26n` and 53 more): annotations CC-BY-4.0; images under their Flickr owners' terms. https://cocodataset.org/#termsofuse
58
- - **WIDER FACE** (training data of `scrfd-2.5g` and 1 more): research-oriented terms; images CC BY-NC-ND. http://shuoyang1213.me/WIDERFACE/
51
+ - **COCO 2017** (training data of `yolo26n` and 50 more): annotations CC-BY-4.0; images under their Flickr owners' terms. https://cocodataset.org/#termsofuse
52
+ - **WIDER FACE** (training data of `yunet-2023mar`): research-oriented terms; images CC BY-NC-ND. http://shuoyang1213.me/WIDERFACE/
59
53
  - **Open Images V7** (training data of `animal-classifier`): annotations CC-BY-4.0 (Google LLC); images listed as CC-BY-2.0, no warranty. https://storage.googleapis.com/openimages/web/factsfigures_v7.html
60
54
  - **DrBimmer/vehicle-classification** (training data of `vehicle-type-v1`): CC-BY-NC-4.0 — treated as binding the trained weights (conservative reading). https://huggingface.co/datasets/DrBimmer/vehicle-classification
61
55
  - **AudioSet ontology/labels** (training data of `yamnet-onnx`): CC-BY-4.0 (Google LLC). https://research.google.com/audioset/
@@ -68,34 +62,34 @@ Training data under attribution terms:
68
62
  cards link here instead of repeating it.
69
63
  -->
70
64
 
71
- The folders `objectDetection/yolo26`, `objectDetection/yolov9`, `packageDetection/yolov8-package`, `plateDetection/yolov8-plate` and `segmentation/yolo26-seg` of the model mirror hold Ultralytics weights,
65
+ The folders `objectDetection/yolo26`, `objectDetection/yolov9`, `plateDetection/yolov8-plate` and `segmentation/yolo26-seg` of the model mirror hold Ultralytics weights,
72
66
  modified by CamStack and distributed under AGPL-3.0. Their Corresponding Source
73
- is the public CamStack source repository, **https://github.com/camstack/server**
74
- (MIT), together with the upstream checkpoints it starts from:
67
+ is published in the mirror itself, in its **`source/`** folder,
68
+ **https://huggingface.co/camstack/camstack-models/tree/main/source**, together
69
+ with the upstream checkpoints it starts from:
75
70
 
76
71
  1. **The upstream checkpoints**, published by Ultralytics at
77
72
  https://github.com/ultralytics/assets/releases: `yolo26{n,s,m,l,x}.pt`,
78
73
  `yolov9{t,s,m,c}.pt`, `yolo26{n,s,m}-seg.pt`.
79
- 2. **The export scripts** in https://github.com/camstack/server:
80
- `scripts/build-camstack-models.py` (ONNX with a dynamic batch axis, CoreML
81
- FP16, OpenVINO IR, OpenVINO NNCF INT8) and `scripts/export-models.py`. The
82
- build instructions (Python environment, invocation, calibration images for
83
- INT8) are in each script's header.
84
- 3. **The software** that downloads and runs these weights, CamStack itself, is
85
- the same repository.
74
+ 2. **The export scripts** in `source/`: `build-camstack-models.py` (ONNX
75
+ with a dynamic batch axis, CoreML FP16, OpenVINO IR, OpenVINO NNCF INT8),
76
+ `convert_lib.py` (the ONNX to OpenVINO / INT8 / CoreML converter it imports,
77
+ the same one CamStack's Model Studio runs), `requirements.txt` and
78
+ `export-models.py`. The build instructions (Python environment,
79
+ invocation, calibration images for INT8) are in `source/README.md` and in
80
+ each script's header. These scripts are CamStack's own code and are offered
81
+ under the MIT licence; the weights they produce keep their upstream licence.
86
82
 
87
- What the repository does **not** cover yet:
83
+ What the published scripts do **not** cover yet:
88
84
 
89
85
  - **Some hosted sizes and input resolutions.** The export scripts do not list
90
86
  every variant in the mirror (for example the `-320`/`-256` YOLO26 exports,
91
87
  `yolov9m`/`yolov9c`, and the `yolo26*-seg` models). Each one is the same
92
- Ultralytics `export()` with a different model id or `imgsz`, but no script in
93
- the repository reproduces it as-is.
88
+ Ultralytics `export()` with a different model id or `imgsz`, but no
89
+ published script reproduces it as-is.
94
90
  - **`yolov8n-plate` has no training checkpoint.** It is a third-party model
95
91
  (trained 2023-12-05 according to its own metadata, origin not identified).
96
92
  The ONNX file is the only form CamStack has.
97
- - **The `yolov8n-package` checkpoint is not published yet.** CamStack
98
- fine-tuned it with Ultralytics.
99
93
 
100
94
  ## Non-commercial models
101
95
 
@@ -104,12 +98,7 @@ commercial use**:
104
98
 
105
99
  | Model | Used for | Terms |
106
100
  | --- | --- | --- |
107
- | `scrfd-2.5g` | Face detection | LicenseRef-InsightFace-NonCommercial-Research; training data: research-oriented terms; images CC BY-NC-ND ([terms](https://github.com/deepinsight/insightface#license)) |
108
101
  | `vehicle-type-v1` (default) | Vehicle classifier | CC-BY-NC-4.0; training data: CC-BY-NC-4.0 — treated as binding the trained weights (conservative reading) ([terms](https://creativecommons.org/licenses/by-nc/4.0/)) |
109
- | `mobileclip-s1` (default) | Semantic search, image encoders (CLIP) | LicenseRef-Apple-AMLR ([terms](https://github.com/apple/ml-mobileclip/blob/main/LICENSE_MODELS)) |
110
- | `mobileclip-s2` | Semantic search, image encoders (CLIP) | LicenseRef-Apple-AMLR ([terms](https://github.com/apple/ml-mobileclip/blob/main/LICENSE_MODELS)) |
111
- | `mobileclip-s1-text` | Semantic search, text encoders (CLIP) | LicenseRef-Apple-AMLR ([terms](https://github.com/apple/ml-mobileclip/blob/main/LICENSE_MODELS)) |
112
- | `mobileclip-s2-text` | Semantic search, text encoders (CLIP) | LicenseRef-Apple-AMLR ([terms](https://github.com/apple/ml-mobileclip/blob/main/LICENSE_MODELS)) |
113
102
 
114
103
  ## Models
115
104
 
@@ -124,32 +113,26 @@ entry kept resolvable for a saved selection but no longer offered.
124
113
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
125
114
  | --- | --- | --- | --- | --- | --- |
126
115
  | `yolo26n` (default), `yolo26s`, `yolo26m`, `yolo26l`, `yolo26x` (hidden), `yolo26n-fp16` (hidden), `yolo26n-int8`, `yolo26s-fp16` (hidden), `yolo26s-int8`, `yolo26m-fp16` (hidden), `yolo26m-int8`, `yolo26l-fp16` (hidden), `yolo26l-int8`, `yolo26n-320`, `yolo26n-320-int8`, `yolo26n-256`, `yolo26n-256-int8`, `yolo26s-320`, `yolo26s-320-int8`, `yolo26s-256`, `yolo26s-256-int8`, `yolo26m-320`, `yolo26m-320-int8`, `yolo26m-256`, `yolo26m-256-int8`, `yolo26l-320`, `yolo26l-320-int8`, `yolo26l-256`, `yolo26l-256-int8`, `yolo26x-fp16` (hidden), `yolo26x-int8` (hidden) | [Ultralytics](https://github.com/ultralytics/ultralytics) | AGPL-3.0 / AGPL-3.0 (`AGPL-3.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | exported to ONNX, OpenVINO IR and CoreML (scripts/build-camstack-models.py); OpenVINO FP16 export; exported to OpenVINO IR, then OpenVINO NNCF INT8 post-training quantisation; re-exported at 320×320 to ONNX, OpenVINO IR and CoreML; re-exported at 320×320 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation; re-exported at 256×256 to ONNX, OpenVINO IR and CoreML; re-exported at 256×256 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation | mirror `objectDetection/yolo26` |
127
- | `ssd-mobilenet-v2-coco-edgetpu` (default), `efficientdet-lite0-edgetpu` | [Google Coral test_data](https://github.com/google-coral/test_data) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | none | https://github.com/google-coral/test_data |
128
- | `ssdlite-mobiledet-coco-edgetpu` | [Google Coral test_data](https://github.com/google-coral/test_data) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | none: ssdlite_mobiledet_coco_qat_postprocess_edgetpu.tflite mirrored byte for byte (sha256 b69e508e…434a2e), with the CPU build and coco_labels.txt beside it (scripts/build-ssdlite-mobiledet-model.py) | mirror `objectDetection/ssdlite-mobiledet` |
116
+ | `ssd-mobilenet-v2-coco-edgetpu` (default) | [Google Coral test_data](https://github.com/google-coral/test_data) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | none | https://github.com/google-coral/test_data |
129
117
  | `yolov9t-320`, `yolov9t-320-int8`, `yolov9t-640`, `yolov9t-640-int8`, `yolov9s-320`, `yolov9s-320-int8`, `yolov9s-640`, `yolov9s-640-int8`, `yolov9m-320`, `yolov9m-320-int8`, `yolov9m-640`, `yolov9m-640-int8`, `yolov9c-320`, `yolov9c-320-int8`, `yolov9c-640`, `yolov9c-640-int8` | [Ultralytics](https://github.com/ultralytics/ultralytics) | AGPL-3.0 / AGPL-3.0 (`AGPL-3.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | exported at 320×320 to ONNX, OpenVINO IR FP16 and CoreML; exported at 320×320 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation; exported at 640×640 to ONNX, OpenVINO IR FP16 and CoreML; exported at 640×640 to OpenVINO IR; OpenVINO NNCF INT8 post-training quantisation | mirror `objectDetection/yolov9` |
130
118
 
131
119
  ### Package detection
132
120
 
133
121
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
134
122
  | --- | --- | --- | --- | --- | --- |
135
- | `yolov8n-package` | [Ultralytics](https://github.com/ultralytics/ultralytics) | AGPL-3.0 / AGPL-3.0 (`AGPL-3.0.txt`) | Roboflow Universe package-at-front-door (most likely): UNVERIFIED | fine-tuned by CamStack with Ultralytics; exported to ONNX, OpenVINO IR and CoreML | mirror `packageDetection/yolov8-package` |
136
123
  | `rfdetr-package` (default) | [Roboflow RF-DETR](https://github.com/roboflow/rf-detr) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | parcel fine-tuning set (not recorded): UNVERIFIED | RF-DETR-M fine-tuned to 2 classes at 576; exported to ONNX, OpenVINO IR and CoreML; training checkpoint .pth also published | mirror `packageDetection/rfdetr-package` |
137
124
 
138
125
  ### Face detection
139
126
 
140
127
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
141
128
  | --- | --- | --- | --- | --- | --- |
142
- | `scrfd-2.5g` | [InsightFace SCRFD](https://github.com/deepinsight/insightface/tree/master/detection/scrfd) | MIT / **LicenseRef-InsightFace-NonCommercial-Research** (`InsightFace-terms.txt`) | WIDER FACE: research-oriented terms; images CC BY-NC-ND | obtained via RuteNL/SCRFD-face-detection-ONNX; converted to OpenVINO IR and CoreML FP16 | mirror `faceDetection/scrfd` |
143
129
  | `yunet-2023mar` (default) | [OpenCV Zoo YuNet](https://github.com/opencv/opencv_zoo/tree/main/models/face_detection_yunet) | BSD-3-Clause (`BSD-3-Clause.txt`) / MIT (`MIT-yunet-reference.txt`) | WIDER FACE: research-oriented terms; images CC BY-NC-ND | input contract baked into the graph (the pool RGB /255 becomes the upstream BGR 0..255: Mul(255) plus a channel Gather); internal tensor names cleared in the OpenVINO IR; ONNX fp32, OpenVINO IR fp32, CoreML fp16 (scripts/build-yunet-model.py) | mirror `faceDetection/yunet` |
144
- | `ssd-mobilenet-v2-face-edgetpu` (hidden) | [Google Coral test_data](https://github.com/google-coral/test_data) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | — | none | https://github.com/google-coral/test_data |
145
130
 
146
131
  ### Face recognition
147
132
 
148
133
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
149
134
  | --- | --- | --- | --- | --- | --- |
150
- | `arcface-r100` | [onnx-community/arcface-onnx (re-upload of garavv/arcface-onnx)](https://huggingface.co/onnx-community/arcface-onnx) | — / **Unknown** | — | converted to OpenVINO IR and CoreML FP16 | mirror `faceRecognition/arcface` |
151
135
  | `auraface-r100` (default) | [fal.ai AuraFace v1 (glintr100.onnx only)](https://huggingface.co/fal/AuraFace-v1) | — / Apache-2.0 (`Apache-2.0.txt`) | — | input scaling y = 2x - 1 baked ahead of the first Conv (scripts/build-auraface-model.py); converted to OpenVINO IR and CoreML FP16 | mirror `faceRecognition/auraface` |
152
- | `inception-resnet-v1` | [Scrypted plugin-models (repository tagged MIT, no per-model terms)](https://huggingface.co/scrypted/plugin-models) | — / **Unknown** | — | ONNX and CoreML fetched from scrypted/plugin-models; OpenVINO IR converted and re-hosted by CamStack | mirror `faceRecognition/inception-resnet-v1` · https://huggingface.co/scrypted/plugin-models |
153
136
 
154
137
  ### Licence plate detection
155
138
 
@@ -162,7 +145,6 @@ entry kept resolvable for a saved selection but no longer offered.
162
145
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
163
146
  | --- | --- | --- | --- | --- | --- |
164
147
  | `cct-s-v2-global` (default) | [ankandrew/fast-plate-ocr](https://github.com/ankandrew/fast-plate-ocr) | MIT / MIT (`MIT-fast-plate-ocr.txt`) | fast-plate-ocr training set: private, not released upstream | input head removed (recorded in the ONNX doc_string); converted to OpenVINO IR and CoreML | mirror `plateRecognition/cct-s-v2-global` |
165
- | `vgg-english-g2` | [JaidedAI EasyOCR (via Scrypted plugin-models)](https://github.com/JaidedAI/EasyOCR) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | — | ONNX and CoreML fetched from scrypted/plugin-models; OpenVINO IR converted and re-hosted by CamStack | mirror `plateRecognition/vgg_english_g2` · https://huggingface.co/scrypted/plugin-models |
166
148
 
167
149
  ### Animal classifier
168
150
 
@@ -175,7 +157,6 @@ entry kept resolvable for a saved selection but no longer offered.
175
157
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
176
158
  | --- | --- | --- | --- | --- | --- |
177
159
  | `bird-classifier` (default) | [Google AIY Vision birds_V1](https://github.com/google-coral/test_data) | — / Apache-2.0 (`Apache-2.0.txt`) | — | converted from TensorFlow with tf2onnx; OpenVINO IR and CoreML derived from it | mirror `animalClassification/bird-classifier` |
178
- | `bird-classifier-aiy-edgetpu` (hidden) | [Google Coral test_data](https://github.com/google-coral/test_data) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | — | none | https://github.com/google-coral/test_data |
179
160
 
180
161
  ### Vehicle classifier
181
162
 
@@ -187,16 +168,12 @@ entry kept resolvable for a saved selection but no longer offered.
187
168
 
188
169
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
189
170
  | --- | --- | --- | --- | --- | --- |
190
- | `mobileclip-s1` (default) | [Apple ml-mobileclip](https://github.com/apple/ml-mobileclip) | MIT / **LicenseRef-Apple-AMLR** (`Apple-AMLR.txt`) | — | model derivative: vision encoders converted to OpenVINO IR and CoreML FP16 | mirror `clip/mobileclip-s1` |
191
- | `mobileclip-s2` | [Apple ml-mobileclip](https://github.com/apple/ml-mobileclip) | MIT / **LicenseRef-Apple-AMLR** (`Apple-AMLR.txt`) | — | model derivative: vision encoders converted to OpenVINO IR and CoreML FP16 | mirror `clip/mobileclip-s2` |
192
- | `siglip2-b16-224` | [Google SigLIP 2 (google/siglip2-base-patch16-224)](https://huggingface.co/google/siglip2-base-patch16-224) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | WebLI: Google internal, not released | input scaling y = 2x - 1 baked ahead of the patch embedding; converted to OpenVINO IR and CoreML FP16, an FP32 ONNX hosted as the verification reference (scripts/build-siglip2-model.py) | mirror `clip/siglip2` |
171
+ | `siglip2-b16-224` (default) | [Google SigLIP 2 (google/siglip2-base-patch16-224)](https://huggingface.co/google/siglip2-base-patch16-224) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | WebLI: Google internal, not released | input scaling y = 2x - 1 baked ahead of the patch embedding; converted to OpenVINO IR and CoreML FP16, an FP32 ONNX hosted as the verification reference (scripts/build-siglip2-model.py) | mirror `clip/siglip2` |
193
172
 
194
173
  ### Semantic search, text encoders (CLIP)
195
174
 
196
175
  | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
197
176
  | --- | --- | --- | --- | --- | --- |
198
- | `mobileclip-s1-text` | [Apple ml-mobileclip](https://github.com/apple/ml-mobileclip) | MIT / **LicenseRef-Apple-AMLR** (`Apple-AMLR.txt`) | — | model derivative: text encoders quantised to INT8 ONNX and converted to OpenVINO IR | mirror `clip/mobileclip-s1` |
199
- | `mobileclip-s2-text` | [Apple ml-mobileclip](https://github.com/apple/ml-mobileclip) | MIT / **LicenseRef-Apple-AMLR** (`Apple-AMLR.txt`) | — | model derivative: text encoders quantised to INT8 ONNX and converted to OpenVINO IR | mirror `clip/mobileclip-s2` |
200
177
  | `siglip2-b16-224-text` | [Google SigLIP 2 (google/siglip2-base-patch16-224)](https://huggingface.co/google/siglip2-base-patch16-224) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | WebLI: Google internal, not released | input sliced to the model's 64 positions in the graph; ONNX weights FP16, OpenVINO IR and CoreML FP16; a Lowercase normalizer prepended to the Gemma tokenizer (scripts/build-siglip2-model.py) | mirror `clip/siglip2` |
201
178
 
202
179
  ### Segmentation
@@ -229,16 +206,13 @@ entry kept resolvable for a saved selection but no longer offered.
229
206
  | `llm-qwen3-vl-8b-instruct-q4` | [Qwen3-VL (GGUF by unsloth)](https://huggingface.co/unsloth) | — / Apache-2.0 (`Apache-2.0.txt`) | — | Q4_K_M GGUF quantisation by unsloth (not re-hosted by CamStack) | https://huggingface.co/unsloth/Qwen3-VL-8B-Instruct-GGUF |
230
207
  | `llm-qwen3.6-35b-a3b-ud-q4` | [Qwen3.6-35B-A3B (GGUF by unsloth)](https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF) | — / Apache-2.0 (`Apache-2.0.txt`) | — | UD-Q4_K_M GGUF quantisation by unsloth (not re-hosted by CamStack) | https://huggingface.co/unsloth/Qwen3.6-35B-A3B-GGUF |
231
208
 
232
- ### Model Studio catalogs (object detection, imported on request)
233
-
234
- | Catalog ids | Upstream | Code / weights | Training data | CamStack changes | Downloaded from |
235
- | --- | --- | --- | --- | --- | --- |
236
- | `scrypted-yolov9t-relu-coreml`, `scrypted-yolov9t-relu-onnx`, `scrypted-yolov9t-relu-test-coreml`, `scrypted-yolov9t-relu-test-onnx`, `scrypted-yolov9s-relu-coreml`, `scrypted-yolov9s-relu-onnx`, `scrypted-yolov9s-relu-test-coreml`, `scrypted-yolov9s-relu-test-onnx`, `scrypted-yolov9m-relu-coreml`, `scrypted-yolov9m-relu-onnx`, `scrypted-yolov9m-relu-test-coreml`, `scrypted-yolov9m-relu-test-onnx`, `scrypted-yolov9c-relu-coreml`, `scrypted-yolov9c-relu-onnx`, `scrypted-yolov9c-relu-test-coreml`, `scrypted-yolov9c-relu-test-onnx`, `scrypted-yolov9t-relu-openvino-fp32`, `scrypted-yolov9s-relu-openvino-fp32`, `scrypted-yolov9m-relu-openvino-fp32`, `scrypted-yolov9c-relu-openvino-fp32`, `scrypted-yolov9t-relu-openvino-int8`, `scrypted-yolov9t-relu-test-openvino-int8`, `scrypted-yolov9s-relu-openvino-int8`, `scrypted-yolov9s-relu-test-openvino-int8`, `scrypted-yolov9m-relu-openvino-int8`, `scrypted-yolov9m-relu-test-openvino-int8`, `scrypted-yolov9c-relu-openvino-int8`, `scrypted-yolov9c-relu-test-openvino-int8`, `scrypted-yolov9s-relu-sep-320-tflite-float16`, `scrypted-yolov9s-relu-sep-320-tflite-float32`, `scrypted-yolov9s-relu-sep-320-tflite-int8`, `scrypted-yolov9s-relu-sep-320-tflite-integer_quant`, `scrypted-yolov9s-relu-sep-320-tflite-full_integer_quant`, `scrypted-yolov9s-relu-sep-320-tflite-edgetpu` | [Scrypted plugin-models (repository tagged MIT, no per-model terms)](https://huggingface.co/scrypted/plugin-models) | GPL-3.0 (`GPL-3.0.txt`) / **Unknown** | not stated by Scrypted: UNKNOWN | none: downloaded as published, at a pinned revision | https://huggingface.co/scrypted/plugin-models |
237
- | `frigate-mobiledet-edgetpu` | [Google Coral test_data](https://github.com/google-coral/test_data) | Apache-2.0 / Apache-2.0 (`Apache-2.0.txt`) | COCO 2017: annotations CC-BY-4.0; images under their Flickr owners' terms | none | https://github.com/google-coral/test_data |
238
-
239
209
  ### Your own models
240
210
 
241
- The Scrypted and Frigate catalogs that Model Studio offers are listed above. A
242
- model you upload or convert yourself, or pull from your own Frigate+ account,
243
- keeps whatever licence its author published. CamStack does not change it and
244
- does not record it.
211
+ Model Studio can import third-party models (for example community
212
+ object-detection catalogs) on request. They are downloaded from their
213
+ publishers, are not hosted or redistributed by CamStack, and keep their
214
+ publishers' licences; Model Studio shows the licence where the source declares
215
+ one. A model you upload or convert
216
+ yourself, or pull from your own Frigate+ account, likewise keeps whatever
217
+ licence its author published. CamStack does not change it and does not record
218
+ it.
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ /**
3
+ * cors-policy — which browser origins may READ this hub's responses (D707).
4
+ *
5
+ * The viewer PWA is hosted at {@link VIEWER_PWA_ORIGIN} and talks to each
6
+ * operator's own hub straight from the browser — no proxy, no cloud. Before
7
+ * D707 the hub answered every request with `Access-Control-Allow-Origin: *`,
8
+ * which let ANY web page a user visited read any hub response its script could
9
+ * reach (an unauthenticated probe of a LAN hub, for one). The policy is now an
10
+ * allowlist:
11
+ *
12
+ * - the hub's OWN origin — the `Origin` whose host is the request's `Host`
13
+ * (or `X-Forwarded-Host`, which a relay such as the Home Assistant panel
14
+ * proxy sets to ITS origin). Same-origin calls never needed CORS; this
15
+ * entry exists for the WebSocket check, where a browser always sends
16
+ * `Origin` and the native clients send their target's;
17
+ * - {@link VIEWER_PWA_ORIGIN}, the hosted viewer;
18
+ * - {@link OPAQUE_ORIGIN} (`null`): the native app's OTA-bundled embed is a
19
+ * `file://` page inside a WKWebView, whose origin is opaque. Removing it
20
+ * breaks live view on iOS;
21
+ * - `CAMSTACK_PUBLIC_ORIGIN`, when set, and the operator's extras
22
+ * (`server.corsOrigins` in config.yaml / `CAMSTACK_CORS_ORIGINS`).
23
+ *
24
+ * The answer to an allowed origin REFLECTS it exactly — never `*` — and never
25
+ * carries `Access-Control-Allow-Credentials`. The cross-origin viewer
26
+ * authenticates with a bearer token in `Authorization` (or tRPC
27
+ * `connectionParams` on the socket), which needs no credentials mode; the
28
+ * httpOnly session cookie therefore never rides a cross-origin request, so the
29
+ * cookie-gated surfaces (`auth.exchangeSession`, `/api/embed-auth`, the
30
+ * admin-ui) stay same-origin by construction.
31
+ *
32
+ * Pure: no Fastify, no I/O — `register-cors-policy.ts` is the glue.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.CORS_PREFLIGHT_MAX_AGE_SEC = exports.CORS_EXPOSED_HEADERS = exports.CORS_ALLOWED_HEADERS = exports.CORS_ALLOWED_METHODS = exports.OPAQUE_ORIGIN = exports.VIEWER_PWA_ORIGIN = void 0;
36
+ exports.normalizeOrigin = normalizeOrigin;
37
+ exports.parseConfiguredOrigins = parseConfiguredOrigins;
38
+ exports.builtInOrigins = builtInOrigins;
39
+ exports.judgeCorsOrigin = judgeCorsOrigin;
40
+ exports.corsResponseHeaders = corsResponseHeaders;
41
+ exports.appendVary = appendVary;
42
+ /** The hosted viewer PWA (operator decision 2026-10-06). */
43
+ exports.VIEWER_PWA_ORIGIN = 'https://camstack.zentik.app';
44
+ /**
45
+ * The serialisation of an opaque origin. The native app's OTA embed
46
+ * (`assets/embed/camstack-embed.html`, loaded from `file://`) sends it.
47
+ */
48
+ exports.OPAQUE_ORIGIN = 'null';
49
+ /** Every method a hub route answers. */
50
+ exports.CORS_ALLOWED_METHODS = 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS';
51
+ /**
52
+ * Request headers a cross-origin caller may send. `authorization` is the
53
+ * whole point; `range` is recorded-media seeking; the `trpc-*` / `x-trpc-*`
54
+ * names are what `@trpc/client` links may add. NOT `x-camstack-exchange`:
55
+ * the cookie→bearer exchange is a same-origin mechanism and a cross-origin
56
+ * page has no business asking for it.
57
+ */
58
+ exports.CORS_ALLOWED_HEADERS = 'authorization, content-type, range, if-range, if-none-match, cache-control, trpc-accept, x-trpc-source, x-trpc-batch-mode';
59
+ /**
60
+ * Response headers a cross-origin script may read. The byte-range trio is
61
+ * recorded playback (a seek reads `content-range`); `content-disposition` is
62
+ * export downloads; `retry-after` is the rate limiter; the `x-camstack-*`
63
+ * trio is the clip-still contract (a 204 that says WHY there is no image).
64
+ */
65
+ exports.CORS_EXPOSED_HEADERS = 'content-range, accept-ranges, content-length, content-disposition, etag, retry-after, x-camstack-reason, x-camstack-reason-code, x-camstack-disposition';
66
+ /** Preflight cache. Chromium caps at 7200 s; Firefox honours up to 86400. */
67
+ exports.CORS_PREFLIGHT_MAX_AGE_SEC = 7200;
68
+ /**
69
+ * The canonical serialisation of an origin (`scheme://host[:port]`, lower
70
+ * case, default port dropped), or `null` when `raw` is not an http(s) origin.
71
+ * `null` (the opaque origin) is accepted verbatim.
72
+ */
73
+ function normalizeOrigin(raw) {
74
+ const trimmed = raw.trim();
75
+ if (trimmed === exports.OPAQUE_ORIGIN)
76
+ return exports.OPAQUE_ORIGIN;
77
+ let url;
78
+ try {
79
+ url = new URL(trimmed);
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ if (url.protocol !== 'https:' && url.protocol !== 'http:')
85
+ return null;
86
+ // An origin has no path, query, fragment or credentials. Accept a bare
87
+ // trailing slash (a common paste), refuse anything that names more.
88
+ if (url.username !== '' || url.password !== '')
89
+ return null;
90
+ if ((url.pathname !== '/' && url.pathname !== '') || url.search !== '' || url.hash !== '') {
91
+ return null;
92
+ }
93
+ return url.origin;
94
+ }
95
+ /** Read the configured value (array from YAML, already split from env). */
96
+ function parseConfiguredOrigins(value) {
97
+ if (!Array.isArray(value))
98
+ return { origins: [], rejected: [] };
99
+ const origins = [];
100
+ const rejected = [];
101
+ for (const entry of value) {
102
+ if (typeof entry !== 'string') {
103
+ rejected.push(String(entry));
104
+ continue;
105
+ }
106
+ const normalized = normalizeOrigin(entry);
107
+ if (normalized === null)
108
+ rejected.push(entry);
109
+ else
110
+ origins.push(normalized);
111
+ }
112
+ return { origins, rejected };
113
+ }
114
+ /** The built-in entries plus the optional public origin. */
115
+ function builtInOrigins(publicOrigin) {
116
+ const entries = [exports.VIEWER_PWA_ORIGIN, exports.OPAQUE_ORIGIN];
117
+ const pub = publicOrigin !== undefined ? normalizeOrigin(publicOrigin) : null;
118
+ if (pub !== null && pub !== exports.OPAQUE_ORIGIN)
119
+ entries.push(pub);
120
+ return entries;
121
+ }
122
+ /** The first value of a possibly comma-joined forwarding header, lower case. */
123
+ function firstHost(value) {
124
+ if (value === undefined)
125
+ return null;
126
+ const first = value.split(',')[0]?.trim().toLowerCase() ?? '';
127
+ return first.length > 0 ? first : null;
128
+ }
129
+ /** `host[:port]` of an http(s) origin, or null. */
130
+ function hostOfOrigin(origin) {
131
+ if (origin === exports.OPAQUE_ORIGIN)
132
+ return null;
133
+ try {
134
+ return new URL(origin).host.toLowerCase();
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ /**
141
+ * Judge one request's `Origin`. `listed` is the full allowlist (built-ins and
142
+ * the operator's extras), already normalised.
143
+ */
144
+ function judgeCorsOrigin(facts, listed) {
145
+ const raw = facts.origin;
146
+ if (raw === undefined || raw.length === 0)
147
+ return { kind: 'no-origin' };
148
+ const normalized = normalizeOrigin(raw);
149
+ if (normalized === null)
150
+ return { kind: 'denied', origin: raw };
151
+ if (listed.has(normalized))
152
+ return { kind: 'allowed', origin: raw, reason: 'listed' };
153
+ const originHost = hostOfOrigin(normalized);
154
+ if (originHost !== null) {
155
+ const ownHosts = [firstHost(facts.host), firstHost(facts.forwardedHost)];
156
+ if (ownHosts.includes(originHost))
157
+ return { kind: 'allowed', origin: raw, reason: 'own-host' };
158
+ }
159
+ return { kind: 'denied', origin: raw };
160
+ }
161
+ /**
162
+ * The headers an ALLOWED origin gets. `preflight` adds the method/header
163
+ * grants and the cache lifetime. The origin is the exact one the browser
164
+ * sent — reflecting it is what lets the browser hand the response to the
165
+ * page, and `Vary: Origin` (set by the glue on every response) keeps a cache
166
+ * from serving one origin's answer to another.
167
+ */
168
+ function corsResponseHeaders(origin, preflight) {
169
+ const base = {
170
+ 'access-control-allow-origin': origin,
171
+ 'access-control-expose-headers': exports.CORS_EXPOSED_HEADERS,
172
+ };
173
+ if (preflight === null)
174
+ return base;
175
+ return {
176
+ ...base,
177
+ 'access-control-allow-methods': exports.CORS_ALLOWED_METHODS,
178
+ 'access-control-allow-headers': exports.CORS_ALLOWED_HEADERS,
179
+ 'access-control-max-age': String(exports.CORS_PREFLIGHT_MAX_AGE_SEC),
180
+ // A public origin (the PWA) calling a LAN hub is a "private network"
181
+ // request to Chromium; its preflight must be answered explicitly.
182
+ ...(preflight.requestPrivateNetwork ? { 'access-control-allow-private-network': 'true' } : {}),
183
+ };
184
+ }
185
+ /** Append `token` to a `Vary` value unless it is already there. */
186
+ function appendVary(existing, token) {
187
+ if (existing === undefined || existing.trim().length === 0)
188
+ return token;
189
+ const parts = existing.split(',').map((p) => p.trim().toLowerCase());
190
+ if (parts.includes('*') || parts.includes(token.toLowerCase()))
191
+ return existing;
192
+ return `${existing}, ${token}`;
193
+ }
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCorsPolicy = createCorsPolicy;
4
+ exports.registerCorsPolicy = registerCorsPolicy;
5
+ exports.refuseForeignWsUpgrade = refuseForeignWsUpgrade;
6
+ const cors_policy_js_1 = require("./cors-policy.js");
7
+ /** Distinct refused origins remembered for log de-duplication. */
8
+ const MAX_LOGGED_DENIALS = 256;
9
+ function headerValue(value) {
10
+ if (Array.isArray(value))
11
+ return value[0];
12
+ return value;
13
+ }
14
+ function varyText(value) {
15
+ if (typeof value === 'string')
16
+ return value;
17
+ if (Array.isArray(value))
18
+ return value.filter((v) => typeof v === 'string').join(', ');
19
+ return undefined;
20
+ }
21
+ function createCorsPolicy(deps) {
22
+ const builtIns = (0, cors_policy_js_1.builtInOrigins)(deps.publicOrigin);
23
+ let lastConfigured = Symbol('unset');
24
+ let listed = new Set(builtIns);
25
+ const loggedDenials = new Set();
26
+ const currentList = () => {
27
+ const configured = deps.configuredOrigins();
28
+ if (configured === lastConfigured)
29
+ return listed;
30
+ lastConfigured = configured;
31
+ const parsed = (0, cors_policy_js_1.parseConfiguredOrigins)(configured);
32
+ if (parsed.rejected.length > 0) {
33
+ deps.logger.warn('cors: ignored configured origins that are not http(s) origins', {
34
+ meta: { rejected: parsed.rejected, setting: 'server.corsOrigins' },
35
+ });
36
+ }
37
+ listed = new Set([...builtIns, ...parsed.origins]);
38
+ return listed;
39
+ };
40
+ return {
41
+ judge(headers) {
42
+ const verdict = (0, cors_policy_js_1.judgeCorsOrigin)({
43
+ origin: headerValue(headers.origin),
44
+ host: headerValue(headers.host),
45
+ forwardedHost: headerValue(headers['x-forwarded-host']),
46
+ }, currentList());
47
+ if (verdict.kind === 'denied' && !loggedDenials.has(verdict.origin)) {
48
+ if (loggedDenials.size < MAX_LOGGED_DENIALS)
49
+ loggedDenials.add(verdict.origin);
50
+ deps.logger.warn('cors: refused a cross-origin caller not on the allowlist', {
51
+ meta: {
52
+ origin: verdict.origin,
53
+ host: headerValue(headers.host) ?? null,
54
+ hint: 'add it to server.corsOrigins (config.yaml) or CAMSTACK_CORS_ORIGINS',
55
+ },
56
+ });
57
+ }
58
+ return verdict;
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * Install the policy on the hub's Fastify instance. Every `OPTIONS` is
64
+ * answered here with 204 (as before D707) — with grants only for an allowed
65
+ * origin, so a refused preflight fails in the browser.
66
+ */
67
+ function registerCorsPolicy(fastify, policy) {
68
+ fastify.addHook('onRequest', async (request, reply) => {
69
+ // The answer depends on Origin for every route, so every response says so
70
+ // — a cached same-origin answer must never be replayed to another origin.
71
+ const vary = (0, cors_policy_js_1.appendVary)(varyText(reply.getHeader('vary')), 'Origin');
72
+ reply.header('vary', vary);
73
+ reply.raw.setHeader('vary', vary);
74
+ const verdict = policy.judge(request.headers);
75
+ const isPreflight = request.method === 'OPTIONS' && request.headers['access-control-request-method'] !== undefined;
76
+ if (verdict.kind === 'allowed') {
77
+ const headers = (0, cors_policy_js_1.corsResponseHeaders)(verdict.origin, isPreflight
78
+ ? {
79
+ requestPrivateNetwork: headerValue(request.headers['access-control-request-private-network']) === 'true',
80
+ }
81
+ : null);
82
+ for (const [name, value] of Object.entries(headers)) {
83
+ reply.header(name, value);
84
+ // Hijacked replies (the data-plane proxy) skip Fastify's header
85
+ // write; Node merges these into their `writeHead`.
86
+ reply.raw.setHeader(name, value);
87
+ }
88
+ }
89
+ if (request.method === 'OPTIONS') {
90
+ return reply.code(204).send();
91
+ }
92
+ return undefined;
93
+ });
94
+ }
95
+ /**
96
+ * Refuse a WebSocket upgrade whose `Origin` is not on the allowlist. Returns
97
+ * `true` when it refused (the socket is answered 403 and destroyed); the
98
+ * caller must then do nothing else with it. A request with no `Origin` (a
99
+ * Node client, an agent) passes — Origin is the browser's signal, and auth is
100
+ * the bearer in `connectionParams` either way.
101
+ */
102
+ function refuseForeignWsUpgrade(policy, headers, socket) {
103
+ const verdict = policy.judge(headers);
104
+ if (verdict.kind !== 'denied')
105
+ return false;
106
+ socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
107
+ socket.destroy();
108
+ return true;
109
+ }
@@ -75,8 +75,8 @@ function tarModelFiles(cwd, files) {
75
75
  * `nodeId === 'hub'` is a no-op (the artifact is already resident).
76
76
  */
77
77
  async function distributeModelWith(seam, input) {
78
- const { nodeId, modelId, format, entry } = input;
79
- if (nodeId === 'hub') {
78
+ const { targetNodeId, modelId, format, entry } = input;
79
+ if (targetNodeId === 'hub') {
80
80
  return { ok: true, sha256: '', bytes: 0, path: seam.modelsDir };
81
81
  }
82
82
  if (!seam.isPresent(entry, format)) {
@@ -88,7 +88,7 @@ async function distributeModelWith(seam, input) {
88
88
  }
89
89
  const tgz = await seam.tar(files);
90
90
  const source = seam.stage(tgz);
91
- const res = await seam.callAgent(nodeId, { modelId, format, source });
91
+ const res = await seam.callAgent(targetNodeId, { modelId, format, source });
92
92
  return { ok: true, sha256: source.sha256, bytes: source.bytes, path: res.path ?? '' };
93
93
  }
94
94
  /**
@@ -120,7 +120,7 @@ function buildModelDistributorProvider(moleculer, loggingService) {
120
120
  return {
121
121
  distributeModel: async (input) => {
122
122
  logger.info('Distributing model to node', {
123
- meta: { modelId: input.modelId, format: input.format, nodeId: input.nodeId },
123
+ meta: { modelId: input.modelId, format: input.format, nodeId: input.targetNodeId },
124
124
  });
125
125
  return distributeModelWith(seam, input);
126
126
  },
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Used by `/api/oauth2/authorize` to admit a `redirect_uri` whose host the hub
6
6
  * cannot know in advance — a self-hosted Home Assistant at
7
- * `http://192.168.x.x:8123/auth/external/callback`. Pairing "private host" with
7
+ * `http://192.168.10.40:8123/auth/external/callback`. Pairing "private host" with
8
8
  * "exact path" is what keeps that from being a wildcard prefix: a public host
9
9
  * never satisfies it, so the worst an attacker can aim a code at is a machine
10
10
  * they already need LAN presence to read.
@@ -101,7 +101,7 @@ function extractUserAgent(req) {
101
101
  return null;
102
102
  }
103
103
  /**
104
- * Strip an IPv4-mapped IPv6 prefix (`::ffff:192.168.x.x` → `192.168.x.x`)
104
+ * Strip an IPv4-mapped IPv6 prefix (`::ffff:192.168.10.5` → `192.168.10.5`)
105
105
  * and any zone id (`fe80::1%en0` → `fe80::1`) so the range checks below
106
106
  * see a clean address.
107
107
  */
@@ -216,13 +216,13 @@ function isPrivateIpv4(ip) {
216
216
  if (a === undefined || b === undefined)
217
217
  return false;
218
218
  if (a === 10)
219
- return true; // 10.x.x.x/8
219
+ return true; // 10.0.0.0/8
220
220
  if (a === 127)
221
221
  return true; // 127.0.0.0/8 loopback
222
222
  if (a === 172 && b >= 16 && b <= 31)
223
223
  return true; // 172.16.0.0/12
224
224
  if (a === 192 && b === 168)
225
- return true; // 192.168.x.x/16
225
+ return true; // 192.168.0.0/16
226
226
  if (a === 169 && b === 254)
227
227
  return true; // 169.254.0.0/16 link-local
228
228
  return false;
@@ -347,7 +347,7 @@ function redactConfigEntries(data) {
347
347
  *
348
348
  * The stream surfaces (`cameraStreams.getRtspEntries`,
349
349
  * `streamBroker.getRtspEntry`, `deviceOps.getStreamSources`, …) hand a
350
- * non-admin caller `rtsp://admin:hunter2@192.168.x.x/…` verbatim: a
350
+ * non-admin caller `rtsp://admin:hunter2@192.168.10.139/…` verbatim: a
351
351
  * read-only token can open every camera by copy-pasting the string. These
352
352
  * are NOT config blobs, so {@link redactRecord} never sees them — the leak
353
353
  * lives in a plain `url` / `mutedUrl` string field.
package/dist/main.js CHANGED
@@ -70,6 +70,7 @@ const precompressed_asset_js_1 = require("./api/static/precompressed-asset.js");
70
70
  const spa_static_1 = require("./api/static/spa-static");
71
71
  const spa_mount_1 = require("./api/static/spa-mount");
72
72
  const frame_policy_1 = require("./api/static/frame-policy");
73
+ const register_cors_policy_1 = require("./api/cors/register-cors-policy");
73
74
  const cap_providers_1 = require("./api/core/cap-providers");
74
75
  const logging_settings_1 = require("./api/core/logging-settings");
75
76
  const request_census_settings_1 = require("./api/core/request-census-settings");
@@ -347,8 +348,18 @@ async function bootstrap() {
347
348
  const fastifyOpts = tlsOptions ? { https: tlsOptions } : {};
348
349
  const app = await (0, manual_boot_1.bootManual)({ infra, fastifyOpts });
349
350
  app.enableShutdownHooks();
350
- app.enableCors();
351
351
  const fastify = app.getHttpAdapter().getInstance();
352
+ // Which browser origins may read hub responses, and open the /trpc socket
353
+ // (D707): the hub's own, the hosted viewer PWA, the native embed's opaque
354
+ // `null`, and the operator's `server.corsOrigins`. First hook on the
355
+ // instance so every route — and every OPTIONS — is judged.
356
+ const corsConfig = app.get(config_service_1.ConfigService);
357
+ const corsPolicy = (0, register_cors_policy_1.createCorsPolicy)({
358
+ configuredOrigins: () => corsConfig.get('server.corsOrigins'),
359
+ publicOrigin: process.env.CAMSTACK_PUBLIC_ORIGIN,
360
+ logger: app.get(logging_service_1.LoggingService).createLogger('cors'),
361
+ });
362
+ (0, register_cors_policy_1.registerCorsPolicy)(fastify, corsPolicy);
352
363
  await fastify.register(cookie_1.default);
353
364
  // Every HTML document the hub serves may be framed by its own origin only
354
365
  // (D425). Stamped here, on the one instance, so no shell-send site added
@@ -1239,6 +1250,9 @@ async function bootstrap() {
1239
1250
  httpServer.on('upgrade', (request, socket, head) => {
1240
1251
  const pathname = (request.url ?? '').split('?')[0];
1241
1252
  if (pathname === '/trpc') {
1253
+ // CORS does not cover a WebSocket: refuse a foreign page here (D707).
1254
+ if ((0, register_cors_policy_1.refuseForeignWsUpgrade)(corsPolicy, request.headers, socket))
1255
+ return;
1242
1256
  wss.handleUpgrade(request, socket, head, (ws) => {
1243
1257
  wss.emit('connection', ws, request);
1244
1258
  });
@@ -394,16 +394,6 @@ async function bootManual(opts) {
394
394
  getHttpAdapter() {
395
395
  return { getInstance: () => fastify };
396
396
  },
397
- enableCors() {
398
- fastify.addHook('onRequest', async (request, reply) => {
399
- reply.header('access-control-allow-origin', '*');
400
- reply.header('access-control-allow-methods', 'GET,POST,PUT,DELETE,OPTIONS,PATCH');
401
- reply.header('access-control-allow-headers', 'content-type,authorization');
402
- if (request.method === 'OPTIONS') {
403
- reply.status(204).send();
404
- }
405
- });
406
- },
407
397
  enableShutdownHooks() {
408
398
  if (shutdownHooksRegistered)
409
399
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.454",
3
+ "version": "1.2.456",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "files": [
@@ -37,18 +37,18 @@
37
37
  ]
38
38
  },
39
39
  "dependencies": {
40
- "@camstack/addon-admin-ui": "1.2.328",
41
- "@camstack/addon-agent-ui": "1.2.172",
40
+ "@camstack/addon-admin-ui": "1.2.330",
41
+ "@camstack/addon-agent-ui": "1.2.174",
42
42
  "@camstack/addon-auth": "1.2.126",
43
43
  "@camstack/addon-decoder-nodeav": "1.2.119",
44
- "@camstack/addon-notifiers": "1.2.126",
45
- "@camstack/addon-pipeline": "1.2.325",
46
- "@camstack/addon-pipeline-orchestrator": "1.2.280",
47
- "@camstack/addon-post-analysis": "1.2.331",
48
- "@camstack/sdk": "1.2.120",
49
- "@camstack/system": "1.2.352",
50
- "@camstack/types": "1.2.303",
51
- "@camstack/ui-library": "1.2.230",
44
+ "@camstack/addon-notifiers": "1.2.128",
45
+ "@camstack/addon-pipeline": "1.2.327",
46
+ "@camstack/addon-pipeline-orchestrator": "1.2.281",
47
+ "@camstack/addon-post-analysis": "1.2.333",
48
+ "@camstack/sdk": "1.2.121",
49
+ "@camstack/system": "1.2.354",
50
+ "@camstack/types": "1.2.305",
51
+ "@camstack/ui-library": "1.2.232",
52
52
  "@fastify/compress": "^9.0.0",
53
53
  "@fastify/cookie": "^11.0.2",
54
54
  "@fastify/cors": "^11.2.0",