registry: describe an Alexa interface as data

src/registry/ holds what the library knows about an interface: namespace, version, the page it was read from,
properties with their value schemas and directives with their payload schemas. Five interfaces are described
(Alexa, PowerController, BrightnessController, TemperatureSensor, EndpointHealth); the other 65 names of
AlexaInterfaceType are stubs with the version and property names of 1.5.2. schema.ts is the run-time check behind
it (241 lines, no new dependency), catalog.ts the vocabularies of the pages: 103 assets (23 units), 6 actions,
9 states, 56 display categories, 22 reserved words, 73 error types under 11 namespaces.

AlexaInterface.getVersion() and getProps() read the registry; the two switch statements are gone (-167 lines).
On the wire: Alexa.EndpointHealth is announced at 3.1 (was 3.3; the page is titled 3.1 and no page mentions 3.3),
and TimeHoldController and Camera.LiveViewController at 3 and 1.7 (1.5.2 sent the string "UNKNOWN").
DisplayCategory gains VACUUM. New exports: registry, DeclarationError, SchemaError, Assets, Units, Actions,
States, DisplayCategories and the descriptor types.

Tests: 20 JSON examples of the five pages under test/fixtures/alexa-docs; every directive payload and property
value in them parses with its descriptor. npm test: 57 pass (was 30) in 10.8 s, also on Node 18.20.8 and 20.20.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 15:18:22 +00:00
parent 128ca35c2a
commit aa0ffd64ea
93 changed files with 4058 additions and 490 deletions

40
dist/types/registry/catalog.d.ts vendored Normal file
View file

@ -0,0 +1,40 @@
/**
* The asset ids a friendly name can refer to: 103, the units of measure among them
* (resources-and-assets.html, "Global Alexa catalog").
*/
export declare const ASSETS: readonly ["Alexa.Actions.Charge", "Alexa.Actions.Clean", "Alexa.Actions.Dispense", "Alexa.Actions.Dock", "Alexa.Actions.Empty", "Alexa.Actions.Mop", "Alexa.Actions.Skip", "Alexa.Actions.Sweep", "Alexa.Actions.Vacuum", "Alexa.Button.OffButton", "Alexa.Button.OnButton", "Alexa.Button.BrightenButton", "Alexa.Button.DimButton", "Alexa.Button.MainButton", "Alexa.Button.TopButton", "Alexa.Button.BottomButton", "Alexa.Button.CenterButton", "Alexa.Button.MiddleButton", "Alexa.Button.UpButton", "Alexa.Button.DownButton", "Alexa.Button.LeftButton", "Alexa.Button.RightButton", "Alexa.Button.FirstButton", "Alexa.Button.SecondButton", "Alexa.Button.ThirdButton", "Alexa.Button.FourthButton", "Alexa.Button.FifthButton", "Alexa.Button.SixthButton", "Alexa.Button.SeventhButton", "Alexa.Button.EighthButton", "Alexa.Button.DoublePress", "Alexa.Button.DoublePush", "Alexa.Button.LongPress", "Alexa.Button.LongPush", "Alexa.Button.SinglePress", "Alexa.Button.SinglePush", "Alexa.DeviceName.AirPurifier", "Alexa.DeviceName.Camera", "Alexa.DeviceName.Fan", "Alexa.DeviceName.Router", "Alexa.DeviceName.Shade", "Alexa.DeviceName.Shower", "Alexa.DeviceName.SpaceHeater", "Alexa.DeviceName.Washer", "Alexa.Gesture.DoubleClick", "Alexa.Gesture.SingleClick", "Alexa.Gesture.SwipeDown", "Alexa.Gesture.SwipeLeft", "Alexa.Gesture.SwipeRight", "Alexa.Gesture.SwipeUp", "Alexa.Gesture.Tap", "Alexa.Gestures.DoubleTap", "Alexa.Setting.2GGuestWiFi", "Alexa.Setting.5GGuestWiFi", "Alexa.Setting.Auto", "Alexa.Setting.Direction", "Alexa.Setting.DryCycle", "Alexa.Setting.FanSpeed", "Alexa.Setting.GuestWiFi", "Alexa.Setting.Heat", "Alexa.Setting.Mode", "Alexa.Setting.Night", "Alexa.Setting.Opening", "Alexa.Setting.Oscillate", "Alexa.Setting.Preset", "Alexa.Setting.Quiet", "Alexa.Setting.Temperature", "Alexa.Setting.WashCycle", "Alexa.Setting.WaterTemperature", "Alexa.Shower.HandHeld", "Alexa.Shower.RainHead", "Alexa.Unit.Angle.Degrees", "Alexa.Unit.Angle.Radians", "Alexa.Unit.Distance.Feet", "Alexa.Unit.Distance.Inches", "Alexa.Unit.Distance.Kilometers", "Alexa.Unit.Distance.Meters", "Alexa.Unit.Distance.Miles", "Alexa.Unit.Distance.Yards", "Alexa.Unit.Mass.Grams", "Alexa.Unit.Mass.Kilograms", "Alexa.Unit.Percent", "Alexa.Unit.Temperature.Celsius", "Alexa.Unit.Temperature.Degrees", "Alexa.Unit.Temperature.Fahrenheit", "Alexa.Unit.Temperature.Kelvin", "Alexa.Unit.Volume.CubicFeet", "Alexa.Unit.Volume.CubicMeters", "Alexa.Unit.Volume.Gallons", "Alexa.Unit.Volume.Liters", "Alexa.Unit.Volume.Pints", "Alexa.Unit.Volume.Quarts", "Alexa.Unit.Weight.Ounces", "Alexa.Unit.Weight.Pounds", "Alexa.Value.Close", "Alexa.Value.Delicate", "Alexa.Value.High", "Alexa.Value.Low", "Alexa.Value.Maximum", "Alexa.Value.Medium", "Alexa.Value.Minimum", "Alexa.Value.Open", "Alexa.Value.QuickWash"];
export type AssetId = (typeof ASSETS)[number];
/** A unit of measure: the 23 Alexa.Unit.* assets, what a RangeController's unitOfMeasure takes. */
export type UnitOfMeasure = Extract<AssetId, `Alexa.Unit.${string}`>;
export declare const UNITS_OF_MEASURE: UnitOfMeasure[];
/** The phrases an action mapping gives to a directive (alexa-discovery-objects.html, "ActionMappings object"). */
export declare const ACTIONS: readonly ["Alexa.Actions.Open", "Alexa.Actions.Close", "Alexa.Actions.Raise", "Alexa.Actions.Lower", "Alexa.Actions.SetEcoOn", "Alexa.Actions.SetEcoOff"];
export type ActionId = (typeof ACTIONS)[number];
/** The states a state mapping gives to a property value (alexa-discovery-objects.html, "StateMappings object"). */
export declare const STATES: readonly ["Alexa.States.Open", "Alexa.States.Closed", "Alexa.States.EcoOn", "Alexa.States.EcoOff", "Alexa.States.Low", "Alexa.States.Empty", "Alexa.States.Full", "Alexa.States.Done", "Alexa.States.Stuck"];
export type StateId = (typeof STATES)[number];
/**
* The 56 display categories (alexa-discovery.html, "Display categories"). The DisplayCategory enum also keeps
* VEHICLE from 1.x, which the page no longer lists.
*/
export declare const DISPLAY_CATEGORIES: readonly ["ACTIVITY_TRIGGER", "AIR_CONDITIONER", "AIR_FRESHENER", "AIR_PURIFIER", "AIR_QUALITY_MONITOR", "ALEXA_VOICE_ENABLED", "AUTO_ACCESSORY", "BLUETOOTH_SPEAKER", "CAMERA", "CHRISTMAS_TREE", "COFFEE_MAKER", "COMPUTER", "CONTACT_SENSOR", "DISHWASHER", "DOOR", "DOORBELL", "DRYER", "EXTERIOR_BLIND", "FAN", "GAME_CONSOLE", "GARAGE_DOOR", "HEADPHONES", "HUB", "INTERIOR_BLIND", "LAPTOP", "LIGHT", "MICROWAVE", "MOBILE_PHONE", "MOTION_SENSOR", "MUSIC_SYSTEM", "NETWORK_HARDWARE", "OTHER", "OVEN", "PHONE", "PRINTER", "REMOTE", "ROUTER", "SCENE_TRIGGER", "SCREEN", "SECURITY_PANEL", "SECURITY_SYSTEM", "SLOW_COOKER", "SMARTLOCK", "SMARTPLUG", "SPEAKER", "STREAMING_DEVICE", "SWITCH", "TABLET", "TEMPERATURE_SENSOR", "THERMOSTAT", "TV", "VACUUM_CLEANER", "VACUUM", "WASHER", "WATER_HEATER", "WEARABLE"];
export type DisplayCategoryName = (typeof DISPLAY_CATEGORIES)[number];
/** Not to be used as a friendly name (resources-and-assets.html, "Reserved words"). */
export declare const RESERVED_WORDS: readonly ["alarm", "alarms", "all alarms", "away mode", "bass", "camera", "date", "date today", "day", "do not disturb", "drop in", "music", "night light", "notification", "playing", "sleep sounds", "time", "timer", "today in music", "treble", "volume", "way f. m."];
/**
* What a discovery answer may hold (alexa-discovery.html, "Interface limits"; alexa-discovery-objects.html,
* "Endpoint object details" and "AdditionalAttributes object details"; alexa-scenecontroller.html, "Discovery").
*/
export declare const LIMITS: {
readonly endpointsPerCustomer: 300;
readonly capabilitiesPerEndpoint: 100;
readonly endpointIdLength: 256;
readonly friendlyNameLength: 256;
readonly sceneFriendlyNameLength: 128;
readonly manufacturerNameLength: 128;
readonly descriptionLength: 128;
readonly additionalAttributeLength: 256;
readonly cookieBytes: 5000;
};
/** The 73 error types of the table, each with the namespace its ErrorResponse goes under. */
export declare const ERROR_TYPES: Readonly<Record<string, string>>;

20
dist/types/registry/index.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
import { Alexa } from "./interfaces/Alexa.js";
import { BrightnessController } from "./interfaces/BrightnessController.js";
import { EndpointHealth } from "./interfaces/EndpointHealth.js";
import { PowerController } from "./interfaces/PowerController.js";
import { TemperatureSensor } from "./interfaces/TemperatureSensor.js";
import type { AnyDescriptor } from "./types.js";
export declare const registry: {
/** Whether an interface of this name is known. */
has(namespace: string): boolean;
/** The descriptor of "Alexa.RangeController" or of AlexaInterfaceType.RANGE_CONTROLLER, which is that string. */
get(namespace: string): AnyDescriptor;
/** Every descriptor, ordered by namespace. */
list(): AnyDescriptor[];
};
export { Alexa, BrightnessController, EndpointHealth, PowerController, TemperatureSensor };
export { DeclarationError, defineInterface } from "./types.js";
export type { AnyDescriptor, CapabilityExtras, Declared, DirectiveDescriptor, EndpointView, EventDescriptor, InterfaceDescriptor, Label, PropertyDescriptor, } from "./types.js";
export { s, SchemaError } from "./schema.js";
export type { Infer, Schema, Temperature, TimeInterval } from "./schema.js";
export * from "./catalog.js";

View file

@ -0,0 +1,10 @@
/**
* The base interface: every endpoint lists it (alexa-interface.html, "Support the Alexa interface in all add-ons").
* Its one directive to a device is ReportState, answered with a StateReport.
*/
export declare const Alexa: import("../types.js").InterfaceDescriptor<{}, {
ReportState: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
}, {}, false>;

View file

@ -0,0 +1,20 @@
/** Both directives turn a light that is off on, at the brightness asked for (alexa-brightnesscontroller.html). */
export declare const BrightnessController: import("../types.js").InterfaceDescriptor<{
brightness: {
name: string;
value: import("../schema.js").Schema<number>;
};
}, {
SetBrightness: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
brightness: import("../schema.js").Schema<number>;
}>>;
};
AdjustBrightness: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{
brightnessDelta: import("../schema.js").Schema<number>;
}>>;
};
}, {}, false>;

View file

@ -0,0 +1,15 @@
export declare const CONNECTIVITY_REASONS: readonly ["WIFI_BAD_PASSWORD", "WIFI_AP_NOT_FOUND", "WIFI_ROUTER_UNREACHABLE", "WIFI_AP_CHANNEL_QUALITY_LOW", "INTERNET_UNREACHABLE", "CAPTIVE_PORTAL_CHECK_FAILED", "UNKNOWN"];
/**
* Version 3.1, as the page is titled. It defines connectivity only. Examples on other pages declare 3 and 3.2;
* alex2node 1.x declared 3.3, which no page mentions.
*/
export declare const EndpointHealth: import("../types.js").InterfaceDescriptor<{
connectivity: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").InferShape<{
value: import("../schema.js").EnumSchema<"OK" | "UNREACHABLE">;
reason: import("../schema.js").OptionalSchema<"WIFI_BAD_PASSWORD" | "WIFI_AP_NOT_FOUND" | "WIFI_ROUTER_UNREACHABLE" | "WIFI_AP_CHANNEL_QUALITY_LOW" | "INTERNET_UNREACHABLE" | "CAPTIVE_PORTAL_CHECK_FAILED" | "UNKNOWN">;
}>>;
note: string;
};
}, {}, {}, false>;

View file

@ -0,0 +1,17 @@
export declare const PowerController: import("../types.js").InterfaceDescriptor<{
powerState: {
name: string;
value: import("../schema.js").EnumSchema<"ON" | "OFF">;
};
}, {
TurnOn: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
TurnOff: {
name: string;
payload: import("../schema.js").Schema<import("../schema.js").InferShape<{}>>;
};
}, import("../schema.js").InferShape<{
verificationsRequired: import("../schema.js").OptionalSchema<("TurnOn" | "TurnOff")[]>;
}>, false>;

View file

@ -0,0 +1,7 @@
/** A sensor: an endpoint that has it declares Alexa.EndpointHealth as well (alexa-temperaturesensor.html). */
export declare const TemperatureSensor: import("../types.js").InterfaceDescriptor<{
temperature: {
name: string;
value: import("../schema.js").Schema<import("../schema.js").Temperature>;
};
}, {}, {}, false>;

View file

@ -0,0 +1,2 @@
import type { AnyDescriptor } from "../types.js";
export declare const STUBS: readonly AnyDescriptor[];

91
dist/types/registry/schema.d.ts vendored Normal file
View file

@ -0,0 +1,91 @@
/** A value that does not fit its schema: path is where ("payload.targetSetpoint.scale"), problem is what. */
export declare class SchemaError extends Error {
readonly path: string;
readonly problem: string;
constructor(path: string, problem: string);
}
export interface Schema<T> {
/** What the schema accepts, in the words of an error message: "an integer from 0 to 100", "ON | OFF". */
readonly expects: string;
/** The checked value. path names it in the error message. */
parse(input: unknown, path?: string): T;
}
/** A schema that accepts a missing value; s.object() makes its key optional in the inferred type. */
export interface OptionalSchema<T> extends Schema<T | undefined> {
readonly optional: true;
}
export interface EnumSchema<V> extends Schema<V> {
readonly values: readonly V[];
}
/** The type a schema produces. */
export type Infer<S> = S extends Schema<infer T> ? T : never;
export type Shape = Record<string, Schema<any>>;
type OptionalKeys<S extends Shape> = {
[K in keyof S]: S[K] extends OptionalSchema<any> ? K : never;
}[keyof S];
export type InferShape<S extends Shape> = {
[K in Exclude<keyof S, OptionalKeys<S>>]: Infer<S[K]>;
} & {
[K in OptionalKeys<S>]?: Infer<S[K]>;
};
export interface NumberRules {
min?: number;
max?: number;
/** Greater than, the bound itself excluded. */
gt?: number;
integer?: boolean;
}
export interface Temperature {
value: number;
scale: "CELSIUS" | "FAHRENHEIT" | "KELVIN";
}
export interface TimeInterval {
start?: string;
end?: string;
duration?: string;
}
declare function number(rules?: NumberRules): Schema<number>;
declare function string(rules?: {
min?: number;
max?: number;
pattern?: RegExp;
expects?: string;
}): Schema<string>;
declare function boolean(): Schema<boolean>;
declare function literal<V extends string | number | boolean | null>(value: V): Schema<V>;
declare function enumeration<V extends readonly string[]>(...values: V): EnumSchema<V[number]>;
declare function unknown(): Schema<unknown>;
declare function optional<T>(inner: Schema<T>): OptionalSchema<T>;
declare function nullable<T>(inner: Schema<T>): Schema<T | null>;
declare function array<T>(item: Schema<T>, rules?: {
min?: number;
max?: number;
}): Schema<T[]>;
/**
* An object with the keys of shape. Keys the shape does not name are kept as they are: a field Alexa adds to a
* directive reaches the handler. unknownKeys "reject" is for what a developer writes, where such a key is a typo.
*/
declare function object<S extends Shape>(shape: S, rules?: {
unknownKeys?: "keep" | "reject";
}): Schema<InferShape<S>>;
declare function temperature(): Schema<Temperature>;
declare function dateTime(): Schema<string>;
declare function duration(): Schema<string>;
declare function timeInterval(): Schema<TimeInterval>;
export declare const s: {
string: typeof string;
number: typeof number;
boolean: typeof boolean;
literal: typeof literal;
enum: typeof enumeration;
unknown: typeof unknown;
optional: typeof optional;
nullable: typeof nullable;
array: typeof array;
object: typeof object;
temperature: typeof temperature;
dateTime: typeof dateTime;
duration: typeof duration;
timeInterval: typeof timeInterval;
};
export {};

123
dist/types/registry/types.d.ts vendored Normal file
View file

@ -0,0 +1,123 @@
import type { Schema } from "./schema.js";
/**
* A friendly name: text in one locale, or an asset of the global Alexa catalog, which stands for several names in
* every language Alexa speaks (resources-and-assets.html, "Label object").
*/
export type Label = {
"@type": "text";
value: {
text: string;
locale: string;
};
} | {
"@type": "asset";
value: {
assetId: string;
};
};
export interface PropertyDescriptor<V> {
name: string;
value: Schema<V>;
note?: string;
}
export interface DirectiveDescriptor<T> {
name: string;
payload: Schema<T>;
/** Alexa sends the directive only to a capability declared like this (AdjustMode: an ordered mode). */
when?: (capability: Declared<any>) => boolean;
note?: string;
}
/** An event a device raises that is not a Response: ActivationStarted, DoorbellPress. */
export interface EventDescriptor {
name: string;
/** Default: the namespace of the interface. */
namespace?: string;
/** Default: the version of the interface. */
payloadVersion?: string;
payload: Schema<any>;
/** response: the answer to a directive. proactive: sent without one. */
topic: "response" | "proactive";
}
/** What an interface adds to the capability object in discovery, next to the fields every capability has. */
export interface CapabilityExtras {
configuration?: Record<string, unknown>;
/** Alexa.EqualizerController spells it in the plural. */
configurations?: Record<string, unknown>;
/** Fields of the capability object itself: supportsDeactivation, supportedOperations, inputs. */
topLevel?: Record<string, unknown>;
/** false: the capability has no properties object (the Alexa interface, a scene). */
properties?: false;
}
/** A capability as declared on an endpoint: what discovery() and validate() of its descriptor are given. */
export interface Declared<O = Record<string, unknown>> {
readonly endpointId: string;
readonly namespace: string;
/** "" when the interface is declared without an instance. */
readonly instance: string;
readonly friendlyNames: readonly Label[];
readonly retrievable: boolean;
readonly proactivelyReported: boolean;
readonly nonControllable?: boolean;
/** The options of the interface, as its options schema returned them. */
readonly options: O;
}
/** The endpoint a capability is declared on, as validate() sees it. */
export interface EndpointView {
readonly endpointId: string;
readonly friendlyName: string;
readonly description: string;
readonly displayCategories: readonly string[];
/** The other capabilities of the endpoint. */
readonly capabilities: readonly Declared[];
}
export type Properties = Record<string, PropertyDescriptor<any>>;
export type Directives = Record<string, DirectiveDescriptor<any>>;
export interface InterfaceDescriptor<P extends Properties = Properties, D extends Directives = Directives, O = {}, I extends boolean = boolean> {
/** "Alexa.RangeController" */
namespace: string;
/** As the title of the interface's page gives it: "3", "3.1", "1.0". */
version: string;
/** The page the descriptor was written from. */
doc: string;
kind: "base" | "controller" | "sensor" | "eventSource";
/** 1 and 2: described in full. 3: a stub, the namespace with its version and property names and nothing checked. */
tier: 1 | 2 | 3;
/** A generic controller: each capability needs an instance name and friendly names. */
instanced: I;
/** The properties the interface reports. */
properties: P;
/** The directives Alexa sends. */
directives: D;
events?: Record<string, EventDescriptor>;
/** What a declaration can set beyond the fields every capability has: a range, the supported modes. */
options?: Schema<O>;
discovery?: (capability: Declared<O>) => CapabilityExtras;
/** The event that answers a directive when it is not Alexa / Response (Arm -> Arm.Response). */
responseFor?: (directive: string) => {
namespace: string;
name: string;
payload?: Schema<any>;
} | undefined;
/** The namespace an ErrorResponse goes under when its type is one of errorTypes. */
errorNamespace?: string;
errorTypes?: readonly string[];
/** Amazon documents a DeferredResponse for the interface. */
deferrable?: boolean;
/** Rules a schema cannot state. Throws DeclarationError. */
validate?: (capability: Declared<O>, endpoint: EndpointView) => void;
}
export type AnyDescriptor = InterfaceDescriptor<any, any, any, boolean>;
/** Types a descriptor from what it is given: the property names, the payload of each directive, the options. */
export declare function defineInterface<P extends Properties, D extends Directives, O = {}, I extends boolean = boolean>(descriptor: InterfaceDescriptor<P, D, O, I>): InterfaceDescriptor<P, D, O, I>;
/** A device, a capability or an interface name declared in a way Alexa would reject. Thrown where it is declared. */
export declare class DeclarationError extends Error {
readonly endpointId?: string;
readonly namespace?: string;
readonly instance?: string;
readonly problem: string;
constructor(where: {
endpointId?: string;
namespace?: string;
instance?: string;
}, problem: string);
}