@estiva-app/protocol 0.1.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/CHANGELOG.md +57 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/bridge.d.ts +91 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +138 -0
- package/dist/bridge.js.map +1 -0
- package/dist/events.d.ts +432 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +616 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/live.d.ts +205 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +398 -0
- package/dist/live.js.map +1 -0
- package/dist/nip19.d.ts +98 -0
- package/dist/nip19.d.ts.map +1 -0
- package/dist/nip19.js +320 -0
- package/dist/nip19.js.map +1 -0
- package/dist/nip98.d.ts +61 -0
- package/dist/nip98.d.ts.map +1 -0
- package/dist/nip98.js +134 -0
- package/dist/nip98.js.map +1 -0
- package/dist/sign.d.ts +67 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +58 -0
- package/dist/sign.js.map +1 -0
- package/dist/subscriptions.d.ts +120 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +68 -0
- package/dist/subscriptions.js.map +1 -0
- package/package.json +59 -0
- package/src/bridge.ts +198 -0
- package/src/events.ts +821 -0
- package/src/index.ts +159 -0
- package/src/live.ts +536 -0
- package/src/nip19.ts +354 -0
- package/src/nip98.ts +164 -0
- package/src/sign.ts +113 -0
- package/src/subscriptions.ts +200 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@estiva-app/protocol` — the Estiva wire format, once.
|
|
3
|
+
*
|
|
4
|
+
* ## What is in here, and what is deliberately not
|
|
5
|
+
*
|
|
6
|
+
* **In:** the bytes. Event construction, the NIP-01 id preimage, NIP-19 `naddr`,
|
|
7
|
+
* NIP-98 HTTP auth, Schnorr signing, and the two relay clients (the HTTP bridge
|
|
8
|
+
* and the live socket). Everything whose correctness the *relay* judges.
|
|
9
|
+
*
|
|
10
|
+
* **Not in: the fold, or anything that interprets events.** How an app turns a
|
|
11
|
+
* stream of events into current truth is where apps are supposed to differ —
|
|
12
|
+
* `estiva-docs/decisions/0001` §8, and the README of this repo. Ship folds
|
|
13
|
+
* issues, Peek folds conversations, and neither should inherit the other's
|
|
14
|
+
* opinion. `foldFolder`, `foldResolution` and the projection all stay in their
|
|
15
|
+
* apps, and each app's conformance fixture stays with it.
|
|
16
|
+
*
|
|
17
|
+
* That line is the one thing to hold when adding to this package. "Both apps
|
|
18
|
+
* need it" is not the test; "the relay would notice if we disagreed" is.
|
|
19
|
+
*
|
|
20
|
+
* ## Why one implementation, when the suite's whole claim is independence
|
|
21
|
+
*
|
|
22
|
+
* SPEC §10 used to say the duplication *was* the architecture: three apps each
|
|
23
|
+
* implementing NIP-01 serialization is what makes "apps sharing no code work on
|
|
24
|
+
* the same data" a real statement. That argument was right about the *claim* and
|
|
25
|
+
* wrong about the *mechanism*, and the third copy is what settled it.
|
|
26
|
+
*
|
|
27
|
+
* A second hand-written event-id hash is not a demonstration of independence, it
|
|
28
|
+
* is a divergence the relay notices and we do not. It had already happened:
|
|
29
|
+
* Peek's `buildMessage` emitted `a` tags and Ship's could not, so the same
|
|
30
|
+
* logical message produced different bytes depending on which app sent it — and
|
|
31
|
+
* nothing failed, because each copy was self-consistent. `diff -r` between two
|
|
32
|
+
* vendored trees was the only thing holding the other two together, and it is a
|
|
33
|
+
* check somebody has to remember to run.
|
|
34
|
+
*
|
|
35
|
+
* What makes the interop claim true is that the apps share **no interpretation
|
|
36
|
+
* and no database** — which is still exactly the case. They now agree on the
|
|
37
|
+
* wire format on purpose rather than by coincidence.
|
|
38
|
+
*
|
|
39
|
+
* ## A MAJOR here is a protocol event, not a TypeScript event
|
|
40
|
+
*
|
|
41
|
+
* A change to the bytes an app publishes — id computation, serialization order,
|
|
42
|
+
* tag semantics, signature input — is a MAJOR even when the TypeScript signature
|
|
43
|
+
* is identical (ADR 0002 §4b). Every release note answers the wire question
|
|
44
|
+
* explicitly, including when the answer is "unchanged".
|
|
45
|
+
*/
|
|
46
|
+
/** Bumped by hand with the version in package.json — `test/version.test.ts`
|
|
47
|
+
* pins the two together. It exists so "the upgrade reached the app" can be
|
|
48
|
+
* checked by grepping a built bundle rather than by trusting a lockfile. */
|
|
49
|
+
export declare const PROTOCOL_VERSION = "0.1.0";
|
|
50
|
+
export { type NostrTag, type UnsignedEvent, type SignedEvent, type Profile, type ThreadRef, type ChannelVisibility, type ChannelKind, type MemberRole, type Label, type ResolutionAction, KIND, MAX_EMOJI_CHARS, MAX_MESSAGE_BYTES, MAX_MENTIONS, MAX_RATIONALE_BYTES, RELAY_AUTH_TOLERANCE_SECS, ASSERTION_SUBTYPE, computeEventId, toNostrSeconds, canonicalChannelName, threadTags, addr, relayAuthUrl, buildProfile, parseProfile, buildCreateChannel, buildDeleteChannel, buildEditChannelMetadata, buildAddMember, buildReaction, buildDeletion, buildMessage, buildResolution, buildFile, buildComponent, buildHighlight, buildUnsignedRelayAuthEvent, } from './events.js';
|
|
51
|
+
export { type AddressPointer, convertBits, bech32Encode, bech32Decode, encodeNaddr, decodeNaddr, pointerToAddress, addressToPointer, addrToNaddr, naddrToAddr, referenceToPointer, NADDR_RE, findNaddrs, stripNaddrs, } from './nip19.js';
|
|
52
|
+
export { type AuthEventArgs, TIMESTAMP_TOLERANCE_SECS, normalizeUrl, buildUnsignedAuthEvent, base64, authorizationHeaderFor, authorizationHeader, } from './nip98.js';
|
|
53
|
+
export { type Signer, type SignerKind, publicKeyFromSecret, signEvent, secretKeySigner, } from './sign.js';
|
|
54
|
+
export { type PublishResult, type QueryResult, type RelayHeaders, type RelayOptions, type FetchLike, parsePublishResponse, parseQueryResponse, Relay, } from './bridge.js';
|
|
55
|
+
export { type RelayState, type SocketLike, type RelayCredential, type LiveRelayOptions, type Subscription, type LiveRelay, toWebSocketUrl, parseFrame, createLiveRelay, } from './live.js';
|
|
56
|
+
export { type ChannelEventHandler, type ChannelSubscription, type ChannelSubscriptions, createChannelSubscriptions, } from './subscriptions.js';
|
|
57
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;4EAE4E;AAC5E,eAAO,MAAM,gBAAgB,UAAU,CAAA;AAEvC,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,gBAAgB,EAErB,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EAEjB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,IAAI,EACJ,YAAY,EAEZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,cAAc,EACnB,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,aAAa,EAClB,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,MAAM,EACN,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,mBAAmB,EACnB,SAAS,EACT,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,GACN,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,cAAc,EACd,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@estiva-app/protocol` — the Estiva wire format, once.
|
|
3
|
+
*
|
|
4
|
+
* ## What is in here, and what is deliberately not
|
|
5
|
+
*
|
|
6
|
+
* **In:** the bytes. Event construction, the NIP-01 id preimage, NIP-19 `naddr`,
|
|
7
|
+
* NIP-98 HTTP auth, Schnorr signing, and the two relay clients (the HTTP bridge
|
|
8
|
+
* and the live socket). Everything whose correctness the *relay* judges.
|
|
9
|
+
*
|
|
10
|
+
* **Not in: the fold, or anything that interprets events.** How an app turns a
|
|
11
|
+
* stream of events into current truth is where apps are supposed to differ —
|
|
12
|
+
* `estiva-docs/decisions/0001` §8, and the README of this repo. Ship folds
|
|
13
|
+
* issues, Peek folds conversations, and neither should inherit the other's
|
|
14
|
+
* opinion. `foldFolder`, `foldResolution` and the projection all stay in their
|
|
15
|
+
* apps, and each app's conformance fixture stays with it.
|
|
16
|
+
*
|
|
17
|
+
* That line is the one thing to hold when adding to this package. "Both apps
|
|
18
|
+
* need it" is not the test; "the relay would notice if we disagreed" is.
|
|
19
|
+
*
|
|
20
|
+
* ## Why one implementation, when the suite's whole claim is independence
|
|
21
|
+
*
|
|
22
|
+
* SPEC §10 used to say the duplication *was* the architecture: three apps each
|
|
23
|
+
* implementing NIP-01 serialization is what makes "apps sharing no code work on
|
|
24
|
+
* the same data" a real statement. That argument was right about the *claim* and
|
|
25
|
+
* wrong about the *mechanism*, and the third copy is what settled it.
|
|
26
|
+
*
|
|
27
|
+
* A second hand-written event-id hash is not a demonstration of independence, it
|
|
28
|
+
* is a divergence the relay notices and we do not. It had already happened:
|
|
29
|
+
* Peek's `buildMessage` emitted `a` tags and Ship's could not, so the same
|
|
30
|
+
* logical message produced different bytes depending on which app sent it — and
|
|
31
|
+
* nothing failed, because each copy was self-consistent. `diff -r` between two
|
|
32
|
+
* vendored trees was the only thing holding the other two together, and it is a
|
|
33
|
+
* check somebody has to remember to run.
|
|
34
|
+
*
|
|
35
|
+
* What makes the interop claim true is that the apps share **no interpretation
|
|
36
|
+
* and no database** — which is still exactly the case. They now agree on the
|
|
37
|
+
* wire format on purpose rather than by coincidence.
|
|
38
|
+
*
|
|
39
|
+
* ## A MAJOR here is a protocol event, not a TypeScript event
|
|
40
|
+
*
|
|
41
|
+
* A change to the bytes an app publishes — id computation, serialization order,
|
|
42
|
+
* tag semantics, signature input — is a MAJOR even when the TypeScript signature
|
|
43
|
+
* is identical (ADR 0002 §4b). Every release note answers the wire question
|
|
44
|
+
* explicitly, including when the answer is "unchanged".
|
|
45
|
+
*/
|
|
46
|
+
/** Bumped by hand with the version in package.json — `test/version.test.ts`
|
|
47
|
+
* pins the two together. It exists so "the upgrade reached the app" can be
|
|
48
|
+
* checked by grepping a built bundle rather than by trusting a lockfile. */
|
|
49
|
+
export const PROTOCOL_VERSION = '0.1.0';
|
|
50
|
+
export {
|
|
51
|
+
// constants
|
|
52
|
+
KIND, MAX_EMOJI_CHARS, MAX_MESSAGE_BYTES, MAX_MENTIONS, MAX_RATIONALE_BYTES, RELAY_AUTH_TOLERANCE_SECS, ASSERTION_SUBTYPE,
|
|
53
|
+
// the id preimage and the pieces that feed it
|
|
54
|
+
computeEventId, toNostrSeconds, canonicalChannelName, threadTags, addr, relayAuthUrl,
|
|
55
|
+
// builders
|
|
56
|
+
buildProfile, parseProfile, buildCreateChannel, buildDeleteChannel, buildEditChannelMetadata, buildAddMember, buildReaction, buildDeletion, buildMessage, buildResolution, buildFile, buildComponent, buildHighlight, buildUnsignedRelayAuthEvent, } from './events.js';
|
|
57
|
+
export { convertBits, bech32Encode, bech32Decode, encodeNaddr, decodeNaddr, pointerToAddress, addressToPointer, addrToNaddr, naddrToAddr, referenceToPointer, NADDR_RE, findNaddrs, stripNaddrs, } from './nip19.js';
|
|
58
|
+
export { TIMESTAMP_TOLERANCE_SECS, normalizeUrl, buildUnsignedAuthEvent, base64, authorizationHeaderFor, authorizationHeader, } from './nip98.js';
|
|
59
|
+
export { publicKeyFromSecret, signEvent, secretKeySigner, } from './sign.js';
|
|
60
|
+
export { parsePublishResponse, parseQueryResponse, Relay, } from './bridge.js';
|
|
61
|
+
export { toWebSocketUrl, parseFrame, createLiveRelay, } from './live.js';
|
|
62
|
+
export { createChannelSubscriptions, } from './subscriptions.js';
|
|
63
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;4EAE4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,OAAO;AAYL,YAAY;AACZ,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB;AACjB,8CAA8C;AAC9C,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,IAAI,EACJ,YAAY;AACZ,WAAW;AACX,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EAEL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EAEL,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,MAAM,EACN,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAGL,mBAAmB,EACnB,SAAS,EACT,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EAML,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,GACN,MAAM,aAAa,CAAA;AAEpB,OAAO,EAOL,cAAc,EACd,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EAIL,0BAA0B,GAC3B,MAAM,oBAAoB,CAAA"}
|
package/dist/live.d.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One live relay socket, with NIP-42 AUTH and reconnect (PEE-5).
|
|
3
|
+
*
|
|
4
|
+
* A WebSocket that stays open, authenticates as the viewer, and keeps
|
|
5
|
+
* subscriptions alive across disconnects. Nothing is *interpreted* here: this
|
|
6
|
+
* delivers frames, and folding them into state is the consuming app's business.
|
|
7
|
+
*
|
|
8
|
+
* The socket belongs in the **client**, not in a backend: a request-scoped
|
|
9
|
+
* server runtime cannot hold one, and more importantly the viewer's own
|
|
10
|
+
* credential is the only thing that can see the viewer's channels. Buzz gates
|
|
11
|
+
* reads per channel (`is_member_cached`), so a service identity would need
|
|
12
|
+
* membership in every conversation — a product decision, not an implementation
|
|
13
|
+
* detail.
|
|
14
|
+
*
|
|
15
|
+
* **This was built inside Peek** (PEE-5, PEE-6) because Gate 2 had not happened
|
|
16
|
+
* when it was due, and it was Peek-only for two days. SHA-3 is the ticket that
|
|
17
|
+
* owed the move; `subscriptions.ts` beside this file is PEE-6's half.
|
|
18
|
+
*
|
|
19
|
+
* No nostr library. Raw `WebSocket` plus `buildUnsignedRelayAuthEvent` is
|
|
20
|
+
* enough, and the tag layout stays in one place.
|
|
21
|
+
*
|
|
22
|
+
* ## Three things the relay does that shape this file
|
|
23
|
+
*
|
|
24
|
+
* All three were read out of `crates/buzz-*` rather than assumed, because each
|
|
25
|
+
* one fails as a healthy-looking socket that delivers nothing.
|
|
26
|
+
*
|
|
27
|
+
* 1. **A second AUTH on an authenticated connection is refused.** `handle_auth`
|
|
28
|
+
* matches on `AuthState::{Pending, Authenticated, Failed}` and answers
|
|
29
|
+
* anything but `Pending` with `OK … false "auth-required: already
|
|
30
|
+
* authenticated"`. So re-authenticating in place is not possible, and
|
|
31
|
+
* {@link LiveRelay} never tries.
|
|
32
|
+
*
|
|
33
|
+
* 2. **A failed AUTH poisons the connection and leaves it open.** Only a *ban*
|
|
34
|
+
* closes the socket; a verification failure sets `AuthState::Failed` and
|
|
35
|
+
* returns. Every later REQ is answered `CLOSED … "auth-required:
|
|
36
|
+
* authenticate before subscribing"`, forever, on a socket whose readyState
|
|
37
|
+
* is OPEN. Treating an AUTH refusal as fatal-for-this-socket and reconnecting
|
|
38
|
+
* is the only way out.
|
|
39
|
+
*
|
|
40
|
+
* 3. **The `relay` tag is bound to the connection's host**, not the deployment
|
|
41
|
+
* URL — see `relayAuthUrl`.
|
|
42
|
+
*
|
|
43
|
+
* ## Token rotation needs nothing here, and that is worth stating
|
|
44
|
+
*
|
|
45
|
+
* PEE-5 asked for re-authentication when the app renews its access token,
|
|
46
|
+
* preferring it to a reconnect. Neither is needed, and the first is impossible
|
|
47
|
+
* (point 1 above).
|
|
48
|
+
*
|
|
49
|
+
* The relay authenticates a **pubkey**, by verifying a Schnorr signature. It
|
|
50
|
+
* never sees the Estiva ID access token and has no idea one exists. Renewal
|
|
51
|
+
* issues a new token for the *same* keypair, so nothing the relay checked has
|
|
52
|
+
* changed and the connection stays valid. The token is needed only to *sign* a
|
|
53
|
+
* fresh 22242, which happens on the next connect.
|
|
54
|
+
*
|
|
55
|
+
* What genuinely does not propagate is offboarding: a person whose Estiva ID
|
|
56
|
+
* access is revoked keeps an already-authenticated socket until it drops. That
|
|
57
|
+
* is the relay's session model — its own ban gate is the control for it — and
|
|
58
|
+
* re-authenticating on a timer would not have fixed it either, since a banned
|
|
59
|
+
* pubkey is caught at connect.
|
|
60
|
+
*
|
|
61
|
+
* ## Never logged, never persisted
|
|
62
|
+
*
|
|
63
|
+
* The AUTH event and the access token appear in no log line. Buzz refuses to
|
|
64
|
+
* store kind:22242 for this reason, and Convex surfaces function arguments in
|
|
65
|
+
* its dashboard logs — which is the entire reason Peek's credential lives in the
|
|
66
|
+
* browser. {@link LiveRelayOptions.log} receives states and reasons, never
|
|
67
|
+
* events or credentials, and this module must keep it that way.
|
|
68
|
+
*/
|
|
69
|
+
import { type SignedEvent, type UnsignedEvent } from './events.js';
|
|
70
|
+
/**
|
|
71
|
+
* What the connection is doing, for PEE-9 to render.
|
|
72
|
+
*
|
|
73
|
+
* `failed` is terminal and deliberate: it means repeated *authentication*
|
|
74
|
+
* refusals, which retrying cannot fix — a missing 22242 grant, a clock more
|
|
75
|
+
* than 60s out, a revoked identity. A relay that is merely down stays in
|
|
76
|
+
* `reconnecting` forever, because that one does fix itself.
|
|
77
|
+
*/
|
|
78
|
+
export type RelayState = 'connecting' | 'authenticating' | 'live' | 'reconnecting' | 'failed';
|
|
79
|
+
/** Just enough of `WebSocket` to be faked in a test. */
|
|
80
|
+
export interface SocketLike {
|
|
81
|
+
send(data: string): void;
|
|
82
|
+
close(): void;
|
|
83
|
+
onopen: ((this: unknown, ev: unknown) => unknown) | null;
|
|
84
|
+
onmessage: ((this: unknown, ev: {
|
|
85
|
+
data: unknown;
|
|
86
|
+
}) => unknown) | null;
|
|
87
|
+
onclose: ((this: unknown, ev: unknown) => unknown) | null;
|
|
88
|
+
onerror: ((this: unknown, ev: unknown) => unknown) | null;
|
|
89
|
+
}
|
|
90
|
+
/** The credential a connect attempt needs. `null` means "not signed in". */
|
|
91
|
+
export interface RelayCredential {
|
|
92
|
+
accessToken: string;
|
|
93
|
+
pubkey: string;
|
|
94
|
+
}
|
|
95
|
+
export interface LiveRelayOptions {
|
|
96
|
+
/**
|
|
97
|
+
* The relay origin, `https://…` or `wss://…`.
|
|
98
|
+
*
|
|
99
|
+
* Peek reads this from a Convex query rather than a `VITE_` build-time
|
|
100
|
+
* variable, so that the bundle and the backend cannot drift. Where it comes
|
|
101
|
+
* from is the app's decision; that it is one value is not.
|
|
102
|
+
*/
|
|
103
|
+
url: string;
|
|
104
|
+
/** Read fresh on every connect, so a reconnect picks up a renewed token. */
|
|
105
|
+
getCredential: () => RelayCredential | null;
|
|
106
|
+
/**
|
|
107
|
+
* Injected so this module does no fetching of its own. Peek passes
|
|
108
|
+
* `signViaEstivaId`; a keyed client passes a `signEvent` wrapper.
|
|
109
|
+
*
|
|
110
|
+
* `expectedPubkey` is not a request — a remote `/sign` signs as the token's
|
|
111
|
+
* subject whatever it is handed, so a mismatch comes back as HTTP 200 with a
|
|
112
|
+
* valid event authored by somebody else. The implementation must check it.
|
|
113
|
+
*/
|
|
114
|
+
sign: (unsigned: UnsignedEvent, token: string, expectedPubkey: string) => Promise<SignedEvent>;
|
|
115
|
+
onState?: (state: RelayState) => void;
|
|
116
|
+
/** Never receives event bodies or credentials. */
|
|
117
|
+
log?: (message: string, detail?: Record<string, unknown>) => void;
|
|
118
|
+
/** Test seams. */
|
|
119
|
+
socketFactory?: (url: string) => SocketLike;
|
|
120
|
+
now?: () => number;
|
|
121
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
122
|
+
clearTimer?: (handle: unknown) => void;
|
|
123
|
+
/** Backoff shape. Exposed so a test does not wait real seconds. */
|
|
124
|
+
backoff?: {
|
|
125
|
+
baseMs: number;
|
|
126
|
+
maxMs: number;
|
|
127
|
+
jitter: () => number;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Prefix for REQ subscription ids. Cosmetic — it appears in the relay's logs
|
|
131
|
+
* and nowhere else — but a relay operator reading two apps' traffic wants to
|
|
132
|
+
* know which is which. Was hardcoded `peek-` while this lived in Peek.
|
|
133
|
+
*/
|
|
134
|
+
subscriptionPrefix?: string;
|
|
135
|
+
}
|
|
136
|
+
export interface Subscription {
|
|
137
|
+
/** Idempotent. Sends CLOSE when the socket is live, and forgets it either way. */
|
|
138
|
+
close(): void;
|
|
139
|
+
}
|
|
140
|
+
export interface LiveRelay {
|
|
141
|
+
/**
|
|
142
|
+
* Drop the current socket and connect again now, ignoring any backoff.
|
|
143
|
+
*
|
|
144
|
+
* For the case a socket cannot detect on its own: the network went away and
|
|
145
|
+
* came back without the connection ever closing. A WebSocket whose peer has
|
|
146
|
+
* become unreachable stays `OPEN` until TCP gives up, which can be minutes —
|
|
147
|
+
* and browser devtools' offline mode frequently does not close it at all. The
|
|
148
|
+
* socket therefore reports `live`, delivers nothing, and would never
|
|
149
|
+
* reconnect, because reconnection is driven by `onclose`.
|
|
150
|
+
*
|
|
151
|
+
* `liveTopics` calls this from the browser's own `online`/`offline` events,
|
|
152
|
+
* which are the one authority here that a socket cannot second-guess.
|
|
153
|
+
*/
|
|
154
|
+
reconnect(): void;
|
|
155
|
+
/**
|
|
156
|
+
* Subscribe now if the socket is live, and on every future reconnect.
|
|
157
|
+
*
|
|
158
|
+
* Registering before `live` is normal and supported — that is what makes a
|
|
159
|
+
* subscription survive a reconnect rather than being lost with the socket.
|
|
160
|
+
*/
|
|
161
|
+
subscribe(filters: Record<string, unknown>[], onEvent: (event: SignedEvent) => void, options?: {
|
|
162
|
+
onEose?: () => void;
|
|
163
|
+
onClosed?: (reason: string) => void;
|
|
164
|
+
}): Subscription;
|
|
165
|
+
state(): RelayState;
|
|
166
|
+
/** Stop for good. Does not reconnect afterwards. */
|
|
167
|
+
close(): void;
|
|
168
|
+
}
|
|
169
|
+
/** `https://` → `wss://`, `http://` → `ws://`; a `ws`-scheme URL is left alone. */
|
|
170
|
+
export declare function toWebSocketUrl(url: string): string;
|
|
171
|
+
/**
|
|
172
|
+
* A relay message, parsed far enough to route.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately tolerant: an unknown verb is ignored rather than throwing, so a
|
|
175
|
+
* relay that grows a frame does not take the socket down with it.
|
|
176
|
+
*/
|
|
177
|
+
type RelayFrame = {
|
|
178
|
+
type: 'AUTH';
|
|
179
|
+
challenge: string;
|
|
180
|
+
} | {
|
|
181
|
+
type: 'OK';
|
|
182
|
+
eventId: string;
|
|
183
|
+
accepted: boolean;
|
|
184
|
+
message: string;
|
|
185
|
+
} | {
|
|
186
|
+
type: 'EVENT';
|
|
187
|
+
subId: string;
|
|
188
|
+
event: SignedEvent;
|
|
189
|
+
} | {
|
|
190
|
+
type: 'EOSE';
|
|
191
|
+
subId: string;
|
|
192
|
+
} | {
|
|
193
|
+
type: 'CLOSED';
|
|
194
|
+
subId: string;
|
|
195
|
+
message: string;
|
|
196
|
+
} | {
|
|
197
|
+
type: 'NOTICE';
|
|
198
|
+
message: string;
|
|
199
|
+
} | {
|
|
200
|
+
type: 'OTHER';
|
|
201
|
+
};
|
|
202
|
+
export declare function parseFrame(raw: unknown): RelayFrame;
|
|
203
|
+
export declare function createLiveRelay(options: LiveRelayOptions): LiveRelay;
|
|
204
|
+
export {};
|
|
205
|
+
//# sourceMappingURL=live.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"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"}
|