transport-io 0.3.0 → 0.5.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 (46) hide show
  1. package/README.md +8 -9
  2. package/dist/cli/certificate.node.js +115 -0
  3. package/dist/cli/certificate.node.js.map +1 -0
  4. package/dist/cli/demo-contract.js +6 -0
  5. package/dist/cli/demo-contract.js.map +1 -0
  6. package/dist/cli/demo.node.js +131 -0
  7. package/dist/cli/demo.node.js.map +1 -0
  8. package/dist/cli/dev-server.node.js +98 -0
  9. package/dist/cli/dev-server.node.js.map +1 -0
  10. package/dist/cli/main.node.js +150 -0
  11. package/dist/cli/main.node.js.map +1 -0
  12. package/dist/client.d.ts +5 -5
  13. package/dist/client.d.ts.map +1 -1
  14. package/dist/client.js +4 -4
  15. package/dist/client.js.map +1 -1
  16. package/dist/contract.d.ts +111 -1
  17. package/dist/contract.d.ts.map +1 -1
  18. package/dist/contract.js +20 -0
  19. package/dist/contract.js.map +1 -1
  20. package/dist/errors.d.ts +1 -1
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js.map +1 -1
  23. package/dist/framer.js +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -3
  27. package/dist/index.js.map +1 -1
  28. package/dist/origin.d.ts +1 -1
  29. package/dist/origin.js +1 -1
  30. package/dist/server.d.ts +46 -9
  31. package/dist/server.d.ts.map +1 -1
  32. package/dist/server.js +43 -1
  33. package/dist/server.js.map +1 -1
  34. package/dist/session.d.ts +44 -18
  35. package/dist/session.d.ts.map +1 -1
  36. package/dist/session.js +122 -45
  37. package/dist/session.js.map +1 -1
  38. package/dist/transport/dev.d.ts +15 -0
  39. package/dist/transport/dev.d.ts.map +1 -0
  40. package/dist/transport/dev.js +77 -0
  41. package/dist/transport/dev.js.map +1 -0
  42. package/dist/transport/fails.node.d.ts +8 -0
  43. package/dist/transport/fails.node.d.ts.map +1 -1
  44. package/dist/transport/fails.node.js +16 -0
  45. package/dist/transport/fails.node.js.map +1 -1
  46. package/package.json +10 -1
package/README.md CHANGED
@@ -8,18 +8,17 @@
8
8
  Real-time apps over WebTransport. Socket.IO's shape, on a transport with multiple streams
9
9
  and datagrams, without Socket.IO's mistakes.
10
10
 
11
- Framing, length prefixes, buffer accumulation and stream lifecycle are hidden. Reliability is
12
- not: an event declares `reliable` or `unreliable` in the contract, and "this message may be
13
- dropped" is a property of your data that lives in the type system. The reliable lane is
14
- carried on QUIC streams, the unreliable lane on QUIC datagrams.
11
+ Framing, length prefixes, buffer accumulation and stream lifecycle are handled for you. An
12
+ event declares `reliable` or `unreliable` in the contract, so "this message may be dropped"
13
+ is visible in the type system. The reliable lane is carried on QUIC streams, the unreliable
14
+ lane on QUIC datagrams.
15
15
 
16
- A request answers with a value or with a **sequence**. `call()` awaits one result;
17
- `stream()` gives an async iterable, and `break` resets the QUIC stream so the server
18
- generator's `finally` runs. Cancellation costs no extra API.
16
+ A request answers with a value or with a sequence. `call()` awaits one result. `stream()`
17
+ returns an async iterable, and leaving the loop resets the QUIC stream so the server
18
+ generator's `finally` runs.
19
19
 
20
20
  **Read [KNOWN-ISSUES.md](https://github.com/transport-io/transport-io/blob/main/KNOWN-ISSUES.md)
21
- before you start.** It is what this library refuses to do and will not change, plus the one
22
- measured defect. Full documentation is in the
21
+ before you start.** It lists what this library will not do, and the one measured defect. Full documentation is in the
23
22
  [repository README](https://github.com/transport-io/transport-io#readme). The short version:
24
23
 
25
24
  - **WebTransport only.** No WebSocket fallback, deliberately - a fallback would silently
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The certificate `transport-io dev` pins.
3
+ *
4
+ * WebTransport will not accept an arbitrary self-signed certificate. It accepts one pinned
5
+ * by hash, under constraints that are not ours and are easy to get wrong: ECDSA P-256, a
6
+ * SHA-256 hash, and a total validity of at most 14 days. A newcomer should never learn any
7
+ * of that, so this module owns all three and the CLI only asks for a usable certificate.
8
+ *
9
+ * Fourteen days means expiry is normal operation rather than an error. Every start checks,
10
+ * and anything expired or expiring within a day is replaced silently. The user finds out
11
+ * only because a line says the certificate was renewed, and because a browser tab holding
12
+ * the old hash has to be reloaded.
13
+ *
14
+ * Minting shells out to `openssl` because Node cannot issue an X.509 certificate:
15
+ * `crypto.Certificate` handles SPKAC only, and there is no issuance API. The alternative is
16
+ * a runtime dependency, and this package has none for the CLI.
17
+ */
18
+ import { execFileSync } from 'node:child_process';
19
+ import { createHash, createPrivateKey, X509Certificate } from 'node:crypto';
20
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ /** The spec's ceiling. Not a preference, and not adjustable. */
23
+ export const MAX_VALIDITY_DAYS = 14;
24
+ /**
25
+ * Replace a certificate with less than this left. A whole day, so a `dev` session started
26
+ * in the morning cannot expire underneath the person running it before evening.
27
+ */
28
+ export const RENEW_WITHIN_MS = 24 * 60 * 60 * 1000;
29
+ function openssl(args) {
30
+ try {
31
+ execFileSync('openssl', [...args], { stdio: 'pipe' });
32
+ }
33
+ catch (e) {
34
+ const detail = e instanceof Error ? e.message : String(e);
35
+ throw new Error(`openssl failed: ${detail}\n\n` +
36
+ 'transport-io dev mints a short-lived certificate with openssl, because Node cannot ' +
37
+ 'issue one. Install openssl and run this again: `brew install openssl` on macOS, ' +
38
+ '`apt install openssl` on Debian or Ubuntu.');
39
+ }
40
+ }
41
+ function readIfUsable(certPath, keyPath) {
42
+ if (!existsSync(certPath) || !existsSync(keyPath))
43
+ return undefined;
44
+ try {
45
+ const cert = readFileSync(certPath, 'utf8');
46
+ const privKey = readFileSync(keyPath, 'utf8');
47
+ const x509 = new X509Certificate(cert);
48
+ const validTo = new Date(x509.validTo);
49
+ if (Number.isNaN(validTo.getTime()))
50
+ return undefined;
51
+ if (validTo.getTime() - Date.now() < RENEW_WITHIN_MS)
52
+ return undefined;
53
+ // The key has to match the certificate, not merely exist. An empty or stale `key.pem`
54
+ // beside a valid `cert.pem` passed an existence check and was handed to the server,
55
+ // which then failed to start for a reason no one running `dev` could be expected to
56
+ // work out. Checking the pair is the only way to catch that here.
57
+ if (!x509.checkPrivateKey(createPrivateKey(privKey)))
58
+ return undefined;
59
+ return {
60
+ cert,
61
+ privKey,
62
+ sha256: new Uint8Array(createHash('sha256').update(x509.raw).digest()),
63
+ validTo,
64
+ renewed: false,
65
+ };
66
+ }
67
+ catch {
68
+ // Corrupt, truncated, or not a certificate. Minting a new one is always the answer.
69
+ return undefined;
70
+ }
71
+ }
72
+ /**
73
+ * Returns a usable certificate, minting one only when there is not already a good one.
74
+ *
75
+ * `dir` should be somewhere already ignored by the project's version control, so this never
76
+ * plants a private key in a commit and never has to edit anyone's `.gitignore`.
77
+ */
78
+ export function ensureCertificate(dir) {
79
+ const certPath = join(dir, 'cert.pem');
80
+ const keyPath = join(dir, 'key.pem');
81
+ const existing = readIfUsable(certPath, keyPath);
82
+ if (existing !== undefined)
83
+ return existing;
84
+ mkdirSync(dir, { recursive: true });
85
+ openssl(['ecparam', '-name', 'prime256v1', '-genkey', '-noout', '-out', keyPath]);
86
+ openssl([
87
+ 'req',
88
+ '-new',
89
+ '-x509',
90
+ '-key',
91
+ keyPath,
92
+ '-out',
93
+ certPath,
94
+ '-days',
95
+ String(MAX_VALIDITY_DAYS),
96
+ '-subj',
97
+ '/CN=localhost',
98
+ '-addext',
99
+ 'subjectAltName=DNS:localhost,IP:127.0.0.1',
100
+ ]);
101
+ const cert = readFileSync(certPath, 'utf8');
102
+ const x509 = new X509Certificate(cert);
103
+ return {
104
+ cert,
105
+ privKey: readFileSync(keyPath, 'utf8'),
106
+ sha256: new Uint8Array(createHash('sha256').update(x509.raw).digest()),
107
+ validTo: new Date(x509.validTo),
108
+ renewed: true,
109
+ };
110
+ }
111
+ /** Whole days left, floored, for the status line. */
112
+ export function daysLeft(validTo, now = Date.now()) {
113
+ return Math.max(0, Math.floor((validTo.getTime() - now) / (24 * 60 * 60 * 1000)));
114
+ }
115
+ //# sourceMappingURL=certificate.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"certificate.node.js","sourceRoot":"","sources":["../../src/cli/certificate.node.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { defineContract, reliable, unreliable } from '../contract.js';
2
+ export const demoContract = defineContract({
3
+ chat: reliable(),
4
+ cursor: unreliable(),
5
+ });
6
+ //# sourceMappingURL=demo-contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo-contract.js","sourceRoot":"","sources":["../../src/cli/demo-contract.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAc,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAajF,MAAM,CAAC,MAAM,YAAY,GASrB,cAAc,CAAC;IACjB,IAAI,EAAE,QAAQ,EAAY;IAC1B,MAAM,EAAE,UAAU,EAAc;CACjC,CAAC,CAAA"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `transport-io dev --demo`: two tabs talking, with no project and no files written.
3
+ *
4
+ * This exists because the acceptance test for `dev` is a person who has never used this
5
+ * library seeing a message cross, and serving a project they do not have cannot meet it. It
6
+ * writes nothing to disk and scaffolds nothing; it serves a page out of the package.
7
+ *
8
+ * It is a second thing to maintain, which is the honest cost. The e2e suite drives it, so it
9
+ * cannot rot quietly.
10
+ */
11
+ import { createServer } from '../server.js';
12
+ import { listenDev } from '../transport/fails.node.js';
13
+ import { demoContract } from './demo-contract.js';
14
+ import { LIB_PREFIX } from './dev-server.node.js';
15
+ const ROOM = 'demo';
16
+ /** Starts the demo's WebTransport server. The certificate comes from the environment. */
17
+ export async function startDemoServer() {
18
+ const server = createServer({ contract: demoContract });
19
+ server.onSession((peer) => {
20
+ void peer.join(ROOM);
21
+ // Reliable: everyone sees it, including the sender, in one order.
22
+ peer.on('chat', (msg) => void server.to(ROOM).emit('chat', msg));
23
+ // Unreliable: excluded from the sender, because you know where your own pointer is.
24
+ peer.on('cursor', (pos) => void server.to(ROOM).except(peer.id).emit('cursor', pos));
25
+ });
26
+ await server.listen(await listenDev(), {
27
+ onAcceptError: (e) => console.error('session refused:', e.message),
28
+ });
29
+ }
30
+ /**
31
+ * The page.
32
+ *
33
+ * It imports the built package over `LIB_PREFIX` as native ESM, which is why the demo needs
34
+ * no bundler, and it imports the same contract module the server above uses, which is why
35
+ * the two cannot disagree.
36
+ */
37
+ export const DEMO_PAGE = `<!doctype html>
38
+ <html lang="en">
39
+ <meta charset="utf-8">
40
+ <title>transport-io demo</title>
41
+ <style>
42
+ :root { color-scheme: light dark; }
43
+ body { font: 15px/1.5 ui-sans-serif, system-ui, sans-serif; margin: 0; height: 100vh;
44
+ display: flex; flex-direction: column; }
45
+ header { padding: .6rem 1rem; border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
46
+ display: flex; gap: 1rem; align-items: baseline; }
47
+ h1 { font-size: 1rem; margin: 0; font-weight: 600; }
48
+ #status { font-size: .8rem; opacity: .7; }
49
+ #log { flex: 1; overflow-y: auto; margin: 0; padding: 1rem; list-style: none; }
50
+ #log li { padding: .15rem 0; }
51
+ #log .me { opacity: .65; }
52
+ form { display: flex; gap: .5rem; padding: 1rem; border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent); }
53
+ input { flex: 1; padding: .5rem .7rem; font: inherit; border-radius: 6px;
54
+ border: 1px solid color-mix(in srgb, currentColor 30%, transparent); background: transparent; color: inherit; }
55
+ button { padding: .5rem 1rem; font: inherit; border-radius: 6px; border: 0;
56
+ background: currentColor; cursor: pointer; }
57
+ button span { color: Canvas; }
58
+ .dot { position: fixed; width: 10px; height: 10px; border-radius: 50%;
59
+ background: crimson; pointer-events: none; transition: translate 60ms linear; }
60
+ </style>
61
+ <header>
62
+ <h1>transport-io</h1>
63
+ <span id="status">connecting</span>
64
+ <span id="hint" style="font-size:.8rem;opacity:.5">open a second tab</span>
65
+ </header>
66
+ <ul id="log"></ul>
67
+ <form id="send">
68
+ <input id="body" placeholder="Type a message and press enter" autocomplete="off" autofocus>
69
+ <button type="submit"><span>Send</span></button>
70
+ </form>
71
+ <script type="module">
72
+ import { Client } from '${LIB_PREFIX}index.js'
73
+ import { connectDev } from '${LIB_PREFIX}transport/dev.js'
74
+ import { demoContract } from '${LIB_PREFIX}cli/demo-contract.js'
75
+
76
+ const me = 'guest-' + Math.random().toString(16).slice(2, 6)
77
+ const log = document.getElementById('log')
78
+ const status = document.getElementById('status')
79
+
80
+ const add = (text, mine) => {
81
+ const li = document.createElement('li')
82
+ li.textContent = text
83
+ if (mine) li.className = 'me'
84
+ log.append(li)
85
+ log.scrollTop = log.scrollHeight
86
+ }
87
+
88
+ const client = new Client({ contract: demoContract, connect: () => connectDev() })
89
+
90
+ client.subscribe(() => {
91
+ const s = client.getSnapshot()
92
+ status.textContent = s.status
93
+ if (s.status === 'closed' && s.lastError) add('! ' + s.lastError.message, true)
94
+ })
95
+
96
+ client.on('chat', (m) => add(m.from + ': ' + m.body, m.from === me))
97
+
98
+ // The unreliable lane. Dropped frames are the point, not a fault.
99
+ const dots = new Map()
100
+ client.on('cursor', (p) => {
101
+ let dot = dots.get(p.from)
102
+ if (!dot) { dot = document.createElement('div'); dot.className = 'dot'; document.body.append(dot); dots.set(p.from, dot) }
103
+ dot.style.translate = p.x + 'px ' + p.y + 'px'
104
+ })
105
+
106
+ document.getElementById('send').addEventListener('submit', (e) => {
107
+ e.preventDefault()
108
+ const input = document.getElementById('body')
109
+ const body = input.value.trim()
110
+ if (body === '') return
111
+ client.emit('chat', { from: me, body, at: Date.now() })
112
+ input.value = ''
113
+ })
114
+
115
+ addEventListener('pointermove', (e) => {
116
+ if (client.getSnapshot().status === 'connected') {
117
+ client.emit('cursor', { from: me, x: e.clientX, y: e.clientY })
118
+ }
119
+ })
120
+
121
+ try {
122
+ await client.connect()
123
+ add('connected as ' + me + '. Open this page in a second tab.', true)
124
+ } catch (err) {
125
+ status.textContent = 'failed'
126
+ add('! ' + (err && err.message ? err.message : String(err)), true)
127
+ }
128
+ </script>
129
+ </html>
130
+ `;
131
+ //# sourceMappingURL=demo.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"demo.node.js","sourceRoot":"","sources":["../../src/cli/demo.node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAgB,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEjD,MAAM,IAAI,GAAG,MAAM,CAAA;AAEnB,yFAAyF;AACzF,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,MAAM,GAAG,YAAY,CAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAA;IAEhE,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE;QACxB,KAAK,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpB,kEAAkE;QAClE,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;QAChE,oFAAoF;QACpF,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,KAAK,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,SAAS,EAAE,EAAE;QACrC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAG,CAAW,CAAC,OAAO,CAAC;KAC9E,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAmCP,UAAU;8BACN,UAAU;gCACR,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwDzC,CAAA"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The plain HTTP half of `transport-io dev`.
3
+ *
4
+ * Three jobs, and deliberately no fourth. It publishes the certificate hash at a fixed
5
+ * endpoint so the browser can pin it; it serves the package's own built ESM so a page can
6
+ * import the library without a bundler; and it serves whatever static files the project
7
+ * already built.
8
+ *
9
+ * It does NOT bundle. Bundling browser TypeScript needs a bundler, which would be the first
10
+ * runtime dependency this package has ever taken for the CLI, so a real project keeps
11
+ * running its own `vite dev` or `bun build --watch` alongside this. The demo needs no
12
+ * bundler because it imports the built ESM directly.
13
+ *
14
+ * `http` rather than `https` on purpose: `http://localhost` is a trustworthy origin, so the
15
+ * page gets a secure context for free and only the WebTransport endpoint needs a certificate.
16
+ */
17
+ import { existsSync, readFileSync, statSync } from 'node:fs';
18
+ import { createServer } from 'node:http';
19
+ import { extname, join, normalize, resolve } from 'node:path';
20
+ import { DEV_ENDPOINT } from '../transport/dev.js';
21
+ const MIME = {
22
+ '.html': 'text/html; charset=utf-8',
23
+ '.js': 'text/javascript; charset=utf-8',
24
+ '.css': 'text/css; charset=utf-8',
25
+ '.json': 'application/json; charset=utf-8',
26
+ '.map': 'application/json; charset=utf-8',
27
+ '.svg': 'image/svg+xml',
28
+ };
29
+ /** Where the package's own built ESM is served, so a page can import it with no build step. */
30
+ export const LIB_PREFIX = '/_transport-io/';
31
+ /**
32
+ * Resolves a URL path inside a root, refusing anything that escapes it.
33
+ *
34
+ * A dev server still serves the filesystem, and `..` in a request path is the oldest way to
35
+ * read a file that was never meant to be public.
36
+ */
37
+ function safeJoin(root, urlPath) {
38
+ const clean = normalize(decodeURIComponent(urlPath)).replace(/^(\.\.[/\\])+/, '');
39
+ const full = resolve(join(root, clean));
40
+ const rootFull = resolve(root);
41
+ if (full !== rootFull && !full.startsWith(rootFull + '/'))
42
+ return undefined;
43
+ return full;
44
+ }
45
+ function sendFile(res, file) {
46
+ if (!existsSync(file))
47
+ return false;
48
+ let stat;
49
+ try {
50
+ stat = statSync(file);
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ if (!stat.isFile())
56
+ return false;
57
+ res.writeHead(200, { 'content-type': MIME[extname(file)] ?? 'application/octet-stream' });
58
+ res.end(readFileSync(file));
59
+ return true;
60
+ }
61
+ export function startDevServer(opts) {
62
+ const server = createServer((req, res) => {
63
+ const path = (req.url ?? '/').split('?')[0] ?? '/';
64
+ if (path === DEV_ENDPOINT) {
65
+ res.writeHead(200, {
66
+ 'content-type': 'application/json; charset=utf-8',
67
+ // The hash changes when the certificate is renewed, so it must never be cached.
68
+ 'cache-control': 'no-store',
69
+ });
70
+ res.end(JSON.stringify(opts.manifest));
71
+ return;
72
+ }
73
+ if (path.startsWith(LIB_PREFIX)) {
74
+ const file = safeJoin(opts.distDir, path.slice(LIB_PREFIX.length));
75
+ if (file !== undefined && sendFile(res, file))
76
+ return;
77
+ res.writeHead(404).end('not found');
78
+ return;
79
+ }
80
+ if (opts.staticDir !== undefined) {
81
+ const rel = path === '/' ? '/index.html' : path;
82
+ const file = safeJoin(opts.staticDir, rel);
83
+ if (file !== undefined && sendFile(res, file))
84
+ return;
85
+ }
86
+ if (opts.indexHtml !== undefined && (path === '/' || path === '/index.html')) {
87
+ res.writeHead(200, { 'content-type': MIME['.html'] });
88
+ res.end(opts.indexHtml);
89
+ return;
90
+ }
91
+ res.writeHead(404).end('not found');
92
+ });
93
+ return new Promise((ok, fail) => {
94
+ server.once('error', fail);
95
+ server.listen(opts.port, '127.0.0.1', () => ok(server));
96
+ });
97
+ }
98
+ //# sourceMappingURL=dev-server.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-server.node.js","sourceRoot":"","sources":["../../src/cli/dev-server.node.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `npx transport-io dev`.
4
+ *
5
+ * The first thirty minutes with this library used to be two openssl invocations, three
6
+ * constraints nobody tells you about, a DER extraction, a SHA-256, and an undocumented hop
7
+ * getting that hash from the server process into the browser bundle. This closes all of it.
8
+ *
9
+ * What it does NOT do is bundle. That needs a bundler, which would be this package's first
10
+ * CLI dependency, so a real project keeps running its own `vite dev` or `bun build --watch`
11
+ * beside this and points `--static` at the output. `--demo` needs no bundler because the
12
+ * page imports the built package as native ESM.
13
+ *
14
+ * Everything here is Node built-ins and this library's own source. The import boundary check
15
+ * enforces that, so a future feature cannot quietly add a dependency.
16
+ */
17
+ import { spawn } from 'node:child_process';
18
+ import { existsSync } from 'node:fs';
19
+ import { dirname, resolve } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { daysLeft, ensureCertificate } from './certificate.node.js';
22
+ import { startDevServer } from './dev-server.node.js';
23
+ const DEFAULT_PORT = 3000;
24
+ const DEFAULT_WT_PORT = 4433;
25
+ function parse(argv) {
26
+ let entry;
27
+ let demo = false;
28
+ let port = DEFAULT_PORT;
29
+ let wtPort = DEFAULT_WT_PORT;
30
+ let staticDir;
31
+ let help = false;
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const a = argv[i];
34
+ if (a === '--demo')
35
+ demo = true;
36
+ else if (a === '--help' || a === '-h')
37
+ help = true;
38
+ else if (a === '--port')
39
+ port = Number(argv[++i]);
40
+ else if (a === '--wt-port')
41
+ wtPort = Number(argv[++i]);
42
+ else if (a === '--static')
43
+ staticDir = argv[++i];
44
+ else if (!a.startsWith('-'))
45
+ entry ??= a;
46
+ }
47
+ return { entry, demo, port, wtPort, staticDir, help };
48
+ }
49
+ const USAGE = `transport-io dev - a local WebTransport server with a pinned certificate
50
+
51
+ npx transport-io dev [entry] run your server, with a certificate already minted
52
+ npx transport-io dev --demo two tabs talking, no project needed
53
+
54
+ Options
55
+ --demo serve the built-in demo instead of a project
56
+ --port <n> HTTP port for the page (default ${DEFAULT_PORT})
57
+ --wt-port <n> WebTransport port (default ${DEFAULT_WT_PORT})
58
+ --static <dir> directory of files to serve (default ./public, then ./web)
59
+
60
+ Your server file should listen with the certificate this command minted:
61
+
62
+ import { listenDev } from 'transport-io/node-transport'
63
+ await server.listen(await listenDev())
64
+
65
+ This does not bundle browser code. Keep running your own bundler and point --static at it.
66
+ `;
67
+ function findStatic(explicit) {
68
+ if (explicit !== undefined)
69
+ return resolve(explicit);
70
+ for (const candidate of ['public', 'web/dist', 'web', 'dist']) {
71
+ const full = resolve(candidate);
72
+ if (existsSync(full))
73
+ return full;
74
+ }
75
+ return undefined;
76
+ }
77
+ async function main() {
78
+ const args = parse(process.argv.slice(2));
79
+ const command = process.argv[2];
80
+ if (args.help || command === undefined || (command !== 'dev' && !command.startsWith('-'))) {
81
+ if (command !== undefined && command !== 'dev' && !command.startsWith('-')) {
82
+ console.error(`unknown command '${command}'\n`);
83
+ }
84
+ console.log(USAGE);
85
+ process.exit(args.help ? 0 : 1);
86
+ }
87
+ // `node_modules/.cache` is ignored by every project already, so a private key never lands
88
+ // in a commit and this never edits anyone's .gitignore.
89
+ const cacheDir = resolve('node_modules/.cache/transport-io');
90
+ const cert = ensureCertificate(cacheDir);
91
+ const wtUrl = `https://127.0.0.1:${args.wtPort}/`;
92
+ const here = dirname(fileURLToPath(import.meta.url));
93
+ // `dist/cli` -> `dist`, the directory served as ESM to the browser.
94
+ const distDir = resolve(here, '..');
95
+ const staticDir = args.demo ? undefined : findStatic(args.staticDir);
96
+ let demoPage;
97
+ if (args.demo) {
98
+ const { DEMO_PAGE, startDemoServer } = await import('./demo.node.js');
99
+ demoPage = DEMO_PAGE;
100
+ process.env.TRANSPORT_IO_DEV_CERT = cert.cert;
101
+ process.env.TRANSPORT_IO_DEV_KEY = cert.privKey;
102
+ process.env.TRANSPORT_IO_DEV_WT_PORT = String(args.wtPort);
103
+ await startDemoServer();
104
+ }
105
+ await startDevServer({
106
+ port: args.port,
107
+ ...(staticDir === undefined ? {} : { staticDir }),
108
+ ...(demoPage === undefined ? {} : { indexHtml: demoPage }),
109
+ distDir,
110
+ manifest: { sha256: [...cert.sha256], url: wtUrl },
111
+ });
112
+ const short = Buffer.from(cert.sha256).toString('hex').slice(0, 6);
113
+ console.log('');
114
+ console.log('transport-io dev');
115
+ console.log('');
116
+ console.log(` page http://localhost:${args.port}`);
117
+ console.log(` webtransport ${wtUrl}`);
118
+ console.log(` certificate valid ${daysLeft(cert.validTo)}d sha-256 ${short}…`);
119
+ if (cert.renewed) {
120
+ console.log(' certificate renewed, so reload any tab you already had open');
121
+ }
122
+ console.log('');
123
+ console.log(' Open the page in two tabs. Chrome or Firefox; Safari cannot connect.');
124
+ if (!args.demo && staticDir === undefined) {
125
+ console.log('');
126
+ console.log(' No static directory found. Pass --static <dir> to serve your built page.');
127
+ }
128
+ console.log('');
129
+ if (!args.demo && args.entry !== undefined) {
130
+ const child = spawn(process.execPath, [args.entry], {
131
+ stdio: 'inherit',
132
+ env: {
133
+ ...process.env,
134
+ TRANSPORT_IO_DEV_CERT: cert.cert,
135
+ TRANSPORT_IO_DEV_KEY: cert.privKey,
136
+ TRANSPORT_IO_DEV_WT_PORT: String(args.wtPort),
137
+ },
138
+ });
139
+ child.on('exit', (code) => process.exit(code ?? 0));
140
+ }
141
+ else if (!args.demo) {
142
+ console.log(' No entry given, so no server was started. Pass one, or use --demo.');
143
+ console.log('');
144
+ }
145
+ }
146
+ main().catch((e) => {
147
+ console.error(e instanceof Error ? e.message : String(e));
148
+ process.exit(1);
149
+ });
150
+ //# sourceMappingURL=main.node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.node.js","sourceRoot":"","sources":["../../src/cli/main.node.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAErD,MAAM,YAAY,GAAG,IAAI,CAAA;AACzB,MAAM,eAAe,GAAG,IAAI,CAAA;AAW5B,SAAS,KAAK,CAAC,IAAuB;IACpC,IAAI,KAAyB,CAAA;IAC7B,IAAI,IAAI,GAAG,KAAK,CAAA;IAChB,IAAI,IAAI,GAAG,YAAY,CAAA;IACvB,IAAI,MAAM,GAAG,eAAe,CAAA;IAC5B,IAAI,SAA6B,CAAA;IACjC,IAAI,IAAI,GAAG,KAAK,CAAA;IAEhB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAW,CAAA;QAC3B,IAAI,CAAC,KAAK,QAAQ;YAAE,IAAI,GAAG,IAAI,CAAA;aAC1B,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;YAAE,IAAI,GAAG,IAAI,CAAA;aAC7C,IAAI,CAAC,KAAK,QAAQ;YAAE,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;aAC5C,IAAI,CAAC,KAAK,WAAW;YAAE,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;aACjD,IAAI,CAAC,KAAK,UAAU;YAAE,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;aAC3C,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,KAAK,KAAK,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;AACvD,CAAC;AAED,MAAM,KAAK,GAAG;;;;;;;gEAOkD,YAAY;gEACZ,eAAe;;;;;;;;;CAS9E,CAAA;AAED,SAAS,UAAU,CAAC,QAA4B;IAC9C,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpD,KAAK,MAAM,SAAS,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;QAC/B,IAAI,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;IACnC,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IACzC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAE/B,IAAI,IAAI,CAAC,IAAI,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,OAAO,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC1F,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3E,OAAO,CAAC,KAAK,CAAC,oBAAoB,OAAO,KAAK,CAAC,CAAA;QACjD,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QAClB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACjC,CAAC;IAED,0FAA0F;IAC1F,wDAAwD;IACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,kCAAkC,CAAC,CAAA;IAC5D,MAAM,IAAI,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAA;IAExC,MAAM,KAAK,GAAG,qBAAqB,IAAI,CAAC,MAAM,GAAG,CAAA;IACjD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACpD,oEAAoE;IACpE,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAEnC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACpE,IAAI,QAA4B,CAAA;IAChC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAA;QACrE,QAAQ,GAAG,SAAS,CAAA;QACpB,OAAO,CAAC,GAAG,CAAC,qBAAqB,GAAG,IAAI,CAAC,IAAI,CAAA;QAC7C,OAAO,CAAC,GAAG,CAAC,oBAAoB,GAAG,IAAI,CAAC,OAAO,CAAA;QAC/C,OAAO,CAAC,GAAG,CAAC,wBAAwB,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC1D,MAAM,eAAe,EAAE,CAAA;IACzB,CAAC;IAED,MAAM,cAAc,CAAC;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC;QACjD,GAAG,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;QAC1D,OAAO;QACP,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE;KACnD,CAAC,CAAA;IAEF,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAClE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACf,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAA;IAC/B,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACf,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IAC5D,OAAO,CAAC,GAAG,CAAC,mBAAmB,KAAK,EAAE,CAAC,CAAA;IACvC,OAAO,CAAC,GAAG,CAAC,yBAAyB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,KAAK,GAAG,CAAC,CAAA;IAClF,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAA;IAC9E,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACf,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAA;IACrF,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACf,OAAO,CAAC,GAAG,CAAC,4EAA4E,CAAC,CAAA;IAC3F,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAEf,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;YAClD,KAAK,EAAE,SAAS;YAChB,GAAG,EAAE;gBACH,GAAG,OAAO,CAAC,GAAG;gBACd,qBAAqB,EAAE,IAAI,CAAC,IAAI;gBAChC,oBAAoB,EAAE,IAAI,CAAC,OAAO;gBAClC,wBAAwB,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;aAC9C;SACF,CAAC,CAAA;QACF,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAA;IACrD,CAAC;SAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,sEAAsE,CAAC,CAAA;QACnF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACjB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAU,EAAE,EAAE;IAC1B,OAAO,CAAC,KAAK,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
package/dist/client.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * scope, so importing this on a server - which Next.js will do - is safe. Feature
6
6
  * detection happens inside connect().
7
7
  */
8
- import { type AnyMap, type CallableOf, type Contract, type StreamableOf } from './contract.ts';
8
+ import { type AnyMap, type CallableOf, type Contract, type Registered, type StreamableOf } from './contract.ts';
9
9
  import { TransportError } from './errors.ts';
10
10
  import { type SessionStats, type StreamResult } from './session.ts';
11
11
  import type { Connection } from './transport/types.ts';
@@ -34,7 +34,7 @@ export interface ClientOptions<C extends Contract = Contract> {
34
34
  /** Test seam: the clock the TTL is measured against. Defaults to `Date.now`. */
35
35
  readonly now?: () => number;
36
36
  }
37
- export declare class Client<M extends AnyMap = AnyMap> {
37
+ export declare class Client<M extends AnyMap = Registered> {
38
38
  #private;
39
39
  constructor(opts: ClientOptions);
40
40
  /**
@@ -56,9 +56,9 @@ export declare class Client<M extends AnyMap = AnyMap> {
56
56
  readonly signal?: AbortSignal;
57
57
  }): Promise<M[K]['returns']>;
58
58
  /**
59
- * Available only on events declaring `yields`. Iterate it, or `.collect()` the whole
60
- * sequence. Leaving the loop with `break` resets the QUIC stream, which is what reaches
61
- * the responder's `ctx.signal`; there is no separate cancel call and none is needed.
59
+ * Available only on events declaring `yields`. Iterate it, or `.toArray()` the whole
60
+ * sequence. Leaving the loop with `break` resets the QUIC stream, and that reset is what
61
+ * fires the responder's `ctx.signal`. There is no separate cancel call.
62
62
  */
63
63
  stream<K extends StreamableOf<M> & string>(event: K, payload: M[K]['payload'], options?: {
64
64
  readonly signal?: AbortSignal;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -85,9 +85,9 @@ export class Client {
85
85
  return (await this.#requireSession().call(event, payload, options));
86
86
  }
87
87
  /**
88
- * Available only on events declaring `yields`. Iterate it, or `.collect()` the whole
89
- * sequence. Leaving the loop with `break` resets the QUIC stream, which is what reaches
90
- * the responder's `ctx.signal`; there is no separate cancel call and none is needed.
88
+ * Available only on events declaring `yields`. Iterate it, or `.toArray()` the whole
89
+ * sequence. Leaving the loop with `break` resets the QUIC stream, and that reset is what
90
+ * fires the responder's `ctx.signal`. There is no separate cancel call.
91
91
  */
92
92
  stream(event, payload, options) {
93
93
  return this.#requireSession().stream(event, payload, options);
@@ -102,7 +102,7 @@ export class Client {
102
102
  const conn = await this.#opts.connect();
103
103
  // Chrome implements neither `requireUnreliable` nor `reliability`, so `undefined`
104
104
  // must pass or every session on the dominant browser would be refused. Only an
105
- // explicit reliable-only is a lie about the unreliable lane.
105
+ // explicit reliable-only would misreport what the unreliable lane does.
106
106
  if (conn.reliability() === 'reliable-only') {
107
107
  // §10.2 code 1006. Throwing without closing left the peer holding a session this
108
108
  // side had already abandoned, with nothing on the wire to say why.