@calimero-network/mero-js 26.10.0 → 27.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.
Files changed (220) hide show
  1. package/README.md +73 -288
  2. package/dist/cloud-v2/attestation.d.ts +91 -0
  3. package/dist/cloud-v2/attestation.js +135 -0
  4. package/dist/cloud-v2/capabilities.d.ts +30 -0
  5. package/dist/cloud-v2/capabilities.js +31 -0
  6. package/dist/cloud-v2/client.d.ts +426 -0
  7. package/dist/cloud-v2/client.js +646 -0
  8. package/dist/cloud-v2/errors.d.ts +98 -0
  9. package/dist/cloud-v2/errors.js +165 -0
  10. package/dist/cloud-v2/index.d.ts +10 -0
  11. package/dist/cloud-v2/index.js +5 -0
  12. package/dist/cloud-v2/passkey.d.ts +27 -0
  13. package/dist/cloud-v2/passkey.js +73 -0
  14. package/dist/crypto/internal.d.ts +3 -37
  15. package/dist/crypto/internal.js +3 -72
  16. package/dist/index.browser.mjs +4 -2
  17. package/dist/index.browser.mjs.map +4 -4
  18. package/dist/index.cjs +1750 -12767
  19. package/dist/index.cjs.map +4 -4
  20. package/dist/index.d.ts +3 -56
  21. package/dist/index.js +14 -75
  22. package/dist/index.mjs +1750 -12767
  23. package/dist/index.mjs.map +4 -4
  24. package/dist/sealed/index.d.ts +4 -4
  25. package/dist/sealed/index.js +2 -2
  26. package/dist/sealed/release.d.ts +21 -76
  27. package/dist/sealed/release.js +24 -108
  28. package/dist/sealed/sealed.d.ts +8 -56
  29. package/dist/sealed/sealed.js +11 -89
  30. package/dist/sealed/verify.d.ts +7 -13
  31. package/dist/sealed/verify.js +3 -7
  32. package/package.json +8 -13
  33. package/dist/account/account-admin.d.ts +0 -80
  34. package/dist/account/account-admin.js +0 -346
  35. package/dist/account/account.d.ts +0 -196
  36. package/dist/account/account.js +0 -269
  37. package/dist/account/application-id.d.ts +0 -50
  38. package/dist/account/application-id.js +0 -83
  39. package/dist/account/bootstrap-from-invitation.d.ts +0 -110
  40. package/dist/account/bootstrap-from-invitation.js +0 -80
  41. package/dist/account/create-context.d.ts +0 -103
  42. package/dist/account/create-context.js +0 -267
  43. package/dist/account/govern.d.ts +0 -24
  44. package/dist/account/govern.js +0 -50
  45. package/dist/account/index.d.ts +0 -26
  46. package/dist/account/index.js +0 -15
  47. package/dist/account/join-as-account.d.ts +0 -23
  48. package/dist/account/join-as-account.js +0 -34
  49. package/dist/account/join-with-node.d.ts +0 -115
  50. package/dist/account/join-with-node.js +0 -161
  51. package/dist/account/node-admin.d.ts +0 -20
  52. package/dist/account/node-admin.js +0 -33
  53. package/dist/account/relay-from-invitation.d.ts +0 -137
  54. package/dist/account/relay-from-invitation.js +0 -180
  55. package/dist/account/relay-login-error.d.ts +0 -32
  56. package/dist/account/relay-login-error.js +0 -34
  57. package/dist/account/session.d.ts +0 -266
  58. package/dist/account/session.js +0 -915
  59. package/dist/account/wordlist.d.ts +0 -16
  60. package/dist/account/wordlist.js +0 -273
  61. package/dist/admin-api/admin-client.d.ts +0 -686
  62. package/dist/admin-api/admin-client.js +0 -1225
  63. package/dist/admin-api/admin-factory.d.ts +0 -14
  64. package/dist/admin-api/admin-factory.js +0 -22
  65. package/dist/admin-api/admin-types.d.ts +0 -2041
  66. package/dist/admin-api/admin-types.js +0 -12
  67. package/dist/admin-api/index.d.ts +0 -4
  68. package/dist/admin-api/index.js +0 -5
  69. package/dist/admin-api/redeem-invitation.d.ts +0 -101
  70. package/dist/admin-api/redeem-invitation.js +0 -200
  71. package/dist/auth/index.d.ts +0 -34
  72. package/dist/auth/index.js +0 -67
  73. package/dist/auth-api/auth-client.d.ts +0 -31
  74. package/dist/auth-api/auth-client.js +0 -117
  75. package/dist/auth-api/auth-factory.d.ts +0 -7
  76. package/dist/auth-api/auth-factory.js +0 -41
  77. package/dist/auth-api/auth-types.d.ts +0 -130
  78. package/dist/auth-api/auth-types.js +0 -2
  79. package/dist/auth-api/index.d.ts +0 -3
  80. package/dist/auth-api/index.js +0 -4
  81. package/dist/capabilities.d.ts +0 -84
  82. package/dist/capabilities.js +0 -117
  83. package/dist/cloud/account-ownership.d.ts +0 -63
  84. package/dist/cloud/account-ownership.js +0 -87
  85. package/dist/cloud/cloud-client.d.ts +0 -949
  86. package/dist/cloud/cloud-client.js +0 -1068
  87. package/dist/cloud/connect.d.ts +0 -213
  88. package/dist/cloud/connect.js +0 -260
  89. package/dist/cloud/enrol-redirect.d.ts +0 -181
  90. package/dist/cloud/enrol-redirect.js +0 -244
  91. package/dist/cloud/index.d.ts +0 -12
  92. package/dist/cloud/index.js +0 -10
  93. package/dist/cloud/link-redirect.d.ts +0 -95
  94. package/dist/cloud/link-redirect.js +0 -121
  95. package/dist/cloud/link-state.d.ts +0 -36
  96. package/dist/cloud/link-state.js +0 -104
  97. package/dist/cloud/routing-proof.d.ts +0 -113
  98. package/dist/cloud/routing-proof.js +0 -96
  99. package/dist/device-cert/device-cert.d.ts +0 -189
  100. package/dist/device-cert/device-cert.js +0 -239
  101. package/dist/device-cert/device-scope.d.ts +0 -58
  102. package/dist/device-cert/device-scope.js +0 -76
  103. package/dist/device-cert/index.d.ts +0 -4
  104. package/dist/device-cert/index.js +0 -3
  105. package/dist/ephemeral/codec.d.ts +0 -4
  106. package/dist/ephemeral/codec.js +0 -12
  107. package/dist/ephemeral/index.d.ts +0 -57
  108. package/dist/ephemeral/index.js +0 -101
  109. package/dist/ephemeral/types.d.ts +0 -27
  110. package/dist/ephemeral/types.js +0 -1
  111. package/dist/errors/classify.d.ts +0 -65
  112. package/dist/errors/classify.js +0 -117
  113. package/dist/errors/index.d.ts +0 -2
  114. package/dist/errors/index.js +0 -1
  115. package/dist/events/group.d.ts +0 -95
  116. package/dist/events/group.js +0 -10
  117. package/dist/events/index.d.ts +0 -5
  118. package/dist/events/index.js +0 -2
  119. package/dist/events/sse.d.ts +0 -127
  120. package/dist/events/sse.js +0 -469
  121. package/dist/events/ws.d.ts +0 -57
  122. package/dist/events/ws.js +0 -212
  123. package/dist/http-client/api-response.d.ts +0 -15
  124. package/dist/http-client/api-response.js +0 -1
  125. package/dist/http-client/http-factory.d.ts +0 -71
  126. package/dist/http-client/http-factory.js +0 -68
  127. package/dist/http-client/http-types.d.ts +0 -87
  128. package/dist/http-client/http-types.js +0 -1
  129. package/dist/http-client/index.d.ts +0 -7
  130. package/dist/http-client/index.js +0 -11
  131. package/dist/http-client/path-segment.d.ts +0 -1
  132. package/dist/http-client/path-segment.js +0 -8
  133. package/dist/http-client/retry.d.ts +0 -5
  134. package/dist/http-client/retry.js +0 -82
  135. package/dist/http-client/signal-utils.d.ts +0 -2
  136. package/dist/http-client/signal-utils.js +0 -41
  137. package/dist/http-client/web-client.d.ts +0 -80
  138. package/dist/http-client/web-client.js +0 -557
  139. package/dist/invitation/index.d.ts +0 -2
  140. package/dist/invitation/index.js +0 -2
  141. package/dist/invitation/invitation.d.ts +0 -106
  142. package/dist/invitation/invitation.js +0 -104
  143. package/dist/login/index.d.ts +0 -4
  144. package/dist/login/index.js +0 -3
  145. package/dist/login/login.d.ts +0 -111
  146. package/dist/login/login.js +0 -110
  147. package/dist/login/session.d.ts +0 -124
  148. package/dist/login/session.js +0 -141
  149. package/dist/mero-js.d.ts +0 -231
  150. package/dist/mero-js.js +0 -469
  151. package/dist/namespace-op/index.d.ts +0 -2
  152. package/dist/namespace-op/index.js +0 -3
  153. package/dist/namespace-op/namespace-op.d.ts +0 -150
  154. package/dist/namespace-op/namespace-op.js +0 -191
  155. package/dist/nodeDiscovery.d.ts +0 -47
  156. package/dist/nodeDiscovery.js +0 -101
  157. package/dist/owner-op/index.d.ts +0 -2
  158. package/dist/owner-op/index.js +0 -3
  159. package/dist/owner-op/owner-op.d.ts +0 -119
  160. package/dist/owner-op/owner-op.js +0 -214
  161. package/dist/presence/relay-presence.d.ts +0 -56
  162. package/dist/presence/relay-presence.js +0 -107
  163. package/dist/presence/statement.d.ts +0 -12
  164. package/dist/presence/statement.js +0 -17
  165. package/dist/relay/index.d.ts +0 -4
  166. package/dist/relay/index.js +0 -2
  167. package/dist/relay/nonce-source.d.ts +0 -72
  168. package/dist/relay/nonce-source.js +0 -111
  169. package/dist/relay/relay-client.d.ts +0 -472
  170. package/dist/relay/relay-client.js +0 -518
  171. package/dist/relay-attestation/attest-node-key.d.ts +0 -66
  172. package/dist/relay-attestation/attest-node-key.js +0 -110
  173. package/dist/relay-attestation/index.d.ts +0 -1
  174. package/dist/relay-attestation/index.js +0 -1
  175. package/dist/request/caller-proof.d.ts +0 -43
  176. package/dist/request/caller-proof.js +0 -36
  177. package/dist/request/index.d.ts +0 -3
  178. package/dist/request/index.js +0 -3
  179. package/dist/request/proof-signer.d.ts +0 -49
  180. package/dist/request/proof-signer.js +0 -59
  181. package/dist/request/request-sig.d.ts +0 -99
  182. package/dist/request/request-sig.js +0 -95
  183. package/dist/request-proof/authorizer.d.ts +0 -41
  184. package/dist/request-proof/authorizer.js +0 -60
  185. package/dist/request-proof/index.d.ts +0 -4
  186. package/dist/request-proof/index.js +0 -2
  187. package/dist/request-proof/request-proof.d.ts +0 -53
  188. package/dist/request-proof/request-proof.js +0 -95
  189. package/dist/rpc/index.d.ts +0 -64
  190. package/dist/rpc/index.js +0 -78
  191. package/dist/signer/index.d.ts +0 -2
  192. package/dist/signer/index.js +0 -3
  193. package/dist/signer/signer.d.ts +0 -92
  194. package/dist/signer/signer.js +0 -147
  195. package/dist/token-store/index.d.ts +0 -27
  196. package/dist/token-store/index.js +0 -62
  197. package/dist/transport/index.d.ts +0 -6
  198. package/dist/transport/index.js +0 -3
  199. package/dist/transport/mero-client.d.ts +0 -294
  200. package/dist/transport/mero-client.js +0 -386
  201. package/dist/transport/relay-observer.d.ts +0 -152
  202. package/dist/transport/relay-observer.js +0 -173
  203. package/dist/transport/relay-transport.d.ts +0 -73
  204. package/dist/transport/relay-transport.js +0 -94
  205. package/dist/transport/types.d.ts +0 -100
  206. package/dist/transport/types.js +0 -32
  207. package/dist/types/api-response.d.ts +0 -15
  208. package/dist/types/api-response.js +0 -1
  209. package/dist/types/index.d.ts +0 -1
  210. package/dist/types/index.js +0 -1
  211. package/dist/warrant/creation-warrant.d.ts +0 -163
  212. package/dist/warrant/creation-warrant.js +0 -194
  213. package/dist/warrant/governance-op.d.ts +0 -202
  214. package/dist/warrant/governance-op.js +0 -355
  215. package/dist/warrant/governance-warrant.d.ts +0 -124
  216. package/dist/warrant/governance-warrant.js +0 -164
  217. package/dist/warrant/index.d.ts +0 -8
  218. package/dist/warrant/index.js +0 -5
  219. package/dist/warrant/warrant.d.ts +0 -188
  220. package/dist/warrant/warrant.js +0 -202
@@ -1,127 +0,0 @@
1
- import type { GroupMembershipEventData, GroupMigrationEventData } from './group.js';
2
- /** How long a subscription POST may take before it counts as failed. */
3
- export declare const SUBSCRIPTION_TIMEOUT_MS = 10000;
4
- export type { GroupMembershipEventData, GroupMigrationEventData };
5
- export interface SseEventData {
6
- contextId: string;
7
- /** The context-event discriminator (core serializes it as a sibling of
8
- * `data`, e.g. `"AppVersionChanged"`). Undefined for events without a tag. */
9
- type?: string;
10
- data: unknown;
11
- }
12
- /** Parsed `AppVersionChanged` context event (bundle-version flip; skew #2). */
13
- export interface AppVersionChangedEvent {
14
- contextId: string;
15
- fromVersion?: string;
16
- toVersion?: string;
17
- }
18
- /** Anything the `'event'` stream can deliver: context events plus the
19
- * group-keyed membership and migration families. */
20
- type SseEvent = SseEventData | GroupMembershipEventData | GroupMigrationEventData;
21
- type SseEventHandler = (event: SseEvent) => void;
22
- type SseConnectHandler = (sessionId: string) => void;
23
- type SseErrorHandler = (error: Error) => void;
24
- /**
25
- * How one request to the SSE endpoints is authenticated.
26
- *
27
- * A bearer token is one value reused across requests; a request proof commits to
28
- * the method, the path and the body, so it must be produced per request. Both
29
- * reduce to the same thing from this client's point of view — headers — so the
30
- * stream takes the general form and {@link SseClient} adapts `getAuthToken` to it.
31
- */
32
- export type SseAuthorizer = (request: {
33
- method: string;
34
- path: string;
35
- body?: string;
36
- }) => Promise<Record<string, string>>;
37
- export declare class SseClient {
38
- private baseUrl;
39
- private authorize;
40
- private reconnectDelayMs;
41
- private sessionId;
42
- private abortController;
43
- private reconnectTimer;
44
- private subscribedContextIds;
45
- private subscribedGroupIds;
46
- /**
47
- * The current presence of each subscribed context, as the node's replay and
48
- * its live deltas left it: author → that author's last `Ephemeral` data and
49
- * when it arrived. The node replays a context's presence once, when the
50
- * context is first subscribed on this connection, so a presence listener
51
- * that comes later would never see it; {@link presenceSeed} gives it this.
52
- */
53
- private presence;
54
- private closed;
55
- private listeners;
56
- private fetchImpl;
57
- constructor(opts: {
58
- baseUrl: string;
59
- /** A bearer token provider. The historical form, and still the default. */
60
- getAuthToken?: () => Promise<string>;
61
- /**
62
- * Per-request authorization, for a credential that commits to the request —
63
- * a device cert signing a request proof. Takes precedence over
64
- * `getAuthToken`; one of the two is required.
65
- */
66
- authorize?: SseAuthorizer;
67
- reconnectDelayMs?: number;
68
- fetch?: typeof fetch;
69
- });
70
- on(event: 'connect', handler: SseConnectHandler): void;
71
- on(event: 'event', handler: SseEventHandler): void;
72
- on(event: 'error', handler: SseErrorHandler): void;
73
- off(event: 'connect', handler: SseConnectHandler): void;
74
- off(event: 'event', handler: SseEventHandler): void;
75
- off(event: 'error', handler: SseErrorHandler): void;
76
- /**
77
- * Typed convenience over the generic `'event'` stream: invokes `handler` only
78
- * for `AppVersionChanged` context events, with the payload already parsed.
79
- * Returns an unsubscribe closure (so callers need not retain the listener).
80
- */
81
- onAppVersionChanged(handler: (e: AppVersionChangedEvent) => void): () => void;
82
- /**
83
- * Typed convenience over the generic `'event'` stream: invokes `handler` only
84
- * for group-migration events, narrowed on `type` so `data` is the matching
85
- * payload. Returns an unsubscribe closure.
86
- *
87
- * A non-admin subscriber sees `MigrationStarted`, `MigrationProgress` and
88
- * `MigrationCompleted` but never `CascadeProgress`, which core delivers to
89
- * namespace admins only.
90
- */
91
- onMigrationEvent(handler: (e: GroupMigrationEventData) => void): () => void;
92
- private emit;
93
- connect(): Promise<void>;
94
- /**
95
- * Whether `controller`'s connection is no longer the one this client holds:
96
- * closed, or replaced by a later connect. Its abort lands asynchronously,
97
- * after a `close()` + `connect()` has already cleared `closed`, so `closed`
98
- * alone cannot tell; reporting it, and reconnecting for it, would abort the
99
- * live connection, whose own abort would do the same, without end.
100
- */
101
- private superseded;
102
- private readStream;
103
- private handleMessage;
104
- subscribe(idsOrOpts: string[] | {
105
- contextIds?: string[];
106
- groupIds?: string[];
107
- }): Promise<void>;
108
- /** Whether this connection holds `contextId`'s events. */
109
- isSubscribed(contextId: string): boolean;
110
- /**
111
- * The presence currently known for `contextId`, as `Ephemeral` event data,
112
- * each entry's `ageMs` advanced by how long ago it arrived. Empty for a
113
- * context this connection does not hold.
114
- */
115
- presenceSeed(contextId: string): Array<Record<string, unknown>>;
116
- private notePresence;
117
- unsubscribe(idsOrOpts: string[] | {
118
- contextIds?: string[];
119
- groupIds?: string[];
120
- }): Promise<void>;
121
- private sendSubscription;
122
- /** Sends one subscription change and returns why it failed, or `null`. */
123
- private postSubscription;
124
- private forceReconnect;
125
- private scheduleReconnect;
126
- close(): void;
127
- }
@@ -1,469 +0,0 @@
1
- import { isGroupMigrationEvent } from './group.js';
2
- import { AuthRevokedError, HTTPError } from '../http-client/index.js';
3
- import { carriesProof, refuseProofRedirect } from '../http-client/web-client.js';
4
- import { signedPath } from '../request-proof/authorizer.js';
5
- /**
6
- * `x-auth-error` reasons that mean the whole token family is gone, mirroring
7
- * the request path in `web-client.ts`. Anything else — `token_expired` above
8
- * all — is recoverable by refreshing and reconnecting.
9
- */
10
- const TERMINAL_AUTH_ERRORS = new Set(['token_reuse', 'token_revoked']);
11
- /**
12
- * Statuses a terminal `x-auth-error` can arrive on.
13
- *
14
- * Measured against merod 0.11.0-rc.41: a revoked token family answers
15
- * `GET /sse` and `POST /sse/subscription` with **403**, `x-auth-error:
16
- * token_revoked` and `content-length: 0` — `token_expired` is the 401.
17
- * Keying the check on 401 alone made the branch below unreachable for the one
18
- * reason a stream actually sees, so a dead token family surfaced as a bare
19
- * `HTTPError(403)` and reconnected on a timer forever.
20
- */
21
- const TERMINAL_AUTH_STATUSES = new Set([401, 403]);
22
- /** How long a subscription POST may take before it counts as failed. */
23
- export const SUBSCRIPTION_TIMEOUT_MS = 10000;
24
- /**
25
- * Whether a failed response means the whole token family is gone, so the caller
26
- * must re-authenticate and the client must stop reconnecting.
27
- *
28
- * The body carries nothing on this path (`content-length: 0`), so the header is
29
- * the only evidence there is.
30
- */
31
- function isTerminalAuthFailure(response) {
32
- const authError = response.headers.get('x-auth-error');
33
- if (!authError)
34
- return null;
35
- if (!TERMINAL_AUTH_STATUSES.has(response.status))
36
- return null;
37
- return TERMINAL_AUTH_ERRORS.has(authError) ? authError : null;
38
- }
39
- export class SseClient {
40
- constructor(opts) {
41
- this.sessionId = null;
42
- this.abortController = null;
43
- this.reconnectTimer = null;
44
- this.subscribedContextIds = new Set();
45
- this.subscribedGroupIds = new Set();
46
- /**
47
- * The current presence of each subscribed context, as the node's replay and
48
- * its live deltas left it: author → that author's last `Ephemeral` data and
49
- * when it arrived. The node replays a context's presence once, when the
50
- * context is first subscribed on this connection, so a presence listener
51
- * that comes later would never see it; {@link presenceSeed} gives it this.
52
- */
53
- this.presence = new Map();
54
- this.closed = false;
55
- this.listeners = { connect: [], event: [], error: [] };
56
- this.baseUrl = opts.baseUrl.replace(/\/+$/, '');
57
- if (opts.authorize) {
58
- this.authorize = opts.authorize;
59
- }
60
- else if (opts.getAuthToken) {
61
- const getAuthToken = opts.getAuthToken;
62
- this.authorize = async () => ({ Authorization: `Bearer ${await getAuthToken()}` });
63
- }
64
- else {
65
- throw new Error('SseClient needs either `getAuthToken` or `authorize`');
66
- }
67
- this.reconnectDelayMs = opts.reconnectDelayMs ?? 3000;
68
- // globalThis.fetch must be called as a method on globalThis, not through a
69
- // variable, or browsers throw "Illegal invocation".
70
- this.fetchImpl = (url, init) => opts.fetch ? opts.fetch(url, init) : globalThis.fetch(url, init);
71
- }
72
- on(event, handler) {
73
- const key = event;
74
- if (key in this.listeners) {
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
- const arr = this.listeners[key];
77
- if (!arr.includes(handler))
78
- arr.push(handler);
79
- }
80
- }
81
- off(event, handler) {
82
- const key = event;
83
- if (key in this.listeners) {
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- const arr = this.listeners[key];
86
- const idx = arr.indexOf(handler);
87
- if (idx !== -1)
88
- arr.splice(idx, 1);
89
- }
90
- }
91
- /**
92
- * Typed convenience over the generic `'event'` stream: invokes `handler` only
93
- * for `AppVersionChanged` context events, with the payload already parsed.
94
- * Returns an unsubscribe closure (so callers need not retain the listener).
95
- */
96
- onAppVersionChanged(handler) {
97
- const listener = (ev) => {
98
- // Core flattens the tagged payload: the event carries `type` as a sibling
99
- // of `data`, and `data` is the `{ fromVersion?, toVersion? }` payload.
100
- if (ev.type !== 'AppVersionChanged')
101
- return;
102
- const d = ev.data;
103
- handler({ contextId: ev.contextId, fromVersion: d?.fromVersion, toVersion: d?.toVersion });
104
- };
105
- this.on('event', listener);
106
- return () => this.off('event', listener);
107
- }
108
- /**
109
- * Typed convenience over the generic `'event'` stream: invokes `handler` only
110
- * for group-migration events, narrowed on `type` so `data` is the matching
111
- * payload. Returns an unsubscribe closure.
112
- *
113
- * A non-admin subscriber sees `MigrationStarted`, `MigrationProgress` and
114
- * `MigrationCompleted` but never `CascadeProgress`, which core delivers to
115
- * namespace admins only.
116
- */
117
- onMigrationEvent(handler) {
118
- const listener = (ev) => {
119
- if (isGroupMigrationEvent(ev))
120
- handler(ev);
121
- };
122
- this.on('event', listener);
123
- return () => this.off('event', listener);
124
- }
125
- emit(event, arg) {
126
- const key = event;
127
- if (key in this.listeners) {
128
- for (const handler of this.listeners[key]) {
129
- try {
130
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
131
- handler(arg);
132
- }
133
- catch {
134
- // Swallow handler errors
135
- }
136
- }
137
- }
138
- }
139
- async connect() {
140
- // Already connected — don't reconnect
141
- if (this.abortController && !this.closed) {
142
- return;
143
- }
144
- this.closed = false;
145
- const controller = new AbortController();
146
- this.abortController = controller;
147
- try {
148
- const url = `${this.baseUrl}/sse`;
149
- const headers = {
150
- ...(await this.authorize({ method: 'GET', path: signedPath(url) })),
151
- 'Accept': 'text/event-stream',
152
- };
153
- const proofAttached = carriesProof(headers);
154
- const response = await this.fetchImpl(url, {
155
- headers,
156
- signal: controller.signal,
157
- ...(proofAttached && { redirect: 'manual' }),
158
- });
159
- if (proofAttached)
160
- refuseProofRedirect(response, url);
161
- if (!response.ok) {
162
- // Throw the same typed errors the request path throws. A bare
163
- // `Error("SSE connection failed: 401")` forced callers to string-match
164
- // the status and guess what it meant, and the honest guess — treat any
165
- // 401 as fatal — logs the user out on a routine reconnect after the
166
- // access token ages out, discarding a refresh token that was still
167
- // good. The distinction is in the header; surface it.
168
- const terminal = isTerminalAuthFailure(response);
169
- const bodyText = await response.text().catch(() => undefined);
170
- if (terminal) {
171
- throw new AuthRevokedError(terminal, response.status, response.statusText, url, response.headers, bodyText);
172
- }
173
- throw new HTTPError(response.status, response.statusText, url, response.headers, bodyText);
174
- }
175
- if (!response.body) {
176
- throw new Error('SSE response has no body');
177
- }
178
- this.readStream(response.body, controller).catch((err) => {
179
- if (this.superseded(controller))
180
- return;
181
- const error = err instanceof Error ? err : new Error(String(err));
182
- this.emit('error', error);
183
- this.scheduleReconnect();
184
- });
185
- }
186
- catch (err) {
187
- if (this.superseded(controller))
188
- return;
189
- const error = err instanceof Error ? err : new Error(String(err));
190
- this.emit('error', error);
191
- // Reconnecting against a revoked token family is a timer that never
192
- // succeeds: every attempt re-sends the same dead credential. The caller
193
- // has to re-authenticate, and an AuthRevokedError is how it is told.
194
- if (error instanceof AuthRevokedError)
195
- return;
196
- this.scheduleReconnect();
197
- }
198
- }
199
- /**
200
- * Whether `controller`'s connection is no longer the one this client holds:
201
- * closed, or replaced by a later connect. Its abort lands asynchronously,
202
- * after a `close()` + `connect()` has already cleared `closed`, so `closed`
203
- * alone cannot tell; reporting it, and reconnecting for it, would abort the
204
- * live connection, whose own abort would do the same, without end.
205
- */
206
- superseded(controller) {
207
- return this.closed || controller.signal.aborted || this.abortController !== controller;
208
- }
209
- async readStream(body, controller) {
210
- const reader = body.getReader();
211
- const decoder = new TextDecoder();
212
- let buffer = '';
213
- try {
214
- for (;;) {
215
- const { done, value } = await reader.read();
216
- if (done)
217
- break;
218
- buffer += decoder.decode(value, { stream: true });
219
- const lines = buffer.split('\n');
220
- // Keep the last partial line in the buffer
221
- buffer = lines.pop() ?? '';
222
- for (const line of lines) {
223
- if (line.startsWith('data:')) {
224
- const jsonStr = line.slice(5).trim();
225
- if (jsonStr) {
226
- this.handleMessage(jsonStr);
227
- }
228
- }
229
- }
230
- }
231
- // Flush remaining bytes from the decoder
232
- buffer += decoder.decode();
233
- if (buffer.startsWith('data:')) {
234
- const jsonStr = buffer.slice(5).trim();
235
- if (jsonStr) {
236
- this.handleMessage(jsonStr);
237
- }
238
- }
239
- }
240
- catch (err) {
241
- if (this.superseded(controller))
242
- return;
243
- const error = err instanceof Error ? err : new Error(String(err));
244
- this.emit('error', error);
245
- }
246
- // Stream ended, reconnect
247
- if (!this.superseded(controller)) {
248
- this.scheduleReconnect();
249
- }
250
- }
251
- handleMessage(jsonStr) {
252
- try {
253
- const msg = JSON.parse(jsonStr);
254
- // Connection message
255
- if (msg.type === 'connect' && msg.session_id) {
256
- const sessionId = msg.session_id;
257
- this.sessionId = sessionId;
258
- // A new connection gets a new replay when it re-subscribes; what the
259
- // old one said may name authors who have since gone.
260
- this.presence.clear();
261
- if (this.subscribedContextIds.size === 0 && this.subscribedGroupIds.size === 0) {
262
- this.emit('connect', sessionId);
263
- return;
264
- }
265
- // A new session starts subscribed to nothing, so announce it once the re-subscribe
266
- // lands; a failed one still announces, then reports the error, as consumers saw before.
267
- void this.postSubscription('subscribe', {
268
- contextIds: [...this.subscribedContextIds],
269
- groupIds: [...this.subscribedGroupIds],
270
- }).then((failure) => {
271
- if (this.closed || this.sessionId !== sessionId)
272
- return;
273
- this.emit('connect', sessionId);
274
- if (failure)
275
- this.emit('error', failure);
276
- });
277
- return;
278
- }
279
- if (!msg.result)
280
- return;
281
- let eventData = msg.result.data;
282
- // Decode byte-array data if needed
283
- if (Array.isArray(eventData)) {
284
- try {
285
- const bytes = new Uint8Array(eventData);
286
- const text = new TextDecoder().decode(bytes);
287
- eventData = JSON.parse(text);
288
- }
289
- catch {
290
- // Keep raw data
291
- }
292
- }
293
- // Context event message
294
- if (msg.result.contextId) {
295
- if (msg.result.type === 'Ephemeral')
296
- this.notePresence(msg.result.contextId, eventData);
297
- this.emit('event', {
298
- contextId: msg.result.contextId,
299
- // Forward the event tag (sibling of `data` in core's flattened
300
- // payload) so typed helpers like `onAppVersionChanged` can discriminate.
301
- type: msg.result.type,
302
- data: eventData,
303
- });
304
- return;
305
- }
306
- // Group-keyed event message (untagged NodeEvent's membership and
307
- // migration variants; the `type` tag tells them apart).
308
- if (msg.result.groupId) {
309
- this.emit('event', {
310
- groupId: msg.result.groupId,
311
- type: msg.result.type,
312
- data: eventData,
313
- });
314
- }
315
- }
316
- catch {
317
- // Invalid JSON, ignore
318
- }
319
- }
320
- async subscribe(idsOrOpts) {
321
- const opts = Array.isArray(idsOrOpts) ? { contextIds: idsOrOpts } : idsOrOpts;
322
- const newContextIds = (opts.contextIds ?? []).filter(id => !this.subscribedContextIds.has(id));
323
- const newGroupIds = (opts.groupIds ?? []).filter(id => !this.subscribedGroupIds.has(id));
324
- for (const id of opts.contextIds ?? []) {
325
- this.subscribedContextIds.add(id);
326
- }
327
- for (const id of opts.groupIds ?? []) {
328
- this.subscribedGroupIds.add(id);
329
- }
330
- if ((newContextIds.length > 0 || newGroupIds.length > 0) && this.sessionId) {
331
- await this.sendSubscription('subscribe', { contextIds: newContextIds, groupIds: newGroupIds });
332
- }
333
- }
334
- /** Whether this connection holds `contextId`'s events. */
335
- isSubscribed(contextId) {
336
- return this.subscribedContextIds.has(contextId);
337
- }
338
- /**
339
- * The presence currently known for `contextId`, as `Ephemeral` event data,
340
- * each entry's `ageMs` advanced by how long ago it arrived. Empty for a
341
- * context this connection does not hold.
342
- */
343
- presenceSeed(contextId) {
344
- const now = Date.now();
345
- return [...(this.presence.get(contextId)?.values() ?? [])].map(({ data, at }) => ({
346
- ...data,
347
- ageMs: (typeof data.ageMs === 'number' ? data.ageMs : 0) + (now - at),
348
- }));
349
- }
350
- notePresence(contextId, data) {
351
- if (!this.subscribedContextIds.has(contextId))
352
- return;
353
- const d = data;
354
- if (!d || typeof d.author !== 'string')
355
- return;
356
- let byAuthor = this.presence.get(contextId);
357
- if (!byAuthor)
358
- this.presence.set(contextId, (byAuthor = new Map()));
359
- if (d.removed)
360
- byAuthor.delete(d.author);
361
- else
362
- byAuthor.set(d.author, { data: d, at: Date.now() });
363
- }
364
- async unsubscribe(idsOrOpts) {
365
- const opts = Array.isArray(idsOrOpts) ? { contextIds: idsOrOpts } : idsOrOpts;
366
- const hadContextIds = (opts.contextIds ?? []).filter(id => this.subscribedContextIds.has(id));
367
- const hadGroupIds = (opts.groupIds ?? []).filter(id => this.subscribedGroupIds.has(id));
368
- for (const id of opts.contextIds ?? []) {
369
- this.subscribedContextIds.delete(id);
370
- this.presence.delete(id);
371
- }
372
- for (const id of opts.groupIds ?? []) {
373
- this.subscribedGroupIds.delete(id);
374
- }
375
- if ((hadContextIds.length > 0 || hadGroupIds.length > 0) && this.sessionId) {
376
- await this.sendSubscription('unsubscribe', { contextIds: hadContextIds, groupIds: hadGroupIds });
377
- }
378
- }
379
- async sendSubscription(method, ids) {
380
- const failure = await this.postSubscription(method, ids);
381
- if (failure)
382
- this.emit('error', failure);
383
- }
384
- /** Sends one subscription change and returns why it failed, or `null`. */
385
- async postSubscription(method, ids) {
386
- const timeout = new AbortController();
387
- const timer = setTimeout(() => timeout.abort(new DOMException(`SSE ${method} timed out`, 'TimeoutError')), SUBSCRIPTION_TIMEOUT_MS);
388
- try {
389
- const params = {};
390
- if (ids.contextIds && ids.contextIds.length > 0)
391
- params.contextIds = ids.contextIds;
392
- if (ids.groupIds && ids.groupIds.length > 0)
393
- params.groupIds = ids.groupIds;
394
- const url = `${this.baseUrl}/sse/subscription`;
395
- // Serialized once and both signed and sent, because a proof commits to the
396
- // body: re-stringifying for the signature risks two byte sequences that
397
- // differ in key order, which fails as a refused credential rather than as
398
- // anything that names the real cause.
399
- const body = JSON.stringify({ id: this.sessionId, method, params });
400
- const headers = {
401
- ...(await this.authorize({ method: 'POST', path: signedPath(url), body })),
402
- 'Content-Type': 'application/json',
403
- };
404
- const proofAttached = carriesProof(headers);
405
- const response = await this.fetchImpl(url, {
406
- method: 'POST',
407
- headers,
408
- body,
409
- signal: timeout.signal,
410
- ...(proofAttached && { redirect: 'manual' }),
411
- });
412
- if (proofAttached)
413
- refuseProofRedirect(response, url);
414
- if (!response.ok) {
415
- // The subscribe path used to report only the status. On the one failure
416
- // that matters it is the whole of what the caller got: a revoked family
417
- // answers 403 with an empty body, so a bare `SSE subscribe failed: 403`
418
- // discarded the only evidence of what went wrong — and a 403 here reads
419
- // identically to the unrelated "not the session owner" refusal, which
420
- // does carry a body.
421
- const terminal = isTerminalAuthFailure(response);
422
- const bodyText = await response.text().catch(() => undefined);
423
- return terminal
424
- ? new AuthRevokedError(terminal, response.status, response.statusText, `${this.baseUrl}/sse/subscription`, response.headers, bodyText)
425
- : new HTTPError(response.status, response.statusText, `${this.baseUrl}/sse/subscription`, response.headers, bodyText);
426
- }
427
- return null;
428
- }
429
- catch (err) {
430
- return err instanceof Error ? err : new Error(`SSE ${method} failed`);
431
- }
432
- finally {
433
- clearTimeout(timer);
434
- }
435
- }
436
- forceReconnect() {
437
- if (this.abortController) {
438
- this.abortController.abort();
439
- this.abortController = null;
440
- }
441
- this.sessionId = null;
442
- this.connect();
443
- }
444
- scheduleReconnect() {
445
- if (this.closed)
446
- return;
447
- if (this.reconnectTimer) {
448
- clearTimeout(this.reconnectTimer);
449
- }
450
- this.reconnectTimer = setTimeout(() => {
451
- this.reconnectTimer = null;
452
- this.forceReconnect();
453
- }, this.reconnectDelayMs);
454
- }
455
- close() {
456
- this.closed = true;
457
- if (this.abortController) {
458
- this.abortController.abort();
459
- this.abortController = null;
460
- }
461
- if (this.reconnectTimer) {
462
- clearTimeout(this.reconnectTimer);
463
- this.reconnectTimer = null;
464
- }
465
- this.sessionId = null;
466
- this.subscribedContextIds.clear();
467
- this.subscribedGroupIds.clear();
468
- }
469
- }
@@ -1,57 +0,0 @@
1
- import type { GroupMembershipEventData, GroupMigrationEventData } from './group.js';
2
- export type { GroupMembershipEventData, GroupMigrationEventData };
3
- export interface WsEventData {
4
- contextId: string;
5
- /** The context-event discriminator (core serializes it as a sibling of
6
- * `data`, e.g. `"AppVersionChanged"`). Undefined for events without a tag. */
7
- type?: string;
8
- data: unknown;
9
- }
10
- /** Anything the `'event'` stream can deliver: context events plus the
11
- * group-keyed membership and migration families. */
12
- type WsEvent = WsEventData | GroupMembershipEventData | GroupMigrationEventData;
13
- type WsEventHandler = (event: WsEvent) => void;
14
- type WsConnectHandler = () => void;
15
- type WsErrorHandler = (error: Error) => void;
16
- /**
17
- * @experimental WebSocket event client. The SSE client (`SseClient`) is the
18
- * recommended transport for production — use `MeroJs.events` instead of `MeroJs.ws`.
19
- */
20
- export declare class WsClient {
21
- private baseUrl;
22
- private getAuthToken;
23
- private ws;
24
- private closed;
25
- private reconnectAttempt;
26
- private reconnectTimer;
27
- private subscribedContextIds;
28
- private subscribedGroupIds;
29
- private listeners;
30
- private static readonly MAX_BACKOFF_MS;
31
- constructor(opts: {
32
- baseUrl: string;
33
- getAuthToken: () => Promise<string>;
34
- });
35
- on(event: 'connect', handler: WsConnectHandler): void;
36
- on(event: 'event', handler: WsEventHandler): void;
37
- on(event: 'error', handler: WsErrorHandler): void;
38
- off(event: 'connect', handler: WsConnectHandler): void;
39
- off(event: 'event', handler: WsEventHandler): void;
40
- off(event: 'error', handler: WsErrorHandler): void;
41
- private emit;
42
- connect(): Promise<void>;
43
- private handleMessage;
44
- subscribe(idsOrOpts: string[] | {
45
- contextIds?: string[];
46
- groupIds?: string[];
47
- }): void;
48
- unsubscribe(idsOrOpts: string[] | {
49
- contextIds?: string[];
50
- groupIds?: string[];
51
- }): void;
52
- private resubscribeAfterReconnect;
53
- private buildSubscriptionParams;
54
- private sendMessage;
55
- private scheduleReconnect;
56
- close(): void;
57
- }