@cofy-x/axern-sdk 0.2.0-bootstrap.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 (92) hide show
  1. package/README.md +169 -0
  2. package/dist/client/index.d.ts +63 -0
  3. package/dist/client/index.js +213 -0
  4. package/dist/config/index.d.ts +15 -0
  5. package/dist/config/index.js +65 -0
  6. package/dist/errors/index.d.ts +46 -0
  7. package/dist/errors/index.js +123 -0
  8. package/dist/generated/proto.d.ts +10 -0
  9. package/dist/generated/proto.js +80 -0
  10. package/dist/index.d.ts +19 -0
  11. package/dist/index.js +13 -0
  12. package/dist/node/archive.d.ts +9 -0
  13. package/dist/node/archive.js +41 -0
  14. package/dist/node/attached_process.d.ts +10 -0
  15. package/dist/node/attached_process.js +60 -0
  16. package/dist/node/capabilities.d.ts +8 -0
  17. package/dist/node/capabilities.js +51 -0
  18. package/dist/node/client.d.ts +44 -0
  19. package/dist/node/client.js +96 -0
  20. package/dist/node/computer_use.d.ts +12 -0
  21. package/dist/node/computer_use.js +81 -0
  22. package/dist/node/context.d.ts +22 -0
  23. package/dist/node/context.js +37 -0
  24. package/dist/node/exec.d.ts +12 -0
  25. package/dist/node/exec.js +93 -0
  26. package/dist/node/files.d.ts +18 -0
  27. package/dist/node/files.js +101 -0
  28. package/dist/node/process.d.ts +36 -0
  29. package/dist/node/process.js +144 -0
  30. package/dist/node/streams.d.ts +8 -0
  31. package/dist/node/streams.js +70 -0
  32. package/dist/proto/axern/common/file/v1/file.proto +31 -0
  33. package/dist/proto/axern/control/admin/v1/allocation_lifecycle.proto +91 -0
  34. package/dist/proto/axern/control/admin/v1/audit.proto +53 -0
  35. package/dist/proto/axern/control/admin/v1/node.proto +49 -0
  36. package/dist/proto/axern/control/admin/v1/reliability.proto +184 -0
  37. package/dist/proto/axern/control/admin/v1/service.proto +18 -0
  38. package/dist/proto/axern/control/admin/v1/storage.proto +81 -0
  39. package/dist/proto/axern/control/agentprofile/v1/agent_profile.proto +169 -0
  40. package/dist/proto/axern/control/catalog/v1/catalog.proto +127 -0
  41. package/dist/proto/axern/control/common/v1/common.proto +171 -0
  42. package/dist/proto/axern/control/environment/v1/environment.proto +94 -0
  43. package/dist/proto/axern/control/function/v1/function.proto +130 -0
  44. package/dist/proto/axern/control/function/v1/function_types.proto +233 -0
  45. package/dist/proto/axern/control/gateway/v1/gateway.proto +60 -0
  46. package/dist/proto/axern/control/namespace/v1/namespace.proto +52 -0
  47. package/dist/proto/axern/control/node/v1/node_control.proto +302 -0
  48. package/dist/proto/axern/control/quota/v1/quota.proto +113 -0
  49. package/dist/proto/axern/control/rollout/v1/rollout.proto +344 -0
  50. package/dist/proto/axern/control/run/v1/run.proto +88 -0
  51. package/dist/proto/axern/control/secret/v1/secret.proto +75 -0
  52. package/dist/proto/axern/control/service/v1/service.proto +96 -0
  53. package/dist/proto/axern/control/service/v1/service_event.proto +47 -0
  54. package/dist/proto/axern/control/service/v1/service_replica.proto +74 -0
  55. package/dist/proto/axern/control/service/v1/service_types.proto +155 -0
  56. package/dist/proto/axern/control/storage/v1/storage.proto +107 -0
  57. package/dist/proto/axern/control/storage/v1/storage_types.proto +125 -0
  58. package/dist/proto/axern/control/tunnel/v1/tunnel.proto +195 -0
  59. package/dist/proto/axern/data/artifact/v1/artifact.proto +19 -0
  60. package/dist/proto/axern/node/sandbox/v1/node.proto +652 -0
  61. package/dist/proto/axern/tunnel/v1/tunnel.proto +50 -0
  62. package/dist/resources.d.ts +13 -0
  63. package/dist/resources.js +107 -0
  64. package/dist/sandbox/archive.d.ts +8 -0
  65. package/dist/sandbox/archive.js +228 -0
  66. package/dist/sandbox/index.d.ts +86 -0
  67. package/dist/sandbox/index.js +204 -0
  68. package/dist/sandbox/lifecycle.d.ts +11 -0
  69. package/dist/sandbox/lifecycle.js +57 -0
  70. package/dist/tunnel/connector.d.ts +34 -0
  71. package/dist/tunnel/connector.js +141 -0
  72. package/dist/tunnel/control.d.ts +22 -0
  73. package/dist/tunnel/control.js +91 -0
  74. package/dist/tunnel/debug.d.ts +6 -0
  75. package/dist/tunnel/debug.js +19 -0
  76. package/dist/tunnel/relay.d.ts +17 -0
  77. package/dist/tunnel/relay.js +41 -0
  78. package/dist/tunnel/runtime.d.ts +34 -0
  79. package/dist/tunnel/runtime.js +118 -0
  80. package/dist/tunnel/session.d.ts +25 -0
  81. package/dist/tunnel/session.js +95 -0
  82. package/dist/tunnel/target.d.ts +7 -0
  83. package/dist/tunnel/target.js +28 -0
  84. package/dist/tunnel/types.d.ts +25 -0
  85. package/dist/tunnel/types.js +6 -0
  86. package/dist/types.d.ts +219 -0
  87. package/dist/types.js +8 -0
  88. package/dist/validation.d.ts +10 -0
  89. package/dist/validation.js +37 -0
  90. package/dist/version.d.ts +7 -0
  91. package/dist/version.js +9 -0
  92. package/package.json +57 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { SandboxTimeoutError, SandboxValidationError } from "../errors/index.js";
7
+ export const defaultSandboxArgv = ["/bin/sh", "-lc", "sleep infinity"];
8
+ export function validateSandboxOptions(options) {
9
+ const sourceCount = [options.templateId, options.image, options.environmentId].filter((source) => source !== undefined && source !== "").length;
10
+ if (sourceCount !== 1) {
11
+ throw new SandboxValidationError("exactly one of templateId, image, or environmentId is required");
12
+ }
13
+ }
14
+ export function sandboxLabels(labels) {
15
+ return {
16
+ "axern.sdk": "typescript",
17
+ ...(labels ?? {}),
18
+ };
19
+ }
20
+ export function sandboxMetadata(options, state) {
21
+ return {
22
+ ...state,
23
+ namespace: options.namespace ?? "default",
24
+ runtimeClass: options.runtimeClass ?? "",
25
+ labels: sandboxLabels(options.labels),
26
+ source: sandboxSource(options),
27
+ };
28
+ }
29
+ export async function waitReadyReplica(serviceId, timeoutMs, listReplicas) {
30
+ const deadline = Date.now() + timeoutMs;
31
+ let lastReplicas = [];
32
+ while (Date.now() < deadline) {
33
+ lastReplicas = await listReplicas(serviceId);
34
+ const candidate = lastReplicas.find((replica) => replica.ready === true &&
35
+ replica.ended !== true &&
36
+ replica.outdated !== true &&
37
+ Number(replica.status ?? 0) === 4);
38
+ if (candidate !== undefined) {
39
+ return candidate;
40
+ }
41
+ await sleep(2_000);
42
+ }
43
+ const details = lastReplicas.map((replica) => `${String(replica.id ?? "")}:${String(replica.status ?? "")}`).join(", ");
44
+ throw new SandboxTimeoutError(`service ${serviceId} did not produce a ready sandbox replica: ${details}`);
45
+ }
46
+ function sandboxSource(options) {
47
+ if (options.templateId !== undefined && options.templateId !== "") {
48
+ return "template";
49
+ }
50
+ if (options.image !== undefined && options.image !== "") {
51
+ return "image";
52
+ }
53
+ return "environment";
54
+ }
55
+ function sleep(ms) {
56
+ return new Promise((resolve) => setTimeout(resolve, ms));
57
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { TunnelConnectorOptions } from "../types.js";
7
+ import type { GatewayTransportOptions } from "./relay.js";
8
+ import type { TunnelSession } from "./types.js";
9
+ export interface TunnelConnectorConfig {
10
+ session: TunnelSession;
11
+ clientToken: string;
12
+ upstream: string;
13
+ transport?: GatewayTransportOptions;
14
+ connector?: TunnelConnectorOptions;
15
+ }
16
+ export declare class TunnelConnector {
17
+ private readonly config;
18
+ private stopped;
19
+ private activeCall?;
20
+ private activeClient?;
21
+ private activeSession?;
22
+ private reconnecting;
23
+ private relayTarget;
24
+ private upstreamTarget?;
25
+ constructor(config: TunnelConnectorConfig);
26
+ start(): Promise<void>;
27
+ stop(): Promise<void>;
28
+ private connectOnce;
29
+ private handleRelayClosed;
30
+ private reconnectLoop;
31
+ private heartbeat;
32
+ private closeActiveRelay;
33
+ private requiredUpstreamTarget;
34
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { serviceConstructor } from "../generated/proto.js";
7
+ import { debugFrame } from "./debug.js";
8
+ import { isTerminalRelayError, relayChannelOptions, relayCredentials } from "./relay.js";
9
+ import { TunnelRelaySession } from "./session.js";
10
+ import { parseTcpTarget } from "./target.js";
11
+ const defaultPingIntervalMs = 15_000;
12
+ const defaultDialTimeoutMs = 5_000;
13
+ const defaultMaxStreams = 128;
14
+ const initialReconnectBackoffMs = 1_000;
15
+ const maxReconnectBackoffMs = 10_000;
16
+ export class TunnelConnector {
17
+ config;
18
+ stopped = false;
19
+ activeCall;
20
+ activeClient;
21
+ activeSession;
22
+ reconnecting = false;
23
+ relayTarget = "";
24
+ upstreamTarget;
25
+ constructor(config) {
26
+ this.config = config;
27
+ }
28
+ async start() {
29
+ const target = this.config.session.client_edge_target || this.config.session.edge_target;
30
+ if (target === undefined || target === "") {
31
+ throw new Error(`tunnel session ${this.config.session.session_id ?? ""} has no relay target`);
32
+ }
33
+ this.upstreamTarget = parseTcpTarget(this.config.upstream);
34
+ this.relayTarget = target;
35
+ this.heartbeat();
36
+ await this.connectOnce();
37
+ }
38
+ async stop() {
39
+ if (this.stopped) {
40
+ return;
41
+ }
42
+ this.stopped = true;
43
+ this.closeActiveRelay();
44
+ }
45
+ async connectOnce() {
46
+ const TunnelRelay = serviceConstructor(["axern", "tunnel", "v1", "TunnelRelay"]);
47
+ const client = new TunnelRelay(this.relayTarget, relayCredentials(this.config.transport), relayChannelOptions(this.config.transport));
48
+ const call = client
49
+ .ConnectPeer();
50
+ const session = new TunnelRelaySession(call, {
51
+ upstreamTarget: this.requiredUpstreamTarget(),
52
+ dialTimeoutMs: this.config.connector?.dialTimeoutMs ?? defaultDialTimeoutMs,
53
+ maxStreams: this.config.connector?.maxStreams ?? defaultMaxStreams,
54
+ });
55
+ this.activeClient = client;
56
+ this.activeCall = call;
57
+ this.activeSession = session;
58
+ call.on("data", (frame) => {
59
+ debugFrame("recv", frame);
60
+ session.handleFrame(frame);
61
+ });
62
+ call.on("error", (error) => this.handleRelayClosed(error));
63
+ call.on("end", () => this.handleRelayClosed());
64
+ session.send({
65
+ peer_open: {
66
+ session_id: this.config.session.session_id ?? "",
67
+ peer_kind: 1,
68
+ token: this.config.clientToken,
69
+ },
70
+ });
71
+ }
72
+ handleRelayClosed(error) {
73
+ if (this.stopped || this.reconnecting) {
74
+ return;
75
+ }
76
+ this.closeActiveRelay();
77
+ if (error !== undefined && isTerminalRelayError(error)) {
78
+ this.stopped = true;
79
+ return;
80
+ }
81
+ this.reconnecting = true;
82
+ void this.reconnectLoop();
83
+ }
84
+ async reconnectLoop() {
85
+ let backoffMs = initialReconnectBackoffMs;
86
+ while (!this.stopped) {
87
+ await sleep(backoffMs);
88
+ if (this.stopped) {
89
+ break;
90
+ }
91
+ try {
92
+ await this.connectOnce();
93
+ this.reconnecting = false;
94
+ return;
95
+ }
96
+ catch (error) {
97
+ if (isTerminalRelayError(error)) {
98
+ this.stopped = true;
99
+ break;
100
+ }
101
+ }
102
+ backoffMs = Math.min(maxReconnectBackoffMs, backoffMs * 2);
103
+ }
104
+ this.reconnecting = false;
105
+ }
106
+ heartbeat() {
107
+ const interval = this.config.connector?.pingIntervalMs ?? defaultPingIntervalMs;
108
+ if (interval <= 0) {
109
+ return;
110
+ }
111
+ const timer = setInterval(() => {
112
+ if (this.stopped) {
113
+ clearInterval(timer);
114
+ return;
115
+ }
116
+ this.activeSession?.send({ ping: { id: String(Date.now()) } });
117
+ }, interval);
118
+ timer.unref();
119
+ }
120
+ closeActiveRelay() {
121
+ const call = this.activeCall;
122
+ const client = this.activeClient;
123
+ const session = this.activeSession;
124
+ this.activeCall = undefined;
125
+ this.activeClient = undefined;
126
+ this.activeSession = undefined;
127
+ call?.removeAllListeners();
128
+ session?.closeAll();
129
+ call?.end();
130
+ client?.close();
131
+ }
132
+ requiredUpstreamTarget() {
133
+ if (this.upstreamTarget === undefined) {
134
+ throw new Error("tunnel upstream target is not initialized");
135
+ }
136
+ return this.upstreamTarget;
137
+ }
138
+ }
139
+ function sleep(ms) {
140
+ return new Promise((resolve) => setTimeout(resolve, ms));
141
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type * as grpc from "@grpc/grpc-js";
7
+ import type { CreateTunnelSessionResult, TunnelSession } from "./types.js";
8
+ export declare class TunnelControlClient {
9
+ private readonly client;
10
+ constructor(client: grpc.Client);
11
+ createSession(options: {
12
+ allocationId: string;
13
+ upstream: string;
14
+ proxyPort: number;
15
+ ttlSeconds: number;
16
+ readyTimeoutMs: number;
17
+ }): Promise<CreateTunnelSessionResult>;
18
+ getSession(sessionId: string): Promise<TunnelSession>;
19
+ listEvents(sessionId: string, limit?: number): Promise<Record<string, unknown>[]>;
20
+ renewSession(sessionId: string, clientToken: string, ttlSeconds: number): Promise<TunnelSession>;
21
+ revokeSession(sessionId: string, reason?: string): Promise<void>;
22
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { mapRpcError } from "../errors/index.js";
7
+ import { unary } from "../generated/proto.js";
8
+ import { required } from "../validation.js";
9
+ export class TunnelControlClient {
10
+ client;
11
+ constructor(client) {
12
+ this.client = client;
13
+ }
14
+ async createSession(options) {
15
+ try {
16
+ const response = await unary(this.client, "CreateTunnelSession", {
17
+ allocation_id: required("allocationId", options.allocationId),
18
+ remote_port: options.proxyPort,
19
+ local_target: required("upstream", options.upstream),
20
+ ttl: duration(options.ttlSeconds * 1000),
21
+ wait_ready: true,
22
+ ready_timeout: duration(options.readyTimeoutMs),
23
+ });
24
+ if (response.session === undefined) {
25
+ throw new Error("create tunnel session returned no session");
26
+ }
27
+ return {
28
+ session: response.session,
29
+ clientToken: response.client_token ?? "",
30
+ };
31
+ }
32
+ catch (error) {
33
+ throw mapRpcError(error, "create tunnel session", options.allocationId);
34
+ }
35
+ }
36
+ async getSession(sessionId) {
37
+ try {
38
+ const response = await unary(this.client, "GetTunnelSession", { session_id: required("sessionId", sessionId) });
39
+ if (response.session === undefined) {
40
+ throw new Error("get tunnel session returned no session");
41
+ }
42
+ return response.session;
43
+ }
44
+ catch (error) {
45
+ throw mapRpcError(error, "get tunnel session");
46
+ }
47
+ }
48
+ async listEvents(sessionId, limit = 30) {
49
+ try {
50
+ const response = await unary(this.client, "ListTunnelSessionEvents", { session_id: required("sessionId", sessionId), limit });
51
+ return response.events ?? [];
52
+ }
53
+ catch (error) {
54
+ throw mapRpcError(error, "list tunnel session events");
55
+ }
56
+ }
57
+ async renewSession(sessionId, clientToken, ttlSeconds) {
58
+ try {
59
+ const response = await unary(this.client, "RenewTunnelSession", {
60
+ session_id: required("sessionId", sessionId),
61
+ client_token: clientToken,
62
+ ttl: duration(ttlSeconds * 1000),
63
+ });
64
+ if (response.session === undefined) {
65
+ throw new Error("renew tunnel session returned no session");
66
+ }
67
+ return response.session;
68
+ }
69
+ catch (error) {
70
+ throw mapRpcError(error, "renew tunnel session");
71
+ }
72
+ }
73
+ async revokeSession(sessionId, reason = "sdk close") {
74
+ try {
75
+ await unary(this.client, "RevokeTunnelSession", {
76
+ session_id: required("sessionId", sessionId),
77
+ reason,
78
+ });
79
+ }
80
+ catch (error) {
81
+ throw mapRpcError(error, "revoke tunnel session");
82
+ }
83
+ }
84
+ }
85
+ function duration(ms) {
86
+ const seconds = Math.floor(ms / 1000);
87
+ return {
88
+ seconds,
89
+ nanos: Math.floor((ms - seconds * 1000) * 1_000_000),
90
+ };
91
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export declare function debugFrame(direction: string, frame: Record<string, unknown>): void;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ export function debugFrame(direction, frame) {
7
+ if (process.env.AXERN_TS_TUNNEL_DEBUG !== "1") {
8
+ return;
9
+ }
10
+ const summary = { ...frame };
11
+ if ("stream_data" in summary) {
12
+ const data = summary.stream_data;
13
+ summary.stream_data = {
14
+ ...data,
15
+ data: Buffer.from(data.data ?? []).length,
16
+ };
17
+ }
18
+ process.stderr.write(`[axern-ts-tunnel] ${direction} ${JSON.stringify(summary)}\n`);
19
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as grpc from "@grpc/grpc-js";
7
+ export interface GatewayTransportOptions {
8
+ insecure?: boolean;
9
+ tlsCaCert?: string;
10
+ tlsCert?: string;
11
+ tlsKey?: string;
12
+ serverName?: string;
13
+ proxyMode?: "env" | "direct";
14
+ }
15
+ export declare function relayCredentials(options: GatewayTransportOptions | undefined): grpc.ChannelCredentials;
16
+ export declare function relayChannelOptions(options: GatewayTransportOptions | undefined): grpc.ChannelOptions;
17
+ export declare function isTerminalRelayError(error: unknown): boolean;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as grpc from "@grpc/grpc-js";
7
+ import { readFileSync } from "node:fs";
8
+ export function relayCredentials(options) {
9
+ if (options?.insecure === true) {
10
+ return grpc.credentials.createInsecure();
11
+ }
12
+ const rootCerts = options?.tlsCaCert === undefined || options.tlsCaCert === ""
13
+ ? undefined
14
+ : readFileSync(options.tlsCaCert);
15
+ const privateKey = options?.tlsKey === undefined || options.tlsKey === ""
16
+ ? undefined
17
+ : readFileSync(options.tlsKey);
18
+ const certChain = options?.tlsCert === undefined || options.tlsCert === ""
19
+ ? undefined
20
+ : readFileSync(options.tlsCert);
21
+ return grpc.credentials.createSsl(rootCerts, privateKey, certChain);
22
+ }
23
+ export function relayChannelOptions(options) {
24
+ const channelOptions = {};
25
+ if (options?.serverName !== undefined && options.serverName !== "") {
26
+ channelOptions["grpc.ssl_target_name_override"] = options.serverName;
27
+ }
28
+ if (options?.proxyMode === "direct") {
29
+ channelOptions["grpc.enable_http_proxy"] = 0;
30
+ }
31
+ return channelOptions;
32
+ }
33
+ export function isTerminalRelayError(error) {
34
+ if (typeof error !== "object" || error === null || !("code" in error)) {
35
+ return false;
36
+ }
37
+ const code = Number(error.code);
38
+ return code === grpc.status.PERMISSION_DENIED ||
39
+ code === grpc.status.UNAUTHENTICATED ||
40
+ code === grpc.status.NOT_FOUND;
41
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type { TunnelOptions, TunnelMetadata } from "../types.js";
7
+ import type { TunnelConnectorConfig } from "./connector.js";
8
+ import type { GatewayTransportOptions } from "./relay.js";
9
+ import type { CreateTunnelSessionResult, TunnelRuntime, TunnelSession } from "./types.js";
10
+ export interface TunnelConnectorRunner {
11
+ start(): Promise<void>;
12
+ stop(): Promise<void>;
13
+ }
14
+ export interface TunnelControl {
15
+ createSession(options: {
16
+ allocationId: string;
17
+ upstream: string;
18
+ proxyPort: number;
19
+ ttlSeconds: number;
20
+ readyTimeoutMs: number;
21
+ }): Promise<CreateTunnelSessionResult>;
22
+ getSession(sessionId: string): Promise<TunnelSession>;
23
+ listEvents(sessionId: string, limit?: number): Promise<Record<string, unknown>[]>;
24
+ renewSession(sessionId: string, clientToken: string, ttlSeconds: number): Promise<TunnelSession>;
25
+ revokeSession(sessionId: string, reason?: string): Promise<void>;
26
+ }
27
+ export declare function startTunnelRuntime(options: {
28
+ control: TunnelControl;
29
+ allocationId: string;
30
+ tunnel: TunnelOptions;
31
+ transport: GatewayTransportOptions;
32
+ connectorFactory?: (config: TunnelConnectorConfig) => TunnelConnectorRunner;
33
+ }): Promise<TunnelRuntime>;
34
+ export declare function tunnelMetadata(runtime: TunnelRuntime | undefined): TunnelMetadata | undefined;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { SandboxValidationError } from "../errors/index.js";
7
+ import { TunnelConnector } from "./connector.js";
8
+ import { parseTcpTarget } from "./target.js";
9
+ const defaultProxyPort = 8786;
10
+ const defaultTtlSeconds = 300;
11
+ const defaultReadyTimeoutMs = 30_000;
12
+ export async function startTunnelRuntime(options) {
13
+ validateTunnelOptions(options.tunnel);
14
+ const ttlSeconds = options.tunnel.ttlSeconds ?? defaultTtlSeconds;
15
+ const proxyPort = options.tunnel.proxyPort ?? defaultProxyPort;
16
+ const result = await options.control.createSession({
17
+ allocationId: options.allocationId,
18
+ upstream: options.tunnel.upstream,
19
+ proxyPort,
20
+ ttlSeconds,
21
+ readyTimeoutMs: options.tunnel.readyTimeoutMs ?? defaultReadyTimeoutMs,
22
+ });
23
+ const sessionId = result.session.session_id ?? "";
24
+ if (sessionId === "") {
25
+ throw new Error("create tunnel session returned no session id");
26
+ }
27
+ if (result.clientToken === "") {
28
+ throw new Error(`tunnel session ${sessionId} returned no client token`);
29
+ }
30
+ const connectorConfig = {
31
+ session: result.session,
32
+ clientToken: result.clientToken,
33
+ upstream: options.tunnel.upstream,
34
+ transport: options.transport,
35
+ connector: options.tunnel.connector,
36
+ };
37
+ const connector = (options.connectorFactory ?? ((config) => new TunnelConnector(config)))(connectorConfig);
38
+ let timer;
39
+ try {
40
+ await connector.start();
41
+ await waitClientConnected(options.control, sessionId, options.tunnel.readyTimeoutMs ?? defaultReadyTimeoutMs);
42
+ const session = await options.control.getSession(sessionId);
43
+ const boundAddr = session.bound_addr ?? "";
44
+ if (boundAddr === "") {
45
+ throw new Error(`tunnel session ${sessionId} has no bound address`);
46
+ }
47
+ const renewEveryMs = options.tunnel.renewEveryMs ?? Math.max(30_000, Math.floor((ttlSeconds * 1000) / 2));
48
+ timer = setInterval(() => {
49
+ void options.control.renewSession(sessionId, result.clientToken, ttlSeconds).catch(() => undefined);
50
+ }, renewEveryMs);
51
+ timer.unref();
52
+ return {
53
+ sessionId,
54
+ clientToken: result.clientToken,
55
+ boundAddr,
56
+ upstream: options.tunnel.upstream,
57
+ proxyPort,
58
+ async stop() {
59
+ if (timer !== undefined) {
60
+ clearInterval(timer);
61
+ timer = undefined;
62
+ }
63
+ await connector.stop();
64
+ await options.control.revokeSession(sessionId).catch(() => undefined);
65
+ },
66
+ };
67
+ }
68
+ catch (error) {
69
+ if (timer !== undefined) {
70
+ clearInterval(timer);
71
+ }
72
+ await connector.stop().catch(() => undefined);
73
+ if (sessionId !== "") {
74
+ await options.control.revokeSession(sessionId).catch(() => undefined);
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+ export function tunnelMetadata(runtime) {
80
+ if (runtime === undefined) {
81
+ return undefined;
82
+ }
83
+ return {
84
+ sessionId: runtime.sessionId,
85
+ boundAddr: runtime.boundAddr,
86
+ upstream: runtime.upstream,
87
+ proxyPort: runtime.proxyPort,
88
+ };
89
+ }
90
+ function validateTunnelOptions(options) {
91
+ if (options.upstream.trim() === "") {
92
+ throw new SandboxValidationError("tunnel.upstream is required");
93
+ }
94
+ parseTcpTarget(options.upstream);
95
+ if (options.proxyPort !== undefined && options.proxyPort < 0) {
96
+ throw new SandboxValidationError("tunnel.proxyPort must be non-negative");
97
+ }
98
+ if (options.ttlSeconds !== undefined && options.ttlSeconds < 0) {
99
+ throw new SandboxValidationError("tunnel.ttlSeconds must be non-negative");
100
+ }
101
+ if (options.readyTimeoutMs !== undefined && options.readyTimeoutMs < 0) {
102
+ throw new SandboxValidationError("tunnel.readyTimeoutMs must be non-negative");
103
+ }
104
+ }
105
+ async function waitClientConnected(control, sessionId, timeoutMs) {
106
+ const deadline = Date.now() + timeoutMs;
107
+ while (Date.now() < deadline) {
108
+ const events = await control.listEvents(sessionId, 30);
109
+ if (events.some((event) => Number(event.event_type ?? 0) === 6)) {
110
+ return;
111
+ }
112
+ await sleep(500);
113
+ }
114
+ throw new Error(`tunnel session ${sessionId} did not connect client peer within ${timeoutMs}ms`);
115
+ }
116
+ function sleep(ms) {
117
+ return new Promise((resolve) => setTimeout(resolve, ms));
118
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 cofy-x
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import type * as grpc from "@grpc/grpc-js";
7
+ import net from "node:net";
8
+ export interface TunnelRelaySessionOptions {
9
+ upstreamTarget: net.NetConnectOpts;
10
+ dialTimeoutMs?: number;
11
+ maxStreams?: number;
12
+ }
13
+ export declare class TunnelRelaySession {
14
+ private readonly call;
15
+ private readonly options;
16
+ private readonly streams;
17
+ constructor(call: grpc.ClientDuplexStream<Record<string, unknown>, Record<string, unknown>>, options: TunnelRelaySessionOptions);
18
+ handleFrame(frame: Record<string, unknown>): void;
19
+ send(frame: Record<string, unknown>): void;
20
+ closeAll(): void;
21
+ private openLocal;
22
+ private writeLocal;
23
+ private closeLocal;
24
+ private sendClose;
25
+ }