miakapi 3.0.31 → 4.0.0-alpha.1

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.
Files changed (124) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +372 -0
  3. package/contracts/miakapp-v3.json +6 -0
  4. package/dist/access-token-provider.d.ts +8 -0
  5. package/dist/access-token-provider.d.ts.map +1 -0
  6. package/dist/access-token-provider.js +167 -0
  7. package/dist/access-token-provider.js.map +1 -0
  8. package/dist/api.d.ts +224 -0
  9. package/dist/api.d.ts.map +1 -0
  10. package/dist/api.js +47 -0
  11. package/dist/api.js.map +1 -0
  12. package/dist/browser-api.d.ts +113 -0
  13. package/dist/browser-api.d.ts.map +1 -0
  14. package/dist/browser-api.js +2 -0
  15. package/dist/browser-api.js.map +1 -0
  16. package/dist/browser-client.d.ts +3 -0
  17. package/dist/browser-client.d.ts.map +1 -0
  18. package/dist/browser-client.js +678 -0
  19. package/dist/browser-client.js.map +1 -0
  20. package/dist/browser-relay-credential-provider.d.ts +3 -0
  21. package/dist/browser-relay-credential-provider.d.ts.map +1 -0
  22. package/dist/browser-relay-credential-provider.js +243 -0
  23. package/dist/browser-relay-credential-provider.js.map +1 -0
  24. package/dist/browser.d.ts +4 -0
  25. package/dist/browser.d.ts.map +1 -0
  26. package/dist/browser.js +4 -0
  27. package/dist/browser.js.map +1 -0
  28. package/dist/coordinator.d.ts +3 -0
  29. package/dist/coordinator.d.ts.map +1 -0
  30. package/dist/coordinator.js +547 -0
  31. package/dist/coordinator.js.map +1 -0
  32. package/dist/index.d.ts +4 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +4 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/internal/browser-errors.d.ts +31 -0
  37. package/dist/internal/browser-errors.d.ts.map +1 -0
  38. package/dist/internal/browser-errors.js +69 -0
  39. package/dist/internal/browser-errors.js.map +1 -0
  40. package/dist/internal/browser-socket.d.ts +8 -0
  41. package/dist/internal/browser-socket.d.ts.map +1 -0
  42. package/dist/internal/browser-socket.js +204 -0
  43. package/dist/internal/browser-socket.js.map +1 -0
  44. package/dist/internal/calls.d.ts +33 -0
  45. package/dist/internal/calls.d.ts.map +1 -0
  46. package/dist/internal/calls.js +525 -0
  47. package/dist/internal/calls.js.map +1 -0
  48. package/dist/internal/control-plane-response.d.ts +10 -0
  49. package/dist/internal/control-plane-response.d.ts.map +1 -0
  50. package/dist/internal/control-plane-response.js +283 -0
  51. package/dist/internal/control-plane-response.js.map +1 -0
  52. package/dist/internal/declarations.d.ts +88 -0
  53. package/dist/internal/declarations.d.ts.map +1 -0
  54. package/dist/internal/declarations.js +561 -0
  55. package/dist/internal/declarations.js.map +1 -0
  56. package/dist/internal/errors.d.ts +31 -0
  57. package/dist/internal/errors.d.ts.map +1 -0
  58. package/dist/internal/errors.js +72 -0
  59. package/dist/internal/errors.js.map +1 -0
  60. package/dist/internal/events.d.ts +27 -0
  61. package/dist/internal/events.d.ts.map +1 -0
  62. package/dist/internal/events.js +207 -0
  63. package/dist/internal/events.js.map +1 -0
  64. package/dist/internal/presence.d.ts +15 -0
  65. package/dist/internal/presence.d.ts.map +1 -0
  66. package/dist/internal/presence.js +99 -0
  67. package/dist/internal/presence.js.map +1 -0
  68. package/dist/internal/resources.d.ts +33 -0
  69. package/dist/internal/resources.d.ts.map +1 -0
  70. package/dist/internal/resources.js +136 -0
  71. package/dist/internal/resources.js.map +1 -0
  72. package/dist/internal/runtime.d.ts +34 -0
  73. package/dist/internal/runtime.d.ts.map +1 -0
  74. package/dist/internal/runtime.js +17 -0
  75. package/dist/internal/runtime.js.map +1 -0
  76. package/dist/internal/session.d.ts +33 -0
  77. package/dist/internal/session.d.ts.map +1 -0
  78. package/dist/internal/session.js +199 -0
  79. package/dist/internal/session.js.map +1 -0
  80. package/dist/internal/socket.d.ts +6 -0
  81. package/dist/internal/socket.d.ts.map +1 -0
  82. package/dist/internal/socket.js +176 -0
  83. package/dist/internal/socket.js.map +1 -0
  84. package/dist/internal/state.d.ts +30 -0
  85. package/dist/internal/state.d.ts.map +1 -0
  86. package/dist/internal/state.js +188 -0
  87. package/dist/internal/state.js.map +1 -0
  88. package/dist/internal/user-calls.d.ts +27 -0
  89. package/dist/internal/user-calls.d.ts.map +1 -0
  90. package/dist/internal/user-calls.js +325 -0
  91. package/dist/internal/user-calls.js.map +1 -0
  92. package/dist/internal/user-session.d.ts +36 -0
  93. package/dist/internal/user-session.d.ts.map +1 -0
  94. package/dist/internal/user-session.js +250 -0
  95. package/dist/internal/user-session.js.map +1 -0
  96. package/dist/internal/user-state.d.ts +21 -0
  97. package/dist/internal/user-state.d.ts.map +1 -0
  98. package/dist/internal/user-state.js +240 -0
  99. package/dist/internal/user-state.js.map +1 -0
  100. package/dist/internal/validation.d.ts +32 -0
  101. package/dist/internal/validation.d.ts.map +1 -0
  102. package/dist/internal/validation.js +553 -0
  103. package/dist/internal/validation.js.map +1 -0
  104. package/dist/protocol/codec.d.ts +75 -0
  105. package/dist/protocol/codec.d.ts.map +1 -0
  106. package/dist/protocol/codec.js +1091 -0
  107. package/dist/protocol/codec.js.map +1 -0
  108. package/dist/protocol/session.d.ts +14 -0
  109. package/dist/protocol/session.d.ts.map +1 -0
  110. package/dist/protocol/session.js +125 -0
  111. package/dist/protocol/session.js.map +1 -0
  112. package/dist/protocol/user-session.d.ts +14 -0
  113. package/dist/protocol/user-session.d.ts.map +1 -0
  114. package/dist/protocol/user-session.js +116 -0
  115. package/dist/protocol/user-session.js.map +1 -0
  116. package/package.json +58 -9
  117. package/.eslintrc.js +0 -16
  118. package/.gitattributes +0 -2
  119. package/main.js +0 -305
  120. package/miakode.js +0 -56
  121. package/screenshots/bigGrid.jpg +0 -0
  122. package/screenshots/formExample.jpg +0 -0
  123. package/screenshots/properties&table.jpg +0 -0
  124. 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,372 @@
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 a home interface with the published CLI:
17
+ `npx @miakapp/cli init --starter app`. [`templates/home`](templates/home) is an
18
+ optional developer example that builds against this repository's sources.
19
+
20
+ ## Coordinator requirements
21
+
22
+ - Bun 1.2.23 or newer (primary coordinator runtime)
23
+ - Node.js 22.9 or newer when running compatibility or migration tooling
24
+ - A Miakapp Home Key or another approved short-lived access-token provider
25
+ - A Miakapp relay implementing wire protocol 1.0
26
+
27
+ The default `miakapi` entry point is server-side software. Do not ship
28
+ coordinator credentials, Home Keys, or coordinator access-token providers to a
29
+ browser or an untrusted plugin runtime.
30
+
31
+ ## Installation
32
+
33
+ ```sh
34
+ bun add miakapi@next
35
+ ```
36
+
37
+ Alpha releases use the `next` npm tag. The package is ESM-only and remains
38
+ Node-compatible so runtime-specific migration adapters can reuse the same SDK.
39
+
40
+ ## Quick start
41
+
42
+ ```ts
43
+ import {
44
+ ApplicationCallError,
45
+ EventDirection,
46
+ createCoordinator,
47
+ createHomeKeyAccessTokenProvider,
48
+ } from 'miakapi';
49
+
50
+ const homeKey = process.env.MIAKAPP_HOME_KEY;
51
+ if (homeKey === undefined) throw new Error('MIAKAPP_HOME_KEY is required');
52
+
53
+ const coordinator = createCoordinator({
54
+ name: 'home-assistant',
55
+ accessTokenProvider: createHomeKeyAccessTokenProvider({
56
+ exchangeEndpoint: 'https://control.miakapp.com/v1/access-tokens:exchange',
57
+ homeKey,
58
+ }),
59
+ });
60
+
61
+ coordinator.configure({
62
+ state: {
63
+ 'climate.living_room.temperature': 20,
64
+ },
65
+ stateAccess: [{
66
+ userId: 'user-id',
67
+ patterns: ['climate.living_room.*'],
68
+ }],
69
+ events: [{
70
+ topic: 'climate.living_room.changed',
71
+ directions:
72
+ EventDirection.acceptFromUsers |
73
+ EventDirection.publishToUsers,
74
+ }],
75
+ eventAccess: [{
76
+ userId: 'user-id',
77
+ publish: ['climate.living_room.changed'],
78
+ subscribe: ['climate.living_room.changed'],
79
+ }],
80
+ functions: {
81
+ async 'climate.living_room.set_target'(call) {
82
+ if (typeof call.arguments !== 'number') {
83
+ throw new ApplicationCallError(2001, 'Target must be a number');
84
+ }
85
+ await call.emit({ phase: 'applying' });
86
+ return { accepted: true, target: call.arguments };
87
+ },
88
+ },
89
+ });
90
+
91
+ coordinator.subscribe(({ current, reason }) => {
92
+ console.log('Miakapp coordinator status:', current, reason?.kind);
93
+ });
94
+
95
+ const session = await coordinator.start();
96
+ console.log('Ready in generation', session.generation);
97
+ ```
98
+
99
+ A complete minimal Bun coordinator matching the Miakapp V4 staging browser is
100
+ available in [`examples/synthetic-coordinator.ts`](examples/synthetic-coordinator.ts).
101
+ It publishes a small home state and implements one real `lighting.toggle` call.
102
+ Run it only in a trusted backend process:
103
+
104
+ ```sh
105
+ bun run build
106
+ bun run examples/synthetic-coordinator.ts
107
+ ```
108
+
109
+ The examples import `miakapi` the way an application does, so they resolve
110
+ through the package's own `exports` and need `dist` to exist. `bun run check`
111
+ builds before it tests for the same reason.
112
+
113
+ The process reads `MIAKAPP_HOME_KEY`,
114
+ `MIAKAPP_CONTROL_PLANE_EXCHANGE_ENDPOINT`, and `MIAKAPP_OWNER_USER_ID` from its
115
+ runtime environment. The Home Key is the only secret; the endpoint and Firebase
116
+ user ID are identifiers. Never pass any of them as command-line arguments.
117
+
118
+ The Home Key provider makes exactly one exchange request for each initial,
119
+ reauthentication, or reconnect demand from the SDK. It sends the Home Key only
120
+ to the configured HTTPS control-plane endpoint, rejects redirects and open or
121
+ overlong responses, and returns only the relay URL, compact access token, and
122
+ expiry to the coordinator core. It performs no independent retry; the
123
+ coordinator's single bounded reconnect schedule remains authoritative.
124
+
125
+ Keep the Home Key in the trusted coordinator backend. Do not place it in a web
126
+ bundle, browser storage, logs, URLs, or relay configuration. Applications with a
127
+ different approved credential store may continue to implement
128
+ `AccessTokenProvider` directly.
129
+
130
+ ### Running a coordinator as a service
131
+
132
+ [`examples/synthetic-coordinator-service.ts`](examples/synthetic-coordinator-service.ts)
133
+ wraps the same coordinator in the shape a container platform expects: an HTTP
134
+ listener that answers immediately, a `/healthz` probe that reports the live
135
+ lifecycle status, graceful `SIGTERM` draining, and a non-zero exit when the
136
+ coordinator stops without a shutdown signal so the platform restarts it.
137
+ [`examples/deploy/Dockerfile`](examples/deploy/Dockerfile) builds it from the
138
+ repository root.
139
+
140
+ Run exactly one coordinator per home. Two coordinators sharing a Home Key take
141
+ turns displacing each other on the relay; the reconnect schedule backs off so
142
+ the fight stays cheap, but only one of them holds the home.
143
+
144
+ `configure` supplies all five declaration slices as one desired snapshot. The
145
+ coordinator becomes `ready` only after the relay acknowledges them in order. A
146
+ later declaration call replaces its complete slice and temporarily returns the
147
+ coordinator to `synchronizing` until atomic activation.
148
+
149
+ ## State, events, and calls
150
+
151
+ State mutations are atomic and use acknowledged string paths:
152
+
153
+ ```ts
154
+ await coordinator.state.set([
155
+ { path: 'climate.living_room.temperature', value: 21.5 },
156
+ ]);
157
+ ```
158
+
159
+ Every declaration change — an ACL, event or function slice, or a reconnect —
160
+ resends the complete state slice (RFC 0001 §7.5). It carries the value the relay
161
+ last acknowledged for each path, not the configured one, so revoking one user
162
+ never rolls the home back for the others. Only mutations confirmed by
163
+ `STATE_SET_OK` are carried, including one whose caller aborted after handoff; a
164
+ rejected mutation or one lost with its connection (`outcome_unknown`) is never
165
+ replayed, and the last acknowledged value is resent instead. An explicit
166
+ `state.declare` or `configure` replaces acknowledged values. `STATE_SYNC` cannot
167
+ declare a path without a value, so an acknowledged deletion is staged with the
168
+ declared value and then reapplied by one SDK-sent deletion right after the
169
+ declaration activates; users may briefly see the declared value in between.
170
+
171
+ Event publication returns a synchronous opaque ID and a transport-handoff
172
+ promise. `sent` is not a delivery receipt; a later correlated relay rejection is
173
+ reported through `coordinator.errors`.
174
+
175
+ ```ts
176
+ const event = coordinator.events.publish(
177
+ 'climate.living_room.changed',
178
+ { temperature: 21.5 },
179
+ );
180
+ await event.sent;
181
+
182
+ coordinator.errors.subscribe((failure) => {
183
+ if (failure.correlation?.localId === event.localId) {
184
+ console.error('The relay rejected the event:', failure.kind);
185
+ }
186
+ });
187
+ ```
188
+
189
+ Outgoing calls expose acceptance, pull-bounded progress, and one terminal
190
+ result:
191
+
192
+ ```ts
193
+ const call = coordinator.calls.start({
194
+ function: 'lighting.scene.activate',
195
+ arguments: { scene: 'evening' },
196
+ timeoutMs: 10_000,
197
+ idempotencyKey: 'intent-018f',
198
+ });
199
+
200
+ await call.accepted;
201
+ for await (const progress of call.stream) console.log(progress);
202
+ const result = await call.result;
203
+ ```
204
+
205
+ MiakAPI never retries state mutations, events, or calls. An idempotency key is
206
+ passed to the callee but does not enable hidden retries.
207
+
208
+ ## Trusted browser client
209
+
210
+ Use the isolated `miakapi/browser` entry point in the first-party Miakapp web
211
+ application. It relies on the browser's native WebSocket implementation and does
212
+ not expose coordinator declarations, Home Keys, or the Node.js `ws` transport.
213
+
214
+ ```ts
215
+ import {
216
+ createBrowserClient,
217
+ createControlPlaneBrowserRelayCredentialProvider,
218
+ } from 'miakapi/browser';
219
+
220
+ const credentialProvider = createControlPlaneBrowserRelayCredentialProvider({
221
+ exchangeEndpoint: 'https://control.example.com/v1/user-relay-tokens:exchange',
222
+ async getFirebaseIdToken({ signal }) {
223
+ if (signal.aborted) throw signal.reason;
224
+ const user = firebaseAuth.currentUser;
225
+ if (user === null) throw new Error('The user is signed out');
226
+ return user.getIdToken();
227
+ },
228
+ async getAppCheckToken({ signal }) {
229
+ if (signal.aborted) throw signal.reason;
230
+ return (await getToken(firebaseAppCheck)).token;
231
+ },
232
+ });
233
+
234
+ const client = createBrowserClient({
235
+ homeId: 'my-home',
236
+ credentialProvider,
237
+ });
238
+
239
+ await client.start();
240
+
241
+ const removeStateListener = client.state.subscribe((snapshot) => {
242
+ if (!snapshot.stale) renderHome(snapshot.values);
243
+ });
244
+
245
+ const removeHomeListener = client.home.subscribe((home) => {
246
+ renderAvailability(home.enrolled, home.coordinators, home.stale);
247
+ });
248
+
249
+ const call = client.calls.start({
250
+ function: 'lighting.scene.activate',
251
+ arguments: { scene: 'evening' },
252
+ timeoutMs: 10_000,
253
+ idempotencyKey: 'intent-018f',
254
+ });
255
+ await call.accepted;
256
+ const result = await call.result;
257
+
258
+ removeStateListener();
259
+ removeHomeListener();
260
+ await client.stop();
261
+ ```
262
+
263
+ This replaces the earlier alpha browser options `relayUrl` and
264
+ `idTokenProvider`. They are intentionally rejected: callers must not pair an
265
+ independently selected relay with a source or access token.
266
+
267
+ The credential provider is invoked for the initial connection, reauthentication,
268
+ and reconnects. Its Firebase ID and App Check callbacks run only inside the
269
+ trusted host and send those source tokens solely to the HTTPS control plane.
270
+ MiakAPI never places them in a relay URL, WebSocket subprotocol, persistent
271
+ browser storage, log, error, `HELLO`, or `REAUTH` frame.
272
+
273
+ The control plane returns an up-to-five-minute Miakapp access token atomically
274
+ with its authoritative relay URL. MiakAPI sends only that audience-bound token
275
+ to the returned relay. If a renewal selects a different relay, the client marks
276
+ the old session stale and closes it. Before any automatic replacement connection,
277
+ including recovery from a transport or protocol failure, the client waits for the
278
+ native transport close event. If closure is not confirmed within ten seconds, the
279
+ client stops fail-closed instead of opening overlapping relay sockets. A routing
280
+ handoff uses the already-issued credential; it does not expose the new token to
281
+ the old relay or repeat the exchange. Stop and discard the client immediately
282
+ when the Firebase user signs out or the selected home changes. Relay routing
283
+ changes arrive through credentials and do not require mutating the client options.
284
+
285
+ Audience binding limits credential replay; it does not encrypt home traffic from
286
+ the selected relay. Users should still choose an operator they trust with the
287
+ plaintext state and calls that transit through it.
288
+
289
+ Browser state snapshots are defensive copies and become `stale` immediately
290
+ when continuity is lost. Revision or dictionary mismatches trigger one
291
+ fail-closed resynchronization request. Browser calls target the home's default
292
+ coordinator by function name, have no progress stream, and are never replayed by
293
+ the SDK. Incoming calls are rejected with an application error because this
294
+ first user profile deliberately exposes no browser call handlers. An
295
+ `outcome_unknown` failure means an effect may already have happened.
296
+
297
+ Token acquisition, protocol welcome, bootstrap, and reauthentication each have
298
+ bounded deadlines. The browser transport also caps individual frames, its
299
+ outbound queue, and rolling inbound bytes and frame counts. A native WebSocket
300
+ still materializes a complete message before JavaScript can reject it, so these
301
+ limits are defense in depth rather than isolation from a malicious relay; a
302
+ Worker boundary remains an option for a later hardened browser profile.
303
+
304
+ ## Failure outcomes
305
+
306
+ Every `CoordinatorFailure` includes an `outcome`:
307
+
308
+ - `not_dispatched`: local validation, offline gating, or an explicit relay
309
+ terminal proves the operation did not dispatch.
310
+ - `sent`: an event frame reached the active transport; delivery is not implied.
311
+ - `accepted`: a call was accepted before its terminal failure.
312
+ - `applied`: a state mutation was acknowledged by the relay.
313
+ - `outcome_unknown`: transport loss, deadline, or post-accept cancellation means
314
+ an external effect may already have happened.
315
+
316
+ Treat `outcome_unknown` as uncertainty, never as rollback. MiakAPI does not turn
317
+ an uncertain physical effect into a safe automatic retry.
318
+
319
+ ## Lifecycle and cleanup
320
+
321
+ `start()` may be called once. `stop()` is idempotent and repeated calls return the
322
+ same terminal promise. It aborts token and handler work, settles pending
323
+ operations conservatively, removes listeners, and closes the owned socket.
324
+
325
+ ```ts
326
+ await coordinator.stop({ deadlineMs: 5_000 });
327
+ ```
328
+
329
+ `deadlineMs` bounds cleanup even when an injected dependency ignores its abort
330
+ signal.
331
+
332
+ Between `start()` and `stop()` the coordinator owns its own reconnect schedule:
333
+ full jitter under a ceiling that doubles from one second up to thirty. The
334
+ ceiling returns to one second only after a session has held for thirty seconds,
335
+ so a session that is torn down as soon as it becomes `ready` keeps backing off
336
+ instead of reconnecting in a tight loop.
337
+
338
+ ## Migration from MiakAPI 3
339
+
340
+ MiakAPI 4 removes the legacy `Miakapi(home, id, secret)` constructor, Firestore
341
+ lookup, mutable `home.variables`, UI callbacks, and notification helpers. Those
342
+ APIs depended on the retired Miakapp 3 transport and are not emulated.
343
+
344
+ Integrations now:
345
+
346
+ 1. obtain short-lived access material through an `AccessTokenProvider`;
347
+ 2. declare complete state, ACL, event, and function slices;
348
+ 3. wait for `start()` readiness before issuing effects;
349
+ 4. handle uncertainty explicitly through typed failures.
350
+
351
+ For UI automation and agent-driven homes, use [miakapp.com](https://miakapp.com/)
352
+ instead of building against the retired page-callback protocol.
353
+
354
+ ## Protocol and conformance
355
+
356
+ The public TypeScript API and wire codec are pinned to the Miakapp-V3 coordinator
357
+ contract at an immutable commit in [`contracts/miakapp-v3.json`](contracts/miakapp-v3.json).
358
+ The external conformance subject runs the real SDK against a deterministic relay
359
+ and must pass every scenario in the `sdk` profile.
360
+
361
+ ```sh
362
+ bun install --frozen-lockfile
363
+ bun run check
364
+ ```
365
+
366
+ The check includes strict type checking, unit and adversarial tests, Node.js and
367
+ browser-bundle smoke tests, canonical external conformance, and an npm package
368
+ dry run.
369
+
370
+ ## License
371
+
372
+ [ISC](LICENSE)
@@ -0,0 +1,6 @@
1
+ {
2
+ "repository": "https://github.com/Miakapp/Miakapp-V3.git",
3
+ "commit": "b927789691dd8cdebc91b673853cdc6711fe057d",
4
+ "profile": "sdk",
5
+ "schema": "miakapp.coordinator-contract/1"
6
+ }
@@ -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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"}