@glasshome/sync-layer 0.4.0 → 0.6.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.
@@ -7,7 +7,6 @@
7
7
  * @packageDocumentation
8
8
  */
9
9
  import type { HAEvent } from "@glasshome/ha-types";
10
- import type { Fixtures } from "../testing/types";
11
10
  /**
12
11
  * Unified connection interface for Home Assistant connections
13
12
  *
@@ -149,13 +148,4 @@ export interface SyncLayerConnection {
149
148
  * ```
150
149
  */
151
150
  addEventListener(handler: (message: any) => void): () => void;
152
- /**
153
- * Get fixtures (MockConnection only)
154
- *
155
- * Returns the fixture data for mock connections. SyncLayer uses this
156
- * to load fixtures into its store during connection setup.
157
- *
158
- * @returns Fixture data (entities, registries, etc.)
159
- */
160
- getFixtures?(): Fixtures;
161
151
  }
@@ -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;
@@ -24,8 +24,6 @@ export interface HaLink {
24
24
  subscribeEvents(callback: (event: unknown) => void, eventType?: string): Promise<() => void | Promise<void>>;
25
25
  }
26
26
  export interface GlassHomeState {
27
- /** Live HA link (real socket, worker bridge facade, or mock) */
28
- conn: HaLink | null;
29
27
  /** Current connection state */
30
28
  connectionState: ConnectionState;
31
29
  /** Connection error (if any) */
@@ -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
@@ -6,13 +6,11 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- export { addDebugIncomingMessageListener, addDebugOutgoingMessageListener, } from "./connection/adapter";
10
9
  export type { ConnectionOptions } from "./connection/manager";
11
- export { disconnect, getConnection, getConnectionState, initConnection, isConnected, } from "./connection/manager";
10
+ export { getConnectionState, isConnected } from "./connection/manager";
12
11
  export { registerEntity } from "./connection/subscription-manager";
13
12
  export type { SyncLayerConnection } from "./connection/types";
14
13
  export type { OAuthOptions } from "./connection/auth";
15
- export { authenticateWithOAuth, authenticateWithToken, isAuthValid, refreshAuth, } from "./connection/auth";
16
14
  export { area, areas } from "./query/area-builder";
17
15
  export { entities, entity } from "./query/builder";
18
16
  export { entitiesByAreaAndDomain, entitiesWhere, searchEntities, } from "./query/helpers";
@@ -22,7 +20,7 @@ export type { CommandOptions, CommandResult, EntityUpdateFields, ServiceCallCont
22
20
  export type { GlassHomeState } from "./core/store";
23
21
  export type { AreaId, AreaView, ConnectionState, DeviceId, DeviceView, EntityDomain, EntityId, EntityView, HassEntity, } from "./core/types";
24
22
  export { extractDomain } from "./core/types";
25
- export { resetStore, setState, state } from "./core/store";
23
+ export { state } from "./core/store";
26
24
  export type { CallServiceRequest, CoreEventType, Domain, EntityCategory, EntityRegistryEntry, EventStateChangedData, HAEvent, HassConfig, HassUnitSystem, RegistryEntryDisabler, RegistryEntryHider, ServiceCall, ServiceName, WsCommandType, WsResult, } from "@glasshome/ha-types";
27
25
  export { fetchStream, fetchStreamData } from "./camera/fetch";
28
26
  export { getStream, refreshStream } from "./camera/query";
@@ -43,9 +41,9 @@ export type { EntityHistoryData, EntityHistoryQueryOptions, EntityHistoryResult,
43
41
  export { fetchForecast, fetchForecastData, fetchForecasts } from "./weather/fetch";
44
42
  export { getForecast, getForecasts, refreshForecast, refreshForecasts } from "./weather/query";
45
43
  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";
44
+ export { applyDemoServiceCall, createDemoFixtures, isDemoMode, startDemoEnergyTicker, stopDemoEnergyTicker, } from "./demo/demo-provider";
47
45
  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";
46
+ export { BridgeNeedsAuthError, type BridgeConnectOptions, type BridgeEvents, type HaBridge, } from "./worker/bridge-client";
47
+ export { claimHostApi, type HostApi } from "./host-api";
50
48
  export type { AuthMode, ConnState, OAuthTokenData } from "./worker/protocol";
51
49
  export { runHaBridgeWorker, type WorkerScope } from "./worker/worker-main";