@glasshome/sync-layer 0.5.0 → 0.7.0

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.
@@ -0,0 +1,19 @@
1
+ import type { EntityId } from "../core/types";
2
+ import type { CalendarWindowOptions } from "./types";
3
+ /**
4
+ * Start tracking live events for a calendar entity. Returns the untrack
5
+ * function. Safe while disconnected: the subscription activates on the next
6
+ * `forceResubscribeCalendars()` (called by the connection layer on connect
7
+ * and reconnect).
8
+ */
9
+ export declare function trackCalendarEvents(entityId: EntityId, options?: CalendarWindowOptions): () => void;
10
+ /**
11
+ * Reconcile every tracked calendar, dropping current subscription handles
12
+ * first. The connection layer calls this on connect and after reconnect:
13
+ * an unchanged window is no proof the old subscription is alive, so each
14
+ * tracked entity resubscribes (HA re-pushes the snapshot, refreshing the
15
+ * store through any missed changes).
16
+ */
17
+ export declare function forceResubscribeCalendars(): Promise<void>;
18
+ /** Test-only: drop all tracking state and timers. */
19
+ export declare function resetCalendarTracking(): void;
@@ -0,0 +1,32 @@
1
+ import type { EntityId } from "../core/types";
2
+ /**
3
+ * One calendar event as HA serializes it over `calendar/event/subscribe`:
4
+ * dates/datetimes are ISO strings (date-only for all-day events), and
5
+ * None-valued fields are omitted.
6
+ */
7
+ export interface CalendarEvent {
8
+ start: string;
9
+ end: string;
10
+ summary: string;
11
+ description?: string;
12
+ location?: string;
13
+ uid?: string;
14
+ recurrence_id?: string;
15
+ rrule?: string;
16
+ }
17
+ /** Live event window for one calendar entity, held in the store. */
18
+ export interface CalendarEventsData {
19
+ entityId: EntityId;
20
+ events: CalendarEvent[];
21
+ /** True until the first snapshot for the current subscription arrives. */
22
+ loading: boolean;
23
+ error: Error | null;
24
+ /** Subscribed window bounds (ISO). */
25
+ start: string;
26
+ end: string;
27
+ }
28
+ /** Per-tracker window request, in whole days relative to now. */
29
+ export interface CalendarWindowOptions {
30
+ daysBehind?: number;
31
+ daysAhead?: number;
32
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The privileged Home Assistant connection, held outside the shared store.
3
+ *
4
+ * This handle tunnels arbitrary HA traffic: the worker's `case "send"` forwards
5
+ * whatever arrives on it after a shape check only, because capability
6
+ * validation belongs on the per-widget MessagePorts, not here.
7
+ *
8
+ * It therefore must not be reachable from widget code. It used to live at
9
+ * `state.conn`, and `state` is exported from this package's entry, which the
10
+ * host serves to every widget bundle through its import map — so any widget
11
+ * could read the handle and call any service on any entity, regardless of the
12
+ * capabilities it declared and the user approved. Proven by execution, not
13
+ * argued: a widget-side `useService()` emitted `call_service`/`lock.unlock`
14
+ * onto the host's own channel (finding 46).
15
+ *
16
+ * Module-local and deliberately absent from `src/index.ts`. Not being exported
17
+ * is the entire mechanism, so exporting it — directly, or by parking the value
18
+ * on anything that is exported — reopens the hole. `check-widget-reachable-
19
+ * surface.ts` fails the build if it ever becomes reachable again.
20
+ *
21
+ * Plain variable rather than store state: every reader takes it imperatively
22
+ * inside an async fetcher or command, so none of them tracked it reactively.
23
+ * `state.connectionState` remains the reactive signal for connection status.
24
+ */
25
+ import type { HaLink } from "./store";
26
+ /** Read the live connection. Null before connect and after disconnect. */
27
+ export declare function privilegedConn(): HaLink | null;
28
+ /** Set by the connection layer on connect/disconnect. Host-side callers only. */
29
+ export declare function setPrivilegedConn(conn: HaLink | null): void;
@@ -1,5 +1,6 @@
1
1
  /** Central reactive state store for sync-layer using SolidJS createStore. */
2
2
  import type { SetStoreFunction, Store } from "solid-js/store";
3
+ import type { CalendarEventsData } from "../calendar/types";
3
4
  import type { CameraStreamData } from "../camera/types";
4
5
  import type { EntityHistoryData } from "../history/types";
5
6
  import type { WeatherForecastsData } from "../weather/types";
@@ -24,8 +25,6 @@ export interface HaLink {
24
25
  subscribeEvents(callback: (event: unknown) => void, eventType?: string): Promise<() => void | Promise<void>>;
25
26
  }
26
27
  export interface GlassHomeState {
27
- /** Live HA link (real socket, worker bridge facade, or mock) */
28
- conn: HaLink | null;
29
28
  /** Current connection state */
30
29
  connectionState: ConnectionState;
31
30
  /** Connection error (if any) */
@@ -57,6 +56,8 @@ export interface GlassHomeState {
57
56
  history: Record<EntityId, EntityHistoryData>;
58
57
  /** Weather forecasts data (keyed by entity ID) */
59
58
  forecasts: Record<EntityId, WeatherForecastsData>;
59
+ /** Live calendar events (keyed by entity ID), maintained by calendar/track */
60
+ calendars: Record<EntityId, CalendarEventsData>;
60
61
  /** Camera stream data (keyed by entity ID) */
61
62
  streams: Record<EntityId, CameraStreamData>;
62
63
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * One-shot handoff of the host-only surface.
3
+ *
4
+ * The problem this solves: the host import map serves this package's entry to
5
+ * every widget bundle, so everything exported there is callable by untrusted
6
+ * widget code. But the host needs privileged operations from the same package —
7
+ * connect the bridge, authenticate, disconnect — and it cannot import them
8
+ * through a private subpath, because the store must be ONE instance shared with
9
+ * widgets. An unserved subpath gets bundled into the host and forks the store
10
+ * (finding 45); a served one is reachable by widgets again (finding 46). A
11
+ * subpath cannot be both, so hiding is not available.
12
+ *
13
+ * So the surface stays on the shared entry and is protected by ORDER instead of
14
+ * by visibility: `claimHostApi()` yields the API to its first caller and null to
15
+ * everyone after. The host claims it while booting; widget bundles are fetched
16
+ * and evaluated later, by which point there is nothing left to take.
17
+ *
18
+ * Why these members are dangerous in a widget's hands:
19
+ * - attachBridgeToStore / createHaBridge: install a connection, i.e. replace the
20
+ * host's link with one the widget controls — every subsequent service call is
21
+ * intercepted and every entity the dashboard renders can be fabricated.
22
+ * - authenticateWithOAuth: returns an `Auth` carrying a live access token.
23
+ * - disconnect / detachBridgeFromStore: drop the household's connection.
24
+ * - loadDemoData / unloadDemoData: overwrite live state with fixtures.
25
+ */
26
+ import { authenticateWithOAuth } from "./connection/auth";
27
+ import { applyBridgeConnState, attachBridgeToStore, detachBridgeFromStore, reloadAfterBridgeReconnect } from "./connection/bridged";
28
+ import { disconnect } from "./connection/manager";
29
+ import { loadDemoData, unloadDemoData } from "./demo/demo-provider";
30
+ import { createHaBridge } from "./worker/bridge-client";
31
+ export interface HostApi {
32
+ attachBridgeToStore: typeof attachBridgeToStore;
33
+ detachBridgeFromStore: typeof detachBridgeFromStore;
34
+ applyBridgeConnState: typeof applyBridgeConnState;
35
+ reloadAfterBridgeReconnect: typeof reloadAfterBridgeReconnect;
36
+ createHaBridge: typeof createHaBridge;
37
+ authenticateWithOAuth: typeof authenticateWithOAuth;
38
+ disconnect: typeof disconnect;
39
+ loadDemoData: typeof loadDemoData;
40
+ unloadDemoData: typeof unloadDemoData;
41
+ }
42
+ /**
43
+ * Take the host-only surface. Returns it once; null on every later call.
44
+ *
45
+ * Call it exactly once, during host boot, and keep the result in a module the
46
+ * import map does not serve. Calling it from anywhere else is what this guards
47
+ * against, so a null return means something already claimed it — treat that as
48
+ * a fatal misconfiguration, not a retry.
49
+ */
50
+ export declare function claimHostApi(): HostApi | null;
package/dist/index.d.ts CHANGED
@@ -4,15 +4,23 @@
4
4
  * Type-safe, reactive state synchronization layer for Home Assistant
5
5
  * Uses SolidJS createStore for reactive state management.
6
6
  *
7
+ * THIS ENTRY IS AN ACCESS-CONTROL SURFACE, not an ordinary barrel. The host
8
+ * import map serves it to every untrusted widget bundle, so anything exported
9
+ * here is callable by widget code. Before adding an export, ask who outside
10
+ * this package needs it: in-package callers use relative imports and need
11
+ * nothing here, host-only lifecycle belongs on claimHostApi(), and widget
12
+ * hooks belong on the ./solid subpath. Types are always safe; they carry no
13
+ * authority. scripts/check-widget-reachable-surface.ts guards this, but it is
14
+ * a DENYLIST of known-dangerous names and is blind to any handle invented
15
+ * after it was written — it cannot be relied on to catch a new mistake.
16
+ *
7
17
  * @packageDocumentation
8
18
  */
9
- export { addDebugIncomingMessageListener, addDebugOutgoingMessageListener, } from "./connection/adapter";
10
19
  export type { ConnectionOptions } from "./connection/manager";
11
- export { disconnect, getConnection, getConnectionState, initConnection, isConnected, } from "./connection/manager";
20
+ export { getConnectionState, isConnected } from "./connection/manager";
12
21
  export { registerEntity } from "./connection/subscription-manager";
13
22
  export type { SyncLayerConnection } from "./connection/types";
14
23
  export type { OAuthOptions } from "./connection/auth";
15
- export { authenticateWithOAuth, authenticateWithToken, isAuthValid, refreshAuth, } from "./connection/auth";
16
24
  export { area, areas } from "./query/area-builder";
17
25
  export { entities, entity } from "./query/builder";
18
26
  export { entitiesByAreaAndDomain, entitiesWhere, searchEntities, } from "./query/helpers";
@@ -22,7 +30,7 @@ export type { CommandOptions, CommandResult, EntityUpdateFields, ServiceCallCont
22
30
  export type { GlassHomeState } from "./core/store";
23
31
  export type { AreaId, AreaView, ConnectionState, DeviceId, DeviceView, EntityDomain, EntityId, EntityView, HassEntity, } from "./core/types";
24
32
  export { extractDomain } from "./core/types";
25
- export { resetStore, setState, state } from "./core/store";
33
+ export { state } from "./core/store";
26
34
  export type { CallServiceRequest, CoreEventType, Domain, EntityCategory, EntityRegistryEntry, EventStateChangedData, HAEvent, HassConfig, HassUnitSystem, RegistryEntryDisabler, RegistryEntryHider, ServiceCall, ServiceName, WsCommandType, WsResult, } from "@glasshome/ha-types";
27
35
  export { fetchStream, fetchStreamData } from "./camera/fetch";
28
36
  export { getStream, refreshStream } from "./camera/query";
@@ -41,11 +49,12 @@ export { fetchStatisticsDuringPeriod, normalizeStatisticTime, } from "./history/
41
49
  export type { StatisticsPeriod, StatisticsQueryOptions, StatisticValue, } from "./history/statistics";
42
50
  export type { EntityHistoryData, EntityHistoryQueryOptions, EntityHistoryResult, EntityHistoryState, HistoryData, HistoryQueryOptions, HistoryResult, HistoryStates, TimelineState, } from "./history/types";
43
51
  export { fetchForecast, fetchForecastData, fetchForecasts } from "./weather/fetch";
52
+ export type { CalendarEvent, CalendarEventsData, CalendarWindowOptions } from "./calendar/types";
44
53
  export { getForecast, getForecasts, refreshForecast, refreshForecasts } from "./weather/query";
45
54
  export type { EntityForecastQueryOptions, ForecastQueryOptions, ForecastResult, ForecastsResult, ForecastType, WeatherForecast, WeatherForecastData, WeatherForecasts, WeatherForecastsData, } from "./weather/types";
46
- export { applyDemoServiceCall, createDemoFixtures, isDemoMode, loadDemoData, startDemoEnergyTicker, stopDemoEnergyTicker, unloadDemoData, } from "./demo/demo-provider";
55
+ export { applyDemoServiceCall, createDemoFixtures, isDemoMode, startDemoEnergyTicker, stopDemoEnergyTicker, } from "./demo/demo-provider";
47
56
  export declare const VERSION = "0.2.1";
48
- export { applyBridgeConnState, attachBridgeToStore, detachBridgeFromStore, reloadAfterBridgeReconnect, } from "./connection/bridged";
49
- export { BridgeNeedsAuthError, createHaBridge, type BridgeConnectOptions, type BridgeEvents, type HaBridge, } from "./worker/bridge-client";
57
+ export { BridgeNeedsAuthError, type BridgeConnectOptions, type BridgeEvents, type HaBridge, } from "./worker/bridge-client";
58
+ export { claimHostApi, type HostApi } from "./host-api";
50
59
  export type { AuthMode, ConnState, OAuthTokenData } from "./worker/protocol";
51
60
  export { runHaBridgeWorker, type WorkerScope } from "./worker/worker-main";