@camstack/addon-provider-bosch 0.1.29 → 0.1.30
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/CHANGELOG.md +6 -0
- package/dist/{account-integration-offline-DMtMWixt.js → account-integration-offline-BMoYdiX2.js} +126 -1
- package/dist/{account-integration-offline-C-XbrgLs.mjs → account-integration-offline-pv6cqOKu.mjs} +115 -2
- package/dist/{dist-CyLs51d2.js → dist-BioIJ3NA.js} +1139 -82
- package/dist/{dist-DzSesBKb.mjs → dist-DBUnIovD.mjs} +1128 -83
- package/dist/home-connect/addon.js +349 -259
- package/dist/home-connect/addon.mjs +349 -259
- package/dist/homecom/addon.js +94 -12
- package/dist/homecom/addon.mjs +94 -12
- package/dist/shc/addon.js +1 -1
- package/dist/shc/addon.mjs +1 -1
- package/i18n/en.json +14 -1
- package/i18n/it.json +14 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "node:crypto";
|
|
2
|
-
//#region ../types/dist/event-category-
|
|
2
|
+
//#region ../types/dist/event-category-hdMR7CTW.mjs
|
|
3
3
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
4
|
EventCategory["SystemBoot"] = "system.boot";
|
|
5
5
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -764,6 +764,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
764
764
|
*/
|
|
765
765
|
EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
|
|
766
766
|
/**
|
|
767
|
+
* An action run started or ended. Emitted by the `actions` surface.
|
|
768
|
+
* Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
|
|
769
|
+
*/
|
|
770
|
+
EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
|
|
771
|
+
/**
|
|
767
772
|
* Progress update from a running model conversion job.
|
|
768
773
|
* Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
|
|
769
774
|
* Emitted by `addon-model-studio` on the converting node.
|
|
@@ -938,7 +943,7 @@ var propertyKeyTypes = /* @__PURE__*/ new Set([
|
|
|
938
943
|
"number",
|
|
939
944
|
"symbol"
|
|
940
945
|
]);
|
|
941
|
-
function escapeRegex(str) {
|
|
946
|
+
function escapeRegex$1(str) {
|
|
942
947
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
943
948
|
}
|
|
944
949
|
function clone(inst, def, params) {
|
|
@@ -1668,7 +1673,7 @@ var $ZodCheckUpperCase = /*@__PURE__*/ $constructor("$ZodCheckUpperCase", (inst,
|
|
|
1668
1673
|
});
|
|
1669
1674
|
var $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, def) => {
|
|
1670
1675
|
$ZodCheck.init(inst, def);
|
|
1671
|
-
const escapedRegex = escapeRegex(def.includes);
|
|
1676
|
+
const escapedRegex = escapeRegex$1(def.includes);
|
|
1672
1677
|
const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
|
|
1673
1678
|
def.pattern = pattern;
|
|
1674
1679
|
inst._zod.onattach.push((inst) => {
|
|
@@ -1691,7 +1696,7 @@ var $ZodCheckIncludes = /*@__PURE__*/ $constructor("$ZodCheckIncludes", (inst, d
|
|
|
1691
1696
|
});
|
|
1692
1697
|
var $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (inst, def) => {
|
|
1693
1698
|
$ZodCheck.init(inst, def);
|
|
1694
|
-
const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);
|
|
1699
|
+
const pattern = new RegExp(`^${escapeRegex$1(def.prefix)}.*`);
|
|
1695
1700
|
def.pattern ?? (def.pattern = pattern);
|
|
1696
1701
|
inst._zod.onattach.push((inst) => {
|
|
1697
1702
|
const bag = inst._zod.bag;
|
|
@@ -1713,7 +1718,7 @@ var $ZodCheckStartsWith = /*@__PURE__*/ $constructor("$ZodCheckStartsWith", (ins
|
|
|
1713
1718
|
});
|
|
1714
1719
|
var $ZodCheckEndsWith = /*@__PURE__*/ $constructor("$ZodCheckEndsWith", (inst, def) => {
|
|
1715
1720
|
$ZodCheck.init(inst, def);
|
|
1716
|
-
const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);
|
|
1721
|
+
const pattern = new RegExp(`.*${escapeRegex$1(def.suffix)}$`);
|
|
1717
1722
|
def.pattern ?? (def.pattern = pattern);
|
|
1718
1723
|
inst._zod.onattach.push((inst) => {
|
|
1719
1724
|
const bag = inst._zod.bag;
|
|
@@ -2949,7 +2954,7 @@ var $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
|
|
|
2949
2954
|
const values = getEnumValues(def.entries);
|
|
2950
2955
|
const valuesSet = new Set(values);
|
|
2951
2956
|
inst._zod.values = valuesSet;
|
|
2952
|
-
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex(o) : o.toString()).join("|")})$`);
|
|
2957
|
+
inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === "string" ? escapeRegex$1(o) : o.toString()).join("|")})$`);
|
|
2953
2958
|
inst._zod.parse = (payload, _ctx) => {
|
|
2954
2959
|
const input = payload.value;
|
|
2955
2960
|
if (valuesSet.has(input)) return payload;
|
|
@@ -2967,7 +2972,7 @@ var $ZodLiteral = /*@__PURE__*/ $constructor("$ZodLiteral", (inst, def) => {
|
|
|
2967
2972
|
if (def.values.length === 0) throw new Error("Cannot create literal schema with no valid values");
|
|
2968
2973
|
const values = new Set(def.values);
|
|
2969
2974
|
inst._zod.values = values;
|
|
2970
|
-
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$`);
|
|
2975
|
+
inst._zod.pattern = new RegExp(`^(${def.values.map((o) => typeof o === "string" ? escapeRegex$1(o) : o ? escapeRegex$1(o.toString()) : String(o)).join("|")})$`);
|
|
2971
2976
|
inst._zod.parse = (payload, _ctx) => {
|
|
2972
2977
|
const input = payload.value;
|
|
2973
2978
|
if (values.has(input)) return payload;
|
|
@@ -5442,7 +5447,7 @@ var ZodIssueCode = {
|
|
|
5442
5447
|
var ZodFirstPartyTypeKind;
|
|
5443
5448
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5444
5449
|
//#endregion
|
|
5445
|
-
//#region ../types/dist/sleep
|
|
5450
|
+
//#region ../types/dist/sleep-BHjNez61.mjs
|
|
5446
5451
|
/**
|
|
5447
5452
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5448
5453
|
* window to float samples (D455).
|
|
@@ -8588,6 +8593,8 @@ Object.freeze({ "doorbell": "lastPressedAt" });
|
|
|
8588
8593
|
/** Every capability name, so addon code can check a name without the schema graph. */
|
|
8589
8594
|
var CAP_NAMES = Object.freeze([
|
|
8590
8595
|
"accessories",
|
|
8596
|
+
"action-source",
|
|
8597
|
+
"actions",
|
|
8591
8598
|
"addon-pages",
|
|
8592
8599
|
"addon-pages-source",
|
|
8593
8600
|
"addon-routes",
|
|
@@ -9890,6 +9897,18 @@ Object.freeze({
|
|
|
9890
9897
|
"path": "foodLevel",
|
|
9891
9898
|
"valueType": "number"
|
|
9892
9899
|
},
|
|
9900
|
+
{
|
|
9901
|
+
"path": "bowlFood",
|
|
9902
|
+
"valueType": "number"
|
|
9903
|
+
},
|
|
9904
|
+
{
|
|
9905
|
+
"path": "eating",
|
|
9906
|
+
"valueType": "boolean"
|
|
9907
|
+
},
|
|
9908
|
+
{
|
|
9909
|
+
"path": "supportsMarkRefilled",
|
|
9910
|
+
"valueType": "boolean"
|
|
9911
|
+
},
|
|
9893
9912
|
{
|
|
9894
9913
|
"path": "food1",
|
|
9895
9914
|
"valueType": "number"
|
|
@@ -15446,7 +15465,23 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
15446
15465
|
kind: literal("schedule"),
|
|
15447
15466
|
cron: string().min(1).max(120)
|
|
15448
15467
|
}),
|
|
15449
|
-
object({ kind: literal("manual") })
|
|
15468
|
+
object({ kind: literal("manual") }),
|
|
15469
|
+
object({
|
|
15470
|
+
kind: literal("detection"),
|
|
15471
|
+
/** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
|
|
15472
|
+
* matches every raw class that maps to it; any other name matches
|
|
15473
|
+
* exactly. */
|
|
15474
|
+
classes: array(string().min(1)).min(1),
|
|
15475
|
+
deviceIds: array(number().int().nonnegative()).min(1),
|
|
15476
|
+
/** The track must have visited at least one of these zones. */
|
|
15477
|
+
zones: array(string().min(1)).min(1).optional(),
|
|
15478
|
+
/** Best-so-far confidence the track must reach. */
|
|
15479
|
+
minScore: number().min(0).max(1).optional()
|
|
15480
|
+
}),
|
|
15481
|
+
object({
|
|
15482
|
+
kind: literal("interval"),
|
|
15483
|
+
minutes: number().int().min(1).max(1440)
|
|
15484
|
+
})
|
|
15450
15485
|
]);
|
|
15451
15486
|
/**
|
|
15452
15487
|
* One action step.
|
|
@@ -15466,28 +15501,47 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
|
|
|
15466
15501
|
* is called. The runner still receives a number and is untouched; the
|
|
15467
15502
|
* resolution is the recipe's job, not the runner's.
|
|
15468
15503
|
*/
|
|
15504
|
+
var AutomationWaitStepSchema = object({
|
|
15505
|
+
kind: literal("wait"),
|
|
15506
|
+
seconds: number().min(0).max(300)
|
|
15507
|
+
});
|
|
15508
|
+
var AutomationCapStepSchema = object({
|
|
15509
|
+
kind: literal("cap"),
|
|
15510
|
+
/**
|
|
15511
|
+
* One device, several same-family devices (locks / climate family / …),
|
|
15512
|
+
* or `{ $var }` for the device that triggered the run. An array is
|
|
15513
|
+
* fan-out at codegen — each id gets the same cap.method call.
|
|
15514
|
+
*/
|
|
15515
|
+
deviceId: union([
|
|
15516
|
+
number().int(),
|
|
15517
|
+
array(number().int()).min(1),
|
|
15518
|
+
object({ $var: string().min(1) })
|
|
15519
|
+
]),
|
|
15520
|
+
cap: string().min(1),
|
|
15521
|
+
method: string().min(1),
|
|
15522
|
+
/** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
|
|
15523
|
+
* SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
|
|
15524
|
+
* the expression leaf. */
|
|
15525
|
+
args: record(string(), unknown()).optional()
|
|
15526
|
+
});
|
|
15527
|
+
/**
|
|
15528
|
+
* The steps an internal Action may contain: cap and wait, and nothing that can
|
|
15529
|
+
* call back into an Action — so an Action can never reach itself (no cycles).
|
|
15530
|
+
*/
|
|
15531
|
+
var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
|
|
15469
15532
|
var AutomationActionSchema = discriminatedUnion("kind", [
|
|
15533
|
+
AutomationWaitStepSchema,
|
|
15534
|
+
AutomationCapStepSchema,
|
|
15470
15535
|
object({
|
|
15471
|
-
kind: literal("
|
|
15472
|
-
|
|
15473
|
-
|
|
15474
|
-
|
|
15475
|
-
|
|
15476
|
-
|
|
15477
|
-
|
|
15478
|
-
|
|
15479
|
-
|
|
15480
|
-
*/
|
|
15481
|
-
deviceId: union([
|
|
15482
|
-
number().int(),
|
|
15483
|
-
array(number().int()).min(1),
|
|
15484
|
-
object({ $var: string().min(1) })
|
|
15485
|
-
]),
|
|
15486
|
-
cap: string().min(1),
|
|
15487
|
-
method: string().min(1),
|
|
15488
|
-
/** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
|
|
15489
|
-
* evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
|
|
15490
|
-
args: record(string(), unknown()).optional()
|
|
15536
|
+
kind: literal("action"),
|
|
15537
|
+
/** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
|
|
15538
|
+
actionId: string().min(1),
|
|
15539
|
+
/** Values for the action's declared variables; the others take their default. */
|
|
15540
|
+
variables: record(string(), union([
|
|
15541
|
+
string(),
|
|
15542
|
+
number(),
|
|
15543
|
+
boolean()
|
|
15544
|
+
])).optional()
|
|
15491
15545
|
}),
|
|
15492
15546
|
object({
|
|
15493
15547
|
kind: literal("code"),
|
|
@@ -15496,10 +15550,356 @@ var AutomationActionSchema = discriminatedUnion("kind", [
|
|
|
15496
15550
|
code: string().min(1).max(2e4)
|
|
15497
15551
|
})
|
|
15498
15552
|
]);
|
|
15553
|
+
/**
|
|
15554
|
+
* How a trigger that arrives while the automation is busy (or cooling down) is
|
|
15555
|
+
* handled. One of the automation's execution modes, never a property of its
|
|
15556
|
+
* actions. Absent means `single`.
|
|
15557
|
+
*
|
|
15558
|
+
* - `single` — drop the trigger while a run is in progress (traced + logged).
|
|
15559
|
+
* - `restart` — abort the run in progress (at its next step boundary), start fresh.
|
|
15560
|
+
* - `queued` — queue up to `max` triggers, drop the rest (traced).
|
|
15561
|
+
* - `parallel` — up to `max` runs at once, drop the rest (traced).
|
|
15562
|
+
* - `cooldown` — `single`, and no new run until `minutes` after the last run
|
|
15563
|
+
* STARTED; persisted across a restart. A manual trigger bypasses only this
|
|
15564
|
+
* wait, never the single/queued limits.
|
|
15565
|
+
*/
|
|
15566
|
+
var AutomationRunModeSchema = discriminatedUnion("mode", [
|
|
15567
|
+
object({ mode: literal("single") }),
|
|
15568
|
+
object({ mode: literal("restart") }),
|
|
15569
|
+
object({
|
|
15570
|
+
mode: literal("queued"),
|
|
15571
|
+
max: number().int().min(1).max(10)
|
|
15572
|
+
}),
|
|
15573
|
+
object({
|
|
15574
|
+
mode: literal("parallel"),
|
|
15575
|
+
max: number().int().min(1).max(10)
|
|
15576
|
+
}),
|
|
15577
|
+
object({
|
|
15578
|
+
mode: literal("cooldown"),
|
|
15579
|
+
minutes: number().int().min(1).max(1440)
|
|
15580
|
+
})
|
|
15581
|
+
]);
|
|
15499
15582
|
var AutomationRecipeSchema = object({
|
|
15500
15583
|
triggers: array(AutomationTriggerSchema),
|
|
15501
15584
|
conditions: AutomationConditionSchema.optional(),
|
|
15502
|
-
actions: array(AutomationActionSchema)
|
|
15585
|
+
actions: array(AutomationActionSchema),
|
|
15586
|
+
runMode: AutomationRunModeSchema.optional(),
|
|
15587
|
+
/** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
|
|
15588
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
15589
|
+
});
|
|
15590
|
+
new Set([
|
|
15591
|
+
{
|
|
15592
|
+
id: "person",
|
|
15593
|
+
name: "Person"
|
|
15594
|
+
},
|
|
15595
|
+
{
|
|
15596
|
+
id: "vehicle",
|
|
15597
|
+
name: "Vehicle"
|
|
15598
|
+
},
|
|
15599
|
+
{
|
|
15600
|
+
id: "animal",
|
|
15601
|
+
name: "Animal"
|
|
15602
|
+
},
|
|
15603
|
+
{
|
|
15604
|
+
id: "package",
|
|
15605
|
+
name: "Package"
|
|
15606
|
+
}
|
|
15607
|
+
].map((l) => l.id));
|
|
15608
|
+
var COCO_TO_MACRO = {
|
|
15609
|
+
mapping: {
|
|
15610
|
+
person: "person",
|
|
15611
|
+
bicycle: "vehicle",
|
|
15612
|
+
car: "vehicle",
|
|
15613
|
+
motorcycle: "vehicle",
|
|
15614
|
+
airplane: "vehicle",
|
|
15615
|
+
bus: "vehicle",
|
|
15616
|
+
train: "vehicle",
|
|
15617
|
+
truck: "vehicle",
|
|
15618
|
+
boat: "vehicle",
|
|
15619
|
+
bird: "animal",
|
|
15620
|
+
cat: "animal",
|
|
15621
|
+
dog: "animal",
|
|
15622
|
+
horse: "animal",
|
|
15623
|
+
sheep: "animal",
|
|
15624
|
+
cow: "animal",
|
|
15625
|
+
elephant: "animal",
|
|
15626
|
+
bear: "animal",
|
|
15627
|
+
zebra: "animal",
|
|
15628
|
+
giraffe: "animal",
|
|
15629
|
+
suitcase: "package",
|
|
15630
|
+
backpack: "package",
|
|
15631
|
+
handbag: "package"
|
|
15632
|
+
},
|
|
15633
|
+
preserveOriginal: false
|
|
15634
|
+
};
|
|
15635
|
+
new Set(Object.values(COCO_TO_MACRO.mapping));
|
|
15636
|
+
/**
|
|
15637
|
+
* Run traces — what an automation (or, later, an Action run) DID, node by node,
|
|
15638
|
+
* so the editor can paint every block green or red with its reason.
|
|
15639
|
+
*
|
|
15640
|
+
* A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
|
|
15641
|
+
* every condition node (containers and leaves, including the ones a
|
|
15642
|
+
* short-circuit never reached), the run-mode verdict and every action step —
|
|
15643
|
+
* each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
|
|
15644
|
+
* for an Action run). Skipped
|
|
15645
|
+
* evaluations are traced too, and identical consecutive skips collapse into a
|
|
15646
|
+
* repeat count so a trigger that fails every 5 minutes cannot flush real runs.
|
|
15647
|
+
*
|
|
15648
|
+
* {@link AutomationStepTraceSchema} is deliberately free of automation
|
|
15649
|
+
* vocabulary so an Action run records the same step shape.
|
|
15650
|
+
*/
|
|
15651
|
+
/** A value a trace may show: JSON primitives and short lists, nothing deeper. */
|
|
15652
|
+
var TraceValueSchema = union([
|
|
15653
|
+
string(),
|
|
15654
|
+
number(),
|
|
15655
|
+
boolean(),
|
|
15656
|
+
_null(),
|
|
15657
|
+
array(union([string(), number()]))
|
|
15658
|
+
]);
|
|
15659
|
+
var AutomationRunOutcomeSchema = _enum([
|
|
15660
|
+
"ran",
|
|
15661
|
+
"skipped-trigger",
|
|
15662
|
+
"skipped-conditions",
|
|
15663
|
+
"skipped-cooldown",
|
|
15664
|
+
"skipped-overlap",
|
|
15665
|
+
"failed",
|
|
15666
|
+
"aborted"
|
|
15667
|
+
]);
|
|
15668
|
+
/** `not-evaluated` = a short-circuit never reached the node. */
|
|
15669
|
+
var TraceNodeOutcomeSchema = _enum([
|
|
15670
|
+
"pass",
|
|
15671
|
+
"fail",
|
|
15672
|
+
"not-evaluated"
|
|
15673
|
+
]);
|
|
15674
|
+
var AutomationTriggerTraceSchema = object({
|
|
15675
|
+
kind: string().min(1),
|
|
15676
|
+
outcome: _enum(["pass", "fail"]),
|
|
15677
|
+
/** Why an event was rejected ("class person not in [dog]") or what fired. */
|
|
15678
|
+
reason: string(),
|
|
15679
|
+
/** deviceId, className, trackId, score … — only what the trigger exposes. */
|
|
15680
|
+
vars: record(string(), TraceValueSchema)
|
|
15681
|
+
});
|
|
15682
|
+
var AutomationConditionTraceNodeSchema = object({
|
|
15683
|
+
/** `conditions` for the root, `conditions.<i>` for its i-th child, … */
|
|
15684
|
+
path: string().min(1),
|
|
15685
|
+
kind: _enum([
|
|
15686
|
+
"all",
|
|
15687
|
+
"any",
|
|
15688
|
+
"not",
|
|
15689
|
+
"condition",
|
|
15690
|
+
"expression"
|
|
15691
|
+
]),
|
|
15692
|
+
outcome: TraceNodeOutcomeSchema,
|
|
15693
|
+
reason: string(),
|
|
15694
|
+
deviceId: number().int().optional(),
|
|
15695
|
+
cap: string().optional(),
|
|
15696
|
+
fieldPath: string().optional(),
|
|
15697
|
+
operator: string().optional(),
|
|
15698
|
+
expected: TraceValueSchema.optional(),
|
|
15699
|
+
/** What the mirror held; `null` when it held nothing (unknown). */
|
|
15700
|
+
observed: TraceValueSchema.optional()
|
|
15701
|
+
});
|
|
15702
|
+
var AutomationRunVerdictSchema = object({
|
|
15703
|
+
mode: AutomationRunModeSchema,
|
|
15704
|
+
decision: _enum([
|
|
15705
|
+
"run",
|
|
15706
|
+
"queued",
|
|
15707
|
+
"restart",
|
|
15708
|
+
"drop",
|
|
15709
|
+
"cooldown",
|
|
15710
|
+
"bypass"
|
|
15711
|
+
]),
|
|
15712
|
+
reason: string()
|
|
15713
|
+
});
|
|
15714
|
+
/** One executed (or not-run) step — shared by automations and Action runs. */
|
|
15715
|
+
var AutomationStepTraceSchema = object({
|
|
15716
|
+
/** `actions.<i>` */
|
|
15717
|
+
path: string().min(1),
|
|
15718
|
+
kind: string().min(1),
|
|
15719
|
+
cap: string().optional(),
|
|
15720
|
+
method: string().optional(),
|
|
15721
|
+
deviceId: number().int().optional(),
|
|
15722
|
+
startedAt: number().optional(),
|
|
15723
|
+
endedAt: number().optional(),
|
|
15724
|
+
outcome: _enum([
|
|
15725
|
+
"ok",
|
|
15726
|
+
"error",
|
|
15727
|
+
"not-run"
|
|
15728
|
+
]),
|
|
15729
|
+
message: string().optional()
|
|
15730
|
+
});
|
|
15731
|
+
var AutomationRunTraceSchema = object({
|
|
15732
|
+
runId: string().min(1),
|
|
15733
|
+
startedAt: number(),
|
|
15734
|
+
endedAt: number(),
|
|
15735
|
+
trigger: AutomationTriggerTraceSchema,
|
|
15736
|
+
conditions: array(AutomationConditionTraceNodeSchema),
|
|
15737
|
+
verdict: AutomationRunVerdictSchema.optional(),
|
|
15738
|
+
steps: array(AutomationStepTraceSchema),
|
|
15739
|
+
outcome: AutomationRunOutcomeSchema,
|
|
15740
|
+
/** How many identical consecutive skips this trace stands for (>= 1). */
|
|
15741
|
+
repeat: number().int().min(1),
|
|
15742
|
+
/** When the latest of those repeats happened. */
|
|
15743
|
+
lastAt: number()
|
|
15744
|
+
});
|
|
15745
|
+
/**
|
|
15746
|
+
* What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
|
|
15747
|
+
* `action:<actionId>`): the cooldown stamp and the trace ring.
|
|
15748
|
+
*/
|
|
15749
|
+
var AutomationStateDocSchema = object({
|
|
15750
|
+
version: literal(1),
|
|
15751
|
+
lastRunStartedAt: number().nullable(),
|
|
15752
|
+
traces: array(AutomationRunTraceSchema)
|
|
15753
|
+
});
|
|
15754
|
+
/**
|
|
15755
|
+
* Actions — named, runnable things an operator (or an automation) can start,
|
|
15756
|
+
* declared as LIST ENTRIES, never as devices.
|
|
15757
|
+
*
|
|
15758
|
+
* An entry is either **internal** (a short list of cap/wait steps kept in the
|
|
15759
|
+
* hub's `actions` record kind) or declared by an addon through the
|
|
15760
|
+
* `action-source` collection (a Home Assistant script, a scene …). The one
|
|
15761
|
+
* surface the UI and automations talk to is the `actions` cap; it keeps no
|
|
15762
|
+
* catalog of its own (D3) — every `list` asks the providers live.
|
|
15763
|
+
*
|
|
15764
|
+
* This file is the data half: the entry schema, the variables an entry may
|
|
15765
|
+
* declare, and the pure resolution of the values a run was given. It holds no
|
|
15766
|
+
* runtime.
|
|
15767
|
+
*/
|
|
15768
|
+
/** What a variable's value is. `select` takes one of `options`. */
|
|
15769
|
+
var ActionVariableTypeSchema = _enum([
|
|
15770
|
+
"string",
|
|
15771
|
+
"number",
|
|
15772
|
+
"boolean",
|
|
15773
|
+
"select"
|
|
15774
|
+
]);
|
|
15775
|
+
var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15776
|
+
var ActionVariableValueSchema = union([
|
|
15777
|
+
string(),
|
|
15778
|
+
number(),
|
|
15779
|
+
boolean()
|
|
15780
|
+
]);
|
|
15781
|
+
/**
|
|
15782
|
+
* One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
|
|
15783
|
+
* value) or `{{vars.name}}` (inside a string) in their args.
|
|
15784
|
+
*/
|
|
15785
|
+
var ActionVariableSchema = object({
|
|
15786
|
+
name: string().regex(ACTION_VARIABLE_NAME_RE),
|
|
15787
|
+
label: string().min(1).max(120).optional(),
|
|
15788
|
+
type: ActionVariableTypeSchema,
|
|
15789
|
+
required: boolean().optional(),
|
|
15790
|
+
default: ActionVariableValueSchema.optional(),
|
|
15791
|
+
/** The allowed values of a `select`. */
|
|
15792
|
+
options: array(string().min(1)).min(1).optional()
|
|
15793
|
+
}).superRefine((v, ctx) => {
|
|
15794
|
+
if (v.type === "select" && v.options === void 0) ctx.addIssue({
|
|
15795
|
+
code: "custom",
|
|
15796
|
+
message: "a select variable needs options",
|
|
15797
|
+
path: ["options"]
|
|
15798
|
+
});
|
|
15799
|
+
if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
|
|
15800
|
+
code: "custom",
|
|
15801
|
+
message: variableValueError(v, v.default) ?? "invalid default",
|
|
15802
|
+
path: ["default"]
|
|
15803
|
+
});
|
|
15804
|
+
});
|
|
15805
|
+
/** Why `value` is not a legal value for the variable, or null when it is. */
|
|
15806
|
+
function variableValueError(variable, value) {
|
|
15807
|
+
switch (variable.type) {
|
|
15808
|
+
case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
|
|
15809
|
+
case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
|
|
15810
|
+
case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
|
|
15811
|
+
case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
|
|
15812
|
+
}
|
|
15813
|
+
}
|
|
15814
|
+
var ActionSourceKindSchema = _enum(["internal", "addon"]);
|
|
15815
|
+
function isInternalActionId(actionId) {
|
|
15816
|
+
return actionId.startsWith("internal:") && actionId.length > 9;
|
|
15817
|
+
}
|
|
15818
|
+
/** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
|
|
15819
|
+
function addonIdOfActionId(actionId) {
|
|
15820
|
+
if (isInternalActionId(actionId)) return null;
|
|
15821
|
+
const at = actionId.indexOf(":");
|
|
15822
|
+
return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
|
|
15823
|
+
}
|
|
15824
|
+
var ActionStatusSchema = object({
|
|
15825
|
+
isRunning: boolean(),
|
|
15826
|
+
/** Ms epoch the latest run STARTED; null when it never ran. */
|
|
15827
|
+
lastRunAt: number().nullable(),
|
|
15828
|
+
/** The latest run's failure; null on success or when it never ran. */
|
|
15829
|
+
lastError: string().nullable()
|
|
15830
|
+
});
|
|
15831
|
+
var ActionEntrySchema = object({
|
|
15832
|
+
/** `internal:<uuid>` or `<addonId>:<localId>`. */
|
|
15833
|
+
id: string().min(3),
|
|
15834
|
+
name: string().min(1).max(120),
|
|
15835
|
+
/** The addon that declared it; `core-blocks` for an internal one. */
|
|
15836
|
+
addonId: string().min(1),
|
|
15837
|
+
source: ActionSourceKindSchema,
|
|
15838
|
+
/** An addon-declared entry is edited where it was declared. */
|
|
15839
|
+
readOnly: boolean(),
|
|
15840
|
+
/** Internal only: cap and wait steps. No action-in-action, so no cycles. */
|
|
15841
|
+
steps: array(ActionStepSchema).min(1).max(50).optional(),
|
|
15842
|
+
variables: array(ActionVariableSchema).max(20).optional(),
|
|
15843
|
+
status: ActionStatusSchema
|
|
15844
|
+
}).superRefine((entry, ctx) => {
|
|
15845
|
+
if (entry.source === "internal") {
|
|
15846
|
+
if (!isInternalActionId(entry.id)) ctx.addIssue({
|
|
15847
|
+
code: "custom",
|
|
15848
|
+
message: "an internal action id is internal:<uuid>",
|
|
15849
|
+
path: ["id"]
|
|
15850
|
+
});
|
|
15851
|
+
return;
|
|
15852
|
+
}
|
|
15853
|
+
if (entry.steps !== void 0) ctx.addIssue({
|
|
15854
|
+
code: "custom",
|
|
15855
|
+
message: "only an internal action carries steps",
|
|
15856
|
+
path: ["steps"]
|
|
15857
|
+
});
|
|
15858
|
+
if (!entry.readOnly) ctx.addIssue({
|
|
15859
|
+
code: "custom",
|
|
15860
|
+
message: "an addon action is read-only here",
|
|
15861
|
+
path: ["readOnly"]
|
|
15862
|
+
});
|
|
15863
|
+
if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
|
|
15864
|
+
code: "custom",
|
|
15865
|
+
message: "an addon action id is <addonId>:<localId>",
|
|
15866
|
+
path: ["id"]
|
|
15867
|
+
});
|
|
15868
|
+
});
|
|
15869
|
+
/** What an operator writes for an internal action. */
|
|
15870
|
+
var ActionInputSchema = object({
|
|
15871
|
+
name: string().min(1).max(120),
|
|
15872
|
+
steps: array(ActionStepSchema).min(1).max(50),
|
|
15873
|
+
variables: array(ActionVariableSchema).max(20).optional()
|
|
15874
|
+
});
|
|
15875
|
+
/** Who started a run. Absent = the operator pressed it. */
|
|
15876
|
+
var ActionRunOriginSchema = object({
|
|
15877
|
+
/** The automation block whose step started it. */
|
|
15878
|
+
blockId: string().min(1).optional() });
|
|
15879
|
+
/** `started` = the caller did not ask to wait. */
|
|
15880
|
+
var ActionRunOutcomeSchema = union([AutomationRunOutcomeSchema, literal("started")]);
|
|
15881
|
+
var ActionRunResultSchema = object({
|
|
15882
|
+
runId: string().min(1),
|
|
15883
|
+
outcome: ActionRunOutcomeSchema,
|
|
15884
|
+
/** Why it did not run, or what failed. */
|
|
15885
|
+
message: string().optional()
|
|
15886
|
+
});
|
|
15887
|
+
_enum([
|
|
15888
|
+
"not-found",
|
|
15889
|
+
"read-only",
|
|
15890
|
+
"invalid-variables",
|
|
15891
|
+
"invalid-steps",
|
|
15892
|
+
"not-linked",
|
|
15893
|
+
"busy"
|
|
15894
|
+
]);
|
|
15895
|
+
object({
|
|
15896
|
+
actionId: string(),
|
|
15897
|
+
runId: string(),
|
|
15898
|
+
isRunning: boolean(),
|
|
15899
|
+
/** Set when the run ended. */
|
|
15900
|
+
outcome: AutomationRunOutcomeSchema.optional(),
|
|
15901
|
+
lastRunAt: number().nullable(),
|
|
15902
|
+
lastError: string().nullable()
|
|
15503
15903
|
});
|
|
15504
15904
|
/**
|
|
15505
15905
|
* `addon-pages` — system-scoped singleton aggregator cap. Public-facing
|
|
@@ -19149,7 +19549,18 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
19149
19549
|
kind: "mutation",
|
|
19150
19550
|
auth: "admin",
|
|
19151
19551
|
access: "view"
|
|
19152
|
-
})
|
|
19552
|
+
}), method(object({
|
|
19553
|
+
blockId: string().min(1),
|
|
19554
|
+
trace: AutomationRunTraceSchema.optional(),
|
|
19555
|
+
runStartedAt: number().optional(),
|
|
19556
|
+
keepTraces: number().int().min(1).max(100).optional()
|
|
19557
|
+
}), _void(), {
|
|
19558
|
+
kind: "mutation",
|
|
19559
|
+
auth: "admin"
|
|
19560
|
+
}), method(object({ blockId: string().min(1) }), AutomationStateDocSchema, { auth: "admin" }), method(object({ blockId: string().min(1) }), object({
|
|
19561
|
+
traces: array(AutomationRunTraceSchema),
|
|
19562
|
+
lastError: string().nullable()
|
|
19563
|
+
}), { auth: "admin" });
|
|
19153
19564
|
/**
|
|
19154
19565
|
* `custom-model-registry` — collection cap exposing operator-registered
|
|
19155
19566
|
* custom detection models. Each provider (today: `addon-model-studio`)
|
|
@@ -44525,51 +44936,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
|
|
|
44525
44936
|
kind: "mutation",
|
|
44526
44937
|
auth: "admin"
|
|
44527
44938
|
});
|
|
44528
|
-
new Set([
|
|
44529
|
-
{
|
|
44530
|
-
id: "person",
|
|
44531
|
-
name: "Person"
|
|
44532
|
-
},
|
|
44533
|
-
{
|
|
44534
|
-
id: "vehicle",
|
|
44535
|
-
name: "Vehicle"
|
|
44536
|
-
},
|
|
44537
|
-
{
|
|
44538
|
-
id: "animal",
|
|
44539
|
-
name: "Animal"
|
|
44540
|
-
},
|
|
44541
|
-
{
|
|
44542
|
-
id: "package",
|
|
44543
|
-
name: "Package"
|
|
44544
|
-
}
|
|
44545
|
-
].map((l) => l.id));
|
|
44546
|
-
var COCO_TO_MACRO = {
|
|
44547
|
-
mapping: {
|
|
44548
|
-
person: "person",
|
|
44549
|
-
bicycle: "vehicle",
|
|
44550
|
-
car: "vehicle",
|
|
44551
|
-
motorcycle: "vehicle",
|
|
44552
|
-
airplane: "vehicle",
|
|
44553
|
-
bus: "vehicle",
|
|
44554
|
-
train: "vehicle",
|
|
44555
|
-
truck: "vehicle",
|
|
44556
|
-
boat: "vehicle",
|
|
44557
|
-
bird: "animal",
|
|
44558
|
-
cat: "animal",
|
|
44559
|
-
dog: "animal",
|
|
44560
|
-
horse: "animal",
|
|
44561
|
-
sheep: "animal",
|
|
44562
|
-
cow: "animal",
|
|
44563
|
-
elephant: "animal",
|
|
44564
|
-
bear: "animal",
|
|
44565
|
-
zebra: "animal",
|
|
44566
|
-
giraffe: "animal",
|
|
44567
|
-
suitcase: "package",
|
|
44568
|
-
backpack: "package",
|
|
44569
|
-
handbag: "package"
|
|
44570
|
-
},
|
|
44571
|
-
preserveOriginal: false
|
|
44572
|
-
};
|
|
44573
44939
|
var AUDIO_MACRO_LABELS = [
|
|
44574
44940
|
{
|
|
44575
44941
|
id: "speech",
|
|
@@ -57375,6 +57741,84 @@ var audioMetricsCapability = {
|
|
|
57375
57741
|
*/
|
|
57376
57742
|
history: "none"
|
|
57377
57743
|
};
|
|
57744
|
+
method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
57745
|
+
actionId: string().min(1),
|
|
57746
|
+
variables: record(string(), ActionVariableValueSchema).optional()
|
|
57747
|
+
}), _void(), {
|
|
57748
|
+
kind: "mutation",
|
|
57749
|
+
auth: "admin"
|
|
57750
|
+
}), method(object({ actionId: string().min(1) }), _void(), {
|
|
57751
|
+
kind: "mutation",
|
|
57752
|
+
auth: "admin"
|
|
57753
|
+
});
|
|
57754
|
+
/**
|
|
57755
|
+
* actions — the ONE surface for runnable actions, for the UI and for
|
|
57756
|
+
* automations. Provided by the core-blocks builtin on the hub.
|
|
57757
|
+
*
|
|
57758
|
+
* An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
|
|
57759
|
+
* steps, kept in the hub's `actions` record kind) or declared by an addon over
|
|
57760
|
+
* `action-source`. `list` asks every `action-source` provider LIVE and overlays
|
|
57761
|
+
* the run status from the hub's `runs` ledger; there is no catalog here to
|
|
57762
|
+
* drift (D3).
|
|
57763
|
+
*
|
|
57764
|
+
* **Permissions follow the camera, as linked devices do.** An action is linked
|
|
57765
|
+
* to cameras by the operator (`setLinks`, D356: nothing links itself). A
|
|
57766
|
+
* non-admin may `get` / `run` an action only through a camera they may see that
|
|
57767
|
+
* links it: they name the camera (`deviceId`, a top-level field so the scope
|
|
57768
|
+
* gate sees it) and the provider refuses an action that camera does not link.
|
|
57769
|
+
* An admin, and an in-process caller (an automation block), may run any action.
|
|
57770
|
+
* Every refusal is typed (`[not-linked] …`) and logged.
|
|
57771
|
+
*
|
|
57772
|
+
* A second `run` of an action that is already running is REFUSED — traced and
|
|
57773
|
+
* logged — never queued.
|
|
57774
|
+
*/
|
|
57775
|
+
var ActionIdInput = object({ actionId: string().min(1) });
|
|
57776
|
+
var ActionLinksSchema = object({
|
|
57777
|
+
deviceId: number().int().nonnegative(),
|
|
57778
|
+
actionIds: array(string().min(1)).max(100)
|
|
57779
|
+
});
|
|
57780
|
+
method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
|
|
57781
|
+
actionId: string().min(1),
|
|
57782
|
+
deviceId: number().int().nonnegative().optional()
|
|
57783
|
+
}), object({ action: ActionEntrySchema.nullable() }), {
|
|
57784
|
+
role: "viewer",
|
|
57785
|
+
auth: "protected",
|
|
57786
|
+
kind: "query",
|
|
57787
|
+
caller: "required"
|
|
57788
|
+
}), method(object({
|
|
57789
|
+
actionId: string().min(1),
|
|
57790
|
+
variables: record(string(), ActionVariableValueSchema).optional(),
|
|
57791
|
+
wait: boolean().optional(),
|
|
57792
|
+
origin: ActionRunOriginSchema.optional(),
|
|
57793
|
+
deviceId: number().int().nonnegative().optional()
|
|
57794
|
+
}), ActionRunResultSchema, {
|
|
57795
|
+
role: "viewer",
|
|
57796
|
+
auth: "protected",
|
|
57797
|
+
kind: "mutation",
|
|
57798
|
+
caller: "required"
|
|
57799
|
+
}), method(ActionIdInput, _void(), {
|
|
57800
|
+
kind: "mutation",
|
|
57801
|
+
auth: "admin"
|
|
57802
|
+
}), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
|
|
57803
|
+
kind: "mutation",
|
|
57804
|
+
auth: "admin"
|
|
57805
|
+
}), method(object({
|
|
57806
|
+
actionId: string().min(1),
|
|
57807
|
+
action: ActionInputSchema.partial()
|
|
57808
|
+
}), object({ action: ActionEntrySchema }), {
|
|
57809
|
+
kind: "mutation",
|
|
57810
|
+
auth: "admin"
|
|
57811
|
+
}), method(ActionIdInput, _void(), {
|
|
57812
|
+
kind: "mutation",
|
|
57813
|
+
auth: "admin"
|
|
57814
|
+
}), method(object({ deviceId: number().int().nonnegative() }), array(ActionEntrySchema), {
|
|
57815
|
+
role: "viewer",
|
|
57816
|
+
auth: "protected",
|
|
57817
|
+
kind: "query"
|
|
57818
|
+
}), method(object({ deviceId: number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }), method(ActionLinksSchema, ActionLinksSchema, {
|
|
57819
|
+
kind: "mutation",
|
|
57820
|
+
auth: "admin"
|
|
57821
|
+
});
|
|
57378
57822
|
/**
|
|
57379
57823
|
* Automation-control cap. Models HA `automation.*` entities on
|
|
57380
57824
|
* `DeviceType.Automation`. An automation is a trigger+condition+
|
|
@@ -63828,10 +64272,20 @@ var PetFeederDeviceStatusSchema = _enum([
|
|
|
63828
64272
|
]);
|
|
63829
64273
|
var gramsPortion = number().int().min(4).max(200);
|
|
63830
64274
|
var PetFeederStatusSchema = object({
|
|
63831
|
-
/** Food
|
|
63832
|
-
*
|
|
63833
|
-
* bowl
|
|
64275
|
+
/** Food in the TANK (hopper), as the feeder's own sensor reports it
|
|
64276
|
+
* (`state.food`). A coarse level code, not grams — `0` is empty. The
|
|
64277
|
+
* amount in the bowl is `bowlFood`. On dual-hopper models this is the
|
|
64278
|
+
* combined reading; per-hopper levels live in `food1`/`food2`. Null when
|
|
64279
|
+
* the device has not reported yet. */
|
|
63834
64280
|
foodLevel: number().nullable().meta(readingMeta("none")),
|
|
64281
|
+
/** Food in the BOWL as the feeder reports it (`state.bowl`), in the
|
|
64282
|
+
* feeder's own units. Null when the model reports none. */
|
|
64283
|
+
bowlFood: number().nullable().meta(readingMeta("none")),
|
|
64284
|
+
/** True while the pet is eating (`state.eating`). */
|
|
64285
|
+
eating: boolean(),
|
|
64286
|
+
/** Whether `markFoodReplenished` does anything on this model. When false
|
|
64287
|
+
* the client must not offer it. */
|
|
64288
|
+
supportsMarkRefilled: boolean(),
|
|
63835
64289
|
/** Hopper-1 food level (grams) on dual-hopper feeders; null on
|
|
63836
64290
|
* single-hopper models. */
|
|
63837
64291
|
food1: number().nullable().meta(readingMeta("none")),
|
|
@@ -63839,8 +64293,8 @@ var PetFeederStatusSchema = object({
|
|
|
63839
64293
|
* single-hopper models. */
|
|
63840
64294
|
food2: number().nullable().meta(readingMeta("none")),
|
|
63841
64295
|
/** Derived low-food flag — mirrors the HA `food_level` binary_sensor
|
|
63842
|
-
* (`device_class: problem`, on = low). True when the
|
|
63843
|
-
*
|
|
64296
|
+
* (`device_class: problem`, on = low). True when the tank reports empty,
|
|
64297
|
+
* or, on a dual-hopper model, when EITHER hopper is empty. */
|
|
63844
64298
|
lowFood: boolean(),
|
|
63845
64299
|
/** Battery charge 0..100 (%). Null on mains-powered models or when the
|
|
63846
64300
|
* device has no battery reading. */
|
|
@@ -63908,7 +64362,9 @@ var petFeederCapability = {
|
|
|
63908
64362
|
kind: "mutation",
|
|
63909
64363
|
auth: "protected"
|
|
63910
64364
|
}),
|
|
63911
|
-
/**
|
|
64365
|
+
/** Tell the PetKit cloud a hopper was refilled so it stops its low-food
|
|
64366
|
+
* reminders (D4H/D4S/D4SH only — see `supportsMarkRefilled`). It does not
|
|
64367
|
+
* change any sensor reading. */
|
|
63912
64368
|
markFoodReplenished: method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
63913
64369
|
kind: "mutation",
|
|
63914
64370
|
auth: "protected"
|
|
@@ -63981,6 +64437,7 @@ var petFeederCapability = {
|
|
|
63981
64437
|
*/
|
|
63982
64438
|
history: { fields: {
|
|
63983
64439
|
foodLevel: { kind: "gauge" },
|
|
64440
|
+
bowlFood: { kind: "gauge" },
|
|
63984
64441
|
food1: { kind: "gauge" },
|
|
63985
64442
|
food2: { kind: "gauge" },
|
|
63986
64443
|
batteryPower: {
|
|
@@ -71958,6 +72415,104 @@ Object.freeze({
|
|
|
71958
72415
|
access: "create",
|
|
71959
72416
|
role: "admin"
|
|
71960
72417
|
},
|
|
72418
|
+
"actions.create": {
|
|
72419
|
+
capName: "actions",
|
|
72420
|
+
capScope: "system",
|
|
72421
|
+
addonId: null,
|
|
72422
|
+
access: "create",
|
|
72423
|
+
role: "admin"
|
|
72424
|
+
},
|
|
72425
|
+
"actions.delete": {
|
|
72426
|
+
capName: "actions",
|
|
72427
|
+
capScope: "system",
|
|
72428
|
+
addonId: null,
|
|
72429
|
+
access: "delete",
|
|
72430
|
+
role: "admin"
|
|
72431
|
+
},
|
|
72432
|
+
"actions.get": {
|
|
72433
|
+
capName: "actions",
|
|
72434
|
+
capScope: "system",
|
|
72435
|
+
addonId: null,
|
|
72436
|
+
access: "view",
|
|
72437
|
+
role: "viewer"
|
|
72438
|
+
},
|
|
72439
|
+
"actions.getLinks": {
|
|
72440
|
+
capName: "actions",
|
|
72441
|
+
capScope: "system",
|
|
72442
|
+
addonId: null,
|
|
72443
|
+
access: "view",
|
|
72444
|
+
role: "admin"
|
|
72445
|
+
},
|
|
72446
|
+
"actions.getTraces": {
|
|
72447
|
+
capName: "actions",
|
|
72448
|
+
capScope: "system",
|
|
72449
|
+
addonId: null,
|
|
72450
|
+
access: "view",
|
|
72451
|
+
role: "admin"
|
|
72452
|
+
},
|
|
72453
|
+
"actions.list": {
|
|
72454
|
+
capName: "actions",
|
|
72455
|
+
capScope: "system",
|
|
72456
|
+
addonId: null,
|
|
72457
|
+
access: "view",
|
|
72458
|
+
role: "admin"
|
|
72459
|
+
},
|
|
72460
|
+
"actions.listForCamera": {
|
|
72461
|
+
capName: "actions",
|
|
72462
|
+
capScope: "system",
|
|
72463
|
+
addonId: null,
|
|
72464
|
+
access: "view",
|
|
72465
|
+
role: "viewer"
|
|
72466
|
+
},
|
|
72467
|
+
"actions.run": {
|
|
72468
|
+
capName: "actions",
|
|
72469
|
+
capScope: "system",
|
|
72470
|
+
addonId: null,
|
|
72471
|
+
access: "create",
|
|
72472
|
+
role: "viewer"
|
|
72473
|
+
},
|
|
72474
|
+
"actions.setLinks": {
|
|
72475
|
+
capName: "actions",
|
|
72476
|
+
capScope: "system",
|
|
72477
|
+
addonId: null,
|
|
72478
|
+
access: "create",
|
|
72479
|
+
role: "admin"
|
|
72480
|
+
},
|
|
72481
|
+
"actions.stop": {
|
|
72482
|
+
capName: "actions",
|
|
72483
|
+
capScope: "system",
|
|
72484
|
+
addonId: null,
|
|
72485
|
+
access: "create",
|
|
72486
|
+
role: "admin"
|
|
72487
|
+
},
|
|
72488
|
+
"actions.update": {
|
|
72489
|
+
capName: "actions",
|
|
72490
|
+
capScope: "system",
|
|
72491
|
+
addonId: null,
|
|
72492
|
+
access: "create",
|
|
72493
|
+
role: "admin"
|
|
72494
|
+
},
|
|
72495
|
+
"actionSource.listActions": {
|
|
72496
|
+
capName: "action-source",
|
|
72497
|
+
capScope: "system",
|
|
72498
|
+
addonId: null,
|
|
72499
|
+
access: "view",
|
|
72500
|
+
role: "admin"
|
|
72501
|
+
},
|
|
72502
|
+
"actionSource.runAction": {
|
|
72503
|
+
capName: "action-source",
|
|
72504
|
+
capScope: "system",
|
|
72505
|
+
addonId: null,
|
|
72506
|
+
access: "create",
|
|
72507
|
+
role: "admin"
|
|
72508
|
+
},
|
|
72509
|
+
"actionSource.stopAction": {
|
|
72510
|
+
capName: "action-source",
|
|
72511
|
+
capScope: "system",
|
|
72512
|
+
addonId: null,
|
|
72513
|
+
access: "create",
|
|
72514
|
+
role: "admin"
|
|
72515
|
+
},
|
|
71961
72516
|
"addonPages.listPages": {
|
|
71962
72517
|
capName: "addon-pages",
|
|
71963
72518
|
capScope: "system",
|
|
@@ -73239,6 +73794,13 @@ Object.freeze({
|
|
|
73239
73794
|
access: "view",
|
|
73240
73795
|
role: "admin"
|
|
73241
73796
|
},
|
|
73797
|
+
"coreBlocks.getAutomationRunState": {
|
|
73798
|
+
capName: "core-blocks",
|
|
73799
|
+
capScope: "system",
|
|
73800
|
+
addonId: null,
|
|
73801
|
+
access: "view",
|
|
73802
|
+
role: "admin"
|
|
73803
|
+
},
|
|
73242
73804
|
"coreBlocks.getBlockSettings": {
|
|
73243
73805
|
capName: "core-blocks",
|
|
73244
73806
|
capScope: "system",
|
|
@@ -73260,6 +73822,13 @@ Object.freeze({
|
|
|
73260
73822
|
access: "view",
|
|
73261
73823
|
role: "admin"
|
|
73262
73824
|
},
|
|
73825
|
+
"coreBlocks.getTraces": {
|
|
73826
|
+
capName: "core-blocks",
|
|
73827
|
+
capScope: "system",
|
|
73828
|
+
addonId: null,
|
|
73829
|
+
access: "view",
|
|
73830
|
+
role: "admin"
|
|
73831
|
+
},
|
|
73263
73832
|
"coreBlocks.getTypeDefs": {
|
|
73264
73833
|
capName: "core-blocks",
|
|
73265
73834
|
capScope: "system",
|
|
@@ -73295,6 +73864,13 @@ Object.freeze({
|
|
|
73295
73864
|
access: "create",
|
|
73296
73865
|
role: "admin"
|
|
73297
73866
|
},
|
|
73867
|
+
"coreBlocks.reportAutomationRun": {
|
|
73868
|
+
capName: "core-blocks",
|
|
73869
|
+
capScope: "system",
|
|
73870
|
+
addonId: null,
|
|
73871
|
+
access: "create",
|
|
73872
|
+
role: "admin"
|
|
73873
|
+
},
|
|
73298
73874
|
"coreBlocks.restart": {
|
|
73299
73875
|
capName: "core-blocks",
|
|
73300
73876
|
capScope: "system",
|
|
@@ -85449,6 +86025,8 @@ var ML = "model and training machinery — no device action";
|
|
|
85449
86025
|
var INTERNAL = "internal plumbing between addons — never operator-facing";
|
|
85450
86026
|
/** Every system-scoped cap, with why it is not a device's vocabulary. */
|
|
85451
86027
|
var SYSTEM_CAP_VOCABULARY = {
|
|
86028
|
+
"action-source": system(INTERNAL),
|
|
86029
|
+
actions: system("declared actions — run from the Actions page and from the cameras that link them"),
|
|
85452
86030
|
"addon-pages": system(UI_PLUMBING),
|
|
85453
86031
|
"addon-pages-source": system(UI_PLUMBING),
|
|
85454
86032
|
"addon-routes": system(UI_PLUMBING),
|
|
@@ -85645,6 +86223,31 @@ Object.freeze({
|
|
|
85645
86223
|
form: "single",
|
|
85646
86224
|
optional: false
|
|
85647
86225
|
}],
|
|
86226
|
+
"actions.get": [{
|
|
86227
|
+
name: "deviceId",
|
|
86228
|
+
form: "single",
|
|
86229
|
+
optional: true
|
|
86230
|
+
}],
|
|
86231
|
+
"actions.getLinks": [{
|
|
86232
|
+
name: "deviceId",
|
|
86233
|
+
form: "single",
|
|
86234
|
+
optional: false
|
|
86235
|
+
}],
|
|
86236
|
+
"actions.listForCamera": [{
|
|
86237
|
+
name: "deviceId",
|
|
86238
|
+
form: "single",
|
|
86239
|
+
optional: false
|
|
86240
|
+
}],
|
|
86241
|
+
"actions.run": [{
|
|
86242
|
+
name: "deviceId",
|
|
86243
|
+
form: "single",
|
|
86244
|
+
optional: true
|
|
86245
|
+
}],
|
|
86246
|
+
"actions.setLinks": [{
|
|
86247
|
+
name: "deviceId",
|
|
86248
|
+
form: "single",
|
|
86249
|
+
optional: false
|
|
86250
|
+
}],
|
|
85648
86251
|
"addonSettings.getDeviceSettings": [{
|
|
85649
86252
|
name: "deviceId",
|
|
85650
86253
|
form: "single",
|
|
@@ -89816,7 +90419,449 @@ function ingestReading(input) {
|
|
|
89816
90419
|
value: converted
|
|
89817
90420
|
};
|
|
89818
90421
|
}
|
|
90422
|
+
/**
|
|
90423
|
+
* Where an OAuth round-trip ends: the admin-ui completion route
|
|
90424
|
+
* (`/sso-complete`, the page every popup sign-in already lands on), with the
|
|
90425
|
+
* OUTCOME in the URL fragment.
|
|
90426
|
+
*
|
|
90427
|
+
* The outcome is a status plus a reason CODE from a closed list. It never
|
|
90428
|
+
* carries a token, a code, a client secret or a free-form error message: the
|
|
90429
|
+
* page words each code itself, and an unknown code reads as `unknown` rather
|
|
90430
|
+
* than being echoed. Fragment only, so nothing here reaches a server log.
|
|
90431
|
+
*
|
|
90432
|
+
* Pure and browser-safe.
|
|
90433
|
+
*/
|
|
90434
|
+
/** The admin-ui route the hub's OAuth completions land on (relative to its mount). */
|
|
90435
|
+
var OAUTH_LANDING_PATH = "/sso-complete";
|
|
90436
|
+
/**
|
|
90437
|
+
* The landing URL for `outcome`. `origin` is the hub address the flow already
|
|
90438
|
+
* runs on (from the `local-network` palette role, never a literal); `null`
|
|
90439
|
+
* gives a hub-relative URL.
|
|
90440
|
+
*/
|
|
90441
|
+
function oauthLandingUrl(origin, outcome) {
|
|
90442
|
+
const params = new URLSearchParams({
|
|
90443
|
+
return: "oauth",
|
|
90444
|
+
status: outcome.status
|
|
90445
|
+
});
|
|
90446
|
+
if (outcome.addonId !== void 0) params.set("addon", outcome.addonId);
|
|
90447
|
+
if (outcome.brokerId !== void 0) params.set("broker", outcome.brokerId);
|
|
90448
|
+
if (outcome.integration !== void 0) params.set("integration", outcome.integration);
|
|
90449
|
+
if (outcome.status === "error") params.set("reason", outcome.reason ?? "unknown");
|
|
90450
|
+
if (outcome.action === "disconnect") params.set("action", "disconnect");
|
|
90451
|
+
return `${origin === null ? "" : origin.replace(/\/+$/, "")}${OAUTH_LANDING_PATH}#${params.toString()}`;
|
|
90452
|
+
}
|
|
90453
|
+
var LOOPBACK_HOSTS = new Set([
|
|
90454
|
+
"127.0.0.1",
|
|
90455
|
+
"localhost",
|
|
90456
|
+
"[::1]"
|
|
90457
|
+
]);
|
|
90458
|
+
function hostWithoutPort(host) {
|
|
90459
|
+
if (host.startsWith("[")) return host.slice(0, host.indexOf("]") + 1);
|
|
90460
|
+
const colon = host.indexOf(":");
|
|
90461
|
+
return colon === -1 ? host : host.slice(0, colon);
|
|
90462
|
+
}
|
|
90463
|
+
function isLoopbackHost(host) {
|
|
90464
|
+
return LOOPBACK_HOSTS.has(hostWithoutPort(host.toLowerCase()));
|
|
90465
|
+
}
|
|
90466
|
+
/** An allow-list entry is valid when it is a bare host[:port] and `http` only targets loopback. */
|
|
90467
|
+
function isValidUpstream(upstream) {
|
|
90468
|
+
if (upstream.host.length === 0 || !/^[a-z0-9.[\]:-]+$/.test(upstream.host)) return false;
|
|
90469
|
+
return upstream.scheme === "https" || isLoopbackHost(upstream.host);
|
|
90470
|
+
}
|
|
90471
|
+
function findUpstream(route, host) {
|
|
90472
|
+
const wanted = host.toLowerCase();
|
|
90473
|
+
return route.upstreams.find((upstream) => upstream.host === wanted);
|
|
90474
|
+
}
|
|
90475
|
+
/** `<basePath>/<host><pathname><search>` — what the browser is sent to. */
|
|
90476
|
+
function proxyPathFor(route, host, pathname, search = "") {
|
|
90477
|
+
return `${route.basePath}/${host}${pathname.startsWith("/") ? pathname : `/${pathname}`}${search}`;
|
|
90478
|
+
}
|
|
90479
|
+
/** Split `/<attemptId>/<host>/<rest…>[?query]` (the data-plane view of a request). */
|
|
90480
|
+
function parseProxyPath(rawUrl) {
|
|
90481
|
+
const queryAt = rawUrl.indexOf("?");
|
|
90482
|
+
const path = queryAt === -1 ? rawUrl : rawUrl.slice(0, queryAt);
|
|
90483
|
+
const search = queryAt === -1 ? "" : rawUrl.slice(queryAt);
|
|
90484
|
+
const match = /^\/([A-Za-z0-9_-]{8,128})\/([^/]+)(\/.*)?$/.exec(path);
|
|
90485
|
+
if (match === null) return null;
|
|
90486
|
+
const attemptId = match[1];
|
|
90487
|
+
const host = match[2];
|
|
90488
|
+
if (attemptId === void 0 || host === void 0) return null;
|
|
90489
|
+
return {
|
|
90490
|
+
attemptId,
|
|
90491
|
+
host: host.toLowerCase(),
|
|
90492
|
+
pathname: match[3] ?? "/",
|
|
90493
|
+
search
|
|
90494
|
+
};
|
|
90495
|
+
}
|
|
90496
|
+
/** The vendor URL a proxied request targets, or null when the host would not stay `host`. */
|
|
90497
|
+
function upstreamUrlFor(upstream, parsed) {
|
|
90498
|
+
try {
|
|
90499
|
+
const url = new URL(`${upstream.scheme}://${upstream.host}${parsed.pathname}${parsed.search}`);
|
|
90500
|
+
return url.host.toLowerCase() === upstream.host ? url : null;
|
|
90501
|
+
} catch {
|
|
90502
|
+
return null;
|
|
90503
|
+
}
|
|
90504
|
+
}
|
|
90505
|
+
function escapeRegex(value) {
|
|
90506
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
90507
|
+
}
|
|
90508
|
+
/** A Location (or any URL value) to a vendor host becomes a proxy path; anything else is unchanged. */
|
|
90509
|
+
function rewriteLocation(location, currentUpstream, route) {
|
|
90510
|
+
let resolved;
|
|
90511
|
+
try {
|
|
90512
|
+
resolved = new URL(location, `${currentUpstream.scheme}://${currentUpstream.host}/`);
|
|
90513
|
+
} catch {
|
|
90514
|
+
return location;
|
|
90515
|
+
}
|
|
90516
|
+
if (resolved.protocol !== "https:" && resolved.protocol !== "http:") return location;
|
|
90517
|
+
const target = findUpstream(route, resolved.host);
|
|
90518
|
+
if (target === void 0) return location;
|
|
90519
|
+
return `${proxyPathFor(route, target.host, resolved.pathname, resolved.search)}${resolved.hash}`;
|
|
90520
|
+
}
|
|
90521
|
+
/** Characters that may precede a URL that STARTS a value (never `=`, `?`, `&`, `%3D`). */
|
|
90522
|
+
var VALUE_START = String.raw`(?<=^|["'(\s>,\[{])`;
|
|
90523
|
+
/**
|
|
90524
|
+
* Rewrite vendor URLs in a text body: absolute (`https://host/…`), protocol-relative
|
|
90525
|
+
* (`//host/…`) and JSON-escaped (`https:\/\/host\/…`) when they start a value; and, in
|
|
90526
|
+
* HTML/CSS, root-relative references (`href="/x"`, `url(/x)`) which would otherwise
|
|
90527
|
+
* escape the proxy mount and land on the hub.
|
|
90528
|
+
*/
|
|
90529
|
+
function rewriteBody(body, kind, currentUpstream, route) {
|
|
90530
|
+
let out = body;
|
|
90531
|
+
for (const upstream of route.upstreams) {
|
|
90532
|
+
const host = escapeRegex(upstream.host);
|
|
90533
|
+
const plain = new RegExp(`${VALUE_START}(?:https?:)?//${host}(?=[/"'?#\\s)\\]},<]|$)`, "g");
|
|
90534
|
+
out = out.replace(plain, `${route.basePath}/${upstream.host}`);
|
|
90535
|
+
const escapedBase = `${route.basePath}/${upstream.host}`.replace(/\//g, "\\/");
|
|
90536
|
+
const escaped = new RegExp(`${VALUE_START}(?:https?:)?\\\\/\\\\/${host}(?=\\\\/|["'?#\\s)\\]},<]|$)`, "g");
|
|
90537
|
+
out = out.replace(escaped, escapedBase);
|
|
90538
|
+
}
|
|
90539
|
+
const current = `${route.basePath}/${currentUpstream.host}/`;
|
|
90540
|
+
const notMounted = `(?!/)(?!${escapeRegex(route.basePath.slice(1))}/)`;
|
|
90541
|
+
if (kind === "html") {
|
|
90542
|
+
const attribute = String.raw`(\b(?:href|src|action|formaction|poster|data-href|data-src)\s*=\s*["'])/`;
|
|
90543
|
+
out = out.replace(new RegExp(`${attribute}${notMounted}`, "gi"), `$1${current}`);
|
|
90544
|
+
}
|
|
90545
|
+
if (kind === "html" || kind === "css") {
|
|
90546
|
+
const cssUrl = String.raw`(url\(\s*["']?)/`;
|
|
90547
|
+
out = out.replace(new RegExp(`${cssUrl}${notMounted}`, "gi"), `$1${current}`);
|
|
90548
|
+
}
|
|
90549
|
+
return out;
|
|
90550
|
+
}
|
|
90551
|
+
/**
|
|
90552
|
+
* A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
|
|
90553
|
+
* origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
|
|
90554
|
+
*/
|
|
90555
|
+
function rewriteSetCookie(cookie, route) {
|
|
90556
|
+
const kept = cookie.split(";").map((part) => part.trim()).filter((part, index) => {
|
|
90557
|
+
if (index === 0) return true;
|
|
90558
|
+
const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
|
|
90559
|
+
return name !== "domain" && name !== "path";
|
|
90560
|
+
});
|
|
90561
|
+
kept.push(`Path=${route.basePath}/`);
|
|
90562
|
+
return kept.join("; ");
|
|
90563
|
+
}
|
|
90564
|
+
/**
|
|
90565
|
+
* A browser `Referer` that points into this proxy, turned back into the vendor URL it
|
|
90566
|
+
* stands for. A Referer that is not ours is dropped (null): it names the hub.
|
|
90567
|
+
*/
|
|
90568
|
+
function rewriteReferer(referer, route) {
|
|
90569
|
+
let url;
|
|
90570
|
+
try {
|
|
90571
|
+
url = new URL(referer, "http://hub.invalid");
|
|
90572
|
+
} catch {
|
|
90573
|
+
return null;
|
|
90574
|
+
}
|
|
90575
|
+
if (!url.pathname.startsWith(`${route.basePath}/`)) return null;
|
|
90576
|
+
const rest = /^\/([^/]+)(\/.*)?$/.exec(url.pathname.slice(route.basePath.length));
|
|
90577
|
+
const host = rest?.[1]?.toLowerCase();
|
|
90578
|
+
if (host === void 0) return null;
|
|
90579
|
+
const upstream = findUpstream(route, host);
|
|
90580
|
+
if (upstream === void 0) return null;
|
|
90581
|
+
return `${upstream.scheme}://${upstream.host}${rest?.[2] ?? "/"}${url.search}`;
|
|
90582
|
+
}
|
|
90583
|
+
/** Cookies the browser holds for the hub itself must never reach a vendor. */
|
|
90584
|
+
function filterForwardedCookies(cookieHeader) {
|
|
90585
|
+
return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
|
|
90586
|
+
}
|
|
90587
|
+
var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
|
|
90588
|
+
var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
|
|
90589
|
+
/** Request headers that are never forwarded to the vendor. */
|
|
90590
|
+
var DROP_REQUEST_HEADERS = new Set([
|
|
90591
|
+
"host",
|
|
90592
|
+
"connection",
|
|
90593
|
+
"keep-alive",
|
|
90594
|
+
"proxy-authorization",
|
|
90595
|
+
"proxy-connection",
|
|
90596
|
+
"te",
|
|
90597
|
+
"trailer",
|
|
90598
|
+
"transfer-encoding",
|
|
90599
|
+
"upgrade",
|
|
90600
|
+
"content-length",
|
|
90601
|
+
"accept-encoding",
|
|
90602
|
+
"authorization",
|
|
90603
|
+
"cookie",
|
|
90604
|
+
"origin",
|
|
90605
|
+
"referer",
|
|
90606
|
+
"x-camstack-dataplane-secret",
|
|
90607
|
+
"x-forwarded-for",
|
|
90608
|
+
"x-forwarded-host",
|
|
90609
|
+
"x-forwarded-proto",
|
|
90610
|
+
"x-forwarded-prefix",
|
|
90611
|
+
"x-real-ip",
|
|
90612
|
+
"forwarded"
|
|
90613
|
+
]);
|
|
90614
|
+
/**
|
|
90615
|
+
* Response headers dropped: framing/CSP/HSTS that break a page under another origin, and
|
|
90616
|
+
* the ones that no longer describe the (decoded, possibly rewritten) body.
|
|
90617
|
+
*/
|
|
90618
|
+
var DROP_RESPONSE_HEADERS = new Set([
|
|
90619
|
+
"content-security-policy",
|
|
90620
|
+
"content-security-policy-report-only",
|
|
90621
|
+
"x-frame-options",
|
|
90622
|
+
"strict-transport-security",
|
|
90623
|
+
"public-key-pins",
|
|
90624
|
+
"report-to",
|
|
90625
|
+
"nel",
|
|
90626
|
+
"content-encoding",
|
|
90627
|
+
"content-length",
|
|
90628
|
+
"transfer-encoding",
|
|
90629
|
+
"connection",
|
|
90630
|
+
"keep-alive",
|
|
90631
|
+
"set-cookie",
|
|
90632
|
+
"location"
|
|
90633
|
+
]);
|
|
90634
|
+
function bodyKindOf(contentType) {
|
|
90635
|
+
const type = contentType.toLowerCase();
|
|
90636
|
+
if (type.includes("text/html") || type.includes("application/xhtml")) return "html";
|
|
90637
|
+
if (type.includes("text/css")) return "css";
|
|
90638
|
+
if (type.startsWith("text/") || type.includes("javascript") || type.includes("json") || type.includes("xml")) return "other";
|
|
90639
|
+
return null;
|
|
90640
|
+
}
|
|
90641
|
+
function firstHeader(value) {
|
|
90642
|
+
return Array.isArray(value) ? value[0] : value;
|
|
90643
|
+
}
|
|
90644
|
+
async function readRequestBody(req) {
|
|
90645
|
+
const chunks = [];
|
|
90646
|
+
let size = 0;
|
|
90647
|
+
for await (const chunk of req) {
|
|
90648
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
|
|
90649
|
+
size += buf.length;
|
|
90650
|
+
if (size > MAX_REQUEST_BODY_BYTES) return "too-large";
|
|
90651
|
+
chunks.push(buf);
|
|
90652
|
+
}
|
|
90653
|
+
return Buffer.concat(chunks);
|
|
90654
|
+
}
|
|
90655
|
+
function sendPlain(res, status, text) {
|
|
90656
|
+
res.writeHead(status, {
|
|
90657
|
+
"content-type": "text/plain; charset=utf-8",
|
|
90658
|
+
"cache-control": "no-store"
|
|
90659
|
+
});
|
|
90660
|
+
res.end(text);
|
|
90661
|
+
}
|
|
90662
|
+
var VendorLoginProxy = class {
|
|
90663
|
+
#addonId;
|
|
90664
|
+
#prefix;
|
|
90665
|
+
#logger;
|
|
90666
|
+
#now;
|
|
90667
|
+
#fetch;
|
|
90668
|
+
#attempts = /* @__PURE__ */ new Map();
|
|
90669
|
+
constructor(options) {
|
|
90670
|
+
this.#addonId = options.addonId;
|
|
90671
|
+
this.#prefix = options.prefix;
|
|
90672
|
+
this.#logger = options.logger;
|
|
90673
|
+
this.#now = options.now ?? (() => Date.now());
|
|
90674
|
+
this.#fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
90675
|
+
}
|
|
90676
|
+
/** The `DataPlaneHandler` to pass to `ctx.dataPlane.serve({ prefix, access: 'admin', … })`. */
|
|
90677
|
+
handler = (req, res) => this.#handle(req, res);
|
|
90678
|
+
/** Register an attempt. A one-shot capability: refused hosts are rejected up front. */
|
|
90679
|
+
open(spec) {
|
|
90680
|
+
for (const upstream of spec.upstreams) if (!isValidUpstream(upstream)) throw new Error(`vendor login proxy: upstream ${upstream.host} is not allowed`);
|
|
90681
|
+
this.#attempts.set(spec.attemptId, {
|
|
90682
|
+
expiresAtMs: spec.expiresAtMs,
|
|
90683
|
+
upstreams: spec.upstreams,
|
|
90684
|
+
evaluate: async (view) => {
|
|
90685
|
+
const verdict = await spec.terminal(view);
|
|
90686
|
+
if (verdict.kind === "continue") return verdict;
|
|
90687
|
+
return {
|
|
90688
|
+
kind: "done",
|
|
90689
|
+
landing: await spec.onDone(verdict.payload)
|
|
90690
|
+
};
|
|
90691
|
+
}
|
|
90692
|
+
});
|
|
90693
|
+
}
|
|
90694
|
+
close(attemptId) {
|
|
90695
|
+
this.#attempts.delete(attemptId);
|
|
90696
|
+
}
|
|
90697
|
+
isOpen(attemptId) {
|
|
90698
|
+
const attempt = this.#attempts.get(attemptId);
|
|
90699
|
+
return attempt !== void 0 && this.#now() <= attempt.expiresAtMs;
|
|
90700
|
+
}
|
|
90701
|
+
/**
|
|
90702
|
+
* The hub-relative path that opens `vendorUrl` through the proxy for this attempt:
|
|
90703
|
+
* the URL the browser is sent to. Null when the host is not in the attempt's list.
|
|
90704
|
+
*/
|
|
90705
|
+
entryPath(attemptId, vendorUrl) {
|
|
90706
|
+
const attempt = this.#attempts.get(attemptId);
|
|
90707
|
+
if (attempt === void 0) return null;
|
|
90708
|
+
const url = new URL(vendorUrl);
|
|
90709
|
+
if (findUpstream({
|
|
90710
|
+
basePath: "",
|
|
90711
|
+
upstreams: attempt.upstreams
|
|
90712
|
+
}, url.host) === void 0) return null;
|
|
90713
|
+
return `/addon/${this.#addonId}/${this.#prefix}/${attemptId}/${url.host}${url.pathname}${url.search}`;
|
|
90714
|
+
}
|
|
90715
|
+
#routeFor(attemptId, attempt, forwardedPrefix) {
|
|
90716
|
+
return {
|
|
90717
|
+
basePath: `${forwardedPrefix}/addon/${this.#addonId}/${this.#prefix}/${attemptId}`,
|
|
90718
|
+
upstreams: attempt.upstreams
|
|
90719
|
+
};
|
|
90720
|
+
}
|
|
90721
|
+
async #handle(req, res) {
|
|
90722
|
+
try {
|
|
90723
|
+
await this.#serve(req, res);
|
|
90724
|
+
} catch (err) {
|
|
90725
|
+
this.#logger.warn("vendor login proxy: request failed", { meta: {
|
|
90726
|
+
addonId: this.#addonId,
|
|
90727
|
+
error: err instanceof Error ? err.name : "unknown"
|
|
90728
|
+
} });
|
|
90729
|
+
if (!res.headersSent) sendPlain(res, 502, "The sign-in page could not be reached.");
|
|
90730
|
+
else res.end();
|
|
90731
|
+
}
|
|
90732
|
+
}
|
|
90733
|
+
async #serve(req, res) {
|
|
90734
|
+
const parsed = parseProxyPath(req.url ?? "/");
|
|
90735
|
+
if (parsed === null) {
|
|
90736
|
+
sendPlain(res, 404, "Not found");
|
|
90737
|
+
return;
|
|
90738
|
+
}
|
|
90739
|
+
const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
|
|
90740
|
+
const attempt = this.#attempts.get(parsed.attemptId);
|
|
90741
|
+
if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
|
|
90742
|
+
if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
|
|
90743
|
+
this.#logger.info("vendor login proxy: attempt refused", { meta: {
|
|
90744
|
+
addonId: this.#addonId,
|
|
90745
|
+
reason: attempt === void 0 ? "missing" : "expired"
|
|
90746
|
+
} });
|
|
90747
|
+
this.#redirect(res, forwardedPrefix, {
|
|
90748
|
+
status: "error",
|
|
90749
|
+
addonId: this.#addonId,
|
|
90750
|
+
reason: "invalid_state"
|
|
90751
|
+
});
|
|
90752
|
+
return;
|
|
90753
|
+
}
|
|
90754
|
+
const route = this.#routeFor(parsed.attemptId, attempt, forwardedPrefix);
|
|
90755
|
+
const upstream = findUpstream(route, parsed.host);
|
|
90756
|
+
const target = upstream === void 0 ? null : upstreamUrlFor(upstream, parsed);
|
|
90757
|
+
if (upstream === void 0 || target === null) {
|
|
90758
|
+
this.#logger.warn("vendor login proxy: host refused", { meta: {
|
|
90759
|
+
addonId: this.#addonId,
|
|
90760
|
+
host: parsed.host
|
|
90761
|
+
} });
|
|
90762
|
+
sendPlain(res, 404, "Not found");
|
|
90763
|
+
return;
|
|
90764
|
+
}
|
|
90765
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
90766
|
+
let body;
|
|
90767
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
90768
|
+
const read = await readRequestBody(req);
|
|
90769
|
+
if (read === "too-large") {
|
|
90770
|
+
sendPlain(res, 413, "Request too large");
|
|
90771
|
+
return;
|
|
90772
|
+
}
|
|
90773
|
+
body = read;
|
|
90774
|
+
}
|
|
90775
|
+
const headers = this.#requestHeaders(req, route, upstream);
|
|
90776
|
+
const upstreamRes = await this.#fetch(target, {
|
|
90777
|
+
method,
|
|
90778
|
+
headers,
|
|
90779
|
+
redirect: "manual",
|
|
90780
|
+
...body !== void 0 && body.length > 0 ? { body: new Uint8Array(body) } : {}
|
|
90781
|
+
});
|
|
90782
|
+
const location = upstreamRes.headers.get("location");
|
|
90783
|
+
const setCookies = upstreamRes.headers.getSetCookie();
|
|
90784
|
+
const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
|
|
90785
|
+
const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
|
|
90786
|
+
const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
|
|
90787
|
+
this.#logger.debug("vendor login proxy: upstream answered", { meta: {
|
|
90788
|
+
addonId: this.#addonId,
|
|
90789
|
+
host: upstream.host,
|
|
90790
|
+
status: upstreamRes.status,
|
|
90791
|
+
method
|
|
90792
|
+
} });
|
|
90793
|
+
const settled = await attempt.evaluate({
|
|
90794
|
+
status: upstreamRes.status,
|
|
90795
|
+
url: target.toString(),
|
|
90796
|
+
location,
|
|
90797
|
+
setCookies,
|
|
90798
|
+
bodyText: text
|
|
90799
|
+
});
|
|
90800
|
+
if (settled.kind === "done") {
|
|
90801
|
+
this.#attempts.delete(parsed.attemptId);
|
|
90802
|
+
this.#logger.info("vendor login proxy: sign-in finished", { meta: {
|
|
90803
|
+
addonId: this.#addonId,
|
|
90804
|
+
status: settled.landing.status
|
|
90805
|
+
} });
|
|
90806
|
+
this.#redirect(res, forwardedPrefix, settled.landing);
|
|
90807
|
+
return;
|
|
90808
|
+
}
|
|
90809
|
+
const out = {};
|
|
90810
|
+
upstreamRes.headers.forEach((value, name) => {
|
|
90811
|
+
if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
|
|
90812
|
+
});
|
|
90813
|
+
if (setCookies.length > 0) out["set-cookie"] = setCookies.map((c) => rewriteSetCookie(c, route));
|
|
90814
|
+
if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
|
|
90815
|
+
out["cache-control"] = "no-store";
|
|
90816
|
+
if (text !== null && kind !== null) {
|
|
90817
|
+
const rewritten = rewriteBody(text, kind, upstream, route);
|
|
90818
|
+
res.writeHead(upstreamRes.status, out);
|
|
90819
|
+
res.end(rewritten);
|
|
90820
|
+
return;
|
|
90821
|
+
}
|
|
90822
|
+
res.writeHead(upstreamRes.status, out);
|
|
90823
|
+
await this.#pipeBody(upstreamRes, res);
|
|
90824
|
+
}
|
|
90825
|
+
#requestHeaders(req, route, upstream) {
|
|
90826
|
+
const headers = {};
|
|
90827
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
90828
|
+
const lower = name.toLowerCase();
|
|
90829
|
+
if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
|
|
90830
|
+
headers[lower] = Array.isArray(value) ? value.join(", ") : value;
|
|
90831
|
+
}
|
|
90832
|
+
const cookie = filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? "");
|
|
90833
|
+
if (cookie.length > 0) headers["cookie"] = cookie;
|
|
90834
|
+
if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
|
|
90835
|
+
const referer = firstHeader(req.headers["referer"]);
|
|
90836
|
+
if (referer !== void 0) {
|
|
90837
|
+
const back = rewriteReferer(referer, route);
|
|
90838
|
+
if (back !== null) headers["referer"] = back;
|
|
90839
|
+
}
|
|
90840
|
+
return headers;
|
|
90841
|
+
}
|
|
90842
|
+
async #pipeBody(upstreamRes, res) {
|
|
90843
|
+
if (upstreamRes.body === null) {
|
|
90844
|
+
res.end();
|
|
90845
|
+
return;
|
|
90846
|
+
}
|
|
90847
|
+
for await (const chunk of upstreamRes.body) {
|
|
90848
|
+
if (!res.write(chunk)) await new Promise((resolve) => {
|
|
90849
|
+
res.once("drain", resolve);
|
|
90850
|
+
res.once("close", resolve);
|
|
90851
|
+
});
|
|
90852
|
+
if (res.destroyed) return;
|
|
90853
|
+
}
|
|
90854
|
+
res.end();
|
|
90855
|
+
}
|
|
90856
|
+
#redirect(res, forwardedPrefix, outcome) {
|
|
90857
|
+
res.writeHead(302, {
|
|
90858
|
+
location: `${forwardedPrefix}${oauthLandingUrl(null, outcome)}`,
|
|
90859
|
+
"cache-control": "no-store"
|
|
90860
|
+
});
|
|
90861
|
+
res.end();
|
|
90862
|
+
}
|
|
90863
|
+
};
|
|
89819
90864
|
var MB = 1024 * 1024;
|
|
89820
90865
|
1024 * MB, 3072 * MB;
|
|
89821
90866
|
//#endregion
|
|
89822
|
-
export {
|
|
90867
|
+
export { defineCustomActions as A, string as B, temperatureSensorCapability as C, errMsg as D, DeviceType as E, boolean as F, unknown as H, literal as I, number as L, isSecretMarkerLike as M, _enum as N, createEvent as O, array as P, object as R, switchCapability as S, DeviceRole as T, EventCategory as U, union as V, numericSensorCapability as _, VendorLoginProxy as a, runningFromDishwasherOperation as b, climateControlCapability as c, deviceAdoptionCapability as d, dishwasherCapability as f, ingestReading as g, heatingValveCapability as h, RUNNING_STATE_COLD as i, isSecretMarker as j, customAction as k, connectionTestCapability as l, eventEmitterCapability as m, BaseDeviceProvider as n, batteryCapability as o, enumSensorCapability as p, DishwasherOperationStateSchema as r, binaryCapability as s, BaseDevice as t, consumablesCapability as u, powerMeterCapability as v, waterHeaterCapability as w, runningStateCapability as x, quantityFromUnit as y, record as z };
|