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