@ts-edge/cloudflare 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.
@@ -0,0 +1,52 @@
1
+ import "@ts-edge/wasm/wasm_exec.js";
2
+ import { type TailscaleNode } from "@ts-edge/wasm";
3
+ export type { TailscaleNode, TailnetSocket, NodeState, NodeConfig, ServeHandler, Server, Peer, } from "@ts-edge/wasm";
4
+ export interface CloudflareNodeConfig {
5
+ /** `import wasm from "@ts-edge/wasm/node.wasm"` — a CompiledWasm module. */
6
+ wasm: WebAssembly.Module;
7
+ authKey: string;
8
+ hostname?: string;
9
+ controlURL?: string;
10
+ }
11
+ /**
12
+ * Create the node and start it. Returns as soon as login is kicked off; await
13
+ * tailNode.waitUntilRunning() for readiness.
14
+ */
15
+ export declare function createCloudflareNode(config: CloudflareNodeConfig): Promise<TailscaleNode>;
16
+ /**
17
+ * A Durable Object base class that hosts one warm tailNode. Extend it and
18
+ * implement getConfig(); the node is created lazily and kept alive until
19
+ * stopTailscaleNode() (its control/DERP WebSockets stay open across requests,
20
+ * which also pins the DO in memory — billed wall-clock duration). Override
21
+ * fetch() to route requests; call this.getRunningTailscaleNode() to obtain
22
+ * the started, Running tailNode.
23
+ */
24
+ export declare abstract class TailscaleNodeDO<Env = unknown> implements DurableObject {
25
+ #private;
26
+ protected ctx: DurableObjectState;
27
+ protected env: Env;
28
+ protected tailscaleNode: TailscaleNode | undefined;
29
+ constructor(ctx: DurableObjectState, env: Env);
30
+ /** Supply the node configuration (typically from env). */
31
+ protected abstract getConfig(): CloudflareNodeConfig | Promise<CloudflareNodeConfig>;
32
+ /**
33
+ * Optional setup hook, called once per node between creation and start() —
34
+ * attach event listeners (tailNode.on) and register servers (tailNode.serve /
35
+ * tailNode.listen) here, while the node is still offline, so no early state
36
+ * transition or inbound connection is missed.
37
+ */
38
+ protected beforeStart?(tailNode: TailscaleNode): void | Promise<void>;
39
+ /** Start the node once and return it after it reaches Running. */
40
+ protected getRunningTailscaleNode(opts?: {
41
+ timeoutMs?: number;
42
+ }): Promise<TailscaleNode>;
43
+ /**
44
+ * Fully stop the node, closing its control/DERP WebSockets. With no outgoing
45
+ * sockets left the DO can hibernate or be evicted, so duration billing stops.
46
+ * The node was ephemeral: the next getRunningTailscaleNode() creates a
47
+ * fresh one (new login, beforeStart runs again). Safe to call at any point,
48
+ * including mid-start.
49
+ */
50
+ protected stopTailscaleNode(): Promise<void>;
51
+ fetch(_request: Request): Promise<Response>;
52
+ }
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ // Copyright (c) 2026 Sam Jean
2
+ // SPDX-License-Identifier: BSD-3-Clause
3
+ // @ts-edge/cloudflare — run a WASM ephemeral Tailscale node inside a Cloudflare
4
+ // Worker. It supplies globalThis.Go (Go's wasm_exec), which workerd lacks, then
5
+ // wraps @ts-edge/wasm. The DERP/control transport uses workerd's native
6
+ // `new WebSocket(url)` client constructor directly.
7
+ import "@ts-edge/wasm/wasm_exec.js"; // side effect: installs globalThis.Go
8
+ import { createNode } from "@ts-edge/wasm";
9
+ /**
10
+ * Create the node and start it. Returns as soon as login is kicked off; await
11
+ * tailNode.waitUntilRunning() for readiness.
12
+ */
13
+ export async function createCloudflareNode(config) {
14
+ const tailNode = await createNode(config);
15
+ await tailNode.start();
16
+ return tailNode;
17
+ }
18
+ /**
19
+ * A Durable Object base class that hosts one warm tailNode. Extend it and
20
+ * implement getConfig(); the node is created lazily and kept alive until
21
+ * stopTailscaleNode() (its control/DERP WebSockets stay open across requests,
22
+ * which also pins the DO in memory — billed wall-clock duration). Override
23
+ * fetch() to route requests; call this.getRunningTailscaleNode() to obtain
24
+ * the started, Running tailNode.
25
+ */
26
+ export class TailscaleNodeDO {
27
+ ctx;
28
+ env;
29
+ tailscaleNode;
30
+ #starting;
31
+ constructor(ctx, env) {
32
+ this.ctx = ctx;
33
+ this.env = env;
34
+ }
35
+ /** Start the node once and return it after it reaches Running. */
36
+ async getRunningTailscaleNode(opts = {}) {
37
+ if (!this.#starting) {
38
+ this.#starting = (async () => {
39
+ const tailNode = await createNode(await this.getConfig());
40
+ this.tailscaleNode = tailNode;
41
+ if (this.beforeStart)
42
+ await this.beforeStart(tailNode);
43
+ await tailNode.start();
44
+ return tailNode;
45
+ })();
46
+ }
47
+ const tailNode = await this.#starting;
48
+ await tailNode.waitUntilRunning(opts);
49
+ return tailNode;
50
+ }
51
+ /**
52
+ * Fully stop the node, closing its control/DERP WebSockets. With no outgoing
53
+ * sockets left the DO can hibernate or be evicted, so duration billing stops.
54
+ * The node was ephemeral: the next getRunningTailscaleNode() creates a
55
+ * fresh one (new login, beforeStart runs again). Safe to call at any point,
56
+ * including mid-start.
57
+ */
58
+ async stopTailscaleNode() {
59
+ const starting = this.#starting;
60
+ this.#starting = undefined;
61
+ this.tailscaleNode = undefined;
62
+ if (!starting)
63
+ return;
64
+ const tailNode = await starting.catch(() => undefined);
65
+ await tailNode?.stop();
66
+ }
67
+ async fetch(_request) {
68
+ const tailNode = await this.getRunningTailscaleNode();
69
+ return new Response(`ts-edge node running: ${tailNode.ipv4 ?? "(no addr)"}\n`);
70
+ }
71
+ }
72
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,wCAAwC;AAExC,gFAAgF;AAChF,gFAAgF;AAChF,wEAAwE;AACxE,oDAAoD;AAEpD,OAAO,4BAA4B,CAAC,CAAC,sCAAsC;AAC3E,OAAO,EAAE,UAAU,EAAsB,MAAM,eAAe,CAAC;AAmB/D;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAA4B;IAE5B,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;IACvB,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAgB,eAAe;IAKvB;IACA;IALF,aAAa,CAA4B;IACnD,SAAS,CAAqC;IAE9C,YACY,GAAuB,EACvB,GAAQ;QADR,QAAG,GAAH,GAAG,CAAoB;QACvB,QAAG,GAAH,GAAG,CAAK;IACjB,CAAC;IAaJ,kEAAkE;IACxD,KAAK,CAAC,uBAAuB,CAAC,OAA+B,EAAE;QACvE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,CAAC,KAAK,IAAI,EAAE;gBAC3B,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;gBAC1D,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;gBAC9B,IAAI,IAAI,CAAC,WAAW;oBAAE,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;gBACvD,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;gBACvB,OAAO,QAAQ,CAAC;YAClB,CAAC,CAAC,EAAE,CAAC;QACP,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC;QACtC,MAAM,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QACtC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;;;;OAMG;IACO,KAAK,CAAC,iBAAiB;QAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,aAAa,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvD,MAAM,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,QAAiB;QAC3B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,uBAAuB,EAAE,CAAC;QACtD,OAAO,IAAI,QAAQ,CAAC,yBAAyB,QAAQ,CAAC,IAAI,IAAI,WAAW,IAAI,CAAC,CAAC;IACjF,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ts-edge/cloudflare",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Run a WASM ephemeral Tailscale node inside a Cloudflare Worker / Durable Object.",
6
+ "license": "BSD-3-Clause",
7
+ "author": "Sam Jean",
8
+ "files": ["dist"],
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "prepare": "tsc -p tsconfig.json",
18
+ "typecheck": "tsc -p tsconfig.json --noEmit"
19
+ },
20
+ "dependencies": {
21
+ "@ts-edge/wasm": "workspace:^"
22
+ },
23
+ "devDependencies": {
24
+ "@cloudflare/workers-types": "^4.20260702.1",
25
+ "typescript": "^6.0.3"
26
+ }
27
+ }