@byoky/sdk 0.2.0 → 0.3.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import type {\n AuthMethod,\n RelayMessage,\n RelayHello,\n WebSocketLike,\n} from '@byoky/core';\nimport {\n ByokyError,\n parseRelayMessage,\n sendRelayMessage,\n WS_READY_STATE,\n} from '@byoky/core';\n\nexport interface ByokyServerOptions {\n /** Keepalive ping interval in ms. Default: 30000. Set to 0 to disable. */\n pingInterval?: number;\n /** Timeout waiting for relay:hello after connection. Default: 10000. */\n helloTimeout?: number;\n}\n\nexport interface ByokyClient {\n readonly sessionId: string;\n readonly providers: Record<string, { available: boolean; authMethod: AuthMethod }>;\n readonly connected: boolean;\n createFetch(providerId: string): typeof fetch;\n close(): void;\n onClose(callback: () => void): () => void;\n}\n\nexport class ByokyServer {\n private pingInterval: number;\n private helloTimeout: number;\n\n constructor(options: ByokyServerOptions = {}) {\n this.pingInterval = options.pingInterval ?? 30_000;\n this.helloTimeout = options.helloTimeout ?? 10_000;\n }\n\n handleConnection(ws: WebSocketLike): Promise<ByokyClient> {\n const self = this;\n return new Promise<ByokyClient>((resolve, reject) => {\n let sessionId = '';\n let providers: Record<string, { available: boolean; authMethod: AuthMethod }> = {};\n let connected = false;\n const closeCallbacks = new Set<() => void>();\n const pendingRequests = new Map<string, {\n resolveMeta: (value: { status: number; statusText: string; headers: Record<string, string> }) => void;\n pushChunk: (chunk: string) => void;\n done: () => void;\n error: (err: Error) => void;\n }>();\n\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let requestCounter = 0;\n\n const helloTimer = setTimeout(() => {\n reject(ByokyError.relayConnectionFailed('Timed out waiting for relay:hello'));\n ws.close(4000, 'Hello timeout');\n }, this.helloTimeout);\n\n ws.onmessage = (event: { data: unknown }) => {\n const msg = parseRelayMessage(event.data);\n if (!msg) return;\n\n switch (msg.type) {\n case 'relay:hello':\n handleHello(msg);\n break;\n case 'relay:response:meta': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n pending.resolveMeta({\n status: msg.status,\n statusText: msg.statusText,\n headers: msg.headers,\n });\n }\n break;\n }\n case 'relay:response:chunk': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) pending.pushChunk(msg.chunk);\n break;\n }\n case 'relay:response:done': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n pending.done();\n pendingRequests.delete(msg.requestId);\n }\n break;\n }\n case 'relay:response:error': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n pending.error(new Error(msg.error.message));\n pendingRequests.delete(msg.requestId);\n }\n break;\n }\n case 'relay:ping':\n sendRelayMessage(ws, { type: 'relay:pong', ts: msg.ts });\n break;\n case 'relay:pong':\n break;\n }\n };\n\n ws.onclose = () => {\n cleanup();\n };\n\n ws.onerror = () => {\n cleanup();\n };\n\n function handleHello(msg: RelayHello) {\n clearTimeout(helloTimer);\n sessionId = msg.sessionId;\n providers = msg.providers;\n connected = true;\n\n // Start keepalive pings\n if (self.pingInterval > 0) {\n pingTimer = setInterval(() => {\n sendRelayMessage(ws, { type: 'relay:ping', ts: Date.now() });\n }, self.pingInterval);\n }\n\n resolve(client);\n }\n\n function cleanup() {\n if (!connected) return;\n connected = false;\n clearTimeout(helloTimer);\n if (pingTimer) clearInterval(pingTimer);\n\n // Reject all pending requests\n for (const pending of pendingRequests.values()) {\n pending.error(ByokyError.relayDisconnected());\n }\n pendingRequests.clear();\n\n for (const cb of closeCallbacks) cb();\n closeCallbacks.clear();\n }\n\n function createClientFetch(providerId: string): typeof fetch {\n return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n if (!connected) throw ByokyError.relayDisconnected();\n\n const url = typeof input === 'string' ? input\n : input instanceof URL ? input.toString()\n : (input as Request).url;\n\n const method = init?.method ?? 'GET';\n const headers = init?.headers\n ? Object.fromEntries(new Headers(init.headers).entries())\n : {};\n const body = init?.body ? await readBody(init.body) : undefined;\n\n const requestId = `relay-${++requestCounter}-${Date.now()}`;\n\n return new Promise<Response>((resolveFetch, rejectFetch) => {\n let metaResolved = false;\n let controller: ReadableStreamDefaultController<Uint8Array>;\n const encoder = new TextEncoder();\n\n const stream = new ReadableStream<Uint8Array>({\n start(c) { controller = c; },\n });\n\n pendingRequests.set(requestId, {\n resolveMeta: (meta) => {\n if (metaResolved) return;\n metaResolved = true;\n resolveFetch(new Response(stream, {\n status: meta.status,\n statusText: meta.statusText,\n headers: new Headers(meta.headers),\n }));\n },\n pushChunk: (chunk) => {\n try { controller.enqueue(encoder.encode(chunk)); } catch {}\n },\n done: () => {\n try { controller.close(); } catch {}\n },\n error: (err) => {\n try { controller.error(err); } catch {}\n if (!metaResolved) {\n metaResolved = true;\n rejectFetch(err);\n }\n },\n });\n\n // Send the request to the frontend\n sendRelayMessage(ws, {\n type: 'relay:request',\n requestId,\n providerId,\n url,\n method,\n headers,\n body,\n });\n });\n };\n }\n\n const client: ByokyClient = {\n get sessionId() { return sessionId; },\n get providers() { return providers; },\n get connected() { return connected; },\n createFetch: (providerId: string) => createClientFetch(providerId),\n close() {\n ws.close(1000, 'Server closed');\n cleanup();\n },\n onClose(callback: () => void) {\n closeCallbacks.add(callback);\n return () => { closeCallbacks.delete(callback); };\n },\n };\n\n });\n }\n}\n\nasync function readBody(body: BodyInit): Promise<string | undefined> {\n if (typeof body === 'string') return body;\n if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);\n if (body instanceof Uint8Array) return new TextDecoder().decode(body);\n if (typeof Blob !== 'undefined' && body instanceof Blob) return body.text();\n if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) return body.toString();\n return undefined;\n}\n\nexport type { WebSocketLike, RelayMessage, AuthMethod } from '@byoky/core';\n"],"mappings":"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","names":[]}
@@ -0,0 +1,28 @@
1
+ import { AuthMethod, WebSocketLike } from '@byoky/core';
2
+ export { AuthMethod, RelayMessage, WebSocketLike } from '@byoky/core';
3
+
4
+ interface ByokyServerOptions {
5
+ /** Keepalive ping interval in ms. Default: 30000. Set to 0 to disable. */
6
+ pingInterval?: number;
7
+ /** Timeout waiting for relay:hello after connection. Default: 10000. */
8
+ helloTimeout?: number;
9
+ }
10
+ interface ByokyClient {
11
+ readonly sessionId: string;
12
+ readonly providers: Record<string, {
13
+ available: boolean;
14
+ authMethod: AuthMethod;
15
+ }>;
16
+ readonly connected: boolean;
17
+ createFetch(providerId: string): typeof fetch;
18
+ close(): void;
19
+ onClose(callback: () => void): () => void;
20
+ }
21
+ declare class ByokyServer {
22
+ private pingInterval;
23
+ private helloTimeout;
24
+ constructor(options?: ByokyServerOptions);
25
+ handleConnection(ws: WebSocketLike): Promise<ByokyClient>;
26
+ }
27
+
28
+ export { type ByokyClient, ByokyServer, type ByokyServerOptions };
@@ -0,0 +1,28 @@
1
+ import { AuthMethod, WebSocketLike } from '@byoky/core';
2
+ export { AuthMethod, RelayMessage, WebSocketLike } from '@byoky/core';
3
+
4
+ interface ByokyServerOptions {
5
+ /** Keepalive ping interval in ms. Default: 30000. Set to 0 to disable. */
6
+ pingInterval?: number;
7
+ /** Timeout waiting for relay:hello after connection. Default: 10000. */
8
+ helloTimeout?: number;
9
+ }
10
+ interface ByokyClient {
11
+ readonly sessionId: string;
12
+ readonly providers: Record<string, {
13
+ available: boolean;
14
+ authMethod: AuthMethod;
15
+ }>;
16
+ readonly connected: boolean;
17
+ createFetch(providerId: string): typeof fetch;
18
+ close(): void;
19
+ onClose(callback: () => void): () => void;
20
+ }
21
+ declare class ByokyServer {
22
+ private pingInterval;
23
+ private helloTimeout;
24
+ constructor(options?: ByokyServerOptions);
25
+ handleConnection(ws: WebSocketLike): Promise<ByokyClient>;
26
+ }
27
+
28
+ export { type ByokyClient, ByokyServer, type ByokyServerOptions };
package/dist/server.js ADDED
@@ -0,0 +1,202 @@
1
+ // src/server.ts
2
+ import {
3
+ ByokyError,
4
+ parseRelayMessage,
5
+ sendRelayMessage
6
+ } from "@byoky/core";
7
+ var ByokyServer = class {
8
+ pingInterval;
9
+ helloTimeout;
10
+ constructor(options = {}) {
11
+ this.pingInterval = options.pingInterval ?? 3e4;
12
+ this.helloTimeout = options.helloTimeout ?? 1e4;
13
+ }
14
+ handleConnection(ws) {
15
+ const self = this;
16
+ return new Promise((resolve, reject) => {
17
+ let sessionId = "";
18
+ let providers = {};
19
+ let connected = false;
20
+ const closeCallbacks = /* @__PURE__ */ new Set();
21
+ const pendingRequests = /* @__PURE__ */ new Map();
22
+ let pingTimer;
23
+ let requestCounter = 0;
24
+ const helloTimer = setTimeout(() => {
25
+ reject(ByokyError.relayConnectionFailed("Timed out waiting for relay:hello"));
26
+ ws.close(4e3, "Hello timeout");
27
+ }, this.helloTimeout);
28
+ ws.onmessage = (event) => {
29
+ const msg = parseRelayMessage(event.data);
30
+ if (!msg) return;
31
+ switch (msg.type) {
32
+ case "relay:hello":
33
+ handleHello(msg);
34
+ break;
35
+ case "relay:response:meta": {
36
+ const pending = pendingRequests.get(msg.requestId);
37
+ if (pending) {
38
+ pending.resolveMeta({
39
+ status: msg.status,
40
+ statusText: msg.statusText,
41
+ headers: msg.headers
42
+ });
43
+ }
44
+ break;
45
+ }
46
+ case "relay:response:chunk": {
47
+ const pending = pendingRequests.get(msg.requestId);
48
+ if (pending) pending.pushChunk(msg.chunk);
49
+ break;
50
+ }
51
+ case "relay:response:done": {
52
+ const pending = pendingRequests.get(msg.requestId);
53
+ if (pending) {
54
+ pending.done();
55
+ pendingRequests.delete(msg.requestId);
56
+ }
57
+ break;
58
+ }
59
+ case "relay:response:error": {
60
+ const pending = pendingRequests.get(msg.requestId);
61
+ if (pending) {
62
+ pending.error(new Error(msg.error.message));
63
+ pendingRequests.delete(msg.requestId);
64
+ }
65
+ break;
66
+ }
67
+ case "relay:ping":
68
+ sendRelayMessage(ws, { type: "relay:pong", ts: msg.ts });
69
+ break;
70
+ case "relay:pong":
71
+ break;
72
+ }
73
+ };
74
+ ws.onclose = () => {
75
+ cleanup();
76
+ };
77
+ ws.onerror = () => {
78
+ cleanup();
79
+ };
80
+ function handleHello(msg) {
81
+ clearTimeout(helloTimer);
82
+ sessionId = msg.sessionId;
83
+ providers = msg.providers;
84
+ connected = true;
85
+ if (self.pingInterval > 0) {
86
+ pingTimer = setInterval(() => {
87
+ sendRelayMessage(ws, { type: "relay:ping", ts: Date.now() });
88
+ }, self.pingInterval);
89
+ }
90
+ resolve(client);
91
+ }
92
+ function cleanup() {
93
+ if (!connected) return;
94
+ connected = false;
95
+ clearTimeout(helloTimer);
96
+ if (pingTimer) clearInterval(pingTimer);
97
+ for (const pending of pendingRequests.values()) {
98
+ pending.error(ByokyError.relayDisconnected());
99
+ }
100
+ pendingRequests.clear();
101
+ for (const cb of closeCallbacks) cb();
102
+ closeCallbacks.clear();
103
+ }
104
+ function createClientFetch(providerId) {
105
+ return async (input, init) => {
106
+ if (!connected) throw ByokyError.relayDisconnected();
107
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
108
+ const method = init?.method ?? "GET";
109
+ const headers = init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {};
110
+ const body = init?.body ? await readBody(init.body) : void 0;
111
+ const requestId = `relay-${++requestCounter}-${Date.now()}`;
112
+ return new Promise((resolveFetch, rejectFetch) => {
113
+ let metaResolved = false;
114
+ let controller;
115
+ const encoder = new TextEncoder();
116
+ const stream = new ReadableStream({
117
+ start(c) {
118
+ controller = c;
119
+ }
120
+ });
121
+ pendingRequests.set(requestId, {
122
+ resolveMeta: (meta) => {
123
+ if (metaResolved) return;
124
+ metaResolved = true;
125
+ resolveFetch(new Response(stream, {
126
+ status: meta.status,
127
+ statusText: meta.statusText,
128
+ headers: new Headers(meta.headers)
129
+ }));
130
+ },
131
+ pushChunk: (chunk) => {
132
+ try {
133
+ controller.enqueue(encoder.encode(chunk));
134
+ } catch {
135
+ }
136
+ },
137
+ done: () => {
138
+ try {
139
+ controller.close();
140
+ } catch {
141
+ }
142
+ },
143
+ error: (err) => {
144
+ try {
145
+ controller.error(err);
146
+ } catch {
147
+ }
148
+ if (!metaResolved) {
149
+ metaResolved = true;
150
+ rejectFetch(err);
151
+ }
152
+ }
153
+ });
154
+ sendRelayMessage(ws, {
155
+ type: "relay:request",
156
+ requestId,
157
+ providerId,
158
+ url,
159
+ method,
160
+ headers,
161
+ body
162
+ });
163
+ });
164
+ };
165
+ }
166
+ const client = {
167
+ get sessionId() {
168
+ return sessionId;
169
+ },
170
+ get providers() {
171
+ return providers;
172
+ },
173
+ get connected() {
174
+ return connected;
175
+ },
176
+ createFetch: (providerId) => createClientFetch(providerId),
177
+ close() {
178
+ ws.close(1e3, "Server closed");
179
+ cleanup();
180
+ },
181
+ onClose(callback) {
182
+ closeCallbacks.add(callback);
183
+ return () => {
184
+ closeCallbacks.delete(callback);
185
+ };
186
+ }
187
+ };
188
+ });
189
+ }
190
+ };
191
+ async function readBody(body) {
192
+ if (typeof body === "string") return body;
193
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
194
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
195
+ if (typeof Blob !== "undefined" && body instanceof Blob) return body.text();
196
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) return body.toString();
197
+ return void 0;
198
+ }
199
+ export {
200
+ ByokyServer
201
+ };
202
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import type {\n AuthMethod,\n RelayMessage,\n RelayHello,\n WebSocketLike,\n} from '@byoky/core';\nimport {\n ByokyError,\n parseRelayMessage,\n sendRelayMessage,\n WS_READY_STATE,\n} from '@byoky/core';\n\nexport interface ByokyServerOptions {\n /** Keepalive ping interval in ms. Default: 30000. Set to 0 to disable. */\n pingInterval?: number;\n /** Timeout waiting for relay:hello after connection. Default: 10000. */\n helloTimeout?: number;\n}\n\nexport interface ByokyClient {\n readonly sessionId: string;\n readonly providers: Record<string, { available: boolean; authMethod: AuthMethod }>;\n readonly connected: boolean;\n createFetch(providerId: string): typeof fetch;\n close(): void;\n onClose(callback: () => void): () => void;\n}\n\nexport class ByokyServer {\n private pingInterval: number;\n private helloTimeout: number;\n\n constructor(options: ByokyServerOptions = {}) {\n this.pingInterval = options.pingInterval ?? 30_000;\n this.helloTimeout = options.helloTimeout ?? 10_000;\n }\n\n handleConnection(ws: WebSocketLike): Promise<ByokyClient> {\n const self = this;\n return new Promise<ByokyClient>((resolve, reject) => {\n let sessionId = '';\n let providers: Record<string, { available: boolean; authMethod: AuthMethod }> = {};\n let connected = false;\n const closeCallbacks = new Set<() => void>();\n const pendingRequests = new Map<string, {\n resolveMeta: (value: { status: number; statusText: string; headers: Record<string, string> }) => void;\n pushChunk: (chunk: string) => void;\n done: () => void;\n error: (err: Error) => void;\n }>();\n\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let requestCounter = 0;\n\n const helloTimer = setTimeout(() => {\n reject(ByokyError.relayConnectionFailed('Timed out waiting for relay:hello'));\n ws.close(4000, 'Hello timeout');\n }, this.helloTimeout);\n\n ws.onmessage = (event: { data: unknown }) => {\n const msg = parseRelayMessage(event.data);\n if (!msg) return;\n\n switch (msg.type) {\n case 'relay:hello':\n handleHello(msg);\n break;\n case 'relay:response:meta': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n pending.resolveMeta({\n status: msg.status,\n statusText: msg.statusText,\n headers: msg.headers,\n });\n }\n break;\n }\n case 'relay:response:chunk': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) pending.pushChunk(msg.chunk);\n break;\n }\n case 'relay:response:done': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n pending.done();\n pendingRequests.delete(msg.requestId);\n }\n break;\n }\n case 'relay:response:error': {\n const pending = pendingRequests.get(msg.requestId);\n if (pending) {\n pending.error(new Error(msg.error.message));\n pendingRequests.delete(msg.requestId);\n }\n break;\n }\n case 'relay:ping':\n sendRelayMessage(ws, { type: 'relay:pong', ts: msg.ts });\n break;\n case 'relay:pong':\n break;\n }\n };\n\n ws.onclose = () => {\n cleanup();\n };\n\n ws.onerror = () => {\n cleanup();\n };\n\n function handleHello(msg: RelayHello) {\n clearTimeout(helloTimer);\n sessionId = msg.sessionId;\n providers = msg.providers;\n connected = true;\n\n // Start keepalive pings\n if (self.pingInterval > 0) {\n pingTimer = setInterval(() => {\n sendRelayMessage(ws, { type: 'relay:ping', ts: Date.now() });\n }, self.pingInterval);\n }\n\n resolve(client);\n }\n\n function cleanup() {\n if (!connected) return;\n connected = false;\n clearTimeout(helloTimer);\n if (pingTimer) clearInterval(pingTimer);\n\n // Reject all pending requests\n for (const pending of pendingRequests.values()) {\n pending.error(ByokyError.relayDisconnected());\n }\n pendingRequests.clear();\n\n for (const cb of closeCallbacks) cb();\n closeCallbacks.clear();\n }\n\n function createClientFetch(providerId: string): typeof fetch {\n return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n if (!connected) throw ByokyError.relayDisconnected();\n\n const url = typeof input === 'string' ? input\n : input instanceof URL ? input.toString()\n : (input as Request).url;\n\n const method = init?.method ?? 'GET';\n const headers = init?.headers\n ? Object.fromEntries(new Headers(init.headers).entries())\n : {};\n const body = init?.body ? await readBody(init.body) : undefined;\n\n const requestId = `relay-${++requestCounter}-${Date.now()}`;\n\n return new Promise<Response>((resolveFetch, rejectFetch) => {\n let metaResolved = false;\n let controller: ReadableStreamDefaultController<Uint8Array>;\n const encoder = new TextEncoder();\n\n const stream = new ReadableStream<Uint8Array>({\n start(c) { controller = c; },\n });\n\n pendingRequests.set(requestId, {\n resolveMeta: (meta) => {\n if (metaResolved) return;\n metaResolved = true;\n resolveFetch(new Response(stream, {\n status: meta.status,\n statusText: meta.statusText,\n headers: new Headers(meta.headers),\n }));\n },\n pushChunk: (chunk) => {\n try { controller.enqueue(encoder.encode(chunk)); } catch {}\n },\n done: () => {\n try { controller.close(); } catch {}\n },\n error: (err) => {\n try { controller.error(err); } catch {}\n if (!metaResolved) {\n metaResolved = true;\n rejectFetch(err);\n }\n },\n });\n\n // Send the request to the frontend\n sendRelayMessage(ws, {\n type: 'relay:request',\n requestId,\n providerId,\n url,\n method,\n headers,\n body,\n });\n });\n };\n }\n\n const client: ByokyClient = {\n get sessionId() { return sessionId; },\n get providers() { return providers; },\n get connected() { return connected; },\n createFetch: (providerId: string) => createClientFetch(providerId),\n close() {\n ws.close(1000, 'Server closed');\n cleanup();\n },\n onClose(callback: () => void) {\n closeCallbacks.add(callback);\n return () => { closeCallbacks.delete(callback); };\n },\n };\n\n });\n }\n}\n\nasync function readBody(body: BodyInit): Promise<string | undefined> {\n if (typeof body === 'string') return body;\n if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);\n if (body instanceof Uint8Array) return new TextDecoder().decode(body);\n if (typeof Blob !== 'undefined' && body instanceof Blob) return body.text();\n if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) return body.toString();\n return undefined;\n}\n\nexport type { WebSocketLike, RelayMessage, AuthMethod } from '@byoky/core';\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byoky/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -10,6 +10,11 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "require": "./dist/index.cjs"
13
+ },
14
+ "./server": {
15
+ "types": "./dist/server.d.ts",
16
+ "import": "./dist/server.js",
17
+ "require": "./dist/server.cjs"
13
18
  }
14
19
  },
15
20
  "files": ["dist", "README.md", "LICENSE"],