@camstack/addon-ai 0.4.15 → 0.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/addon.js +308 -31
  2. package/dist/addon.mjs +308 -31
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -15,7 +15,7 @@ let node_zlib = require("node:zlib");
15
15
  let node_fs_promises = require("node:fs/promises");
16
16
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
17
17
  let node_child_process = require("node:child_process");
18
- //#region ../types/dist/event-category-XfKNtfCc.mjs
18
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
19
19
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
20
20
  EventCategory["SystemBoot"] = "system.boot";
21
21
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -31,6 +31,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
31
31
  */
32
32
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
33
33
  /**
34
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
35
+ * Emitted only when the material on disk actually changed, so an
36
+ * operator who trusted the old certificate by hand is told rather than
37
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
38
+ *
39
+ * Rule: docs/decisions/adr-0227-*.md
40
+ */
41
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
42
+ /**
34
43
  * A newer addon or server-root package version was found by the
35
44
  * authoritative registry check. Emitted once when any observed
36
45
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8217,6 +8226,15 @@ var LabelDefinitionSchema = object({
8217
8226
  description: string().optional(),
8218
8227
  icon: string().optional()
8219
8228
  });
8229
+ var ClassMapDefinitionSchema = object({
8230
+ mapping: record(string(), _enum([
8231
+ "person",
8232
+ "vehicle",
8233
+ "animal",
8234
+ "package"
8235
+ ])),
8236
+ preserveOriginal: boolean()
8237
+ });
8220
8238
  var MODEL_FORMATS = [
8221
8239
  "onnx",
8222
8240
  "coreml",
@@ -8395,7 +8413,13 @@ var ModelCatalogEntrySchema = object({
8395
8413
  * `id` stays the source of truth for resolution/download/persistence; grouping
8396
8414
  * is a presentation overlay resolved back to an `id`.
8397
8415
  */
8398
- group: ModelVariantGroupSchema.optional()
8416
+ group: ModelVariantGroupSchema.optional(),
8417
+ /**
8418
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8419
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8420
+ * labels already ARE the CamStack macros (Scrypted identity map).
8421
+ */
8422
+ classMap: ClassMapDefinitionSchema.optional()
8399
8423
  });
8400
8424
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8401
8425
  format: literal("openvino"),
@@ -8424,7 +8448,8 @@ var ModelConvertMetadataSchema = object({
8424
8448
  "ocr",
8425
8449
  "segmentation"
8426
8450
  ]),
8427
- faceAlignment: boolean().optional()
8451
+ faceAlignment: boolean().optional(),
8452
+ classMap: ClassMapDefinitionSchema.optional()
8428
8453
  });
8429
8454
  var ConvertResultSchema = object({
8430
8455
  entry: ModelCatalogEntrySchema,
@@ -17496,6 +17521,33 @@ var NativeCropRefSchema = object({
17496
17521
  h: number$1()
17497
17522
  })
17498
17523
  });
17524
+ object({
17525
+ crop: object({
17526
+ left: number$1(),
17527
+ top: number$1(),
17528
+ width: number$1().positive(),
17529
+ height: number$1().positive()
17530
+ }).optional(),
17531
+ content: object({
17532
+ width: number$1().int().positive(),
17533
+ height: number$1().int().positive()
17534
+ }),
17535
+ fit: _enum(["stretch", "contain"]),
17536
+ format: _enum([
17537
+ "rgb",
17538
+ "gray",
17539
+ "jpeg"
17540
+ ])
17541
+ });
17542
+ var FrameRefSchema = object({
17543
+ registryId: string().min(1),
17544
+ id: string().min(1),
17545
+ width: number$1().int().positive(),
17546
+ height: number$1().int().positive(),
17547
+ format: _enum(["rgb", "gray"]),
17548
+ timestamp: number$1(),
17549
+ capturedAt: number$1().optional()
17550
+ });
17499
17551
  var ModelFormatSchema$1 = _enum([
17500
17552
  "onnx",
17501
17553
  "coreml",
@@ -17740,6 +17792,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17740
17792
  steps: array(PipelineStepInputSchema).min(1),
17741
17793
  frame: FrameInputSchema.optional(),
17742
17794
  /**
17795
+ * Process-local lazy frame. Valid only when caller and provider resolve
17796
+ * in the same execution-group process; split/cross-node callers use
17797
+ * `frame`/`image` inline compatibility instead.
17798
+ */
17799
+ frameRef: FrameRefSchema.optional(),
17800
+ /**
17743
17801
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17744
17802
  * the decoded pixels live in. One more member of the one-of
17745
17803
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -17995,7 +18053,10 @@ var NativeCropResultSchema = object({
17995
18053
  * Which source served this crop, so a quality-sensitive consumer (the native
17996
18054
  * `keyFrame`) can reject a degraded fallback:
17997
18055
  * - `native` — cut from the decode worker's retained NATIVE surface (the
17998
- * quality path).
18056
+ * quality path). A subject-tile serve is also native-resolution and stays
18057
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18058
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18059
+ * internal crop result (`nativeHits` vs `tileHits`).
17999
18060
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18000
18061
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18001
18062
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18486,12 +18547,41 @@ var RunnerLocalLoadSchema = object({
18486
18547
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18487
18548
  * working unchanged when they switch to reading from the runner cap.
18488
18549
  */
18550
+ var FrameLazyCountersSchema = object({
18551
+ framesDecoded: number$1(),
18552
+ framesAdmitted: number$1(),
18553
+ framesDroppedPixelFree: number$1(),
18554
+ viewsMaterialized: number$1(),
18555
+ viewsSkipped: number$1(),
18556
+ workerToRunnerBytes: number$1(),
18557
+ runnerToPoolRawBytes: number$1(),
18558
+ runnerToPoolJpegBytes: number$1(),
18559
+ onDemandFullFrameRequests: number$1(),
18560
+ onDemandCropRequests: number$1(),
18561
+ nativeHits: number$1(),
18562
+ nativeMisses: number$1(),
18563
+ tileHits: number$1(),
18564
+ tileMisses: number$1(),
18565
+ fallbackHits: number$1(),
18566
+ fallbackMisses: number$1(),
18567
+ retainedWritesAvoided: number$1(),
18568
+ residentRefs: number$1(),
18569
+ residentBytes: number$1(),
18570
+ releases: number$1(),
18571
+ evictions: number$1(),
18572
+ staleMisses: number$1()
18573
+ });
18574
+ var FrameLazyMetricsSchema = object({
18575
+ node: FrameLazyCountersSchema,
18576
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number$1() }))
18577
+ });
18489
18578
  var RunnerLocalMetricsSchema = object({
18490
18579
  nodeId: string(),
18491
18580
  activeCameras: number$1(),
18492
18581
  throttledCameras: number$1(),
18493
18582
  avgInferenceTimeMs: number$1(),
18494
- queueDepth: number$1()
18583
+ queueDepth: number$1(),
18584
+ frameLazy: FrameLazyMetricsSchema.optional()
18495
18585
  });
18496
18586
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number$1() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number$1() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number$1()).readonly()), method(object({
18497
18587
  handle: FrameHandleSchema,
@@ -19791,6 +19881,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19791
19881
  location: StorageLocationSchema,
19792
19882
  relativePath: string()
19793
19883
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number$1().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19884
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19885
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19886
+ var ProfileSettingsBagSchema = record(string(), unknown());
19794
19887
  /**
19795
19888
  * A live terminal session hosted by the provider addon. Output and input do
19796
19889
  * NOT flow through the capability — they use the addon data plane
@@ -19820,7 +19913,14 @@ var TerminalSessionInfoSchema = object({
19820
19913
  var TerminalProfileInfoSchema = object({
19821
19914
  profileId: string(),
19822
19915
  label: string(),
19823
- description: string().optional()
19916
+ description: string().optional(),
19917
+ /** Spawn defaults the instance form copies on create. */
19918
+ executable: string().optional(),
19919
+ args: array(string()).readonly().optional(),
19920
+ cwd: string().optional(),
19921
+ environment: array(string()).readonly().optional(),
19922
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19923
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19824
19924
  });
19825
19925
  /**
19826
19926
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19833,7 +19933,12 @@ var TerminalInstanceInfoSchema = object({
19833
19933
  profileId: string(),
19834
19934
  profileLabel: string(),
19835
19935
  name: string(),
19836
- enabled: boolean()
19936
+ enabled: boolean(),
19937
+ executable: string(),
19938
+ args: array(string()).readonly(),
19939
+ cwd: string(),
19940
+ environment: array(string()).readonly(),
19941
+ profileSettings: ProfileSettingsBagSchema
19837
19942
  });
19838
19943
  var TerminalLegacyCameraSchema = object({
19839
19944
  stableId: string(),
@@ -19863,7 +19968,23 @@ var TerminalOutputBatchSchema = object({
19863
19968
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19864
19969
  targetNodeId: string().min(1),
19865
19970
  profileId: string().min(1),
19866
- name: string().trim().min(1).max(160).optional()
19971
+ name: string().trim().min(1).max(160).optional(),
19972
+ executable: string().max(1024).optional(),
19973
+ args: array(string().max(2048)).max(64).optional(),
19974
+ cwd: string().max(1024).optional(),
19975
+ environment: array(string().max(4096)).max(64).optional(),
19976
+ profileSettings: ProfileSettingsBagSchema.optional()
19977
+ }), TerminalInstanceInfoSchema, {
19978
+ kind: "mutation",
19979
+ auth: "admin"
19980
+ }), method(object({
19981
+ instanceId: string().min(1),
19982
+ name: string().trim().min(1).max(160).optional(),
19983
+ executable: string().max(1024).optional(),
19984
+ args: array(string().max(2048)).max(64).optional(),
19985
+ cwd: string().max(1024).optional(),
19986
+ environment: array(string().max(4096)).max(64).optional(),
19987
+ profileSettings: ProfileSettingsBagSchema.optional()
19867
19988
  }), TerminalInstanceInfoSchema, {
19868
19989
  kind: "mutation",
19869
19990
  auth: "admin"
@@ -19885,7 +20006,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19885
20006
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19886
20007
  profileId: string(),
19887
20008
  cols: number$1().int().positive(),
19888
- rows: number$1().int().positive()
20009
+ rows: number$1().int().positive(),
20010
+ executable: string().max(1024).optional(),
20011
+ args: array(string().max(2048)).max(64).optional(),
20012
+ cwd: string().max(1024).optional(),
20013
+ environment: array(string().max(4096)).max(64).optional()
19889
20014
  }), TerminalSessionInfoSchema, {
19890
20015
  kind: "mutation",
19891
20016
  auth: "admin"
@@ -22667,10 +22792,10 @@ DeviceType.LawnMower, method(object({ deviceId: number$1().int().nonnegative() }
22667
22792
  *
22668
22793
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22669
22794
  * to receive an ordered list of candidate base URLs it should race
22670
- * on connect — LAN IPv4 first (lowest latency when on same network),
22671
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22672
- * race them with short timeouts and stick with the winner for the
22673
- * session.
22795
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
22796
+ * when on the same network), then public hostname (if a tunnel is
22797
+ * up). The SDK can race them with short timeouts and stick with the
22798
+ * winner for the session.
22674
22799
  *
22675
22800
  * Why hub-only: agents are not directly addressable by the operator's
22676
22801
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22825,6 +22950,17 @@ var NotificationEndpointSchema = object({
22825
22950
  /** What the ranking currently resolves to (null when nothing is reachable). */
22826
22951
  resolved: string().nullable()
22827
22952
  });
22953
+ /**
22954
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
22955
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
22956
+ * currently expands to, so the UI can show the effective set either way.
22957
+ */
22958
+ var ViewerEndpointsSchema = object({
22959
+ /** The operator's explicit race set, or empty for AUTO. */
22960
+ baseUrls: array(string()).readonly(),
22961
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
22962
+ resolved: array(string()).readonly()
22963
+ });
22828
22964
  var AllowedAddressesSchema = object({
22829
22965
  /**
22830
22966
  * Allowlist of interface addresses operators have explicitly opted
@@ -22833,6 +22969,20 @@ var AllowedAddressesSchema = object({
22833
22969
  * Network Addresses admin page and persisted by the addon.
22834
22970
  */
22835
22971
  addresses: array(string()).readonly() });
22972
+ var TlsStatusSchema = object({
22973
+ mode: _enum([
22974
+ "generated",
22975
+ "uploaded",
22976
+ "disabled"
22977
+ ]),
22978
+ leafFingerprintSha256: string().nullable(),
22979
+ caFingerprintSha256: string().nullable(),
22980
+ validTo: string().nullable(),
22981
+ sans: array(string()),
22982
+ caCertPem: string().nullable(),
22983
+ reissueError: string().nullable(),
22984
+ restartRequired: boolean()
22985
+ });
22836
22986
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22837
22987
  /**
22838
22988
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22842,17 +22992,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22842
22992
  */
22843
22993
  port: number$1().int().min(1).max(65535).optional(),
22844
22994
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22845
- * candidate. Default `true`. */
22995
+ * candidate. Default `false` — loopback is not a client route. */
22846
22996
  includeLoopback: boolean().optional(),
22847
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22848
- * Default `false`. */
22997
+ /** Skip IPv6 entries. Default `false` the palette includes stable
22998
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
22999
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22849
23000
  ipv4Only: boolean().optional(),
22850
23001
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22851
23002
  * Pass `'https'` when the caller is itself loaded over HTTPS
22852
23003
  * to avoid mixed-content blocks in the browser. The public
22853
23004
  * tunnel always emits `https://` regardless. */
22854
23005
  scheme: _enum(["http", "https"]).optional()
22855
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23006
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
23007
+ kind: "mutation",
23008
+ auth: "admin"
23009
+ }), method(object({
23010
+ certPem: string().min(1),
23011
+ keyPem: string().min(1),
23012
+ caPem: string().optional()
23013
+ }), TlsStatusSchema, {
23014
+ kind: "mutation",
23015
+ auth: "admin"
23016
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23017
+ kind: "mutation",
23018
+ auth: "admin"
23019
+ });
22856
23020
  object({
22857
23021
  /** Lifecycle state of the lock. `jammed` means the motor reported
22858
23022
  * failure to reach the target — operator intervention required. */
@@ -28615,6 +28779,12 @@ Object.freeze({
28615
28779
  addonId: null,
28616
28780
  access: "create"
28617
28781
  },
28782
+ "localNetwork.downloadCa": {
28783
+ capName: "local-network",
28784
+ capScope: "system",
28785
+ addonId: null,
28786
+ access: "view"
28787
+ },
28618
28788
  "localNetwork.getAllowedAddresses": {
28619
28789
  capName: "local-network",
28620
28790
  capScope: "system",
@@ -28639,18 +28809,42 @@ Object.freeze({
28639
28809
  addonId: null,
28640
28810
  access: "view"
28641
28811
  },
28812
+ "localNetwork.getTlsStatus": {
28813
+ capName: "local-network",
28814
+ capScope: "system",
28815
+ addonId: null,
28816
+ access: "view"
28817
+ },
28818
+ "localNetwork.getViewerEndpoints": {
28819
+ capName: "local-network",
28820
+ capScope: "system",
28821
+ addonId: null,
28822
+ access: "view"
28823
+ },
28642
28824
  "localNetwork.list": {
28643
28825
  capName: "local-network",
28644
28826
  capScope: "system",
28645
28827
  addonId: null,
28646
28828
  access: "view"
28647
28829
  },
28830
+ "localNetwork.regenerateCertificate": {
28831
+ capName: "local-network",
28832
+ capScope: "system",
28833
+ addonId: null,
28834
+ access: "create"
28835
+ },
28648
28836
  "localNetwork.resetAllowlistToBestMatch": {
28649
28837
  capName: "local-network",
28650
28838
  capScope: "system",
28651
28839
  addonId: null,
28652
28840
  access: "delete"
28653
28841
  },
28842
+ "localNetwork.revertToGeneratedCertificate": {
28843
+ capName: "local-network",
28844
+ capScope: "system",
28845
+ addonId: null,
28846
+ access: "create"
28847
+ },
28654
28848
  "localNetwork.setAllowedAddresses": {
28655
28849
  capName: "local-network",
28656
28850
  capScope: "system",
@@ -28663,6 +28857,18 @@ Object.freeze({
28663
28857
  addonId: null,
28664
28858
  access: "create"
28665
28859
  },
28860
+ "localNetwork.setViewerEndpoints": {
28861
+ capName: "local-network",
28862
+ capScope: "system",
28863
+ addonId: null,
28864
+ access: "create"
28865
+ },
28866
+ "localNetwork.uploadCertificate": {
28867
+ capName: "local-network",
28868
+ capScope: "system",
28869
+ addonId: null,
28870
+ access: "create"
28871
+ },
28666
28872
  "lockControl.lock": {
28667
28873
  capName: "lock-control",
28668
28874
  capScope: "device",
@@ -31543,6 +31749,12 @@ Object.freeze({
31543
31749
  addonId: null,
31544
31750
  access: "create"
31545
31751
  },
31752
+ "terminalSession.updateInstance": {
31753
+ capName: "terminal-session",
31754
+ capScope: "system",
31755
+ addonId: null,
31756
+ access: "create"
31757
+ },
31546
31758
  "terminalSession.writeInput": {
31547
31759
  capName: "terminal-session",
31548
31760
  capScope: "system",
@@ -34030,6 +34242,35 @@ Object.freeze(Object.fromEntries([{
34030
34242
  }]
34031
34243
  }].map((s) => [s.stepId, s.defaultModelId])));
34032
34244
  string().min(1);
34245
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34246
+ stepId: "face-embedding",
34247
+ key: "minLandmarkFaceSize",
34248
+ label: "Min face size for recognition (detection px)",
34249
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
34250
+ type: "slider",
34251
+ min: 0,
34252
+ max: 64,
34253
+ step: 2,
34254
+ default: 24
34255
+ }];
34256
+ function clusterStepSettingKey(stepId, fieldKey) {
34257
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34258
+ }
34259
+ var ClusterSettingNumberSchema = number$1().finite();
34260
+ function readClusterStepSettings(config) {
34261
+ const out = {};
34262
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34263
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34264
+ const value = parsed.success ? parsed.data : field.default;
34265
+ const existing = out[field.stepId] ?? {};
34266
+ out[field.stepId] = {
34267
+ ...existing,
34268
+ [field.key]: value
34269
+ };
34270
+ }
34271
+ return out;
34272
+ }
34273
+ readClusterStepSettings({});
34033
34274
  object({
34034
34275
  /**
34035
34276
  * Fraction of the box's own size added on EACH side before cutting.
@@ -71706,7 +71947,7 @@ function entryForRef(ref) {
71706
71947
  };
71707
71948
  }
71708
71949
  //#endregion
71709
- //#region ../system/dist/file-data-plane-CuE_hBli.mjs
71950
+ //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
71710
71951
  function isNonEmptyFile(filePath) {
71711
71952
  return node_fs.existsSync(filePath) && node_fs.statSync(filePath).size > 0;
71712
71953
  }
@@ -71728,21 +71969,56 @@ function buildHeaders(url) {
71728
71969
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
71729
71970
  return headers;
71730
71971
  }
71731
- /**
71732
- * Download a single file from a URL to a destination path.
71733
- * Uses native fetch() (Node 22+) which handles redirects natively.
71734
- * Streams to disk with optional progress callback.
71735
- * Returns the destination path. Skips download if file already exists.
71736
- */
71737
- async function downloadFile(url, destPath, onProgress) {
71972
+ var DEFAULT_MAX_REDIRECTS = 5;
71973
+ function normalizeDownloadOptions(third) {
71974
+ if (typeof third === "function") return { onProgress: third };
71975
+ return third ?? {};
71976
+ }
71977
+ function isRedirectStatus(status) {
71978
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
71979
+ }
71980
+ function resolveRedirectUrl(current, location) {
71981
+ return new URL(location, current);
71982
+ }
71983
+ async function downloadFile(url, destPath, onProgressOrOptions) {
71738
71984
  if (node_fs.existsSync(destPath)) return destPath;
71985
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
71986
+ const fetchImpl = opts.fetchImpl ?? fetch;
71987
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
71739
71988
  node_fs.mkdirSync(node_path$1.dirname(destPath), { recursive: true });
71740
71989
  const tmpPath = destPath + ".downloading";
71741
71990
  try {
71742
- const response = await fetch(url, {
71743
- redirect: "follow",
71744
- headers: buildHeaders(url)
71745
- });
71991
+ let current = url;
71992
+ const seen = /* @__PURE__ */ new Set();
71993
+ let response;
71994
+ const manual = opts.redirectPolicy !== void 0;
71995
+ for (let hop = 0; hop <= maxRedirects; hop++) {
71996
+ const parsed = new URL(current);
71997
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
71998
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
71999
+ seen.add(parsed.href);
72000
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
72001
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
72002
+ try {
72003
+ response = await fetchImpl(current, {
72004
+ redirect: manual ? "manual" : "follow",
72005
+ headers: buildHeaders(current),
72006
+ ...controller ? { signal: controller.signal } : {}
72007
+ });
72008
+ } finally {
72009
+ if (timer) clearTimeout(timer);
72010
+ }
72011
+ if (manual && isRedirectStatus(response.status)) {
72012
+ const location = response.headers.get("location");
72013
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
72014
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
72015
+ current = resolveRedirectUrl(current, location).href;
72016
+ continue;
72017
+ }
72018
+ break;
72019
+ }
72020
+ if (!response) throw new Error(`No response downloading ${url}`);
72021
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
71746
72022
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
71747
72023
  if (!response.body) throw new Error(`No response body from ${url}`);
71748
72024
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -71753,9 +72029,10 @@ async function downloadFile(url, destPath, onProgress) {
71753
72029
  for (;;) {
71754
72030
  const { done, value } = await reader.read();
71755
72031
  if (done || !value) break;
71756
- fileStream.write(value);
71757
72032
  downloaded += value.length;
71758
- onProgress?.(downloaded, total);
72033
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
72034
+ fileStream.write(value);
72035
+ opts.onProgress?.(downloaded, total);
71759
72036
  }
71760
72037
  } finally {
71761
72038
  fileStream.end();
package/dist/addon.mjs CHANGED
@@ -41,7 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
41
41
  }) : target, mod));
42
42
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
43
43
  //#endregion
44
- //#region ../types/dist/event-category-XfKNtfCc.mjs
44
+ //#region ../types/dist/event-category-CIa_iT6b.mjs
45
45
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
46
46
  EventCategory["SystemBoot"] = "system.boot";
47
47
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -57,6 +57,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
57
57
  */
58
58
  EventCategory["SystemRestartCompleted"] = "system.restart-completed";
59
59
  /**
60
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
61
+ * Emitted only when the material on disk actually changed, so an
62
+ * operator who trusted the old certificate by hand is told rather than
63
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
64
+ *
65
+ * Rule: docs/decisions/adr-0227-*.md
66
+ */
67
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
68
+ /**
60
69
  * A newer addon or server-root package version was found by the
61
70
  * authoritative registry check. Emitted once when any observed
62
71
  * `latestVersion` changes (or a package/node first appears behind);
@@ -8243,6 +8252,15 @@ var LabelDefinitionSchema = object({
8243
8252
  description: string().optional(),
8244
8253
  icon: string().optional()
8245
8254
  });
8255
+ var ClassMapDefinitionSchema = object({
8256
+ mapping: record(string(), _enum([
8257
+ "person",
8258
+ "vehicle",
8259
+ "animal",
8260
+ "package"
8261
+ ])),
8262
+ preserveOriginal: boolean()
8263
+ });
8246
8264
  var MODEL_FORMATS = [
8247
8265
  "onnx",
8248
8266
  "coreml",
@@ -8421,7 +8439,13 @@ var ModelCatalogEntrySchema = object({
8421
8439
  * `id` stays the source of truth for resolution/download/persistence; grouping
8422
8440
  * is a presentation overlay resolved back to an `id`.
8423
8441
  */
8424
- group: ModelVariantGroupSchema.optional()
8442
+ group: ModelVariantGroupSchema.optional(),
8443
+ /**
8444
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
8445
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
8446
+ * labels already ARE the CamStack macros (Scrypted identity map).
8447
+ */
8448
+ classMap: ClassMapDefinitionSchema.optional()
8425
8449
  });
8426
8450
  var ConvertTargetSchema = discriminatedUnion("format", [object({
8427
8451
  format: literal("openvino"),
@@ -8450,7 +8474,8 @@ var ModelConvertMetadataSchema = object({
8450
8474
  "ocr",
8451
8475
  "segmentation"
8452
8476
  ]),
8453
- faceAlignment: boolean().optional()
8477
+ faceAlignment: boolean().optional(),
8478
+ classMap: ClassMapDefinitionSchema.optional()
8454
8479
  });
8455
8480
  var ConvertResultSchema = object({
8456
8481
  entry: ModelCatalogEntrySchema,
@@ -17522,6 +17547,33 @@ var NativeCropRefSchema = object({
17522
17547
  h: number$1()
17523
17548
  })
17524
17549
  });
17550
+ object({
17551
+ crop: object({
17552
+ left: number$1(),
17553
+ top: number$1(),
17554
+ width: number$1().positive(),
17555
+ height: number$1().positive()
17556
+ }).optional(),
17557
+ content: object({
17558
+ width: number$1().int().positive(),
17559
+ height: number$1().int().positive()
17560
+ }),
17561
+ fit: _enum(["stretch", "contain"]),
17562
+ format: _enum([
17563
+ "rgb",
17564
+ "gray",
17565
+ "jpeg"
17566
+ ])
17567
+ });
17568
+ var FrameRefSchema = object({
17569
+ registryId: string().min(1),
17570
+ id: string().min(1),
17571
+ width: number$1().int().positive(),
17572
+ height: number$1().int().positive(),
17573
+ format: _enum(["rgb", "gray"]),
17574
+ timestamp: number$1(),
17575
+ capturedAt: number$1().optional()
17576
+ });
17525
17577
  var ModelFormatSchema$1 = _enum([
17526
17578
  "onnx",
17527
17579
  "coreml",
@@ -17766,6 +17818,12 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
17766
17818
  steps: array(PipelineStepInputSchema).min(1),
17767
17819
  frame: FrameInputSchema.optional(),
17768
17820
  /**
17821
+ * Process-local lazy frame. Valid only when caller and provider resolve
17822
+ * in the same execution-group process; split/cross-node callers use
17823
+ * `frame`/`image` inline compatibility instead.
17824
+ */
17825
+ frameRef: FrameRefSchema.optional(),
17826
+ /**
17769
17827
  * CB5 shm passthrough — a `FrameHandle` naming the same ring slot
17770
17828
  * the decoded pixels live in. One more member of the one-of
17771
17829
  * frame/frameHandle/image/imageBase64/referenceImage group.
@@ -18021,7 +18079,10 @@ var NativeCropResultSchema = object({
18021
18079
  * Which source served this crop, so a quality-sensitive consumer (the native
18022
18080
  * `keyFrame`) can reject a degraded fallback:
18023
18081
  * - `native` — cut from the decode worker's retained NATIVE surface (the
18024
- * quality path).
18082
+ * quality path). A subject-tile serve is also native-resolution and stays
18083
+ * `native` here: the public enum cannot name `tile` without a breaking cap
18084
+ * change. Runner telemetry distinguishes lease vs tile via `source` on the
18085
+ * internal crop result (`nativeHits` vs `tileHits`).
18025
18086
  * - `ram-fullframe` — the native surface MISSED but the request was full-frame,
18026
18087
  * so the ≤640 RAM `RetainedFrameStore` served it (honest lower-res; legit for
18027
18088
  * the blank-frame guard / 640-snapshot resolve, NOT for the clean keyFrame).
@@ -18512,12 +18573,41 @@ var RunnerLocalLoadSchema = object({
18512
18573
  * legacy `OrchestratorMetricsSchema` shape so existing dashboards keep
18513
18574
  * working unchanged when they switch to reading from the runner cap.
18514
18575
  */
18576
+ var FrameLazyCountersSchema = object({
18577
+ framesDecoded: number$1(),
18578
+ framesAdmitted: number$1(),
18579
+ framesDroppedPixelFree: number$1(),
18580
+ viewsMaterialized: number$1(),
18581
+ viewsSkipped: number$1(),
18582
+ workerToRunnerBytes: number$1(),
18583
+ runnerToPoolRawBytes: number$1(),
18584
+ runnerToPoolJpegBytes: number$1(),
18585
+ onDemandFullFrameRequests: number$1(),
18586
+ onDemandCropRequests: number$1(),
18587
+ nativeHits: number$1(),
18588
+ nativeMisses: number$1(),
18589
+ tileHits: number$1(),
18590
+ tileMisses: number$1(),
18591
+ fallbackHits: number$1(),
18592
+ fallbackMisses: number$1(),
18593
+ retainedWritesAvoided: number$1(),
18594
+ residentRefs: number$1(),
18595
+ residentBytes: number$1(),
18596
+ releases: number$1(),
18597
+ evictions: number$1(),
18598
+ staleMisses: number$1()
18599
+ });
18600
+ var FrameLazyMetricsSchema = object({
18601
+ node: FrameLazyCountersSchema,
18602
+ cameras: array(FrameLazyCountersSchema.extend({ deviceId: number$1() }))
18603
+ });
18515
18604
  var RunnerLocalMetricsSchema = object({
18516
18605
  nodeId: string(),
18517
18606
  activeCameras: number$1(),
18518
18607
  throttledCameras: number$1(),
18519
18608
  avgInferenceTimeMs: number$1(),
18520
- queueDepth: number$1()
18609
+ queueDepth: number$1(),
18610
+ frameLazy: FrameLazyMetricsSchema.optional()
18521
18611
  });
18522
18612
  method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mutation" }), method(object({ deviceId: number$1() }), object({ success: literal(true) }), { kind: "mutation" }), method(ReportMotionInputSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), RunnerLocalLoadSchema), method(_void(), RunnerLocalMetricsSchema), method(object({ deviceId: number$1() }), CameraMetricsSchema.nullable()), method(_void(), array(CameraMetricsWithDeviceIdSchema).readonly()), method(_void(), array(number$1()).readonly()), method(object({
18523
18613
  handle: FrameHandleSchema,
@@ -19817,6 +19907,9 @@ method(_void(), ProviderInfoSchema), method(object({ config: record(string(), un
19817
19907
  location: StorageLocationSchema,
19818
19908
  relativePath: string()
19819
19909
  }), _void(), { kind: "mutation" }), method(object({ location: StorageLocationSchema }), number$1().nullable()), method(BeginUploadInputSchema, BeginUploadResultSchema, { kind: "mutation" }), method(WriteChunkInputSchema, _void(), { kind: "mutation" }), method(FinalizeUploadInputSchema, _void(), { kind: "mutation" }), method(AbortUploadInputSchema, _void(), { kind: "mutation" }), method(BeginDownloadInputSchema, BeginDownloadResultSchema, { kind: "mutation" }), method(ReadChunkInputSchema, _instanceof(Uint8Array)), method(EndDownloadInputSchema, _void(), { kind: "mutation" });
19910
+ /** Profile-exported FormBuilder schema. Shape is ConfigUISchema at the UI. */
19911
+ var ProfileSettingsSchemaBridge = unknown().nullable();
19912
+ var ProfileSettingsBagSchema = record(string(), unknown());
19820
19913
  /**
19821
19914
  * A live terminal session hosted by the provider addon. Output and input do
19822
19915
  * NOT flow through the capability — they use the addon data plane
@@ -19846,7 +19939,14 @@ var TerminalSessionInfoSchema = object({
19846
19939
  var TerminalProfileInfoSchema = object({
19847
19940
  profileId: string(),
19848
19941
  label: string(),
19849
- description: string().optional()
19942
+ description: string().optional(),
19943
+ /** Spawn defaults the instance form copies on create. */
19944
+ executable: string().optional(),
19945
+ args: array(string()).readonly().optional(),
19946
+ cwd: string().optional(),
19947
+ environment: array(string()).readonly().optional(),
19948
+ /** ConfigUISchema for instance knobs, or null when the profile has none. */
19949
+ settingsSchema: ProfileSettingsSchemaBridge.optional()
19850
19950
  });
19851
19951
  /**
19852
19952
  * A durable operator-created Terminal instance. Profiles are templates; only
@@ -19859,7 +19959,12 @@ var TerminalInstanceInfoSchema = object({
19859
19959
  profileId: string(),
19860
19960
  profileLabel: string(),
19861
19961
  name: string(),
19862
- enabled: boolean()
19962
+ enabled: boolean(),
19963
+ executable: string(),
19964
+ args: array(string()).readonly(),
19965
+ cwd: string(),
19966
+ environment: array(string()).readonly(),
19967
+ profileSettings: ProfileSettingsBagSchema
19863
19968
  });
19864
19969
  var TerminalLegacyCameraSchema = object({
19865
19970
  stableId: string(),
@@ -19889,7 +19994,23 @@ var TerminalOutputBatchSchema = object({
19889
19994
  method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
19890
19995
  targetNodeId: string().min(1),
19891
19996
  profileId: string().min(1),
19892
- name: string().trim().min(1).max(160).optional()
19997
+ name: string().trim().min(1).max(160).optional(),
19998
+ executable: string().max(1024).optional(),
19999
+ args: array(string().max(2048)).max(64).optional(),
20000
+ cwd: string().max(1024).optional(),
20001
+ environment: array(string().max(4096)).max(64).optional(),
20002
+ profileSettings: ProfileSettingsBagSchema.optional()
20003
+ }), TerminalInstanceInfoSchema, {
20004
+ kind: "mutation",
20005
+ auth: "admin"
20006
+ }), method(object({
20007
+ instanceId: string().min(1),
20008
+ name: string().trim().min(1).max(160).optional(),
20009
+ executable: string().max(1024).optional(),
20010
+ args: array(string().max(2048)).max(64).optional(),
20011
+ cwd: string().max(1024).optional(),
20012
+ environment: array(string().max(4096)).max(64).optional(),
20013
+ profileSettings: ProfileSettingsBagSchema.optional()
19893
20014
  }), TerminalInstanceInfoSchema, {
19894
20015
  kind: "mutation",
19895
20016
  auth: "admin"
@@ -19911,7 +20032,11 @@ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }),
19911
20032
  }), method(_void(), array(TerminalSessionInfoSchema).readonly(), { auth: "admin" }), method(object({
19912
20033
  profileId: string(),
19913
20034
  cols: number$1().int().positive(),
19914
- rows: number$1().int().positive()
20035
+ rows: number$1().int().positive(),
20036
+ executable: string().max(1024).optional(),
20037
+ args: array(string().max(2048)).max(64).optional(),
20038
+ cwd: string().max(1024).optional(),
20039
+ environment: array(string().max(4096)).max(64).optional()
19915
20040
  }), TerminalSessionInfoSchema, {
19916
20041
  kind: "mutation",
19917
20042
  auth: "admin"
@@ -22693,10 +22818,10 @@ DeviceType.LawnMower, method(object({ deviceId: number$1().int().nonnegative() }
22693
22818
  *
22694
22819
  * • The SDK (mobile / web client) consumes `getConnectionEndpoints()`
22695
22820
  * to receive an ordered list of candidate base URLs it should race
22696
- * on connect — LAN IPv4 first (lowest latency when on same network),
22697
- * then public hostname (if a tunnel is up), then IPv6. The SDK can
22698
- * race them with short timeouts and stick with the winner for the
22699
- * session.
22821
+ * on connect — LAN IPv4 and stable LAN IPv6 first (lowest latency
22822
+ * when on the same network), then public hostname (if a tunnel is
22823
+ * up). The SDK can race them with short timeouts and stick with the
22824
+ * winner for the session.
22700
22825
  *
22701
22826
  * Why hub-only: agents are not directly addressable by the operator's
22702
22827
  * clients — they reverse-connect to the hub. Exposing their interfaces
@@ -22851,6 +22976,17 @@ var NotificationEndpointSchema = object({
22851
22976
  /** What the ranking currently resolves to (null when nothing is reachable). */
22852
22977
  resolved: string().nullable()
22853
22978
  });
22979
+ /**
22980
+ * The URLs the SDK / viewer should race for API access. `baseUrls` empty =
22981
+ * AUTO (every LAN IPv4 + the public tunnel). `resolved` is what that choice
22982
+ * currently expands to, so the UI can show the effective set either way.
22983
+ */
22984
+ var ViewerEndpointsSchema = object({
22985
+ /** The operator's explicit race set, or empty for AUTO. */
22986
+ baseUrls: array(string()).readonly(),
22987
+ /** What the ranking currently resolves to (may be empty if nothing is up). */
22988
+ resolved: array(string()).readonly()
22989
+ });
22854
22990
  var AllowedAddressesSchema = object({
22855
22991
  /**
22856
22992
  * Allowlist of interface addresses operators have explicitly opted
@@ -22859,6 +22995,20 @@ var AllowedAddressesSchema = object({
22859
22995
  * Network Addresses admin page and persisted by the addon.
22860
22996
  */
22861
22997
  addresses: array(string()).readonly() });
22998
+ var TlsStatusSchema = object({
22999
+ mode: _enum([
23000
+ "generated",
23001
+ "uploaded",
23002
+ "disabled"
23003
+ ]),
23004
+ leafFingerprintSha256: string().nullable(),
23005
+ caFingerprintSha256: string().nullable(),
23006
+ validTo: string().nullable(),
23007
+ sans: array(string()),
23008
+ caCertPem: string().nullable(),
23009
+ reissueError: string().nullable(),
23010
+ restartRequired: boolean()
23011
+ });
22862
23012
  method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
22863
23013
  /**
22864
23014
  * LEGACY HINT — do not send from new code. Kept optional so clients
@@ -22868,17 +23018,31 @@ method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(obje
22868
23018
  */
22869
23019
  port: number$1().int().min(1).max(65535).optional(),
22870
23020
  /** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
22871
- * candidate. Default `true`. */
23021
+ * candidate. Default `false` — loopback is not a client route. */
22872
23022
  includeLoopback: boolean().optional(),
22873
- /** Skip IPv6 entries. Some legacy clients can't parse them.
22874
- * Default `false`. */
23023
+ /** Skip IPv6 entries. Default `false` the palette includes stable
23024
+ * LAN IPv6 (ICE/WebRTC uses dual-stack regardless). Pass `true` to
23025
+ * hide them. The viewer HTTP/WS race is `getViewerEndpoints`. */
22875
23026
  ipv4Only: boolean().optional(),
22876
23027
  /** Scheme to emit for LAN/loopback URLs. Default `'http'`.
22877
23028
  * Pass `'https'` when the caller is itself loaded over HTTPS
22878
23029
  * to avoid mixed-content blocks in the browser. The public
22879
23030
  * tunnel always emits `https://` regardless. */
22880
23031
  scheme: _enum(["http", "https"]).optional()
22881
- }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" });
23032
+ }), GetConnectionEndpointsResultSchema), method(_void(), NotificationEndpointSchema), method(object({ baseUrl: string().nullable() }), NotificationEndpointSchema, { kind: "mutation" }), method(_void(), ViewerEndpointsSchema), method(object({ baseUrls: array(string()).readonly() }), ViewerEndpointsSchema, { kind: "mutation" }), method(_void(), AllowedAddressesSchema), method(AllowedAddressesSchema, object({ success: literal(true) }), { kind: "mutation" }), method(_void(), AllowedAddressesSchema, { kind: "mutation" }), method(_void(), TlsStatusSchema), method(object({ reason: string().optional() }), TlsStatusSchema, {
23033
+ kind: "mutation",
23034
+ auth: "admin"
23035
+ }), method(object({
23036
+ certPem: string().min(1),
23037
+ keyPem: string().min(1),
23038
+ caPem: string().optional()
23039
+ }), TlsStatusSchema, {
23040
+ kind: "mutation",
23041
+ auth: "admin"
23042
+ }), method(_void(), object({ pem: string() })), method(_void(), TlsStatusSchema, {
23043
+ kind: "mutation",
23044
+ auth: "admin"
23045
+ });
22882
23046
  object({
22883
23047
  /** Lifecycle state of the lock. `jammed` means the motor reported
22884
23048
  * failure to reach the target — operator intervention required. */
@@ -28641,6 +28805,12 @@ Object.freeze({
28641
28805
  addonId: null,
28642
28806
  access: "create"
28643
28807
  },
28808
+ "localNetwork.downloadCa": {
28809
+ capName: "local-network",
28810
+ capScope: "system",
28811
+ addonId: null,
28812
+ access: "view"
28813
+ },
28644
28814
  "localNetwork.getAllowedAddresses": {
28645
28815
  capName: "local-network",
28646
28816
  capScope: "system",
@@ -28665,18 +28835,42 @@ Object.freeze({
28665
28835
  addonId: null,
28666
28836
  access: "view"
28667
28837
  },
28838
+ "localNetwork.getTlsStatus": {
28839
+ capName: "local-network",
28840
+ capScope: "system",
28841
+ addonId: null,
28842
+ access: "view"
28843
+ },
28844
+ "localNetwork.getViewerEndpoints": {
28845
+ capName: "local-network",
28846
+ capScope: "system",
28847
+ addonId: null,
28848
+ access: "view"
28849
+ },
28668
28850
  "localNetwork.list": {
28669
28851
  capName: "local-network",
28670
28852
  capScope: "system",
28671
28853
  addonId: null,
28672
28854
  access: "view"
28673
28855
  },
28856
+ "localNetwork.regenerateCertificate": {
28857
+ capName: "local-network",
28858
+ capScope: "system",
28859
+ addonId: null,
28860
+ access: "create"
28861
+ },
28674
28862
  "localNetwork.resetAllowlistToBestMatch": {
28675
28863
  capName: "local-network",
28676
28864
  capScope: "system",
28677
28865
  addonId: null,
28678
28866
  access: "delete"
28679
28867
  },
28868
+ "localNetwork.revertToGeneratedCertificate": {
28869
+ capName: "local-network",
28870
+ capScope: "system",
28871
+ addonId: null,
28872
+ access: "create"
28873
+ },
28680
28874
  "localNetwork.setAllowedAddresses": {
28681
28875
  capName: "local-network",
28682
28876
  capScope: "system",
@@ -28689,6 +28883,18 @@ Object.freeze({
28689
28883
  addonId: null,
28690
28884
  access: "create"
28691
28885
  },
28886
+ "localNetwork.setViewerEndpoints": {
28887
+ capName: "local-network",
28888
+ capScope: "system",
28889
+ addonId: null,
28890
+ access: "create"
28891
+ },
28892
+ "localNetwork.uploadCertificate": {
28893
+ capName: "local-network",
28894
+ capScope: "system",
28895
+ addonId: null,
28896
+ access: "create"
28897
+ },
28692
28898
  "lockControl.lock": {
28693
28899
  capName: "lock-control",
28694
28900
  capScope: "device",
@@ -31569,6 +31775,12 @@ Object.freeze({
31569
31775
  addonId: null,
31570
31776
  access: "create"
31571
31777
  },
31778
+ "terminalSession.updateInstance": {
31779
+ capName: "terminal-session",
31780
+ capScope: "system",
31781
+ addonId: null,
31782
+ access: "create"
31783
+ },
31572
31784
  "terminalSession.writeInput": {
31573
31785
  capName: "terminal-session",
31574
31786
  capScope: "system",
@@ -34056,6 +34268,35 @@ Object.freeze(Object.fromEntries([{
34056
34268
  }]
34057
34269
  }].map((s) => [s.stepId, s.defaultModelId])));
34058
34270
  string().min(1);
34271
+ var CLUSTER_STEP_SETTING_FIELDS = [{
34272
+ stepId: "face-embedding",
34273
+ key: "minLandmarkFaceSize",
34274
+ label: "Min face size for recognition (detection px)",
34275
+ description: "Refuse to embed a face smaller than this IN THE DETECTION FRAME. Applies to every node — the enrolled gallery is one index, and two admission floors would fill it from two policies. 0 disables the gate.",
34276
+ type: "slider",
34277
+ min: 0,
34278
+ max: 64,
34279
+ step: 2,
34280
+ default: 24
34281
+ }];
34282
+ function clusterStepSettingKey(stepId, fieldKey) {
34283
+ return `clusterStepSetting:${stepId}:${fieldKey}`;
34284
+ }
34285
+ var ClusterSettingNumberSchema = number$1().finite();
34286
+ function readClusterStepSettings(config) {
34287
+ const out = {};
34288
+ for (const field of CLUSTER_STEP_SETTING_FIELDS) {
34289
+ const parsed = ClusterSettingNumberSchema.safeParse(config[clusterStepSettingKey(field.stepId, field.key)]);
34290
+ const value = parsed.success ? parsed.data : field.default;
34291
+ const existing = out[field.stepId] ?? {};
34292
+ out[field.stepId] = {
34293
+ ...existing,
34294
+ [field.key]: value
34295
+ };
34296
+ }
34297
+ return out;
34298
+ }
34299
+ readClusterStepSettings({});
34059
34300
  object({
34060
34301
  /**
34061
34302
  * Fraction of the box's own size added on EACH side before cutting.
@@ -71732,7 +71973,7 @@ function entryForRef(ref) {
71732
71973
  };
71733
71974
  }
71734
71975
  //#endregion
71735
- //#region ../system/dist/file-data-plane-CuE_hBli.mjs
71976
+ //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
71736
71977
  function isNonEmptyFile(filePath) {
71737
71978
  return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
71738
71979
  }
@@ -71754,21 +71995,56 @@ function buildHeaders(url) {
71754
71995
  if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
71755
71996
  return headers;
71756
71997
  }
71757
- /**
71758
- * Download a single file from a URL to a destination path.
71759
- * Uses native fetch() (Node 22+) which handles redirects natively.
71760
- * Streams to disk with optional progress callback.
71761
- * Returns the destination path. Skips download if file already exists.
71762
- */
71763
- async function downloadFile(url, destPath, onProgress) {
71998
+ var DEFAULT_MAX_REDIRECTS = 5;
71999
+ function normalizeDownloadOptions(third) {
72000
+ if (typeof third === "function") return { onProgress: third };
72001
+ return third ?? {};
72002
+ }
72003
+ function isRedirectStatus(status) {
72004
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
72005
+ }
72006
+ function resolveRedirectUrl(current, location) {
72007
+ return new URL(location, current);
72008
+ }
72009
+ async function downloadFile(url, destPath, onProgressOrOptions) {
71764
72010
  if (fs.existsSync(destPath)) return destPath;
72011
+ const opts = normalizeDownloadOptions(onProgressOrOptions);
72012
+ const fetchImpl = opts.fetchImpl ?? fetch;
72013
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
71765
72014
  fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
71766
72015
  const tmpPath = destPath + ".downloading";
71767
72016
  try {
71768
- const response = await fetch(url, {
71769
- redirect: "follow",
71770
- headers: buildHeaders(url)
71771
- });
72017
+ let current = url;
72018
+ const seen = /* @__PURE__ */ new Set();
72019
+ let response;
72020
+ const manual = opts.redirectPolicy !== void 0;
72021
+ for (let hop = 0; hop <= maxRedirects; hop++) {
72022
+ const parsed = new URL(current);
72023
+ if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
72024
+ if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
72025
+ seen.add(parsed.href);
72026
+ const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
72027
+ const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
72028
+ try {
72029
+ response = await fetchImpl(current, {
72030
+ redirect: manual ? "manual" : "follow",
72031
+ headers: buildHeaders(current),
72032
+ ...controller ? { signal: controller.signal } : {}
72033
+ });
72034
+ } finally {
72035
+ if (timer) clearTimeout(timer);
72036
+ }
72037
+ if (manual && isRedirectStatus(response.status)) {
72038
+ const location = response.headers.get("location");
72039
+ if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
72040
+ if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
72041
+ current = resolveRedirectUrl(current, location).href;
72042
+ continue;
72043
+ }
72044
+ break;
72045
+ }
72046
+ if (!response) throw new Error(`No response downloading ${url}`);
72047
+ if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
71772
72048
  if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
71773
72049
  if (!response.body) throw new Error(`No response body from ${url}`);
71774
72050
  const total = parseInt(response.headers.get("content-length") ?? "0", 10);
@@ -71779,9 +72055,10 @@ async function downloadFile(url, destPath, onProgress) {
71779
72055
  for (;;) {
71780
72056
  const { done, value } = await reader.read();
71781
72057
  if (done || !value) break;
71782
- fileStream.write(value);
71783
72058
  downloaded += value.length;
71784
- onProgress?.(downloaded, total);
72059
+ if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
72060
+ fileStream.write(value);
72061
+ opts.onProgress?.(downloaded, total);
71785
72062
  }
71786
72063
  } finally {
71787
72064
  fileStream.end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-ai",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
4
4
  "description": "AI addon for CamStack — the `llm` collection provider (cloud, LAN, and camstack-managed local llama.cpp profiles) plus the per-node `llm-runtime` managed executor.",
5
5
  "keywords": [
6
6
  "camstack",