@smarthome.exchange/api 0.1.0 → 1.0.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.
package/readme.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  🔌 Typed API client for connecting tools, CLIs, automations, and apps to a smarthome.exchange hub.
4
4
 
5
- This package wraps the hub's TypedRequest boundary in a programmer-friendly client. It speaks HTTP by default, switches to TypedSocket after `start()`, and returns the exact response shapes from `@smarthome.exchange/interfaces`.
5
+ This package wraps the hub's authenticated HTTP and Node WebSocket boundaries. Each client is bound to one home and returns the exact response shapes from `@smarthome.exchange/interfaces`.
6
6
 
7
7
  ## Issue Reporting and Security
8
8
 
@@ -21,6 +21,11 @@ import { SmarthomeExchangeApiClient } from '@smarthome.exchange/api';
21
21
 
22
22
  const client = new SmarthomeExchangeApiClient({
23
23
  hubUrl: 'http://localhost:8080',
24
+ homeId: 'home:primary',
25
+ authorizationHeaderProvider: async ({ abortSignal }) => {
26
+ const accessToken = await yourCredentialProvider.getAccessToken({ abortSignal });
27
+ return `Bearer ${accessToken}`;
28
+ },
24
29
  });
25
30
 
26
31
  const snapshotResponse = await client.getHomeSnapshot();
@@ -32,26 +37,51 @@ const deviceResponse = await client.listDevices({
32
37
  console.log(deviceResponse.devices.map((deviceArg) => deviceArg.name));
33
38
  ```
34
39
 
35
- ## Socket Mode
40
+ `yourCredentialProvider` is the application's configured credential service; the client does not perform sign-in or persist tokens. The asynchronous `authorizationHeaderProvider` is required. It runs once per HTTP request and before each explicitly started physical Node socket connection. It receives the immutable home, final transport endpoint, operation name, transport, and cancellation signal. A socket context uses `method: 'connect'` and `transport: 'websocket'`, with the owning attempt and preparation deadline. Return the complete Authorization header value. Provider failure, an empty value, or invalid HTTP header syntax rejects the call without sending an anonymous request.
41
+
42
+ The constructor captures the home and provider from plain own data properties. Changing the original options or a filter while credentials are being obtained cannot change the prepared request. Every operation includes `context: { homeId }`; filter parameters cannot replace that context or supply trusted identity. Nested action inputs must contain finite, acyclic JSON data. Accessors, symbolic properties, and class instances are rejected without evaluating their getters, before credential preparation. Tool plans and calls cannot supply identity or grant fields. The home selects a resource. The server derives identity and checks home grants from the transport credentials.
43
+
44
+ ## Transport and Cancellation
45
+
46
+ HTTP calls use published TypedRequest 9.1 and `${hubUrl}/typedrequest`. Credentials stay in HTTP headers outside RPC JSON. Header-bearing calls bypass the shared response cache, including when `useCache: true`, and reject redirects before credentials can be forwarded. Configure the final hub URL without embedded credentials, query parameters, or a fragment. HTTPS and server-managed browser CORS remain deployment responsibilities.
47
+
48
+ Every method accepts optional request options:
36
49
 
37
50
  ```ts
38
- const client = new SmarthomeExchangeApiClient({ hubUrl: 'http://localhost:8080' });
51
+ const abortController = new AbortController();
52
+ const snapshot = await client.getHomeSnapshot({
53
+ abortSignal: abortController.signal,
54
+ timeoutMs: 5000,
55
+ });
56
+ const devices = await client.listDevices({ room: 'kitchen' }, {
57
+ abortSignal: abortController.signal,
58
+ });
59
+ ```
39
60
 
40
- await client.start();
61
+ Cancellation is checked before and after credential preparation and passed to TypedRequest. Cancellation settles the API request even when a credential provider ignores its signal; late provider results or failures are observed and cannot open a request. The provider still owns its own resources and must cooperate with the supplied signal to release them. The client cannot terminate foreign provider work or synchronous code. `timeoutMs` is TypedRequest's HTTP deadline, beginning after credential preparation and covering transport and response decoding. API operations set the owning transport’s `maxRetries` to zero: a server-requested retry raises `TypedRequestRetryLimitError` after the first attempt. HTTP abort and timeout do not establish whether an already admitted device action completed. Inspect its durable approval or audit outcome before another attempt.
41
62
 
63
+ ## Authenticated Node Socket Mode
64
+
65
+ ```ts
66
+ await client.start();
42
67
  try {
43
- const agents = await client.listAgents();
44
- console.log(agents.statuses);
68
+ const { agents, statuses } = await client.listAgents();
69
+ console.log(agents.length, statuses.length);
45
70
  } finally {
46
71
  await client.stop();
47
72
  }
48
73
  ```
49
74
 
50
- Calling `start()` creates a TypedSocket 7 client and resolves after its package-major compatibility handshake. That handshake establishes transport compatibility only; application authentication and authorization remain server responsibilities. Calls made before `start()` or after `stop()` use HTTP requests against `${hubUrl}/typedrequest`.
75
+ Node `start()` consumes published TypedSocket 9 and obtains an Authorization header before the WebSocket upgrade, including its transport compatibility handshake. Credentials stay outside URLs and RPC frames; upgrade redirects are disabled. Socket calls carry the captured home context, and the hub must enforce current subject grants on every message. The package-major handshake establishes compatibility only.
51
76
 
52
- ## Version 7 Transport
77
+ While started, methods use that authenticated socket and its bound subject; the header provider is not reevaluated for individual socket messages. To rotate credentials or change subjects, stop and explicitly start a new connection. Automatic reconnects are disabled, so denied upgrade admission settles immediately without repeatedly presenting stale credentials. Calls before startup and after successful stop use authenticated HTTP. An in-flight socket request that fails or is stopped is never resent over HTTP. In socket mode, `timeoutMs` configures both the TypedRequest deadline and TypedSocket’s per-call timeout, with the owning maximum of `300000` milliseconds; omitting it retains the socket’s `30000`-millisecond default. Larger HTTP deadlines are unavailable in socket mode.
78
+
79
+ Browser and Deno WebSocket APIs cannot supply Authorization upgrade headers. Their `start()` fails with the owning `TypedSocketConnectionPreparationError`, code `callback-rejected`, whose `cause` is a `TypedSocketConnectionPreparationError` with code `unsupported-headers`. The capability check runs before obtaining credentials or opening a socket. Use the authenticated HTTP methods and snapshot polling in those runtimes. The hub has no published application cookie-ticket issuer/consumer contract; this client creates no ticket endpoint and sends no authentication RPC. Native Fetch CORS and credential policy remain server/application responsibilities.
80
+
81
+ TypedSocket requires TLS for remote Node sockets; insecure `ws:` is permitted only on loopback. Configure an HTTPS hub URL for remote socket use. Credential preparation has the owning ten-second default/maximum deadline; set constructor `socketConnectionPreparationTimeoutMs` to a positive integer no greater than `10000` to reduce it. Its signal aborts on timeout, cancellation, disconnect, and stop. A noncooperative provider cannot hold socket stop open or open a connection with a late credential result.
82
+
83
+ Concurrent identical `start()`/`stop()` calls coalesce. Mixed lifecycle calls run in order, with at most 64 active and queued operations. `stop()` supersedes earlier queued starts and cancels an in-flight startup using TypedSocket's exported public startup-aborted error. A late-created socket remains owned until its exact cleanup succeeds. A cleanup failure retains that socket for a later `stop()` and blocks new startup; failed startup is retryable through a later explicit `start()`.
53
84
 
54
- The client has migrated both transport paths to the version 7 stack: HTTP calls use TypedRequest 7 envelopes and socket calls use TypedSocket 7 with its exact package-major handshake. Request helper method arguments are derived from each shared contract's literal `method`, keeping every client method aligned with `@smarthome.exchange/interfaces`. The socket handshake authorizes transport compatibility only; application authentication and authorization remain server responsibilities.
55
85
 
56
86
  ## Client Methods
57
87
 
@@ -68,13 +98,35 @@ The client has migrated both transport paths to the version 7 stack: HTTP calls
68
98
 
69
99
  ## Approval Decisions
70
100
 
71
- In v0.1, `submitApproval()` is decision-record-only. It records `approved` or `rejected` on the approval and returns the updated record plus its audit receipt. It never resumes or executes the related tool call, and an approved decision must not be interpreted as an execution result.
101
+ Approval decisions bind to the exact action digest returned by `listApprovals()`:
102
+
103
+ ```ts
104
+ const { approvals } = await client.listApprovals({ status: 'pending' });
105
+ const approval = approvals[0];
106
+ if (approval) {
107
+ const result = await client.submitApproval({
108
+ approvalId: approval.id,
109
+ expectedActionDigest: approval.actionDigest,
110
+ decision: 'approved',
111
+ });
112
+ console.log(result.approval.execution.status);
113
+ }
114
+ ```
115
+
116
+ The hub decides whether the authenticated subject may approve the snapshotted action. The shared contract distinguishes decision state from `approval.execution`; an approved decision alone is not proof of a physical effect. An interrupted dispatch may be `indeterminate`. The API client does not automatically resubmit approval decisions or device calls.
72
117
 
73
118
  ## Snapshot Watching
74
119
 
75
- `watchHomeSnapshot(callback, options?)` is a client-local polling helper around `getHomeSnapshot()`. It does not add a server push contract. The callback receives the same `{ availability, snapshot }` response returned by `getHomeSnapshot()`. `availability` is `live` for a successfully served live hub response and `stale` when the response carries stale snapshot data. A raw `IHomeSnapshot` without this response envelope is not automatically live. The async stop function clears the polling timer and waits for active transport work unless a user callback or `onError` handler is already running. It never waits on active user handlers, so they may await their own stop function without a circular wait and external shutdown cannot be held open by user code. A handler already in progress may finish after stopping resolves, but no later poll is scheduled. `intervalMs` defaults to `5000` and must be an integer from `1` through `2147483647`.
120
+ `watchHomeSnapshot(callback, options?)` owns polling through `getHomeSnapshot()`. The cancellable watcher contract requires `@smarthome.exchange/api` 1.0.0 or newer. It captures and validates plain options once: `abortSignal` terminates the watcher lifetime, `timeoutMs` sets each request deadline, `intervalMs` defaults to `5000` and must be an integer from `1` through `2147483647`, and `immediate: false` defers its first poll. Accessors and unsupported properties are rejected before credential preparation.
121
+
122
+ Every poll has an owned cancellation signal passed to the credential provider and transport. The async stop function clears the timer, aborts the active request, and drains API-owned request work. External lifetime cancellation is terminal, and a pre-aborted watcher starts no timer or request. No canceled or stopped poll publishes a later callback/error or schedules another poll. A normal request deadline is reported to `onError`; polling continues unless the watcher is stopped. HTTP deadlines begin after credential preparation, as for direct requests; providers must bound their own credential work.
123
+
124
+ User callbacks and `onError` handlers may await their own stop function. Stop never waits for active user handlers, so user code cannot hold external close open; a handler already running may finish later. Likewise, cancellation settles credential preparation without waiting for a provider that ignores its signal, but provider-owned resources require the provider's cooperative cleanup. An ignored provider's late result never dispatches a request. Watcher stop does not stop the client's independently owned socket; stop the client separately when its socket is also application-owned.
125
+
126
+ The callback receives the same `{ availability, snapshot }` response returned by `getHomeSnapshot()`. `availability` is `live` for a successfully served live hub response and `stale` when the response carries stale snapshot data. A raw `IHomeSnapshot` without this response envelope is not automatically live. Polling does not add a server push contract. Explicit refreshes use the existing `getHomeSnapshot({ abortSignal, timeoutMs })` request options.
76
127
 
77
128
  ```ts
129
+ const watchLifetime = new AbortController();
78
130
  const stopWatching = client.watchHomeSnapshot(
79
131
  async ({ availability, snapshot }) => {
80
132
  console.log(availability, snapshot.home.name, snapshot.devices.length);
@@ -82,6 +134,8 @@ const stopWatching = client.watchHomeSnapshot(
82
134
  {
83
135
  intervalMs: 5000,
84
136
  immediate: true,
137
+ abortSignal: watchLifetime.signal,
138
+ timeoutMs: 5000,
85
139
  onError: async (errorArg) => {
86
140
  console.error('Snapshot refresh failed:', errorArg);
87
141
  },
@@ -89,6 +143,7 @@ const stopWatching = client.watchHomeSnapshot(
89
143
  );
90
144
 
91
145
  // Later, when the UI or process is shutting down:
146
+ watchLifetime.abort();
92
147
  await stopWatching();
93
148
  ```
94
149
 
@@ -124,6 +179,8 @@ for (const callResult of result.results) {
124
179
 
125
180
  Use a tool identifier returned by `listTools()` for a currently normalized integration-owned entity. Do not construct identifiers from seeded demo device IDs: a device without a live integration owner cannot dispatch a physical write and fails closed. Inspect every per-call result because a successful `executeToolPlan()` request may still contain `failed`, `indeterminate`, `suggested`, or `queuedForApproval` outcomes.
126
181
 
182
+ The plan and call `callerId` fields remain part of the authoring contract. They do not grant authority: the server resolves the subject from HTTP authentication. Reuse stable plan and call identifiers when inspecting an uncertain operation; do not generate fresh identifiers to retry a possibly dispatched effect.
183
+
127
184
  ## Scripts
128
185
 
129
186
  ```bash
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@smarthome.exchange/api',
6
- version: '0.1.0',
6
+ version: '1.0.0',
7
7
  description: 'Typed API client for smarthome.exchange hubs.'
8
8
  }