@camstack/addon-import-alexa 0.2.165 → 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/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-Crlmu_tO.mjs
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-DIorGYeC.mjs
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-DwA0uVW5.mjs
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).
@@ -58827,6 +58876,318 @@ onBrightnessChanged: { data: object({
58827
58876
  } } }
58828
58877
  };
58829
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
+ /**
58830
59191
  * button — device-scoped capability for HA `button.*` / `input_button.*`
58831
59192
  * fire-only actuators. A button entity has no readable state — pressing it
58832
59193
  * triggers an action on the HA side. There is no `status` / `runtimeState`:
@@ -58848,6 +59209,56 @@ var buttonCapability = {
58848
59209
  }) }
58849
59210
  };
58850
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
+ /**
58851
59262
  * `calendar-source` — collection cap exposing per-provider calendars and
58852
59263
  * their events. Every addon that contributes calendars (Home Assistant,
58853
59264
  * a future Google Calendar addon, …) registers a provider here. The hub
@@ -62944,9 +63355,35 @@ var MediaInfoSchema = object({
62944
63355
  /** Optional cover-art / thumbnail URL. */
62945
63356
  imageUrl: string().optional()
62946
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
+ ]);
62947
63373
  var MediaPlayerStatusSchema = object({
62948
63374
  /** Playback lifecycle state. */
62949
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([]),
62950
63387
  /** Volume as 0..100 inclusive. Null when the device has no volume
62951
63388
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62952
63389
  volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
@@ -64874,9 +65311,14 @@ var PetFeederStatusSchema = object({
64874
65311
  * (`device_class: problem`, on = low). True when the tank reports empty,
64875
65312
  * or, on a dual-hopper model, when EITHER hopper is empty. */
64876
65313
  lowFood: boolean(),
64877
- /** Battery charge 0..100 (%). Null on mains-powered models or when the
64878
- * device has no battery reading. */
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. */
64879
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(),
64880
65322
  /**
64881
65323
  * Desiccant life remaining, stored in seconds (duration base, D944).
64882
65324
  * Providers convert from the days the feeder reports. Null when the
@@ -64904,6 +65346,14 @@ var PetFeederStatusSchema = object({
64904
65346
  feedSound: boolean(),
64905
65347
  /** Speaker / prompt volume level (device-scaled integer). */
64906
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(),
64907
65357
  /** Ms epoch when the slice was last refreshed from the cloud. */
64908
65358
  lastFetchedAt: number()
64909
65359
  });
@@ -70941,9 +71391,17 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
70941
71391
  kind: "mutation",
70942
71392
  auth: "admin"
70943
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
+ })]);
70944
71402
  DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70945
71403
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70946
- 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;
70947
71405
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70948
71406
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70949
71407
  new Set(CAP_NAMES);
@@ -76096,6 +76554,20 @@ Object.freeze({
76096
76554
  access: "create",
76097
76555
  role: "viewer"
76098
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
+ },
76099
76571
  "keychain.delete": {
76100
76572
  capName: "keychain",
76101
76573
  capScope: "system",
@@ -77328,6 +77800,13 @@ Object.freeze({
77328
77800
  access: "create",
77329
77801
  role: "admin"
77330
77802
  },
77803
+ "nodeRemovalHook.onNodeRemoved": {
77804
+ capName: "node-removal-hook",
77805
+ capScope: "system",
77806
+ addonId: null,
77807
+ access: "create",
77808
+ role: "admin"
77809
+ },
77331
77810
  "nodes.clusterAddonStatus": {
77332
77811
  capName: "nodes",
77333
77812
  capScope: "system",
@@ -80219,6 +80698,41 @@ Object.freeze({
80219
80698
  access: "create",
80220
80699
  role: "admin"
80221
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
+ },
80222
80736
  "runningState.getStatus": {
80223
80737
  capName: "running-state",
80224
80738
  capScope: "device",
@@ -81409,6 +81923,41 @@ Object.freeze({
81409
81923
  access: "view",
81410
81924
  role: "viewer"
81411
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
+ },
81412
81961
  "terminalSession.adoptLegacyMonitor": {
81413
81962
  capName: "terminal-session",
81414
81963
  capScope: "system",
@@ -83719,6 +84268,11 @@ var ACTUATOR_CAP_VOCABULARY = {
83719
84268
  icon: "circle-dot"
83720
84269
  }]
83721
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
+ },
83722
84276
  control: {
83723
84277
  scope: "device",
83724
84278
  noun: t("Control", "Controllo"),
@@ -86196,6 +86750,7 @@ var SYSTEM_CAP_VOCABULARY = {
86196
86750
  "device-manager": system("the device registry itself — rename, move and remove are administration"),
86197
86751
  "device-provider": system(INTERNAL),
86198
86752
  "device-removal-hook": system(INTERNAL),
86753
+ "node-removal-hook": system(INTERNAL),
86199
86754
  "device-state": system("the runtime-state mirror every trigger already reads through"),
86200
86755
  "display-palette": system("the cluster display palette — one colour table for every client, not a device action"),
86201
86756
  "embedding-encoder": system(ML),
@@ -86235,6 +86790,7 @@ var SYSTEM_CAP_VOCABULARY = {
86235
86790
  "recording-archive": system(STORAGE),
86236
86791
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
86237
86792
  records: system(INTERNAL),
86793
+ remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
86238
86794
  "server-management": system(CLUSTER),
86239
86795
  "settings-store": system(INTERNAL),
86240
86796
  "smtp-provider": system(INTERNAL),
@@ -86247,6 +86803,7 @@ var SYSTEM_CAP_VOCABULARY = {
86247
86803
  "storage-provider": system(STORAGE),
86248
86804
  "stream-broker": system(PIPELINE),
86249
86805
  system: system(CLUSTER),
86806
+ template: system("operator-authored templates — edited in their own editor, never actuated"),
86250
86807
  "terminal-session": system(CLUSTER),
86251
86808
  toast: system(UI_PLUMBING),
86252
86809
  training: system(ML),
@@ -87438,6 +87995,16 @@ Object.freeze({
87438
87995
  form: "single",
87439
87996
  optional: false
87440
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
+ }],
87441
88008
  "keychain.deleteDeviceScope": [{
87442
88009
  name: "deviceId",
87443
88010
  form: "single",
@@ -88663,6 +89230,31 @@ Object.freeze({
88663
89230
  form: "single",
88664
89231
  optional: true
88665
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
+ }],
88666
89258
  "runningState.getStatus": [{
88667
89259
  name: "deviceId",
88668
89260
  form: "single",
@@ -90897,6 +91489,81 @@ function rewriteFormBody(body, route) {
90897
91489
  function filterForwardedCookies(cookieHeader) {
90898
91490
  return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
90899
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
+ }
90900
91567
  var MAX_REQUEST_BODY_BYTES = 2 * 1024 * 1024;
90901
91568
  var MAX_TEXT_BODY_BYTES = 4 * 1024 * 1024;
90902
91569
  /** Request headers that are never forwarded to the vendor. */
@@ -91001,6 +91668,8 @@ var VendorLoginProxy = class {
91001
91668
  #now;
91002
91669
  #fetch;
91003
91670
  #attempts = /* @__PURE__ */ new Map();
91671
+ /** Trace lines written per attempt (bounded by FLOW_TRACE_MAX_LINES). */
91672
+ #traced = /* @__PURE__ */ new Map();
91004
91673
  constructor(options) {
91005
91674
  this.#addonId = options.addonId;
91006
91675
  this.#logger = options.logger;
@@ -91027,6 +91696,7 @@ var VendorLoginProxy = class {
91027
91696
  }
91028
91697
  close(attemptId) {
91029
91698
  this.#attempts.delete(attemptId);
91699
+ this.#traced.delete(attemptId);
91030
91700
  }
91031
91701
  isOpen(attemptId) {
91032
91702
  const attempt = this.#attempts.get(attemptId);
@@ -91073,7 +91743,7 @@ var VendorLoginProxy = class {
91073
91743
  const forwardedPrefix = (firstHeader(req.headers["x-forwarded-prefix"]) ?? "").replace(/\/+$/, "");
91074
91744
  const attempt = this.#attempts.get(parsed.attemptId);
91075
91745
  if (attempt === void 0 || this.#now() > attempt.expiresAtMs) {
91076
- if (attempt !== void 0) this.#attempts.delete(parsed.attemptId);
91746
+ if (attempt !== void 0) this.close(parsed.attemptId);
91077
91747
  this.#logger.info("vendor login proxy: attempt refused", { meta: {
91078
91748
  addonId: this.#addonId,
91079
91749
  reason: attempt === void 0 ? "missing" : "expired"
@@ -91098,13 +91768,17 @@ var VendorLoginProxy = class {
91098
91768
  }
91099
91769
  const method = (req.method ?? "GET").toUpperCase();
91100
91770
  let body;
91771
+ let formBodyRaw = null;
91101
91772
  if (method !== "GET" && method !== "HEAD") {
91102
91773
  const read = await readRequestBody(req);
91103
91774
  if (read === "too-large") {
91104
91775
  sendPlain(res, 413, "Request too large");
91105
91776
  return;
91106
91777
  }
91107
- body = isFormEncoded(req.headers["content-type"]) ? Buffer.from(rewriteFormBody(read.toString("utf8"), route), "utf8") : read;
91778
+ if (isFormEncoded(req.headers["content-type"])) {
91779
+ formBodyRaw = read.toString("utf8");
91780
+ body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
91781
+ } else body = read;
91108
91782
  }
91109
91783
  const headers = this.#requestHeaders(req, route, upstream);
91110
91784
  const upstreamRes = await this.#fetch(target, {
@@ -91129,6 +91803,19 @@ var VendorLoginProxy = class {
91129
91803
  sentNames: cookieNames(headers["cookie"]),
91130
91804
  vendorSets: setCookies.map(setCookieShape).join(" | ")
91131
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
+ });
91132
91819
  const settled = await attempt.evaluate({
91133
91820
  status: upstreamRes.status,
91134
91821
  url: target.toString(),
@@ -91137,7 +91824,7 @@ var VendorLoginProxy = class {
91137
91824
  bodyText: text
91138
91825
  });
91139
91826
  if (settled.kind === "done") {
91140
- this.#attempts.delete(parsed.attemptId);
91827
+ this.close(parsed.attemptId);
91141
91828
  this.#logger.info("vendor login proxy: sign-in finished", { meta: {
91142
91829
  addonId: this.#addonId,
91143
91830
  status: settled.landing.status
@@ -91161,6 +91848,16 @@ var VendorLoginProxy = class {
91161
91848
  res.writeHead(upstreamRes.status, out);
91162
91849
  await this.#pipeBody(upstreamRes, res);
91163
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
+ }
91164
91861
  #requestHeaders(req, route, upstream) {
91165
91862
  const headers = {};
91166
91863
  for (const [name, value] of Object.entries(req.headers)) {
@@ -91202,6 +91899,14 @@ var VendorLoginProxy = class {
91202
91899
  };
91203
91900
  var MB = 1024 * 1024;
91204
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
+ });
91205
91910
  //#endregion
91206
91911
  //#region src/config.ts
91207
91912
  /**
@@ -133140,20 +133845,36 @@ var ADDON_ID = "import-alexa";
133140
133845
  var ATTEMPT_TTL_MS = 600 * 1e3;
133141
133846
  /** How long the browser waits on the Amazon page for alexa-cookie2 to finish registering. */
133142
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";
133143
133850
  /**
133144
- * Terminal condition: alexa-cookie2 turns Amazon's final redirect into a 302 to its own
133145
- * `/cookie-success` page once it holds the login cookie. Matching on the path of the raw
133146
- * `Location` keeps this independent of the page's HTML.
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).
133147
133858
  */
133148
133859
  function cookieSuccessTerminal(view) {
133149
- if (view.location === null) return { kind: "continue" };
133150
- try {
133151
- return new URL(view.location, `http://127.0.0.1`).pathname === "/cookie-success" ? {
133860
+ for (const raw of [view.url, view.location]) {
133861
+ if (raw === null) continue;
133862
+ const end = loginEnd(raw);
133863
+ if (end !== null) return {
133152
133864
  kind: "done",
133153
- payload: true
133154
- } : { kind: "continue" };
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";
133155
133876
  } catch {
133156
- return { kind: "continue" };
133877
+ return null;
133157
133878
  }
133158
133879
  }
133159
133880
  var AlexaProxyLoginManager = class {
@@ -133189,7 +133910,15 @@ var AlexaProxyLoginManager = class {
133189
133910
  scheme: "http"
133190
133911
  }],
133191
133912
  terminal: cookieSuccessTerminal,
133192
- 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
+ }
133193
133922
  return await this.#session?.whenCaptured(this.#captureWaitMs) === true ? {
133194
133923
  status: "success",
133195
133924
  addonId: ADDON_ID