@camstack/addon-import-alexa 0.2.164 → 0.2.166
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 +12 -0
- package/dist/addon.js +809 -21
- package/dist/addon.mjs +809 -21
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -5626,7 +5626,7 @@ var ZodIssueCode = {
|
|
|
5626
5626
|
var ZodFirstPartyTypeKind;
|
|
5627
5627
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5628
5628
|
//#endregion
|
|
5629
|
-
//#region ../types/dist/sleep-
|
|
5629
|
+
//#region ../types/dist/sleep-C_u6cEAJ.mjs
|
|
5630
5630
|
/**
|
|
5631
5631
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5632
5632
|
* window to float samples (D455).
|
|
@@ -9019,6 +9019,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9019
9019
|
"image-settings",
|
|
9020
9020
|
"integrations",
|
|
9021
9021
|
"intercom",
|
|
9022
|
+
"ir-transmitter",
|
|
9022
9023
|
"keychain",
|
|
9023
9024
|
"keychain-admin",
|
|
9024
9025
|
"lawn-mower-control",
|
|
@@ -9047,6 +9048,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9047
9048
|
"network-link",
|
|
9048
9049
|
"network-quality",
|
|
9049
9050
|
"node-certificates",
|
|
9051
|
+
"node-removal-hook",
|
|
9050
9052
|
"nodes",
|
|
9051
9053
|
"notification-output",
|
|
9052
9054
|
"notification-rules",
|
|
@@ -9077,6 +9079,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9077
9079
|
"recording-onboard",
|
|
9078
9080
|
"recording-signal",
|
|
9079
9081
|
"records",
|
|
9082
|
+
"remote",
|
|
9080
9083
|
"running-state",
|
|
9081
9084
|
"scene-monitor",
|
|
9082
9085
|
"script-runner",
|
|
@@ -9100,6 +9103,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9100
9103
|
"system",
|
|
9101
9104
|
"tamper",
|
|
9102
9105
|
"temperature-sensor",
|
|
9106
|
+
"template",
|
|
9103
9107
|
"terminal-session",
|
|
9104
9108
|
"toast",
|
|
9105
9109
|
"training",
|
|
@@ -10040,6 +10044,11 @@ Object.freeze({
|
|
|
10040
10044
|
"standby"
|
|
10041
10045
|
]
|
|
10042
10046
|
},
|
|
10047
|
+
{
|
|
10048
|
+
"path": "stateSource",
|
|
10049
|
+
"valueType": "enum",
|
|
10050
|
+
"enumValues": ["reported", "assumed"]
|
|
10051
|
+
},
|
|
10043
10052
|
{
|
|
10044
10053
|
"path": "volumeLevel",
|
|
10045
10054
|
"valueType": "number"
|
|
@@ -10266,6 +10275,11 @@ Object.freeze({
|
|
|
10266
10275
|
"path": "batteryPower",
|
|
10267
10276
|
"valueType": "number"
|
|
10268
10277
|
},
|
|
10278
|
+
{
|
|
10279
|
+
"path": "powerSource",
|
|
10280
|
+
"valueType": "enum",
|
|
10281
|
+
"enumValues": ["mains", "battery"]
|
|
10282
|
+
},
|
|
10269
10283
|
{
|
|
10270
10284
|
"path": "desiccantLeftDays",
|
|
10271
10285
|
"valueType": "number"
|
|
@@ -10310,6 +10324,18 @@ Object.freeze({
|
|
|
10310
10324
|
{
|
|
10311
10325
|
"path": "volume",
|
|
10312
10326
|
"valueType": "number"
|
|
10327
|
+
},
|
|
10328
|
+
{
|
|
10329
|
+
"path": "lastFeedGrams",
|
|
10330
|
+
"valueType": "number"
|
|
10331
|
+
},
|
|
10332
|
+
{
|
|
10333
|
+
"path": "lastFeedHopper1",
|
|
10334
|
+
"valueType": "number"
|
|
10335
|
+
},
|
|
10336
|
+
{
|
|
10337
|
+
"path": "lastFeedHopper2",
|
|
10338
|
+
"valueType": "number"
|
|
10313
10339
|
}
|
|
10314
10340
|
],
|
|
10315
10341
|
"power-meter": [
|
|
@@ -10849,7 +10875,7 @@ object({
|
|
|
10849
10875
|
params: record(string(), union([string(), number()])).optional()
|
|
10850
10876
|
});
|
|
10851
10877
|
//#endregion
|
|
10852
|
-
//#region ../types/dist/err-msg-
|
|
10878
|
+
//#region ../types/dist/err-msg-BCdlR2Li.mjs
|
|
10853
10879
|
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
10854
10880
|
DeviceType["Camera"] = "camera";
|
|
10855
10881
|
DeviceType["Hub"] = "hub";
|
|
@@ -11195,6 +11221,9 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
11195
11221
|
/** Fallback when the notifier service name doesn't match a known
|
|
11196
11222
|
* pattern. */
|
|
11197
11223
|
DeviceRole["GenericNotifier"] = "generic-notifier";
|
|
11224
|
+
/** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
|
|
11225
|
+
* cap. A `DeviceType.Generic` device. */
|
|
11226
|
+
DeviceRole["IrTransmitter"] = "ir-transmitter";
|
|
11198
11227
|
return DeviceRole;
|
|
11199
11228
|
}({});
|
|
11200
11229
|
/**
|
|
@@ -11208,7 +11237,7 @@ function errMsg(err) {
|
|
|
11208
11237
|
return String(err);
|
|
11209
11238
|
}
|
|
11210
11239
|
//#endregion
|
|
11211
|
-
//#region ../types/dist/composition-
|
|
11240
|
+
//#region ../types/dist/composition-bIBW5_13.mjs
|
|
11212
11241
|
/**
|
|
11213
11242
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11214
11243
|
*
|
|
@@ -16754,9 +16783,21 @@ var AlertSeveritySchema = _enum([
|
|
|
16754
16783
|
"error",
|
|
16755
16784
|
"critical"
|
|
16756
16785
|
]);
|
|
16786
|
+
/**
|
|
16787
|
+
* An action that RUNS something instead of going somewhere: the admin UI calls
|
|
16788
|
+
* the addon's custom action (`addons.custom`) with exactly these fields. The
|
|
16789
|
+
* producer owns the meaning; the UI knows nothing about the addon or the kind.
|
|
16790
|
+
*/
|
|
16791
|
+
var AlertActionInvokeSchema = object({
|
|
16792
|
+
addonId: string().min(1),
|
|
16793
|
+
action: string().min(1),
|
|
16794
|
+
input: record(string(), unknown()).optional()
|
|
16795
|
+
});
|
|
16796
|
+
/** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
|
|
16757
16797
|
var AlertActionSchema = object({
|
|
16758
16798
|
label: string(),
|
|
16759
16799
|
href: string().optional(),
|
|
16800
|
+
invoke: AlertActionInvokeSchema.optional(),
|
|
16760
16801
|
/**
|
|
16761
16802
|
* How `href` is followed. Absent / `app`: an admin-ui route (client-side).
|
|
16762
16803
|
* `server`: a route the HUB serves (an addon HTTP route such as an OAuth
|
|
@@ -43259,6 +43300,14 @@ method(object({
|
|
|
43259
43300
|
kind: "mutation",
|
|
43260
43301
|
auth: "admin"
|
|
43261
43302
|
});
|
|
43303
|
+
method(object({
|
|
43304
|
+
nodeId: string().min(1),
|
|
43305
|
+
/** Epoch ms the node was forgotten (the tombstone's `removedAt`). */
|
|
43306
|
+
removedAt: number()
|
|
43307
|
+
}), object({ counts: record(string(), number().int().nonnegative()) }), {
|
|
43308
|
+
kind: "mutation",
|
|
43309
|
+
auth: "admin"
|
|
43310
|
+
});
|
|
43262
43311
|
/**
|
|
43263
43312
|
* `device-removal-hook` — the reliable way an addon learns a device was DELETED,
|
|
43264
43313
|
* so it can delete what it keeps for that device (D865).
|
|
@@ -43906,6 +43955,17 @@ var LlmProfileSchema = object({
|
|
|
43906
43955
|
* field in its editor only when its descriptor lists it in `profileFields`. */
|
|
43907
43956
|
topK: number().int().positive().optional(),
|
|
43908
43957
|
maxTokens: number().int().positive().optional(),
|
|
43958
|
+
/** How hard the model "thinks" before it answers. Omitted = the model's own
|
|
43959
|
+
* default. `off` matters for structured output: a thinking model can spend
|
|
43960
|
+
* the whole completion on reasoning and return an empty `content`. Carried
|
|
43961
|
+
* only by the wires that have it — a kind offers the field in its editor
|
|
43962
|
+
* only when its descriptor lists it in `profileFields`. */
|
|
43963
|
+
reasoningEffort: _enum([
|
|
43964
|
+
"off",
|
|
43965
|
+
"low",
|
|
43966
|
+
"medium",
|
|
43967
|
+
"high"
|
|
43968
|
+
]).optional(),
|
|
43909
43969
|
/** Prompt context window. Advisory for cloud kinds (they enforce their own). */
|
|
43910
43970
|
contextLength: number().int().positive().optional(),
|
|
43911
43971
|
/** Default system prompt. A caller's `system` REPLACES it (never appends —
|
|
@@ -43944,6 +44004,7 @@ var LlmProfileFieldSchema = _enum([
|
|
|
43944
44004
|
"topP",
|
|
43945
44005
|
"topK",
|
|
43946
44006
|
"maxTokens",
|
|
44007
|
+
"reasoningEffort",
|
|
43947
44008
|
"contextLength",
|
|
43948
44009
|
"systemPrompt",
|
|
43949
44010
|
"timeoutMs",
|
|
@@ -58815,6 +58876,318 @@ onBrightnessChanged: { data: object({
|
|
|
58815
58876
|
} } }
|
|
58816
58877
|
};
|
|
58817
58878
|
/**
|
|
58879
|
+
* The role vocabulary of a remote (D963): what a button MEANS, independent of
|
|
58880
|
+
* how it is wired. A layout template places buttons by role; a device binds a
|
|
58881
|
+
* role to an action; an exporter asks for a role by name. Roles are shared so
|
|
58882
|
+
* one layout fits every appliance and one voice command fits every binding.
|
|
58883
|
+
*
|
|
58884
|
+
* A role outside this list is `custom:<key>` — the operator's own button (a
|
|
58885
|
+
* SmartIR source, a channel preset). The key is a lowercase slug.
|
|
58886
|
+
*/
|
|
58887
|
+
var REMOTE_KNOWN_ROLES = [
|
|
58888
|
+
"power",
|
|
58889
|
+
"power_on",
|
|
58890
|
+
"power_off",
|
|
58891
|
+
"volume_up",
|
|
58892
|
+
"volume_down",
|
|
58893
|
+
"mute",
|
|
58894
|
+
"channel_up",
|
|
58895
|
+
"channel_down",
|
|
58896
|
+
"up",
|
|
58897
|
+
"down",
|
|
58898
|
+
"left",
|
|
58899
|
+
"right",
|
|
58900
|
+
"ok",
|
|
58901
|
+
"back",
|
|
58902
|
+
"home",
|
|
58903
|
+
"menu",
|
|
58904
|
+
"info",
|
|
58905
|
+
"guide",
|
|
58906
|
+
"exit",
|
|
58907
|
+
"input",
|
|
58908
|
+
"play",
|
|
58909
|
+
"pause",
|
|
58910
|
+
"play_pause",
|
|
58911
|
+
"stop",
|
|
58912
|
+
"next",
|
|
58913
|
+
"previous",
|
|
58914
|
+
"fast_forward",
|
|
58915
|
+
"rewind",
|
|
58916
|
+
"digit_0",
|
|
58917
|
+
"digit_1",
|
|
58918
|
+
"digit_2",
|
|
58919
|
+
"digit_3",
|
|
58920
|
+
"digit_4",
|
|
58921
|
+
"digit_5",
|
|
58922
|
+
"digit_6",
|
|
58923
|
+
"digit_7",
|
|
58924
|
+
"digit_8",
|
|
58925
|
+
"digit_9"
|
|
58926
|
+
];
|
|
58927
|
+
var CUSTOM_KEY_RE = /^[a-z0-9][a-z0-9_]{0,47}$/;
|
|
58928
|
+
var KNOWN_ROLES = new Set(REMOTE_KNOWN_ROLES);
|
|
58929
|
+
function isRemoteKnownRole(value) {
|
|
58930
|
+
return KNOWN_ROLES.has(value);
|
|
58931
|
+
}
|
|
58932
|
+
function isRemoteCustomKey(key) {
|
|
58933
|
+
return CUSTOM_KEY_RE.test(key);
|
|
58934
|
+
}
|
|
58935
|
+
/** The key of a `custom:<key>` role, or null for any other string. */
|
|
58936
|
+
function customKeyOfRole(role) {
|
|
58937
|
+
if (!role.startsWith("custom:")) return null;
|
|
58938
|
+
const key = role.slice(7);
|
|
58939
|
+
return isRemoteCustomKey(key) ? key : null;
|
|
58940
|
+
}
|
|
58941
|
+
function isRemoteRole(value) {
|
|
58942
|
+
return isRemoteKnownRole(value) || customKeyOfRole(value) !== null;
|
|
58943
|
+
}
|
|
58944
|
+
var RemoteRoleSchema = string().refine(isRemoteRole, { message: "a role is one of the known roles or custom:<key> (lowercase letters, digits, _)" });
|
|
58945
|
+
var RemoteBindingSchema = object({ action: AutomationCapStepSchema.extend({ deviceId: number().int().nonnegative() }) });
|
|
58946
|
+
var RemoteCustomButtonSchema = object({
|
|
58947
|
+
/** The slug after `custom:` in the role. */
|
|
58948
|
+
key: string().refine(isRemoteCustomKey, { message: "a lowercase slug" }),
|
|
58949
|
+
/** What the operator calls it, and what a voice command may name. */
|
|
58950
|
+
name: string().min(1).max(60)
|
|
58951
|
+
});
|
|
58952
|
+
var RemotePowerModeSchema = _enum(["toggle", "discrete"]);
|
|
58953
|
+
var RemoteConfigSchema = object({
|
|
58954
|
+
layoutTemplateId: string().min(1).nullable(),
|
|
58955
|
+
/**
|
|
58956
|
+
* The transmitter device new bindings and an import send through. A binding
|
|
58957
|
+
* names its own device, so one command may override it.
|
|
58958
|
+
*/
|
|
58959
|
+
defaultTransmitterId: number().int().nonnegative().nullable().default(null),
|
|
58960
|
+
/** Keyed by role. A role with no entry falls back to the native cap. */
|
|
58961
|
+
bindings: record(string(), RemoteBindingSchema),
|
|
58962
|
+
customButtons: array(RemoteCustomButtonSchema).max(100),
|
|
58963
|
+
power: object({ mode: RemotePowerModeSchema })
|
|
58964
|
+
}).superRefine((config, ctx) => {
|
|
58965
|
+
const roles = Object.keys(config.bindings);
|
|
58966
|
+
if (roles.length > 200) ctx.addIssue({
|
|
58967
|
+
code: "custom",
|
|
58968
|
+
message: "too many bindings",
|
|
58969
|
+
path: ["bindings"]
|
|
58970
|
+
});
|
|
58971
|
+
for (const role of roles) if (!isRemoteRole(role)) ctx.addIssue({
|
|
58972
|
+
code: "custom",
|
|
58973
|
+
message: `"${role}" is not a role`,
|
|
58974
|
+
path: ["bindings", role]
|
|
58975
|
+
});
|
|
58976
|
+
const keys = /* @__PURE__ */ new Set();
|
|
58977
|
+
config.customButtons.forEach((button, index) => {
|
|
58978
|
+
if (keys.has(button.key)) ctx.addIssue({
|
|
58979
|
+
code: "custom",
|
|
58980
|
+
message: `custom button "${button.key}" is listed twice`,
|
|
58981
|
+
path: [
|
|
58982
|
+
"customButtons",
|
|
58983
|
+
index,
|
|
58984
|
+
"key"
|
|
58985
|
+
]
|
|
58986
|
+
});
|
|
58987
|
+
keys.add(button.key);
|
|
58988
|
+
});
|
|
58989
|
+
for (const role of roles) {
|
|
58990
|
+
const key = customKeyOfRole(role);
|
|
58991
|
+
if (key !== null && !keys.has(key)) ctx.addIssue({
|
|
58992
|
+
code: "custom",
|
|
58993
|
+
message: `${role} is bound but has no entry in customButtons`,
|
|
58994
|
+
path: ["bindings", role]
|
|
58995
|
+
});
|
|
58996
|
+
}
|
|
58997
|
+
});
|
|
58998
|
+
object({
|
|
58999
|
+
power: object({
|
|
59000
|
+
state: _enum(["on", "off"]),
|
|
59001
|
+
at: number()
|
|
59002
|
+
}).nullable(),
|
|
59003
|
+
muted: object({
|
|
59004
|
+
value: boolean(),
|
|
59005
|
+
at: number()
|
|
59006
|
+
}).nullable()
|
|
59007
|
+
});
|
|
59008
|
+
var RemoteLayoutButtonSchema = object({
|
|
59009
|
+
id: string().min(1).max(40),
|
|
59010
|
+
/** 0-based grid position. */
|
|
59011
|
+
row: number().int().min(0).max(24),
|
|
59012
|
+
col: number().int().min(0).max(24),
|
|
59013
|
+
rowSpan: number().int().min(1).max(24),
|
|
59014
|
+
colSpan: number().int().min(1).max(24),
|
|
59015
|
+
icon: string().min(1).max(60).optional(),
|
|
59016
|
+
label: string().min(1).max(40).optional(),
|
|
59017
|
+
role: RemoteRoleSchema
|
|
59018
|
+
});
|
|
59019
|
+
object({
|
|
59020
|
+
columns: number().int().min(1).max(24),
|
|
59021
|
+
rows: number().int().min(1).max(24),
|
|
59022
|
+
buttons: array(RemoteLayoutButtonSchema).max(200)
|
|
59023
|
+
}).superRefine((grid, ctx) => {
|
|
59024
|
+
const ids = /* @__PURE__ */ new Set();
|
|
59025
|
+
const taken = /* @__PURE__ */ new Map();
|
|
59026
|
+
grid.buttons.forEach((button, index) => {
|
|
59027
|
+
if (ids.has(button.id)) ctx.addIssue({
|
|
59028
|
+
code: "custom",
|
|
59029
|
+
message: `button id "${button.id}" is used twice`,
|
|
59030
|
+
path: [
|
|
59031
|
+
"buttons",
|
|
59032
|
+
index,
|
|
59033
|
+
"id"
|
|
59034
|
+
]
|
|
59035
|
+
});
|
|
59036
|
+
ids.add(button.id);
|
|
59037
|
+
if (button.row + button.rowSpan > grid.rows || button.col + button.colSpan > grid.columns) {
|
|
59038
|
+
ctx.addIssue({
|
|
59039
|
+
code: "custom",
|
|
59040
|
+
message: `button "${button.id}" leaves the grid`,
|
|
59041
|
+
path: ["buttons", index]
|
|
59042
|
+
});
|
|
59043
|
+
return;
|
|
59044
|
+
}
|
|
59045
|
+
for (let r = button.row; r < button.row + button.rowSpan; r += 1) for (let c = button.col; c < button.col + button.colSpan; c += 1) {
|
|
59046
|
+
const cell = `${r}:${c}`;
|
|
59047
|
+
const other = taken.get(cell);
|
|
59048
|
+
if (other !== void 0) {
|
|
59049
|
+
ctx.addIssue({
|
|
59050
|
+
code: "custom",
|
|
59051
|
+
message: `buttons "${other}" and "${button.id}" overlap`,
|
|
59052
|
+
path: ["buttons", index]
|
|
59053
|
+
});
|
|
59054
|
+
return;
|
|
59055
|
+
}
|
|
59056
|
+
taken.set(cell, button.id);
|
|
59057
|
+
}
|
|
59058
|
+
});
|
|
59059
|
+
});
|
|
59060
|
+
var RemoteBoundCommandSchema = object({
|
|
59061
|
+
role: RemoteRoleSchema,
|
|
59062
|
+
/** The operator's name for a custom button; absent for a known role. */
|
|
59063
|
+
label: string().optional(),
|
|
59064
|
+
custom: boolean()
|
|
59065
|
+
});
|
|
59066
|
+
var RemoteCommandListSchema = object({
|
|
59067
|
+
layoutTemplateId: string().nullable(),
|
|
59068
|
+
power: object({ mode: RemotePowerModeSchema }),
|
|
59069
|
+
/** Only the roles that are BOUND — the ones a press can reach. */
|
|
59070
|
+
commands: array(RemoteBoundCommandSchema)
|
|
59071
|
+
});
|
|
59072
|
+
var RemoteRefusalReasonSchema = _enum([
|
|
59073
|
+
"no-remote",
|
|
59074
|
+
"unbound-role",
|
|
59075
|
+
"action-failed"
|
|
59076
|
+
]);
|
|
59077
|
+
var RemoteSendResultSchema = discriminatedUnion("status", [
|
|
59078
|
+
object({
|
|
59079
|
+
status: literal("sent"),
|
|
59080
|
+
/** How many times the action ran. */
|
|
59081
|
+
sends: number().int().min(1)
|
|
59082
|
+
}),
|
|
59083
|
+
object({
|
|
59084
|
+
status: literal("skipped"),
|
|
59085
|
+
reason: literal("already-in-state")
|
|
59086
|
+
}),
|
|
59087
|
+
object({
|
|
59088
|
+
status: literal("refused"),
|
|
59089
|
+
reason: RemoteRefusalReasonSchema,
|
|
59090
|
+
detail: string().optional()
|
|
59091
|
+
})
|
|
59092
|
+
]);
|
|
59093
|
+
var RemoteMediaCommandSchema = _enum([
|
|
59094
|
+
"play",
|
|
59095
|
+
"pause",
|
|
59096
|
+
"stop",
|
|
59097
|
+
"next",
|
|
59098
|
+
"previous",
|
|
59099
|
+
"setMute",
|
|
59100
|
+
"selectSource"
|
|
59101
|
+
]);
|
|
59102
|
+
/** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
|
|
59103
|
+
var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
|
|
59104
|
+
var RemoteSetConfigResultSchema = discriminatedUnion("status", [object({ status: literal("saved") }), object({
|
|
59105
|
+
status: literal("refused"),
|
|
59106
|
+
reason: _enum(["unknown-template", "not-a-media-player"]),
|
|
59107
|
+
detail: string()
|
|
59108
|
+
})]);
|
|
59109
|
+
var deviceInput = object({ deviceId: number().int().nonnegative() });
|
|
59110
|
+
DeviceType.MediaPlayer;
|
|
59111
|
+
method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.extend({ config: RemoteConfigSchema }), RemoteSetConfigResultSchema, {
|
|
59112
|
+
kind: "mutation",
|
|
59113
|
+
auth: "admin"
|
|
59114
|
+
}), method(deviceInput, RemoteCommandListSchema, { auth: "protected" }), method(deviceInput.extend({
|
|
59115
|
+
role: RemoteRoleSchema,
|
|
59116
|
+
/** Sends in a row (volume steps). */
|
|
59117
|
+
repeat: number().int().min(1).max(50).default(1)
|
|
59118
|
+
}), RemoteSendResultSchema, {
|
|
59119
|
+
kind: "mutation",
|
|
59120
|
+
auth: "protected"
|
|
59121
|
+
}), method(deviceInput.extend({
|
|
59122
|
+
command: RemoteMediaCommandSchema,
|
|
59123
|
+
muted: boolean().optional(),
|
|
59124
|
+
source: string().optional(),
|
|
59125
|
+
repeat: number().int().min(1).max(50).default(1)
|
|
59126
|
+
}), RemoteMediaCommandResultSchema, {
|
|
59127
|
+
kind: "mutation",
|
|
59128
|
+
auth: "protected"
|
|
59129
|
+
});
|
|
59130
|
+
var TemplateKindSchema = _enum(["remote"]);
|
|
59131
|
+
/**
|
|
59132
|
+
* template — the generic store of reusable, named operator data (D963). A
|
|
59133
|
+
* record is `{ id, kind, name, payload }`; the kinds (today only `remote`, the
|
|
59134
|
+
* button grid of a remote) and the schema each payload must satisfy are
|
|
59135
|
+
* declared by the system in `template-kinds.ts`. Nothing here is specific to a
|
|
59136
|
+
* kind: the engine validates a payload against its kind's schema and, on a
|
|
59137
|
+
* delete, asks the kind's owner whether anything still points at the record.
|
|
59138
|
+
*
|
|
59139
|
+
* Every refusal is a typed result — a UI says "unknown kind" or names the
|
|
59140
|
+
* devices that still use a template, without parsing text.
|
|
59141
|
+
*/
|
|
59142
|
+
var TemplateRecordSchema = object({
|
|
59143
|
+
id: string().min(1),
|
|
59144
|
+
kind: TemplateKindSchema,
|
|
59145
|
+
name: string().min(1).max(120),
|
|
59146
|
+
/** Shaped by the kind; `checkTemplatePayload(kind, payload)` reads it. */
|
|
59147
|
+
payload: unknown(),
|
|
59148
|
+
createdAt: number(),
|
|
59149
|
+
updatedAt: number()
|
|
59150
|
+
});
|
|
59151
|
+
var TemplateWriteResultSchema = discriminatedUnion("status", [object({
|
|
59152
|
+
status: literal("saved"),
|
|
59153
|
+
template: TemplateRecordSchema
|
|
59154
|
+
}), object({
|
|
59155
|
+
status: literal("refused"),
|
|
59156
|
+
reason: _enum(["invalid-payload", "not-found"]),
|
|
59157
|
+
detail: string()
|
|
59158
|
+
})]);
|
|
59159
|
+
/** A device still pointing at a template. */
|
|
59160
|
+
var TemplateUserSchema = object({
|
|
59161
|
+
deviceId: number().int().nonnegative(),
|
|
59162
|
+
name: string()
|
|
59163
|
+
});
|
|
59164
|
+
var TemplateDeleteResultSchema = discriminatedUnion("status", [
|
|
59165
|
+
object({ status: literal("deleted") }),
|
|
59166
|
+
object({ status: literal("not-found") }),
|
|
59167
|
+
object({
|
|
59168
|
+
status: literal("in-use"),
|
|
59169
|
+
devices: array(TemplateUserSchema).min(1)
|
|
59170
|
+
})
|
|
59171
|
+
]);
|
|
59172
|
+
method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSchema), { auth: "protected" }), method(object({ id: string().min(1) }), TemplateRecordSchema.nullable(), { auth: "protected" }), method(object({
|
|
59173
|
+
kind: TemplateKindSchema,
|
|
59174
|
+
name: string().min(1).max(120),
|
|
59175
|
+
payload: unknown()
|
|
59176
|
+
}), TemplateWriteResultSchema, {
|
|
59177
|
+
kind: "mutation",
|
|
59178
|
+
auth: "admin"
|
|
59179
|
+
}), method(object({
|
|
59180
|
+
id: string().min(1),
|
|
59181
|
+
name: string().min(1).max(120).optional(),
|
|
59182
|
+
payload: unknown().optional()
|
|
59183
|
+
}), TemplateWriteResultSchema, {
|
|
59184
|
+
kind: "mutation",
|
|
59185
|
+
auth: "admin"
|
|
59186
|
+
}), method(object({ id: string().min(1) }), TemplateDeleteResultSchema, {
|
|
59187
|
+
kind: "mutation",
|
|
59188
|
+
auth: "admin"
|
|
59189
|
+
});
|
|
59190
|
+
/**
|
|
58818
59191
|
* button — device-scoped capability for HA `button.*` / `input_button.*`
|
|
58819
59192
|
* fire-only actuators. A button entity has no readable state — pressing it
|
|
58820
59193
|
* triggers an action on the HA side. There is no `status` / `runtimeState`:
|
|
@@ -58836,6 +59209,56 @@ var buttonCapability = {
|
|
|
58836
59209
|
}) }
|
|
58837
59210
|
};
|
|
58838
59211
|
/**
|
|
59212
|
+
* A pulse train in signed microseconds: mark (+) first, then space (-), strictly
|
|
59213
|
+
* alternating, no zeros, 2..1024 entries.
|
|
59214
|
+
*/
|
|
59215
|
+
var IrTimingsSchema = array(number().int()).min(2).max(1024).superRefine((timings, ctx) => {
|
|
59216
|
+
timings.forEach((value, index) => {
|
|
59217
|
+
const expectMark = index % 2 === 0;
|
|
59218
|
+
if (value === 0 || value > 0 !== expectMark) ctx.addIssue({
|
|
59219
|
+
code: "custom",
|
|
59220
|
+
message: `timing ${index} must be a non-zero ${expectMark ? "mark (positive)" : "space (negative)"}`,
|
|
59221
|
+
path: [index]
|
|
59222
|
+
});
|
|
59223
|
+
});
|
|
59224
|
+
});
|
|
59225
|
+
/** Per-command transmit options. Every key is optional; a transmitter applies what it can. */
|
|
59226
|
+
var TransmitSettingsSchema = object({
|
|
59227
|
+
carrierHz: number().int().positive().optional(),
|
|
59228
|
+
dutyCyclePercent: number().int().min(1).max(100).optional(),
|
|
59229
|
+
repeat: number().int().min(0).max(20).optional(),
|
|
59230
|
+
repeatDelayMs: number().int().min(0).optional()
|
|
59231
|
+
});
|
|
59232
|
+
/** The setting keys a transmitter may declare as settable (`getInfo.settable`). */
|
|
59233
|
+
var TransmitSettingKeySchema = _enum([
|
|
59234
|
+
"carrierHz",
|
|
59235
|
+
"dutyCyclePercent",
|
|
59236
|
+
"repeat",
|
|
59237
|
+
"repeatDelayMs"
|
|
59238
|
+
]);
|
|
59239
|
+
var IrMediumSchema = _enum(["ir", "rf"]);
|
|
59240
|
+
var IrSendResultSchema = object({
|
|
59241
|
+
ok: literal(true),
|
|
59242
|
+
/** Settings the transmitter could not apply; the train was still sent. */
|
|
59243
|
+
ignoredSettings: array(string())
|
|
59244
|
+
});
|
|
59245
|
+
var IrTransmitterInfoSchema = object({
|
|
59246
|
+
media: array(IrMediumSchema),
|
|
59247
|
+
settable: array(TransmitSettingKeySchema),
|
|
59248
|
+
maxTimings: number().int().positive()
|
|
59249
|
+
});
|
|
59250
|
+
DeviceType.Generic, method(object({
|
|
59251
|
+
deviceId: number().int().nonnegative(),
|
|
59252
|
+
timings: IrTimingsSchema,
|
|
59253
|
+
settings: TransmitSettingsSchema.optional()
|
|
59254
|
+
}), IrSendResultSchema, {
|
|
59255
|
+
kind: "mutation",
|
|
59256
|
+
auth: "protected"
|
|
59257
|
+
}), method(object({ deviceId: number().int().nonnegative() }), IrTransmitterInfoSchema, {
|
|
59258
|
+
kind: "query",
|
|
59259
|
+
auth: "protected"
|
|
59260
|
+
});
|
|
59261
|
+
/**
|
|
58839
59262
|
* `calendar-source` — collection cap exposing per-provider calendars and
|
|
58840
59263
|
* their events. Every addon that contributes calendars (Home Assistant,
|
|
58841
59264
|
* a future Google Calendar addon, …) registers a provider here. The hub
|
|
@@ -62932,9 +63355,35 @@ var MediaInfoSchema = object({
|
|
|
62932
63355
|
/** Optional cover-art / thumbnail URL. */
|
|
62933
63356
|
imageUrl: string().optional()
|
|
62934
63357
|
});
|
|
63358
|
+
/**
|
|
63359
|
+
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
63360
|
+
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
63361
|
+
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
63362
|
+
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
63363
|
+
* guess, never as a reading.
|
|
63364
|
+
*/
|
|
63365
|
+
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
63366
|
+
/** The fields of a media-player status a driver can only guess. */
|
|
63367
|
+
var MediaPlayerAssumedFieldSchema = _enum([
|
|
63368
|
+
"state",
|
|
63369
|
+
"volumeLevel",
|
|
63370
|
+
"isMuted",
|
|
63371
|
+
"source"
|
|
63372
|
+
]);
|
|
62935
63373
|
var MediaPlayerStatusSchema = object({
|
|
62936
63374
|
/** Playback lifecycle state. */
|
|
62937
63375
|
state: MediaPlayerStateSchema,
|
|
63376
|
+
/**
|
|
63377
|
+
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
63378
|
+
* A slice written before this field existed parses as `reported`.
|
|
63379
|
+
*/
|
|
63380
|
+
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
63381
|
+
/**
|
|
63382
|
+
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
63383
|
+
* this is not empty; a field not listed here is a reading even then (power
|
|
63384
|
+
* from a running-state source, mute guessed from the last send).
|
|
63385
|
+
*/
|
|
63386
|
+
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
62938
63387
|
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
62939
63388
|
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
62940
63389
|
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
@@ -64862,9 +65311,14 @@ var PetFeederStatusSchema = object({
|
|
|
64862
65311
|
* (`device_class: problem`, on = low). True when the tank reports empty,
|
|
64863
65312
|
* or, on a dual-hopper model, when EITHER hopper is empty. */
|
|
64864
65313
|
lowFood: boolean(),
|
|
64865
|
-
/** Battery charge 0..100 (%).
|
|
64866
|
-
*
|
|
65314
|
+
/** Battery charge 0..100 (%). On a feeder running on `mains` this is the
|
|
65315
|
+
* BACKUP battery's charge, not the power source. Null when the device has
|
|
65316
|
+
* no battery reading. */
|
|
64867
65317
|
batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
65318
|
+
/** What the feeder is powered from right now: `mains` (plugged in, the
|
|
65319
|
+
* battery is only a backup) or `battery`. Null when the device does not
|
|
65320
|
+
* report it. */
|
|
65321
|
+
powerSource: _enum(["mains", "battery"]).nullable(),
|
|
64868
65322
|
/**
|
|
64869
65323
|
* Desiccant life remaining, stored in seconds (duration base, D944).
|
|
64870
65324
|
* Providers convert from the days the feeder reports. Null when the
|
|
@@ -64892,6 +65346,14 @@ var PetFeederStatusSchema = object({
|
|
|
64892
65346
|
feedSound: boolean(),
|
|
64893
65347
|
/** Speaker / prompt volume level (device-scaled integer). */
|
|
64894
65348
|
volume: number().meta(readingMeta("none")),
|
|
65349
|
+
/** Portion (grams) of the last manual feed sent from a single-hopper
|
|
65350
|
+
* stepper — the default the next manual feed starts from. Persisted per
|
|
65351
|
+
* device by the provider; null until a manual feed has been sent. */
|
|
65352
|
+
lastFeedGrams: gramsPortion.nullable(),
|
|
65353
|
+
/** Hopper-1 portion of the last manual feed (dual-hopper); null until sent. */
|
|
65354
|
+
lastFeedHopper1: gramsPortion.nullable(),
|
|
65355
|
+
/** Hopper-2 portion of the last manual feed (dual-hopper); null until sent. */
|
|
65356
|
+
lastFeedHopper2: gramsPortion.nullable(),
|
|
64895
65357
|
/** Ms epoch when the slice was last refreshed from the cloud. */
|
|
64896
65358
|
lastFetchedAt: number()
|
|
64897
65359
|
});
|
|
@@ -70929,9 +71391,17 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
|
|
|
70929
71391
|
kind: "mutation",
|
|
70930
71392
|
auth: "admin"
|
|
70931
71393
|
});
|
|
71394
|
+
discriminatedUnion("kind", [object({
|
|
71395
|
+
kind: literal("raw"),
|
|
71396
|
+
timings: array(number().int()),
|
|
71397
|
+
settings: TransmitSettingsSchema.optional()
|
|
71398
|
+
}), object({
|
|
71399
|
+
kind: literal("pronto"),
|
|
71400
|
+
code: string()
|
|
71401
|
+
})]);
|
|
70932
71402
|
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
70933
71403
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
70934
|
-
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
71404
|
+
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
|
|
70935
71405
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
70936
71406
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
70937
71407
|
new Set(CAP_NAMES);
|
|
@@ -76084,6 +76554,20 @@ Object.freeze({
|
|
|
76084
76554
|
access: "create",
|
|
76085
76555
|
role: "viewer"
|
|
76086
76556
|
},
|
|
76557
|
+
"irTransmitter.getInfo": {
|
|
76558
|
+
capName: "ir-transmitter",
|
|
76559
|
+
capScope: "device",
|
|
76560
|
+
addonId: null,
|
|
76561
|
+
access: "view",
|
|
76562
|
+
role: "viewer"
|
|
76563
|
+
},
|
|
76564
|
+
"irTransmitter.send": {
|
|
76565
|
+
capName: "ir-transmitter",
|
|
76566
|
+
capScope: "device",
|
|
76567
|
+
addonId: null,
|
|
76568
|
+
access: "create",
|
|
76569
|
+
role: "viewer"
|
|
76570
|
+
},
|
|
76087
76571
|
"keychain.delete": {
|
|
76088
76572
|
capName: "keychain",
|
|
76089
76573
|
capScope: "system",
|
|
@@ -77316,6 +77800,13 @@ Object.freeze({
|
|
|
77316
77800
|
access: "create",
|
|
77317
77801
|
role: "admin"
|
|
77318
77802
|
},
|
|
77803
|
+
"nodeRemovalHook.onNodeRemoved": {
|
|
77804
|
+
capName: "node-removal-hook",
|
|
77805
|
+
capScope: "system",
|
|
77806
|
+
addonId: null,
|
|
77807
|
+
access: "create",
|
|
77808
|
+
role: "admin"
|
|
77809
|
+
},
|
|
77319
77810
|
"nodes.clusterAddonStatus": {
|
|
77320
77811
|
capName: "nodes",
|
|
77321
77812
|
capScope: "system",
|
|
@@ -80207,6 +80698,41 @@ Object.freeze({
|
|
|
80207
80698
|
access: "create",
|
|
80208
80699
|
role: "admin"
|
|
80209
80700
|
},
|
|
80701
|
+
"remote.getConfig": {
|
|
80702
|
+
capName: "remote",
|
|
80703
|
+
capScope: "system",
|
|
80704
|
+
addonId: null,
|
|
80705
|
+
access: "view",
|
|
80706
|
+
role: "admin"
|
|
80707
|
+
},
|
|
80708
|
+
"remote.listCommands": {
|
|
80709
|
+
capName: "remote",
|
|
80710
|
+
capScope: "system",
|
|
80711
|
+
addonId: null,
|
|
80712
|
+
access: "view",
|
|
80713
|
+
role: "admin"
|
|
80714
|
+
},
|
|
80715
|
+
"remote.sendCommand": {
|
|
80716
|
+
capName: "remote",
|
|
80717
|
+
capScope: "system",
|
|
80718
|
+
addonId: null,
|
|
80719
|
+
access: "create",
|
|
80720
|
+
role: "admin"
|
|
80721
|
+
},
|
|
80722
|
+
"remote.sendMediaCommand": {
|
|
80723
|
+
capName: "remote",
|
|
80724
|
+
capScope: "system",
|
|
80725
|
+
addonId: null,
|
|
80726
|
+
access: "create",
|
|
80727
|
+
role: "admin"
|
|
80728
|
+
},
|
|
80729
|
+
"remote.setConfig": {
|
|
80730
|
+
capName: "remote",
|
|
80731
|
+
capScope: "system",
|
|
80732
|
+
addonId: null,
|
|
80733
|
+
access: "create",
|
|
80734
|
+
role: "admin"
|
|
80735
|
+
},
|
|
80210
80736
|
"runningState.getStatus": {
|
|
80211
80737
|
capName: "running-state",
|
|
80212
80738
|
capScope: "device",
|
|
@@ -81397,6 +81923,41 @@ Object.freeze({
|
|
|
81397
81923
|
access: "view",
|
|
81398
81924
|
role: "viewer"
|
|
81399
81925
|
},
|
|
81926
|
+
"template.create": {
|
|
81927
|
+
capName: "template",
|
|
81928
|
+
capScope: "system",
|
|
81929
|
+
addonId: null,
|
|
81930
|
+
access: "create",
|
|
81931
|
+
role: "admin"
|
|
81932
|
+
},
|
|
81933
|
+
"template.delete": {
|
|
81934
|
+
capName: "template",
|
|
81935
|
+
capScope: "system",
|
|
81936
|
+
addonId: null,
|
|
81937
|
+
access: "delete",
|
|
81938
|
+
role: "admin"
|
|
81939
|
+
},
|
|
81940
|
+
"template.get": {
|
|
81941
|
+
capName: "template",
|
|
81942
|
+
capScope: "system",
|
|
81943
|
+
addonId: null,
|
|
81944
|
+
access: "view",
|
|
81945
|
+
role: "admin"
|
|
81946
|
+
},
|
|
81947
|
+
"template.list": {
|
|
81948
|
+
capName: "template",
|
|
81949
|
+
capScope: "system",
|
|
81950
|
+
addonId: null,
|
|
81951
|
+
access: "view",
|
|
81952
|
+
role: "admin"
|
|
81953
|
+
},
|
|
81954
|
+
"template.update": {
|
|
81955
|
+
capName: "template",
|
|
81956
|
+
capScope: "system",
|
|
81957
|
+
addonId: null,
|
|
81958
|
+
access: "create",
|
|
81959
|
+
role: "admin"
|
|
81960
|
+
},
|
|
81400
81961
|
"terminalSession.adoptLegacyMonitor": {
|
|
81401
81962
|
capName: "terminal-session",
|
|
81402
81963
|
capScope: "system",
|
|
@@ -83707,6 +84268,11 @@ var ACTUATOR_CAP_VOCABULARY = {
|
|
|
83707
84268
|
icon: "circle-dot"
|
|
83708
84269
|
}]
|
|
83709
84270
|
},
|
|
84271
|
+
"ir-transmitter": {
|
|
84272
|
+
scope: "device",
|
|
84273
|
+
noun: t("IR / RF transmitter", "Trasmettitore IR / RF"),
|
|
84274
|
+
notOperatorFacing: { send: "a raw pulse train is not something an operator composes in an automation; the remote that owns the command table sends it" }
|
|
84275
|
+
},
|
|
83710
84276
|
control: {
|
|
83711
84277
|
scope: "device",
|
|
83712
84278
|
noun: t("Control", "Controllo"),
|
|
@@ -86184,6 +86750,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
86184
86750
|
"device-manager": system("the device registry itself — rename, move and remove are administration"),
|
|
86185
86751
|
"device-provider": system(INTERNAL),
|
|
86186
86752
|
"device-removal-hook": system(INTERNAL),
|
|
86753
|
+
"node-removal-hook": system(INTERNAL),
|
|
86187
86754
|
"device-state": system("the runtime-state mirror every trigger already reads through"),
|
|
86188
86755
|
"display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
|
|
86189
86756
|
"embedding-encoder": system(ML),
|
|
@@ -86223,6 +86790,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
86223
86790
|
"recording-archive": system(STORAGE),
|
|
86224
86791
|
"recording-export": system("recording exports — requested from the timeline, not an automation step"),
|
|
86225
86792
|
records: system(INTERNAL),
|
|
86793
|
+
remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
|
|
86226
86794
|
"server-management": system(CLUSTER),
|
|
86227
86795
|
"settings-store": system(INTERNAL),
|
|
86228
86796
|
"smtp-provider": system(INTERNAL),
|
|
@@ -86235,6 +86803,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
86235
86803
|
"storage-provider": system(STORAGE),
|
|
86236
86804
|
"stream-broker": system(PIPELINE),
|
|
86237
86805
|
system: system(CLUSTER),
|
|
86806
|
+
template: system("operator-authored templates — edited in their own editor, never actuated"),
|
|
86238
86807
|
"terminal-session": system(CLUSTER),
|
|
86239
86808
|
toast: system(UI_PLUMBING),
|
|
86240
86809
|
training: system(ML),
|
|
@@ -87426,6 +87995,16 @@ Object.freeze({
|
|
|
87426
87995
|
form: "single",
|
|
87427
87996
|
optional: false
|
|
87428
87997
|
}],
|
|
87998
|
+
"irTransmitter.getInfo": [{
|
|
87999
|
+
name: "deviceId",
|
|
88000
|
+
form: "single",
|
|
88001
|
+
optional: false
|
|
88002
|
+
}],
|
|
88003
|
+
"irTransmitter.send": [{
|
|
88004
|
+
name: "deviceId",
|
|
88005
|
+
form: "single",
|
|
88006
|
+
optional: false
|
|
88007
|
+
}],
|
|
87429
88008
|
"keychain.deleteDeviceScope": [{
|
|
87430
88009
|
name: "deviceId",
|
|
87431
88010
|
form: "single",
|
|
@@ -88651,6 +89230,31 @@ Object.freeze({
|
|
|
88651
89230
|
form: "single",
|
|
88652
89231
|
optional: true
|
|
88653
89232
|
}],
|
|
89233
|
+
"remote.getConfig": [{
|
|
89234
|
+
name: "deviceId",
|
|
89235
|
+
form: "single",
|
|
89236
|
+
optional: false
|
|
89237
|
+
}],
|
|
89238
|
+
"remote.listCommands": [{
|
|
89239
|
+
name: "deviceId",
|
|
89240
|
+
form: "single",
|
|
89241
|
+
optional: false
|
|
89242
|
+
}],
|
|
89243
|
+
"remote.sendCommand": [{
|
|
89244
|
+
name: "deviceId",
|
|
89245
|
+
form: "single",
|
|
89246
|
+
optional: false
|
|
89247
|
+
}],
|
|
89248
|
+
"remote.sendMediaCommand": [{
|
|
89249
|
+
name: "deviceId",
|
|
89250
|
+
form: "single",
|
|
89251
|
+
optional: false
|
|
89252
|
+
}],
|
|
89253
|
+
"remote.setConfig": [{
|
|
89254
|
+
name: "deviceId",
|
|
89255
|
+
form: "single",
|
|
89256
|
+
optional: false
|
|
89257
|
+
}],
|
|
88654
89258
|
"runningState.getStatus": [{
|
|
88655
89259
|
name: "deviceId",
|
|
88656
89260
|
form: "single",
|
|
@@ -90817,6 +91421,29 @@ function rewriteSetCookie(cookie, route) {
|
|
|
90817
91421
|
return kept.join("; ");
|
|
90818
91422
|
}
|
|
90819
91423
|
/**
|
|
91424
|
+
* Every Set-Cookie of one vendor response, rewritten. Dropping `Domain` folds cookies
|
|
91425
|
+
* the vendor keeps apart (`session-id` on `.amazon.it` and on `.www.amazon.it`) into
|
|
91426
|
+
* one, so a past-dated set that only deleted the other domain's copy would delete the
|
|
91427
|
+
* live one the same response just set: that deletion is dropped.
|
|
91428
|
+
*/
|
|
91429
|
+
function rewriteSetCookies(cookies, route) {
|
|
91430
|
+
const now = Date.now();
|
|
91431
|
+
const liveNames = new Set(cookies.filter((c) => !isExpiredCookie(c, now)).map(cookieName));
|
|
91432
|
+
return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).map((c) => rewriteSetCookie(c, route));
|
|
91433
|
+
}
|
|
91434
|
+
function cookieName(cookie) {
|
|
91435
|
+
return cookie.split(";")[0]?.split("=")[0]?.trim() ?? "";
|
|
91436
|
+
}
|
|
91437
|
+
function isExpiredCookie(cookie, now) {
|
|
91438
|
+
const attrs = cookie.split(";").slice(1).map((part) => part.trim());
|
|
91439
|
+
const maxAge = attrs.find((a) => /^max-age=/i.test(a));
|
|
91440
|
+
if (maxAge !== void 0) return Number(maxAge.slice(8)) <= 0;
|
|
91441
|
+
const expires = attrs.find((a) => /^expires=/i.test(a));
|
|
91442
|
+
if (expires === void 0) return false;
|
|
91443
|
+
const at = Date.parse(expires.slice(8));
|
|
91444
|
+
return Number.isFinite(at) && at <= now;
|
|
91445
|
+
}
|
|
91446
|
+
/**
|
|
90820
91447
|
* A browser `Referer` that points into this proxy, turned back into the vendor URL it
|
|
90821
91448
|
* stands for. A Referer that is not ours is dropped (null): it names the hub.
|
|
90822
91449
|
*/
|
|
@@ -90862,6 +91489,81 @@ function rewriteFormBody(body, route) {
|
|
|
90862
91489
|
function filterForwardedCookies(cookieHeader) {
|
|
90863
91490
|
return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
|
|
90864
91491
|
}
|
|
91492
|
+
/** A short stable label for the attempt: enough to group lines, useless as a capability. */
|
|
91493
|
+
function attemptLabel(attemptId) {
|
|
91494
|
+
let hash = 2166136261;
|
|
91495
|
+
for (let i = 0; i < attemptId.length; i += 1) hash = Math.imul(hash ^ attemptId.charCodeAt(i), 16777619) >>> 0;
|
|
91496
|
+
return hash.toString(16).padStart(8, "0");
|
|
91497
|
+
}
|
|
91498
|
+
/** Distinct parameter names of a `?a=1&b=2` string (or of a urlencoded body), in order. */
|
|
91499
|
+
function paramNames(query) {
|
|
91500
|
+
const names = [];
|
|
91501
|
+
const raw = query.startsWith("?") ? query.slice(1) : query;
|
|
91502
|
+
for (const pair of raw.split("&")) {
|
|
91503
|
+
if (pair.length === 0) continue;
|
|
91504
|
+
const eq = pair.indexOf("=");
|
|
91505
|
+
const rawName = eq === -1 ? pair : pair.slice(0, eq);
|
|
91506
|
+
let name;
|
|
91507
|
+
try {
|
|
91508
|
+
name = decodeURIComponent(rawName.replace(/\+/g, " "));
|
|
91509
|
+
} catch {
|
|
91510
|
+
name = rawName;
|
|
91511
|
+
}
|
|
91512
|
+
if (!names.includes(name)) names.push(name);
|
|
91513
|
+
}
|
|
91514
|
+
return names;
|
|
91515
|
+
}
|
|
91516
|
+
function hasOa2(names) {
|
|
91517
|
+
return names.some((name) => name.startsWith("openid.oa2.") || name === "openid.ns.oa2");
|
|
91518
|
+
}
|
|
91519
|
+
function cookieNamesOf(setCookies) {
|
|
91520
|
+
return setCookies.map((c) => c.split(";")[0]?.split("=")[0]?.trim() ?? "");
|
|
91521
|
+
}
|
|
91522
|
+
function mediaType(contentType) {
|
|
91523
|
+
return (contentType ?? "none").split(";")[0]?.trim().toLowerCase() || "none";
|
|
91524
|
+
}
|
|
91525
|
+
/** Host and path of a Location (no query), or 'unparsable'. */
|
|
91526
|
+
function locationShape(location, base) {
|
|
91527
|
+
try {
|
|
91528
|
+
const url = new URL(location, base);
|
|
91529
|
+
return {
|
|
91530
|
+
target: `${url.host}${url.pathname}`,
|
|
91531
|
+
names: paramNames(url.search)
|
|
91532
|
+
};
|
|
91533
|
+
} catch {
|
|
91534
|
+
return {
|
|
91535
|
+
target: "unparsable",
|
|
91536
|
+
names: []
|
|
91537
|
+
};
|
|
91538
|
+
}
|
|
91539
|
+
}
|
|
91540
|
+
/** The `meta` of one trace line. Values: never. */
|
|
91541
|
+
function flowTraceMeta(input) {
|
|
91542
|
+
const queryNames = paramNames(input.requestSearch);
|
|
91543
|
+
const formNames = input.formBody === null ? null : paramNames(input.formBody);
|
|
91544
|
+
const meta = {
|
|
91545
|
+
session: attemptLabel(input.attemptId),
|
|
91546
|
+
method: input.method,
|
|
91547
|
+
upstream: `${input.upstreamHost}${input.upstreamPath}`,
|
|
91548
|
+
queryNames: queryNames.join(",") || "none",
|
|
91549
|
+
oa2InQuery: hasOa2(queryNames),
|
|
91550
|
+
status: input.status,
|
|
91551
|
+
setCookieNames: cookieNamesOf(input.setCookies).join(",") || "none",
|
|
91552
|
+
cookiesSent: (input.cookieHeader ?? "").split(";").filter((p) => p.trim().length > 0).length
|
|
91553
|
+
};
|
|
91554
|
+
if (formNames !== null) {
|
|
91555
|
+
meta["contentType"] = mediaType(input.contentType);
|
|
91556
|
+
meta["formNames"] = formNames.join(",") || "none";
|
|
91557
|
+
meta["oa2InForm"] = hasOa2(formNames);
|
|
91558
|
+
}
|
|
91559
|
+
if (input.location !== null) {
|
|
91560
|
+
const loc = locationShape(input.location, `http://${input.upstreamHost}/`);
|
|
91561
|
+
meta["location"] = loc.target;
|
|
91562
|
+
meta["locationQueryNames"] = loc.names.join(",") || "none";
|
|
91563
|
+
meta["oa2InLocation"] = hasOa2(loc.names);
|
|
91564
|
+
}
|
|
91565
|
+
return meta;
|
|
91566
|
+
}
|
|
90865
91567
|
var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
|
|
90866
91568
|
var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
|
|
90867
91569
|
/** Request headers that are never forwarded to the vendor. */
|
|
@@ -90933,6 +91635,26 @@ async function readRequestBody(req) {
|
|
|
90933
91635
|
}
|
|
90934
91636
|
return Buffer.concat(chunks);
|
|
90935
91637
|
}
|
|
91638
|
+
/** The path of the Referer sent upstream, or why none was sent — the query is never logged. */
|
|
91639
|
+
function forwardedRefererPath(forwarded, received) {
|
|
91640
|
+
if (forwarded !== void 0) return URL.canParse(forwarded) ? new URL(forwarded).pathname : "unparsable";
|
|
91641
|
+
return received === void 0 ? "absent" : "unmapped";
|
|
91642
|
+
}
|
|
91643
|
+
/** The form targets a vendor page posts to, path only (no query), so a lost path prefix is visible. */
|
|
91644
|
+
function formActionPaths(html) {
|
|
91645
|
+
const actions = [...html.matchAll(/<form\b[^>]*\baction\s*=\s*["']([^"']*)["']/gi)].map((m) => (m[1] ?? "").split("?")[0] ?? "").slice(0, 5);
|
|
91646
|
+
return actions.length === 0 ? "none" : actions.join(" | ");
|
|
91647
|
+
}
|
|
91648
|
+
/** Cookie NAMES only — values are session secrets and are never logged. */
|
|
91649
|
+
function cookieNames(header) {
|
|
91650
|
+
if (header === void 0 || header.length === 0) return "none";
|
|
91651
|
+
return header.split(";").map((part) => part.split("=")[0]?.trim() ?? "").join(",");
|
|
91652
|
+
}
|
|
91653
|
+
/** A Set-Cookie's name and attributes without its value, so a cookie the browser refuses is visible. */
|
|
91654
|
+
function setCookieShape(cookie) {
|
|
91655
|
+
const [first, ...attrs] = cookie.split(";").map((part) => part.trim());
|
|
91656
|
+
return [first?.split("=")[0] ?? "", ...attrs.filter((a) => !/^expires=/i.test(a))].join(";");
|
|
91657
|
+
}
|
|
90936
91658
|
function sendPlain(res, status, text) {
|
|
90937
91659
|
res.writeHead(status, {
|
|
90938
91660
|
"content-type": "text/plain; charset=utf-8",
|
|
@@ -90946,6 +91668,8 @@ var VendorLoginProxy = class {
|
|
|
90946
91668
|
#now;
|
|
90947
91669
|
#fetch;
|
|
90948
91670
|
#attempts = /* @__PURE__ */ new Map();
|
|
91671
|
+
/** Trace lines written per attempt (bounded by FLOW_TRACE_MAX_LINES). */
|
|
91672
|
+
#traced = /* @__PURE__ */ new Map();
|
|
90949
91673
|
constructor(options) {
|
|
90950
91674
|
this.#addonId = options.addonId;
|
|
90951
91675
|
this.#logger = options.logger;
|
|
@@ -90972,6 +91696,7 @@ var VendorLoginProxy = class {
|
|
|
90972
91696
|
}
|
|
90973
91697
|
close(attemptId) {
|
|
90974
91698
|
this.#attempts.delete(attemptId);
|
|
91699
|
+
this.#traced.delete(attemptId);
|
|
90975
91700
|
}
|
|
90976
91701
|
isOpen(attemptId) {
|
|
90977
91702
|
const attempt = this.#attempts.get(attemptId);
|
|
@@ -91018,7 +91743,7 @@ var VendorLoginProxy = class {
|
|
|
91018
91743
|
const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
|
|
91019
91744
|
const attempt = this.#attempts.get(parsed.attemptId);
|
|
91020
91745
|
if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
|
|
91021
|
-
if (attempt !== void 0) this
|
|
91746
|
+
if (attempt !== void 0) this.close(parsed.attemptId);
|
|
91022
91747
|
this.#logger.info("vendor login proxy: attempt refused", { meta: {
|
|
91023
91748
|
addonId: this.#addonId,
|
|
91024
91749
|
reason: attempt === void 0 ? "missing" : "expired"
|
|
@@ -91043,13 +91768,17 @@ var VendorLoginProxy = class {
|
|
|
91043
91768
|
}
|
|
91044
91769
|
const method = (req.method ?? "GET").toUpperCase();
|
|
91045
91770
|
let body;
|
|
91771
|
+
let formBodyRaw = null;
|
|
91046
91772
|
if (method !== "GET" && method !== "HEAD") {
|
|
91047
91773
|
const read = await readRequestBody(req);
|
|
91048
91774
|
if (read === "too-large") {
|
|
91049
91775
|
sendPlain(res, 413, "Request too large");
|
|
91050
91776
|
return;
|
|
91051
91777
|
}
|
|
91052
|
-
|
|
91778
|
+
if (isFormEncoded(req.headers["content-type"])) {
|
|
91779
|
+
formBodyRaw = read.toString("utf8");
|
|
91780
|
+
body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
|
|
91781
|
+
} else body = read;
|
|
91053
91782
|
}
|
|
91054
91783
|
const headers = this.#requestHeaders(req, route, upstream);
|
|
91055
91784
|
const upstreamRes = await this.#fetch(target, {
|
|
@@ -91068,8 +91797,25 @@ var VendorLoginProxy = class {
|
|
|
91068
91797
|
host: upstream.host,
|
|
91069
91798
|
path: target.pathname,
|
|
91070
91799
|
status: upstreamRes.status,
|
|
91071
|
-
method
|
|
91800
|
+
method,
|
|
91801
|
+
refererPath: forwardedRefererPath(headers["referer"], req.headers["referer"]),
|
|
91802
|
+
formActions: kind === "html" && text !== null ? formActionPaths(text) : void 0,
|
|
91803
|
+
sentNames: cookieNames(headers["cookie"]),
|
|
91804
|
+
vendorSets: setCookies.map(setCookieShape).join(" | ")
|
|
91072
91805
|
} });
|
|
91806
|
+
this.#trace(parsed.attemptId, {
|
|
91807
|
+
attemptId: parsed.attemptId,
|
|
91808
|
+
method,
|
|
91809
|
+
upstreamHost: upstream.host,
|
|
91810
|
+
upstreamPath: target.pathname,
|
|
91811
|
+
requestSearch: target.search,
|
|
91812
|
+
formBody: formBodyRaw,
|
|
91813
|
+
contentType: firstHeader(req.headers["content-type"]),
|
|
91814
|
+
status: upstreamRes.status,
|
|
91815
|
+
location,
|
|
91816
|
+
setCookies,
|
|
91817
|
+
cookieHeader: headers["cookie"]
|
|
91818
|
+
});
|
|
91073
91819
|
const settled = await attempt.evaluate({
|
|
91074
91820
|
status: upstreamRes.status,
|
|
91075
91821
|
url: target.toString(),
|
|
@@ -91078,7 +91824,7 @@ var VendorLoginProxy = class {
|
|
|
91078
91824
|
bodyText: text
|
|
91079
91825
|
});
|
|
91080
91826
|
if (settled.kind === "done") {
|
|
91081
|
-
this
|
|
91827
|
+
this.close(parsed.attemptId);
|
|
91082
91828
|
this.#logger.info("vendor login proxy: sign-in finished", { meta: {
|
|
91083
91829
|
addonId: this.#addonId,
|
|
91084
91830
|
status: settled.landing.status
|
|
@@ -91090,7 +91836,7 @@ var VendorLoginProxy = class {
|
|
|
91090
91836
|
upstreamRes.headers.forEach((value, name) => {
|
|
91091
91837
|
if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
|
|
91092
91838
|
});
|
|
91093
|
-
if (setCookies.length > 0) out["set-cookie"] = setCookies
|
|
91839
|
+
if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route);
|
|
91094
91840
|
if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
|
|
91095
91841
|
out["cache-control"] = "no-store";
|
|
91096
91842
|
if (text !== null && kind !== null) {
|
|
@@ -91102,6 +91848,16 @@ var VendorLoginProxy = class {
|
|
|
91102
91848
|
res.writeHead(upstreamRes.status, out);
|
|
91103
91849
|
await this.#pipeBody(upstreamRes, res);
|
|
91104
91850
|
}
|
|
91851
|
+
/** One names-only line per proxied request, at most FLOW_TRACE_MAX_LINES per attempt. */
|
|
91852
|
+
#trace(attemptId, input) {
|
|
91853
|
+
const written = this.#traced.get(attemptId) ?? 0;
|
|
91854
|
+
if (written >= 200) return;
|
|
91855
|
+
this.#traced.set(attemptId, written + 1);
|
|
91856
|
+
this.#logger.info("vendor login proxy: flow", { meta: {
|
|
91857
|
+
addonId: this.#addonId,
|
|
91858
|
+
...flowTraceMeta(input)
|
|
91859
|
+
} });
|
|
91860
|
+
}
|
|
91105
91861
|
#requestHeaders(req, route, upstream) {
|
|
91106
91862
|
const headers = {};
|
|
91107
91863
|
for (const [name, value] of Object.entries(req.headers)) {
|
|
@@ -91143,6 +91899,14 @@ var VendorLoginProxy = class {
|
|
|
91143
91899
|
};
|
|
91144
91900
|
var MB = 1024 * 1024;
|
|
91145
91901
|
1024 * MB, 3072 * MB;
|
|
91902
|
+
object({
|
|
91903
|
+
manufacturer: string().optional(),
|
|
91904
|
+
supportedModels: array(string()).optional(),
|
|
91905
|
+
supportedController: string().optional(),
|
|
91906
|
+
commandsEncoding: string(),
|
|
91907
|
+
delay: number().nonnegative().optional(),
|
|
91908
|
+
commands: record(string(), unknown())
|
|
91909
|
+
});
|
|
91146
91910
|
//#endregion
|
|
91147
91911
|
//#region src/config.ts
|
|
91148
91912
|
/**
|
|
@@ -133081,20 +133845,36 @@ var ADDON_ID = "import-alexa";
|
|
|
133081
133845
|
var ATTEMPT_TTL_MS = 600 * 1e3;
|
|
133082
133846
|
/** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
|
|
133083
133847
|
var CAPTURE_WAIT_MS = 6e4;
|
|
133848
|
+
/** The query parameter Amazon sets on `/ap/maplanding` when it honoured the oa2 request. */
|
|
133849
|
+
var AUTHORIZATION_CODE_PARAM = "openid.oa2.authorization_code";
|
|
133084
133850
|
/**
|
|
133085
|
-
* Terminal condition
|
|
133086
|
-
* `/cookie-success` page
|
|
133087
|
-
* `
|
|
133851
|
+
* Terminal condition. The login is over when alexa-cookie2 turns Amazon's final redirect into
|
|
133852
|
+
* a 302 to its own `/cookie-success` page (it only does so when the landing carries the oa2
|
|
133853
|
+
* authorization code), OR when Amazon's `/ap/maplanding` itself shows up as a `Location` or as
|
|
133854
|
+
* the request. A maplanding WITHOUT the authorization code is a dead end: alexa-cookie2 cannot
|
|
133855
|
+
* register a device from it and no cookie will ever come, so it ends at once as
|
|
133856
|
+
* `no_authorization_code` instead of after the capture wait. Only paths and parameter NAMES are
|
|
133857
|
+
* read, never a value (the query carries signed openid values).
|
|
133088
133858
|
*/
|
|
133089
133859
|
function cookieSuccessTerminal(view) {
|
|
133090
|
-
|
|
133091
|
-
|
|
133092
|
-
|
|
133860
|
+
for (const raw of [view.url, view.location]) {
|
|
133861
|
+
if (raw === null) continue;
|
|
133862
|
+
const end = loginEnd(raw);
|
|
133863
|
+
if (end !== null) return {
|
|
133093
133864
|
kind: "done",
|
|
133094
|
-
payload:
|
|
133095
|
-
}
|
|
133865
|
+
payload: end
|
|
133866
|
+
};
|
|
133867
|
+
}
|
|
133868
|
+
return { kind: "continue" };
|
|
133869
|
+
}
|
|
133870
|
+
function loginEnd(raw) {
|
|
133871
|
+
try {
|
|
133872
|
+
const url = new URL(raw, `http://${ALEXA_PROXY_LOOPBACK_HOST}`);
|
|
133873
|
+
if (url.pathname === "/cookie-success") return "registering";
|
|
133874
|
+
if (!url.pathname.endsWith("/ap/maplanding")) return null;
|
|
133875
|
+
return url.searchParams.has(AUTHORIZATION_CODE_PARAM) ? "registering" : "no_authorization_code";
|
|
133096
133876
|
} catch {
|
|
133097
|
-
return
|
|
133877
|
+
return null;
|
|
133098
133878
|
}
|
|
133099
133879
|
}
|
|
133100
133880
|
var AlexaProxyLoginManager = class {
|
|
@@ -133130,7 +133910,15 @@ var AlexaProxyLoginManager = class {
|
|
|
133130
133910
|
scheme: "http"
|
|
133131
133911
|
}],
|
|
133132
133912
|
terminal: cookieSuccessTerminal,
|
|
133133
|
-
onDone: async () => {
|
|
133913
|
+
onDone: async (end) => {
|
|
133914
|
+
if (end === "no_authorization_code") {
|
|
133915
|
+
this.#logger.warn("alexa proxy-login: Amazon ended the login without an authorization code", { meta: { sessionId } });
|
|
133916
|
+
return {
|
|
133917
|
+
status: "error",
|
|
133918
|
+
addonId: ADDON_ID,
|
|
133919
|
+
reason: "no_authorization_code"
|
|
133920
|
+
};
|
|
133921
|
+
}
|
|
133134
133922
|
return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
|
|
133135
133923
|
status: "success",
|
|
133136
133924
|
addonId: ADDON_ID
|