miakapi 3.0.31 → 4.0.0-alpha.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/LICENSE +15 -0
- package/README.md +358 -0
- package/contracts/miakapp-v3.json +6 -0
- package/dist/access-token-provider.d.ts +8 -0
- package/dist/access-token-provider.d.ts.map +1 -0
- package/dist/access-token-provider.js +167 -0
- package/dist/access-token-provider.js.map +1 -0
- package/dist/api.d.ts +224 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +47 -0
- package/dist/api.js.map +1 -0
- package/dist/browser-api.d.ts +113 -0
- package/dist/browser-api.d.ts.map +1 -0
- package/dist/browser-api.js +2 -0
- package/dist/browser-api.js.map +1 -0
- package/dist/browser-client.d.ts +3 -0
- package/dist/browser-client.d.ts.map +1 -0
- package/dist/browser-client.js +678 -0
- package/dist/browser-client.js.map +1 -0
- package/dist/browser-relay-credential-provider.d.ts +3 -0
- package/dist/browser-relay-credential-provider.d.ts.map +1 -0
- package/dist/browser-relay-credential-provider.js +243 -0
- package/dist/browser-relay-credential-provider.js.map +1 -0
- package/dist/browser.d.ts +4 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +4 -0
- package/dist/browser.js.map +1 -0
- package/dist/coordinator.d.ts +3 -0
- package/dist/coordinator.d.ts.map +1 -0
- package/dist/coordinator.js +544 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/browser-errors.d.ts +31 -0
- package/dist/internal/browser-errors.d.ts.map +1 -0
- package/dist/internal/browser-errors.js +69 -0
- package/dist/internal/browser-errors.js.map +1 -0
- package/dist/internal/browser-socket.d.ts +8 -0
- package/dist/internal/browser-socket.d.ts.map +1 -0
- package/dist/internal/browser-socket.js +204 -0
- package/dist/internal/browser-socket.js.map +1 -0
- package/dist/internal/calls.d.ts +33 -0
- package/dist/internal/calls.d.ts.map +1 -0
- package/dist/internal/calls.js +525 -0
- package/dist/internal/calls.js.map +1 -0
- package/dist/internal/control-plane-response.d.ts +10 -0
- package/dist/internal/control-plane-response.d.ts.map +1 -0
- package/dist/internal/control-plane-response.js +283 -0
- package/dist/internal/control-plane-response.js.map +1 -0
- package/dist/internal/declarations.d.ts +62 -0
- package/dist/internal/declarations.d.ts.map +1 -0
- package/dist/internal/declarations.js +427 -0
- package/dist/internal/declarations.js.map +1 -0
- package/dist/internal/errors.d.ts +31 -0
- package/dist/internal/errors.d.ts.map +1 -0
- package/dist/internal/errors.js +72 -0
- package/dist/internal/errors.js.map +1 -0
- package/dist/internal/events.d.ts +27 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +207 -0
- package/dist/internal/events.js.map +1 -0
- package/dist/internal/presence.d.ts +15 -0
- package/dist/internal/presence.d.ts.map +1 -0
- package/dist/internal/presence.js +99 -0
- package/dist/internal/presence.js.map +1 -0
- package/dist/internal/resources.d.ts +33 -0
- package/dist/internal/resources.d.ts.map +1 -0
- package/dist/internal/resources.js +136 -0
- package/dist/internal/resources.js.map +1 -0
- package/dist/internal/runtime.d.ts +34 -0
- package/dist/internal/runtime.d.ts.map +1 -0
- package/dist/internal/runtime.js +17 -0
- package/dist/internal/runtime.js.map +1 -0
- package/dist/internal/session.d.ts +33 -0
- package/dist/internal/session.d.ts.map +1 -0
- package/dist/internal/session.js +199 -0
- package/dist/internal/session.js.map +1 -0
- package/dist/internal/socket.d.ts +6 -0
- package/dist/internal/socket.d.ts.map +1 -0
- package/dist/internal/socket.js +176 -0
- package/dist/internal/socket.js.map +1 -0
- package/dist/internal/state.d.ts +21 -0
- package/dist/internal/state.d.ts.map +1 -0
- package/dist/internal/state.js +133 -0
- package/dist/internal/state.js.map +1 -0
- package/dist/internal/user-calls.d.ts +27 -0
- package/dist/internal/user-calls.d.ts.map +1 -0
- package/dist/internal/user-calls.js +325 -0
- package/dist/internal/user-calls.js.map +1 -0
- package/dist/internal/user-session.d.ts +36 -0
- package/dist/internal/user-session.d.ts.map +1 -0
- package/dist/internal/user-session.js +250 -0
- package/dist/internal/user-session.js.map +1 -0
- package/dist/internal/user-state.d.ts +21 -0
- package/dist/internal/user-state.d.ts.map +1 -0
- package/dist/internal/user-state.js +240 -0
- package/dist/internal/user-state.js.map +1 -0
- package/dist/internal/validation.d.ts +32 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +553 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/protocol/codec.d.ts +75 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/codec.js +1091 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/session.d.ts +14 -0
- package/dist/protocol/session.d.ts.map +1 -0
- package/dist/protocol/session.js +125 -0
- package/dist/protocol/session.js.map +1 -0
- package/dist/protocol/user-session.d.ts +14 -0
- package/dist/protocol/user-session.d.ts.map +1 -0
- package/dist/protocol/user-session.js +116 -0
- package/dist/protocol/user-session.js.map +1 -0
- package/package.json +57 -9
- package/.eslintrc.js +0 -16
- package/.gitattributes +0 -2
- package/main.js +0 -305
- package/miakode.js +0 -56
- package/screenshots/bigGrid.jpg +0 -0
- package/screenshots/formExample.jpg +0 -0
- package/screenshots/properties&table.jpg +0 -0
- package/test.js +0 -45
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mathieu Colmon
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# MiakAPI
|
|
2
|
+
|
|
3
|
+
MiakAPI is the typed SDK for running a trusted Bun/TypeScript coordinator and
|
|
4
|
+
connecting a first-party browser application to a Miakapp home. A coordinator
|
|
5
|
+
owns complete state, access, event, and function declarations for one
|
|
6
|
+
integration. The isolated browser entry point exposes the authenticated user role
|
|
7
|
+
without bundling server-runtime or coordinator-only dependencies.
|
|
8
|
+
|
|
9
|
+
Version 4 is a complete replacement for the legacy callback-based MiakAPI 3
|
|
10
|
+
client. It is currently an alpha while the Miakapp 4 stack is being completed.
|
|
11
|
+
|
|
12
|
+
**Building a home with a coding agent?** Read
|
|
13
|
+
[docs/agent-guide.md](docs/agent-guide.md) first. It covers the judgment the
|
|
14
|
+
reference below does not: what the coordinator must authorize, why component
|
|
15
|
+
requirements and coordinator grants intersect silently, and which failures must
|
|
16
|
+
never be retried. Start from [`templates/home`](templates/home).
|
|
17
|
+
|
|
18
|
+
## Coordinator requirements
|
|
19
|
+
|
|
20
|
+
- Bun 1.2.23 or newer (primary coordinator runtime)
|
|
21
|
+
- Node.js 22.9 or newer when running compatibility or migration tooling
|
|
22
|
+
- A Miakapp Home Key or another approved short-lived access-token provider
|
|
23
|
+
- A Miakapp relay implementing wire protocol 1.0
|
|
24
|
+
|
|
25
|
+
The default `miakapi` entry point is server-side software. Do not ship
|
|
26
|
+
coordinator credentials, Home Keys, or coordinator access-token providers to a
|
|
27
|
+
browser or an untrusted plugin runtime.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
bun add miakapi@next
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Alpha releases use the `next` npm tag. The package is ESM-only and remains
|
|
36
|
+
Node-compatible so runtime-specific migration adapters can reuse the same SDK.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import {
|
|
42
|
+
ApplicationCallError,
|
|
43
|
+
EventDirection,
|
|
44
|
+
createCoordinator,
|
|
45
|
+
createHomeKeyAccessTokenProvider,
|
|
46
|
+
} from 'miakapi';
|
|
47
|
+
|
|
48
|
+
const homeKey = process.env.MIAKAPP_HOME_KEY;
|
|
49
|
+
if (homeKey === undefined) throw new Error('MIAKAPP_HOME_KEY is required');
|
|
50
|
+
|
|
51
|
+
const coordinator = createCoordinator({
|
|
52
|
+
name: 'home-assistant',
|
|
53
|
+
accessTokenProvider: createHomeKeyAccessTokenProvider({
|
|
54
|
+
exchangeEndpoint: 'https://control.miakapp.com/v1/access-tokens:exchange',
|
|
55
|
+
homeKey,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
coordinator.configure({
|
|
60
|
+
state: {
|
|
61
|
+
'climate.living_room.temperature': 20,
|
|
62
|
+
},
|
|
63
|
+
stateAccess: [{
|
|
64
|
+
userId: 'user-id',
|
|
65
|
+
patterns: ['climate.living_room.*'],
|
|
66
|
+
}],
|
|
67
|
+
events: [{
|
|
68
|
+
topic: 'climate.living_room.changed',
|
|
69
|
+
directions:
|
|
70
|
+
EventDirection.acceptFromUsers |
|
|
71
|
+
EventDirection.publishToUsers,
|
|
72
|
+
}],
|
|
73
|
+
eventAccess: [{
|
|
74
|
+
userId: 'user-id',
|
|
75
|
+
publish: ['climate.living_room.changed'],
|
|
76
|
+
subscribe: ['climate.living_room.changed'],
|
|
77
|
+
}],
|
|
78
|
+
functions: {
|
|
79
|
+
async 'climate.living_room.set_target'(call) {
|
|
80
|
+
if (typeof call.arguments !== 'number') {
|
|
81
|
+
throw new ApplicationCallError(2001, 'Target must be a number');
|
|
82
|
+
}
|
|
83
|
+
await call.emit({ phase: 'applying' });
|
|
84
|
+
return { accepted: true, target: call.arguments };
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
coordinator.subscribe(({ current, reason }) => {
|
|
90
|
+
console.log('Miakapp coordinator status:', current, reason?.kind);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const session = await coordinator.start();
|
|
94
|
+
console.log('Ready in generation', session.generation);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A complete minimal Bun coordinator matching the Miakapp V4 staging browser is
|
|
98
|
+
available in [`examples/synthetic-coordinator.ts`](examples/synthetic-coordinator.ts).
|
|
99
|
+
It publishes a small home state and implements one real `lighting.toggle` call.
|
|
100
|
+
Run it only in a trusted backend process:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
bun run build
|
|
104
|
+
bun run examples/synthetic-coordinator.ts
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The examples import `miakapi` the way an application does, so they resolve
|
|
108
|
+
through the package's own `exports` and need `dist` to exist. `bun run check`
|
|
109
|
+
builds before it tests for the same reason.
|
|
110
|
+
|
|
111
|
+
The process reads `MIAKAPP_HOME_KEY`,
|
|
112
|
+
`MIAKAPP_CONTROL_PLANE_EXCHANGE_ENDPOINT`, and `MIAKAPP_OWNER_USER_ID` from its
|
|
113
|
+
runtime environment. The Home Key is the only secret; the endpoint and Firebase
|
|
114
|
+
user ID are identifiers. Never pass any of them as command-line arguments.
|
|
115
|
+
|
|
116
|
+
The Home Key provider makes exactly one exchange request for each initial,
|
|
117
|
+
reauthentication, or reconnect demand from the SDK. It sends the Home Key only
|
|
118
|
+
to the configured HTTPS control-plane endpoint, rejects redirects and open or
|
|
119
|
+
overlong responses, and returns only the relay URL, compact access token, and
|
|
120
|
+
expiry to the coordinator core. It performs no independent retry; the
|
|
121
|
+
coordinator's single bounded reconnect schedule remains authoritative.
|
|
122
|
+
|
|
123
|
+
Keep the Home Key in the trusted coordinator backend. Do not place it in a web
|
|
124
|
+
bundle, browser storage, logs, URLs, or relay configuration. Applications with a
|
|
125
|
+
different approved credential store may continue to implement
|
|
126
|
+
`AccessTokenProvider` directly.
|
|
127
|
+
|
|
128
|
+
### Running a coordinator as a service
|
|
129
|
+
|
|
130
|
+
[`examples/synthetic-coordinator-service.ts`](examples/synthetic-coordinator-service.ts)
|
|
131
|
+
wraps the same coordinator in the shape a container platform expects: an HTTP
|
|
132
|
+
listener that answers immediately, a `/healthz` probe that reports the live
|
|
133
|
+
lifecycle status, graceful `SIGTERM` draining, and a non-zero exit when the
|
|
134
|
+
coordinator stops without a shutdown signal so the platform restarts it.
|
|
135
|
+
[`examples/deploy/Dockerfile`](examples/deploy/Dockerfile) builds it from the
|
|
136
|
+
repository root.
|
|
137
|
+
|
|
138
|
+
Run exactly one coordinator per home. Two coordinators sharing a Home Key take
|
|
139
|
+
turns displacing each other on the relay; the reconnect schedule backs off so
|
|
140
|
+
the fight stays cheap, but only one of them holds the home.
|
|
141
|
+
|
|
142
|
+
`configure` supplies all five declaration slices as one desired snapshot. The
|
|
143
|
+
coordinator becomes `ready` only after the relay acknowledges them in order. A
|
|
144
|
+
later declaration call replaces its complete slice and temporarily returns the
|
|
145
|
+
coordinator to `synchronizing` until atomic activation.
|
|
146
|
+
|
|
147
|
+
## State, events, and calls
|
|
148
|
+
|
|
149
|
+
State mutations are atomic and use acknowledged string paths:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
await coordinator.state.set([
|
|
153
|
+
{ path: 'climate.living_room.temperature', value: 21.5 },
|
|
154
|
+
]);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Event publication returns a synchronous opaque ID and a transport-handoff
|
|
158
|
+
promise. `sent` is not a delivery receipt; a later correlated relay rejection is
|
|
159
|
+
reported through `coordinator.errors`.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const event = coordinator.events.publish(
|
|
163
|
+
'climate.living_room.changed',
|
|
164
|
+
{ temperature: 21.5 },
|
|
165
|
+
);
|
|
166
|
+
await event.sent;
|
|
167
|
+
|
|
168
|
+
coordinator.errors.subscribe((failure) => {
|
|
169
|
+
if (failure.correlation?.localId === event.localId) {
|
|
170
|
+
console.error('The relay rejected the event:', failure.kind);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Outgoing calls expose acceptance, pull-bounded progress, and one terminal
|
|
176
|
+
result:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
const call = coordinator.calls.start({
|
|
180
|
+
function: 'lighting.scene.activate',
|
|
181
|
+
arguments: { scene: 'evening' },
|
|
182
|
+
timeoutMs: 10_000,
|
|
183
|
+
idempotencyKey: 'intent-018f',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await call.accepted;
|
|
187
|
+
for await (const progress of call.stream) console.log(progress);
|
|
188
|
+
const result = await call.result;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
MiakAPI never retries state mutations, events, or calls. An idempotency key is
|
|
192
|
+
passed to the callee but does not enable hidden retries.
|
|
193
|
+
|
|
194
|
+
## Trusted browser client
|
|
195
|
+
|
|
196
|
+
Use the isolated `miakapi/browser` entry point in the first-party Miakapp web
|
|
197
|
+
application. It relies on the browser's native WebSocket implementation and does
|
|
198
|
+
not expose coordinator declarations, Home Keys, or the Node.js `ws` transport.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import {
|
|
202
|
+
createBrowserClient,
|
|
203
|
+
createControlPlaneBrowserRelayCredentialProvider,
|
|
204
|
+
} from 'miakapi/browser';
|
|
205
|
+
|
|
206
|
+
const credentialProvider = createControlPlaneBrowserRelayCredentialProvider({
|
|
207
|
+
exchangeEndpoint: 'https://control.example.com/v1/user-relay-tokens:exchange',
|
|
208
|
+
async getFirebaseIdToken({ signal }) {
|
|
209
|
+
if (signal.aborted) throw signal.reason;
|
|
210
|
+
const user = firebaseAuth.currentUser;
|
|
211
|
+
if (user === null) throw new Error('The user is signed out');
|
|
212
|
+
return user.getIdToken();
|
|
213
|
+
},
|
|
214
|
+
async getAppCheckToken({ signal }) {
|
|
215
|
+
if (signal.aborted) throw signal.reason;
|
|
216
|
+
return (await getToken(firebaseAppCheck)).token;
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const client = createBrowserClient({
|
|
221
|
+
homeId: 'my-home',
|
|
222
|
+
credentialProvider,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await client.start();
|
|
226
|
+
|
|
227
|
+
const removeStateListener = client.state.subscribe((snapshot) => {
|
|
228
|
+
if (!snapshot.stale) renderHome(snapshot.values);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const removeHomeListener = client.home.subscribe((home) => {
|
|
232
|
+
renderAvailability(home.enrolled, home.coordinators, home.stale);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const call = client.calls.start({
|
|
236
|
+
function: 'lighting.scene.activate',
|
|
237
|
+
arguments: { scene: 'evening' },
|
|
238
|
+
timeoutMs: 10_000,
|
|
239
|
+
idempotencyKey: 'intent-018f',
|
|
240
|
+
});
|
|
241
|
+
await call.accepted;
|
|
242
|
+
const result = await call.result;
|
|
243
|
+
|
|
244
|
+
removeStateListener();
|
|
245
|
+
removeHomeListener();
|
|
246
|
+
await client.stop();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
This replaces the earlier alpha browser options `relayUrl` and
|
|
250
|
+
`idTokenProvider`. They are intentionally rejected: callers must not pair an
|
|
251
|
+
independently selected relay with a source or access token.
|
|
252
|
+
|
|
253
|
+
The credential provider is invoked for the initial connection, reauthentication,
|
|
254
|
+
and reconnects. Its Firebase ID and App Check callbacks run only inside the
|
|
255
|
+
trusted host and send those source tokens solely to the HTTPS control plane.
|
|
256
|
+
MiakAPI never places them in a relay URL, WebSocket subprotocol, persistent
|
|
257
|
+
browser storage, log, error, `HELLO`, or `REAUTH` frame.
|
|
258
|
+
|
|
259
|
+
The control plane returns an up-to-five-minute Miakapp access token atomically
|
|
260
|
+
with its authoritative relay URL. MiakAPI sends only that audience-bound token
|
|
261
|
+
to the returned relay. If a renewal selects a different relay, the client marks
|
|
262
|
+
the old session stale and closes it. Before any automatic replacement connection,
|
|
263
|
+
including recovery from a transport or protocol failure, the client waits for the
|
|
264
|
+
native transport close event. If closure is not confirmed within ten seconds, the
|
|
265
|
+
client stops fail-closed instead of opening overlapping relay sockets. A routing
|
|
266
|
+
handoff uses the already-issued credential; it does not expose the new token to
|
|
267
|
+
the old relay or repeat the exchange. Stop and discard the client immediately
|
|
268
|
+
when the Firebase user signs out or the selected home changes. Relay routing
|
|
269
|
+
changes arrive through credentials and do not require mutating the client options.
|
|
270
|
+
|
|
271
|
+
Audience binding limits credential replay; it does not encrypt home traffic from
|
|
272
|
+
the selected relay. Users should still choose an operator they trust with the
|
|
273
|
+
plaintext state and calls that transit through it.
|
|
274
|
+
|
|
275
|
+
Browser state snapshots are defensive copies and become `stale` immediately
|
|
276
|
+
when continuity is lost. Revision or dictionary mismatches trigger one
|
|
277
|
+
fail-closed resynchronization request. Browser calls target the home's default
|
|
278
|
+
coordinator by function name, have no progress stream, and are never replayed by
|
|
279
|
+
the SDK. Incoming calls are rejected with an application error because this
|
|
280
|
+
first user profile deliberately exposes no browser call handlers. An
|
|
281
|
+
`outcome_unknown` failure means an effect may already have happened.
|
|
282
|
+
|
|
283
|
+
Token acquisition, protocol welcome, bootstrap, and reauthentication each have
|
|
284
|
+
bounded deadlines. The browser transport also caps individual frames, its
|
|
285
|
+
outbound queue, and rolling inbound bytes and frame counts. A native WebSocket
|
|
286
|
+
still materializes a complete message before JavaScript can reject it, so these
|
|
287
|
+
limits are defense in depth rather than isolation from a malicious relay; a
|
|
288
|
+
Worker boundary remains an option for a later hardened browser profile.
|
|
289
|
+
|
|
290
|
+
## Failure outcomes
|
|
291
|
+
|
|
292
|
+
Every `CoordinatorFailure` includes an `outcome`:
|
|
293
|
+
|
|
294
|
+
- `not_dispatched`: local validation, offline gating, or an explicit relay
|
|
295
|
+
terminal proves the operation did not dispatch.
|
|
296
|
+
- `sent`: an event frame reached the active transport; delivery is not implied.
|
|
297
|
+
- `accepted`: a call was accepted before its terminal failure.
|
|
298
|
+
- `applied`: a state mutation was acknowledged by the relay.
|
|
299
|
+
- `outcome_unknown`: transport loss, deadline, or post-accept cancellation means
|
|
300
|
+
an external effect may already have happened.
|
|
301
|
+
|
|
302
|
+
Treat `outcome_unknown` as uncertainty, never as rollback. MiakAPI does not turn
|
|
303
|
+
an uncertain physical effect into a safe automatic retry.
|
|
304
|
+
|
|
305
|
+
## Lifecycle and cleanup
|
|
306
|
+
|
|
307
|
+
`start()` may be called once. `stop()` is idempotent and repeated calls return the
|
|
308
|
+
same terminal promise. It aborts token and handler work, settles pending
|
|
309
|
+
operations conservatively, removes listeners, and closes the owned socket.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
await coordinator.stop({ deadlineMs: 5_000 });
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`deadlineMs` bounds cleanup even when an injected dependency ignores its abort
|
|
316
|
+
signal.
|
|
317
|
+
|
|
318
|
+
Between `start()` and `stop()` the coordinator owns its own reconnect schedule:
|
|
319
|
+
full jitter under a ceiling that doubles from one second up to thirty. The
|
|
320
|
+
ceiling returns to one second only after a session has held for thirty seconds,
|
|
321
|
+
so a session that is torn down as soon as it becomes `ready` keeps backing off
|
|
322
|
+
instead of reconnecting in a tight loop.
|
|
323
|
+
|
|
324
|
+
## Migration from MiakAPI 3
|
|
325
|
+
|
|
326
|
+
MiakAPI 4 removes the legacy `Miakapi(home, id, secret)` constructor, Firestore
|
|
327
|
+
lookup, mutable `home.variables`, UI callbacks, and notification helpers. Those
|
|
328
|
+
APIs depended on the retired Miakapp 3 transport and are not emulated.
|
|
329
|
+
|
|
330
|
+
Integrations now:
|
|
331
|
+
|
|
332
|
+
1. obtain short-lived access material through an `AccessTokenProvider`;
|
|
333
|
+
2. declare complete state, ACL, event, and function slices;
|
|
334
|
+
3. wait for `start()` readiness before issuing effects;
|
|
335
|
+
4. handle uncertainty explicitly through typed failures.
|
|
336
|
+
|
|
337
|
+
For UI automation and agent-driven homes, use [miakapp.com](https://miakapp.com/)
|
|
338
|
+
instead of building against the retired page-callback protocol.
|
|
339
|
+
|
|
340
|
+
## Protocol and conformance
|
|
341
|
+
|
|
342
|
+
The public TypeScript API and wire codec are pinned to the Miakapp-V3 coordinator
|
|
343
|
+
contract at an immutable commit in [`contracts/miakapp-v3.json`](contracts/miakapp-v3.json).
|
|
344
|
+
The external conformance subject runs the real SDK against a deterministic relay
|
|
345
|
+
and must pass every scenario in the `sdk` profile.
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
bun install --frozen-lockfile
|
|
349
|
+
bun run check
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The check includes strict type checking, unit and adversarial tests, Node.js and
|
|
353
|
+
browser-bundle smoke tests, canonical external conformance, and an npm package
|
|
354
|
+
dry run.
|
|
355
|
+
|
|
356
|
+
## License
|
|
357
|
+
|
|
358
|
+
[ISC](LICENSE)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AccessTokenProvider } from './api.js';
|
|
2
|
+
export interface HomeKeyAccessTokenProviderOptions {
|
|
3
|
+
readonly exchangeEndpoint: string;
|
|
4
|
+
readonly homeKey: string;
|
|
5
|
+
readonly fetch?: (input: string, init: RequestInit) => Promise<Response>;
|
|
6
|
+
}
|
|
7
|
+
export declare function createHomeKeyAccessTokenProvider(value: HomeKeyAccessTokenProviderOptions): AccessTokenProvider;
|
|
8
|
+
//# sourceMappingURL=access-token-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-token-provider.d.ts","sourceRoot":"","sources":["../src/access-token-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,mBAAmB,EAEpB,MAAM,UAAU,CAAC;AAgBlB,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC1E;AAsGD,wBAAgB,gCAAgC,CAC9C,KAAK,EAAE,iCAAiC,GACvC,mBAAmB,CA4DrB"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { boundedResponseBody, boundedResponseString, cancelResponseBody, canonicalRelayUrl, parseResponseJson, } from './internal/control-plane-response.js';
|
|
2
|
+
const MAXIMUM_ACCESS_TOKEN_BYTES = 8_192;
|
|
3
|
+
const MAXIMUM_ACCESS_TOKEN_LIFETIME_MS = 330_000;
|
|
4
|
+
const HOME_KEY = /^mhk1_([A-Za-z0-9_-]{22})_([A-Za-z0-9_-]{43})$/;
|
|
5
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
6
|
+
const COORDINATOR_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
7
|
+
function exchangeFailure() {
|
|
8
|
+
throw new Error('Miakapp access-token exchange failed');
|
|
9
|
+
}
|
|
10
|
+
function exactRecord(value, required, optional) {
|
|
11
|
+
if (value === null || Array.isArray(value) || typeof value !== 'object')
|
|
12
|
+
return exchangeFailure();
|
|
13
|
+
const keys = Object.keys(value);
|
|
14
|
+
const allowed = new Set([...required, ...optional]);
|
|
15
|
+
if (required.some((key) => !Object.hasOwn(value, key))
|
|
16
|
+
|| keys.some((key) => !allowed.has(key)))
|
|
17
|
+
return exchangeFailure();
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function canonicalExchangeEndpoint(value) {
|
|
21
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 2_048) {
|
|
22
|
+
return exchangeFailure();
|
|
23
|
+
}
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = new URL(value);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return exchangeFailure();
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol !== 'https:'
|
|
32
|
+
|| parsed.username !== ''
|
|
33
|
+
|| parsed.password !== ''
|
|
34
|
+
|| parsed.search !== ''
|
|
35
|
+
|| parsed.hash !== ''
|
|
36
|
+
|| parsed.pathname !== '/v1/access-tokens:exchange'
|
|
37
|
+
|| parsed.href !== value)
|
|
38
|
+
return exchangeFailure();
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function decodeCanonicalBase64URL(value, bytes) {
|
|
42
|
+
if (!BASE64URL.test(value))
|
|
43
|
+
return false;
|
|
44
|
+
const decoded = Buffer.from(value, 'base64url');
|
|
45
|
+
return decoded.byteLength === bytes && decoded.toString('base64url') === value;
|
|
46
|
+
}
|
|
47
|
+
function validHomeKey(value) {
|
|
48
|
+
if (typeof value !== 'string')
|
|
49
|
+
return exchangeFailure();
|
|
50
|
+
const match = HOME_KEY.exec(value);
|
|
51
|
+
if (match === null
|
|
52
|
+
|| match[1] === undefined
|
|
53
|
+
|| match[2] === undefined
|
|
54
|
+
|| !decodeCanonicalBase64URL(match[1], 16)
|
|
55
|
+
|| !decodeCanonicalBase64URL(match[2], 32))
|
|
56
|
+
return exchangeFailure();
|
|
57
|
+
return { value, keyId: match[1] };
|
|
58
|
+
}
|
|
59
|
+
function accessTokenResponse(value, keyId, now) {
|
|
60
|
+
const response = exactRecord(value, [
|
|
61
|
+
'schema', 'access_token', 'token_type', 'expires_at_ms', 'relay_url', 'key',
|
|
62
|
+
], []);
|
|
63
|
+
const key = exactRecord(response.key, ['id', 'label'], []);
|
|
64
|
+
const accessToken = boundedResponseString(response.access_token, 1, MAXIMUM_ACCESS_TOKEN_BYTES);
|
|
65
|
+
if (response.schema !== 'miakapp.access-token/1'
|
|
66
|
+
|| response.token_type !== 'Bearer'
|
|
67
|
+
|| accessToken.split('.').length !== 3
|
|
68
|
+
|| !accessToken.split('.').every((segment) => BASE64URL.test(segment))
|
|
69
|
+
|| key.id !== keyId)
|
|
70
|
+
return exchangeFailure();
|
|
71
|
+
boundedResponseString(key.label, 1, 64);
|
|
72
|
+
const expiresAtMs = response.expires_at_ms;
|
|
73
|
+
if (typeof expiresAtMs !== 'number'
|
|
74
|
+
|| !Number.isSafeInteger(expiresAtMs)
|
|
75
|
+
|| expiresAtMs <= now
|
|
76
|
+
|| expiresAtMs > now + MAXIMUM_ACCESS_TOKEN_LIFETIME_MS)
|
|
77
|
+
return exchangeFailure();
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
relayUrl: canonicalRelayUrl(response.relay_url),
|
|
80
|
+
token: accessToken,
|
|
81
|
+
expiresAtMs,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function validateTokenRequest(value) {
|
|
85
|
+
const request = exactRecord(value, ['coordinatorName', 'reason', 'signal'], ['relayHost']);
|
|
86
|
+
if (typeof request.coordinatorName !== 'string'
|
|
87
|
+
|| !COORDINATOR_NAME.test(request.coordinatorName)
|
|
88
|
+
|| (request.reason !== 'initial' && request.reason !== 'reauth' && request.reason !== 'reconnect')
|
|
89
|
+
|| request.signal === null
|
|
90
|
+
|| typeof request.signal !== 'object'
|
|
91
|
+
|| !('aborted' in request.signal)
|
|
92
|
+
|| !('addEventListener' in request.signal)
|
|
93
|
+
|| (request.relayHost !== undefined
|
|
94
|
+
&& (typeof request.relayHost !== 'string' || request.relayHost.length === 0 || request.relayHost.length > 255))) {
|
|
95
|
+
return exchangeFailure();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// createHomeKeyAccessTokenProvider maps the RFC 0004 Home Key exchange onto
|
|
99
|
+
// MiakAPI's three-field access-token boundary. It performs exactly one request
|
|
100
|
+
// per SDK demand and never exposes the Home Key to the WebSocket layer.
|
|
101
|
+
export function createHomeKeyAccessTokenProvider(value) {
|
|
102
|
+
const options = exactRecord(value, ['exchangeEndpoint', 'homeKey'], ['fetch']);
|
|
103
|
+
const endpoint = canonicalExchangeEndpoint(options.exchangeEndpoint);
|
|
104
|
+
const homeKey = validHomeKey(options.homeKey);
|
|
105
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
106
|
+
if (typeof fetcher !== 'function')
|
|
107
|
+
return exchangeFailure();
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
async getAccessToken(request) {
|
|
110
|
+
validateTokenRequest(request);
|
|
111
|
+
let response;
|
|
112
|
+
try {
|
|
113
|
+
response = await fetcher(endpoint, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
accept: 'application/json',
|
|
117
|
+
authorization: `Bearer ${homeKey.value}`,
|
|
118
|
+
'content-type': 'application/json',
|
|
119
|
+
},
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
purpose: 'relay',
|
|
122
|
+
role: 'coordinator',
|
|
123
|
+
coordinator_name: request.coordinatorName,
|
|
124
|
+
reason: request.reason,
|
|
125
|
+
}),
|
|
126
|
+
cache: 'no-store',
|
|
127
|
+
credentials: 'omit',
|
|
128
|
+
redirect: 'error',
|
|
129
|
+
referrerPolicy: 'no-referrer',
|
|
130
|
+
signal: request.signal,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
if (request.signal.aborted)
|
|
135
|
+
throw request.signal.reason;
|
|
136
|
+
return exchangeFailure();
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
if (request.signal.aborted) {
|
|
140
|
+
cancelResponseBody(response);
|
|
141
|
+
throw request.signal.reason;
|
|
142
|
+
}
|
|
143
|
+
if (response.status !== 200) {
|
|
144
|
+
cancelResponseBody(response);
|
|
145
|
+
return exchangeFailure();
|
|
146
|
+
}
|
|
147
|
+
if (response.headers.get('cache-control') !== 'no-store'
|
|
148
|
+
|| response.headers.get('pragma') !== 'no-cache'
|
|
149
|
+
|| response.headers.get('referrer-policy') !== 'no-referrer'
|
|
150
|
+
|| response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json') {
|
|
151
|
+
cancelResponseBody(response);
|
|
152
|
+
return exchangeFailure();
|
|
153
|
+
}
|
|
154
|
+
const body = await boundedResponseBody(response, request.signal);
|
|
155
|
+
if (request.signal.aborted)
|
|
156
|
+
throw request.signal.reason;
|
|
157
|
+
return accessTokenResponse(parseResponseJson(body), homeKey.keyId, Date.now());
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
if (request.signal.aborted)
|
|
161
|
+
throw request.signal.reason;
|
|
162
|
+
return exchangeFailure();
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=access-token-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"access-token-provider.js","sourceRoot":"","sources":["../src/access-token-provider.ts"],"names":[],"mappings":"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"}
|