@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.
- package/THIRD_PARTY_MODELS.md +29 -55
- package/dist/api/cors/cors-policy.js +193 -0
- package/dist/api/cors/register-cors-policy.js +109 -0
- package/dist/api/model-distributor.js +4 -4
- package/dist/api/oauth2/private-host.js +1 -1
- package/dist/api/trpc/client-ip.js +3 -3
- package/dist/api/trpc/device-config-secret-redaction.js +1 -1
- package/dist/main.js +15 -1
- package/dist/manual-boot.js +0 -10
- package/package.json +11 -11
package/THIRD_PARTY_MODELS.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
58
|
-
- **WIDER FACE** (training data of `
|
|
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`, `
|
|
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
|
|
74
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
93
|
-
|
|
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)
|
|
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
|
-
| `
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 {
|
|
79
|
-
if (
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
});
|
package/dist/manual-boot.js
CHANGED
|
@@ -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.
|
|
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.
|
|
41
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
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.
|
|
45
|
-
"@camstack/addon-pipeline": "1.2.
|
|
46
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
47
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
48
|
-
"@camstack/sdk": "1.2.
|
|
49
|
-
"@camstack/system": "1.2.
|
|
50
|
-
"@camstack/types": "1.2.
|
|
51
|
-
"@camstack/ui-library": "1.2.
|
|
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",
|