@floegence/flowersec-core 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/dist/browser/controlplane.d.ts +5 -30
- package/dist/browser/controlplane.js +5 -112
- package/dist/browser/reconnectConfig.d.ts +7 -8
- package/dist/browser/reconnectConfig.js +4 -29
- package/dist/client-connect/tunnelAttachCloseReason.d.ts +1 -1
- package/dist/client-connect/tunnelAttachCloseReason.js +4 -1
- package/dist/controlplane/index.d.ts +2 -0
- package/dist/controlplane/index.js +1 -0
- package/dist/controlplane/request.d.ts +50 -0
- package/dist/controlplane/request.js +195 -0
- package/dist/e2ee/secureChannel.js +2 -5
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +1 -0
- package/dist/node/reconnectConfig.d.ts +42 -0
- package/dist/node/reconnectConfig.js +78 -0
- package/dist/observability/observer.d.ts +1 -1
- package/dist/observability/observer.js +34 -7
- package/dist/proxy/bootstrap.d.ts +19 -1
- package/dist/proxy/bootstrap.js +48 -5
- package/dist/proxy/cookieJar.d.ts +3 -2
- package/dist/proxy/cookieJar.js +68 -20
- package/dist/proxy/index.d.ts +1 -0
- package/dist/proxy/index.js +1 -0
- package/dist/proxy/runtime.js +2 -2
- package/dist/proxy/runtimeScope.d.ts +49 -0
- package/dist/proxy/runtimeScope.js +223 -0
- package/dist/proxy/serviceWorker.js +5 -10
- package/dist/reconnect/artifactControlplane.d.ts +13 -0
- package/dist/reconnect/artifactControlplane.js +34 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.js +3 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @floegence/flowersec-core
|
|
2
2
|
|
|
3
|
-
Flowersec core TypeScript library for building
|
|
3
|
+
Flowersec core TypeScript library for building end-to-end encrypted, multiplexed connections over WebSocket in browsers and Node.js.
|
|
4
4
|
|
|
5
5
|
Status: experimental; not audited.
|
|
6
6
|
|
|
@@ -10,12 +10,13 @@ Status: experimental; not audited.
|
|
|
10
10
|
npm install @floegence/flowersec-core
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Recommended usage
|
|
14
14
|
|
|
15
|
-
Browser
|
|
15
|
+
Browser:
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { connectBrowser
|
|
18
|
+
import { connectBrowser } from "@floegence/flowersec-core/browser";
|
|
19
|
+
import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
|
|
19
20
|
|
|
20
21
|
const artifact = await requestConnectArtifact({
|
|
21
22
|
endpointId: "env_demo",
|
|
@@ -26,27 +27,41 @@ await client.ping();
|
|
|
26
27
|
client.close();
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
Node.js
|
|
30
|
+
Node.js:
|
|
30
31
|
|
|
31
32
|
```ts
|
|
32
|
-
import { connectNode } from "@floegence/flowersec-core/node";
|
|
33
|
+
import { connectNode, createNodeReconnectConfig } from "@floegence/flowersec-core/node";
|
|
34
|
+
import { requestConnectArtifact } from "@floegence/flowersec-core/controlplane";
|
|
33
35
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}).then((r) => r.json());
|
|
36
|
+
const artifact = await requestConnectArtifact({
|
|
37
|
+
baseUrl: "https://your-app.example/api/flowersec",
|
|
38
|
+
endpointId: "env_demo",
|
|
39
|
+
});
|
|
39
40
|
|
|
40
|
-
const client = await connectNode(
|
|
41
|
+
const client = await connectNode(artifact, {
|
|
41
42
|
origin: "https://your-app.example",
|
|
42
43
|
});
|
|
43
44
|
await client.ping();
|
|
44
45
|
client.close();
|
|
46
|
+
|
|
47
|
+
const reconnectConfig = createNodeReconnectConfig({
|
|
48
|
+
artifactControlplane: {
|
|
49
|
+
baseUrl: "https://your-app.example/api/flowersec",
|
|
50
|
+
endpointId: "env_demo",
|
|
51
|
+
},
|
|
52
|
+
connect: {
|
|
53
|
+
origin: "https://your-app.example",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
45
56
|
```
|
|
46
57
|
|
|
58
|
+
Browser `requestConnectArtifact(...)`, `requestEntryConnectArtifact(...)`, and `ControlplaneRequestError` remain available from `@floegence/flowersec-core/browser` as stable aliases.
|
|
59
|
+
|
|
47
60
|
## Docs
|
|
48
61
|
|
|
49
62
|
- Frontend quickstart: `docs/FRONTEND_QUICKSTART.md`
|
|
50
63
|
- Integration guide: `docs/INTEGRATION_GUIDE.md`
|
|
51
64
|
- API surface contract: `docs/API_SURFACE.md`
|
|
65
|
+
- Controlplane artifact fetch: `docs/CONTROLPLANE_ARTIFACT_FETCH.md`
|
|
52
66
|
- Error model: `docs/ERROR_MODEL.md`
|
|
67
|
+
- Migration guide: `docs/V0_19_MIGRATION.md`
|
|
@@ -1,40 +1,15 @@
|
|
|
1
1
|
import { type ChannelInitGrant } from "../facade.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type BaseControlplaneRequestConfig = Readonly<{
|
|
5
|
-
baseUrl?: string;
|
|
2
|
+
import type { ControlplaneBaseConfig as SharedControlplaneBaseConfig, RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/request.js";
|
|
3
|
+
export { ControlplaneRequestError, requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.js";
|
|
4
|
+
type BaseControlplaneRequestConfig = SharedControlplaneBaseConfig & Readonly<{
|
|
6
5
|
endpointId: string;
|
|
7
6
|
payload?: Record<string, unknown>;
|
|
8
|
-
headers?: HeadersInit;
|
|
9
|
-
credentials?: RequestCredentials;
|
|
10
|
-
fetch?: FetchLike;
|
|
11
7
|
}>;
|
|
12
8
|
export type ControlplaneConfig = BaseControlplaneRequestConfig;
|
|
13
9
|
export type EntryControlplaneConfig = BaseControlplaneRequestConfig & Readonly<{
|
|
14
10
|
entryTicket: string;
|
|
15
11
|
}>;
|
|
16
|
-
export type ConnectArtifactRequestConfig =
|
|
17
|
-
|
|
18
|
-
correlation?: Readonly<{
|
|
19
|
-
traceId?: string;
|
|
20
|
-
}>;
|
|
21
|
-
}>;
|
|
22
|
-
export type EntryConnectArtifactRequestConfig = ConnectArtifactRequestConfig & Readonly<{
|
|
23
|
-
entryTicket: string;
|
|
24
|
-
}>;
|
|
25
|
-
export declare class ControlplaneRequestError extends Error {
|
|
26
|
-
readonly status: number;
|
|
27
|
-
readonly code: string;
|
|
28
|
-
readonly responseBody: unknown;
|
|
29
|
-
constructor(args: Readonly<{
|
|
30
|
-
status: number;
|
|
31
|
-
message: string;
|
|
32
|
-
code?: string;
|
|
33
|
-
responseBody?: unknown;
|
|
34
|
-
}>);
|
|
35
|
-
}
|
|
12
|
+
export type ConnectArtifactRequestConfig = RequestConnectArtifactInput;
|
|
13
|
+
export type EntryConnectArtifactRequestConfig = RequestEntryConnectArtifactInput;
|
|
36
14
|
export declare function requestChannelGrant(config: ControlplaneConfig): Promise<ChannelInitGrant>;
|
|
37
15
|
export declare function requestEntryChannelGrant(config: EntryControlplaneConfig): Promise<ChannelInitGrant>;
|
|
38
|
-
export declare function requestConnectArtifact(config: ConnectArtifactRequestConfig): Promise<ConnectArtifact>;
|
|
39
|
-
export declare function requestEntryConnectArtifact(config: EntryConnectArtifactRequestConfig): Promise<ConnectArtifact>;
|
|
40
|
-
export {};
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
import { assertChannelInitGrant } from "../facade.js";
|
|
2
|
-
import {
|
|
3
|
-
export
|
|
4
|
-
status;
|
|
5
|
-
code;
|
|
6
|
-
responseBody;
|
|
7
|
-
constructor(args) {
|
|
8
|
-
super(args.message);
|
|
9
|
-
this.name = "ControlplaneRequestError";
|
|
10
|
-
this.status = args.status;
|
|
11
|
-
this.code = String(args.code ?? "").trim();
|
|
12
|
-
this.responseBody = args.responseBody;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function resolveFetch(fetchImpl) {
|
|
16
|
-
if (fetchImpl)
|
|
17
|
-
return fetchImpl;
|
|
18
|
-
if (typeof globalThis.fetch === "function")
|
|
19
|
-
return globalThis.fetch.bind(globalThis);
|
|
20
|
-
throw new Error("global fetch is not available");
|
|
21
|
-
}
|
|
2
|
+
import { requestControlplaneJSON, } from "../controlplane/request.js";
|
|
3
|
+
export { ControlplaneRequestError, requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.js";
|
|
22
4
|
function buildURL(baseUrl, path) {
|
|
23
5
|
const base = String(baseUrl ?? "").trim();
|
|
24
6
|
if (base === "")
|
|
@@ -38,46 +20,7 @@ function buildPayload(endpointId, payload) {
|
|
|
38
20
|
return out;
|
|
39
21
|
}
|
|
40
22
|
async function requestGrant(url, init) {
|
|
41
|
-
|
|
42
|
-
const response = await runFetch(url, {
|
|
43
|
-
...init,
|
|
44
|
-
cache: "no-store",
|
|
45
|
-
});
|
|
46
|
-
if (!response.ok) {
|
|
47
|
-
const rawBody = await response.text();
|
|
48
|
-
const bodyText = String(rawBody ?? "").trim();
|
|
49
|
-
let responseBody = bodyText;
|
|
50
|
-
if (bodyText !== "") {
|
|
51
|
-
try {
|
|
52
|
-
responseBody = JSON.parse(bodyText);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
responseBody = bodyText;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
let message = `Failed to get channel grant: ${response.status}`;
|
|
59
|
-
let code = "";
|
|
60
|
-
if (responseBody && typeof responseBody === "object") {
|
|
61
|
-
const error = responseBody.error;
|
|
62
|
-
if (error && typeof error === "object") {
|
|
63
|
-
const nextMessage = String(error.message ?? "").trim();
|
|
64
|
-
if (nextMessage !== "") {
|
|
65
|
-
message = nextMessage;
|
|
66
|
-
}
|
|
67
|
-
code = String(error.code ?? "").trim();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
else if (typeof responseBody === "string" && responseBody !== "") {
|
|
71
|
-
message = responseBody;
|
|
72
|
-
}
|
|
73
|
-
throw new ControlplaneRequestError({
|
|
74
|
-
status: response.status,
|
|
75
|
-
message,
|
|
76
|
-
code,
|
|
77
|
-
responseBody,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return await response.json();
|
|
23
|
+
return await requestControlplaneJSON(url, init);
|
|
81
24
|
}
|
|
82
25
|
export async function requestChannelGrant(config) {
|
|
83
26
|
const headers = new Headers(config.headers);
|
|
@@ -90,6 +33,7 @@ export async function requestChannelGrant(config) {
|
|
|
90
33
|
credentials: config.credentials ?? "omit",
|
|
91
34
|
headers,
|
|
92
35
|
body: JSON.stringify(buildPayload(config.endpointId, config.payload)),
|
|
36
|
+
...(config.signal === undefined ? {} : { signal: config.signal }),
|
|
93
37
|
}));
|
|
94
38
|
if (!data?.grant_client) {
|
|
95
39
|
throw new Error("Invalid controlplane response: missing `grant_client`");
|
|
@@ -112,61 +56,10 @@ export async function requestEntryChannelGrant(config) {
|
|
|
112
56
|
credentials: config.credentials ?? "omit",
|
|
113
57
|
headers,
|
|
114
58
|
body: JSON.stringify(buildPayload(endpointId, config.payload)),
|
|
59
|
+
...(config.signal === undefined ? {} : { signal: config.signal }),
|
|
115
60
|
}));
|
|
116
61
|
if (!data?.grant_client) {
|
|
117
62
|
throw new Error("Invalid controlplane response: missing `grant_client`");
|
|
118
63
|
}
|
|
119
64
|
return assertChannelInitGrant(data.grant_client);
|
|
120
65
|
}
|
|
121
|
-
function buildConnectArtifactPayload(config) {
|
|
122
|
-
const body = {
|
|
123
|
-
endpoint_id: String(config.endpointId ?? "").trim(),
|
|
124
|
-
};
|
|
125
|
-
if (body.endpoint_id === "")
|
|
126
|
-
throw new Error("endpointId is required");
|
|
127
|
-
if (config.payload !== undefined)
|
|
128
|
-
body.payload = { ...config.payload };
|
|
129
|
-
const traceId = String(config.correlation?.traceId ?? "").trim();
|
|
130
|
-
if (traceId !== "") {
|
|
131
|
-
body.correlation = { trace_id: traceId };
|
|
132
|
-
}
|
|
133
|
-
return body;
|
|
134
|
-
}
|
|
135
|
-
export async function requestConnectArtifact(config) {
|
|
136
|
-
const headers = new Headers(config.headers);
|
|
137
|
-
if (!headers.has("Content-Type")) {
|
|
138
|
-
headers.set("Content-Type", "application/json");
|
|
139
|
-
}
|
|
140
|
-
const data = (await requestGrant(buildURL(config.baseUrl, config.path ?? "/v1/connect/artifact"), {
|
|
141
|
-
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
142
|
-
method: "POST",
|
|
143
|
-
credentials: config.credentials ?? "omit",
|
|
144
|
-
headers,
|
|
145
|
-
body: JSON.stringify(buildConnectArtifactPayload(config)),
|
|
146
|
-
}));
|
|
147
|
-
if (!data?.connect_artifact) {
|
|
148
|
-
throw new Error("Invalid controlplane response: missing `connect_artifact`");
|
|
149
|
-
}
|
|
150
|
-
return assertConnectArtifact(data.connect_artifact);
|
|
151
|
-
}
|
|
152
|
-
export async function requestEntryConnectArtifact(config) {
|
|
153
|
-
const entryTicket = String(config.entryTicket ?? "").trim();
|
|
154
|
-
if (entryTicket === "")
|
|
155
|
-
throw new Error("entryTicket is required");
|
|
156
|
-
const headers = new Headers(config.headers);
|
|
157
|
-
headers.set("Authorization", `Bearer ${entryTicket}`);
|
|
158
|
-
if (!headers.has("Content-Type")) {
|
|
159
|
-
headers.set("Content-Type", "application/json");
|
|
160
|
-
}
|
|
161
|
-
const data = (await requestGrant(buildURL(config.baseUrl, config.path ?? "/v1/connect/artifact/entry"), {
|
|
162
|
-
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
163
|
-
method: "POST",
|
|
164
|
-
credentials: config.credentials ?? "omit",
|
|
165
|
-
headers,
|
|
166
|
-
body: JSON.stringify(buildConnectArtifactPayload(config)),
|
|
167
|
-
}));
|
|
168
|
-
if (!data?.connect_artifact) {
|
|
169
|
-
throw new Error("Invalid controlplane response: missing `connect_artifact`");
|
|
170
|
-
}
|
|
171
|
-
return assertConnectArtifact(data.connect_artifact);
|
|
172
|
-
}
|
|
@@ -3,26 +3,25 @@ import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
|
3
3
|
import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
4
4
|
import type { ClientObserverLike } from "../observability/observer.js";
|
|
5
5
|
import type { AutoReconnectConfig, ConnectConfig as ReconnectConnectConfig } from "../reconnect/index.js";
|
|
6
|
+
import { type ArtifactAwareReconnectConfig, type ArtifactFactoryArgs } from "../reconnect/artifactControlplane.js";
|
|
7
|
+
import type { RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/index.js";
|
|
6
8
|
import type { DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
|
|
7
|
-
import { type
|
|
9
|
+
import { type ControlplaneConfig } from "./controlplane.js";
|
|
8
10
|
type SharedReconnectOptions = Readonly<{
|
|
9
11
|
observer?: ClientObserverLike;
|
|
10
12
|
autoReconnect?: AutoReconnectConfig;
|
|
11
13
|
}>;
|
|
12
|
-
type ArtifactFactoryArgs = Readonly<{
|
|
13
|
-
traceId?: string;
|
|
14
|
-
}>;
|
|
15
14
|
type TunnelReconnectConnectOptions = Omit<TunnelConnectBrowserOptions, "observer" | "signal">;
|
|
16
15
|
type DirectReconnectConnectOptions = Omit<DirectConnectBrowserOptions, "observer" | "signal">;
|
|
17
|
-
type ArtifactAwareTunnelReconnectConfig = Readonly<{
|
|
16
|
+
type ArtifactAwareTunnelReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
|
|
18
17
|
artifact?: ConnectArtifact;
|
|
19
18
|
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
20
|
-
artifactControlplane?:
|
|
19
|
+
artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
|
|
21
20
|
}>;
|
|
22
|
-
type ArtifactAwareDirectReconnectConfig = Readonly<{
|
|
21
|
+
type ArtifactAwareDirectReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
|
|
23
22
|
artifact?: ConnectArtifact;
|
|
24
23
|
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
25
|
-
artifactControlplane?:
|
|
24
|
+
artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
|
|
26
25
|
}>;
|
|
27
26
|
export type TunnelBrowserReconnectConfig = SharedReconnectOptions & ArtifactAwareTunnelReconnectConfig & Readonly<{
|
|
28
27
|
mode?: "tunnel";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { resolveConnectArtifact, updateTraceId, } from "../reconnect/artifactControlplane.js";
|
|
1
2
|
import { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
-
import { requestChannelGrant,
|
|
3
|
+
import { requestChannelGrant, } from "./controlplane.js";
|
|
3
4
|
async function resolveTunnelGrant(config) {
|
|
4
5
|
if (config.getGrant)
|
|
5
6
|
return await config.getGrant();
|
|
@@ -16,32 +17,6 @@ async function resolveDirectInfo(config) {
|
|
|
16
17
|
return config.directInfo;
|
|
17
18
|
throw new Error("Direct reconnect config requires `getDirectInfo` or `directInfo`");
|
|
18
19
|
}
|
|
19
|
-
async function resolveConnectArtifact(config, traceId) {
|
|
20
|
-
if (config.getArtifact) {
|
|
21
|
-
return await config.getArtifact(traceId === undefined ? {} : { traceId });
|
|
22
|
-
}
|
|
23
|
-
if (config.artifact)
|
|
24
|
-
return config.artifact;
|
|
25
|
-
if (config.artifactControlplane) {
|
|
26
|
-
const correlation = traceId === undefined
|
|
27
|
-
? config.artifactControlplane.correlation
|
|
28
|
-
: { traceId };
|
|
29
|
-
if ("entryTicket" in config.artifactControlplane) {
|
|
30
|
-
return await requestEntryConnectArtifact({
|
|
31
|
-
...config.artifactControlplane,
|
|
32
|
-
...(correlation === undefined ? {} : { correlation }),
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
return await requestConnectArtifact({
|
|
36
|
-
...config.artifactControlplane,
|
|
37
|
-
...(correlation === undefined ? {} : { correlation }),
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
throw new Error("Artifact reconnect config requires `getArtifact`, `artifact`, or `artifactControlplane`");
|
|
41
|
-
}
|
|
42
|
-
function updateTraceId(current, artifact) {
|
|
43
|
-
return artifact.correlation?.trace_id ?? current;
|
|
44
|
-
}
|
|
45
20
|
export function createTunnelBrowserReconnectConfig(config) {
|
|
46
21
|
let traceId = config.artifact?.correlation?.trace_id;
|
|
47
22
|
return {
|
|
@@ -49,7 +24,7 @@ export function createTunnelBrowserReconnectConfig(config) {
|
|
|
49
24
|
...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
|
|
50
25
|
connectOnce: async ({ signal, observer }) => {
|
|
51
26
|
if (config.getArtifact || config.artifact || config.artifactControlplane) {
|
|
52
|
-
const artifact = await resolveConnectArtifact(config, traceId);
|
|
27
|
+
const artifact = await resolveConnectArtifact(config, traceId, signal);
|
|
53
28
|
if (artifact.transport !== "tunnel") {
|
|
54
29
|
throw new Error("Tunnel reconnect config requires a tunnel ConnectArtifact");
|
|
55
30
|
}
|
|
@@ -75,7 +50,7 @@ export function createDirectBrowserReconnectConfig(config) {
|
|
|
75
50
|
...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
|
|
76
51
|
connectOnce: async ({ signal, observer }) => {
|
|
77
52
|
if (config.getArtifact || config.artifact || config.artifactControlplane) {
|
|
78
|
-
const artifact = await resolveConnectArtifact(config, traceId);
|
|
53
|
+
const artifact = await resolveConnectArtifact(config, traceId, signal);
|
|
79
54
|
if (artifact.transport !== "direct") {
|
|
80
55
|
throw new Error("Direct reconnect config requires a direct ConnectArtifact");
|
|
81
56
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export declare const tunnelAttachCloseReasons: readonly ["too_many_connections", "expected_attach", "invalid_attach", "invalid_token", "channel_mismatch", "init_exp_mismatch", "idle_timeout_mismatch", "role_mismatch", "token_replay", "replace_rate_limited", "attach_failed", "timeout", "canceled"];
|
|
1
|
+
export declare const tunnelAttachCloseReasons: readonly ["too_many_connections", "expected_attach", "invalid_attach", "invalid_token", "channel_mismatch", "init_exp_mismatch", "idle_timeout_mismatch", "role_mismatch", "token_replay", "tenant_mismatch", "policy_denied", "policy_error", "replace_rate_limited", "attach_failed", "timeout", "canceled"];
|
|
2
2
|
export type TunnelAttachCloseReason = (typeof tunnelAttachCloseReasons)[number];
|
|
3
3
|
export declare function isTunnelAttachCloseReason(v: string | undefined): v is TunnelAttachCloseReason;
|
|
@@ -8,10 +8,13 @@ export const tunnelAttachCloseReasons = [
|
|
|
8
8
|
"idle_timeout_mismatch",
|
|
9
9
|
"role_mismatch",
|
|
10
10
|
"token_replay",
|
|
11
|
+
"tenant_mismatch",
|
|
12
|
+
"policy_denied",
|
|
13
|
+
"policy_error",
|
|
11
14
|
"replace_rate_limited",
|
|
12
15
|
"attach_failed",
|
|
13
16
|
"timeout",
|
|
14
|
-
"canceled"
|
|
17
|
+
"canceled",
|
|
15
18
|
];
|
|
16
19
|
export function isTunnelAttachCloseReason(v) {
|
|
17
20
|
if (v == null)
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export type { ArtifactRequestCorrelation, ConnectArtifactEnvelope, ControlplaneBaseConfig, ControlplaneErrorEnvelope, RequestConnectArtifactInput, RequestEntryConnectArtifactInput, } from "./request.js";
|
|
2
|
+
export { ControlplaneRequestError, DEFAULT_CONNECT_ARTIFACT_PATH, DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH, requestConnectArtifact, requestEntryConnectArtifact, } from "./request.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ControlplaneRequestError, DEFAULT_CONNECT_ARTIFACT_PATH, DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH, requestConnectArtifact, requestEntryConnectArtifact, } from "./request.js";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ConnectArtifact } from "../connect/artifact.js";
|
|
2
|
+
type FetchLike = typeof fetch;
|
|
3
|
+
export type ControlplaneBaseConfig = Readonly<{
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
headers?: HeadersInit;
|
|
7
|
+
credentials?: RequestCredentials;
|
|
8
|
+
fetch?: FetchLike;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}>;
|
|
11
|
+
export type ArtifactRequestCorrelation = Readonly<{
|
|
12
|
+
traceId?: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type RequestConnectArtifactInput = ControlplaneBaseConfig & Readonly<{
|
|
15
|
+
endpointId: string;
|
|
16
|
+
payload?: Record<string, unknown>;
|
|
17
|
+
correlation?: ArtifactRequestCorrelation;
|
|
18
|
+
}>;
|
|
19
|
+
export type RequestEntryConnectArtifactInput = RequestConnectArtifactInput & Readonly<{
|
|
20
|
+
entryTicket: string;
|
|
21
|
+
}>;
|
|
22
|
+
export type ConnectArtifactEnvelope = Readonly<{
|
|
23
|
+
connect_artifact: ConnectArtifact;
|
|
24
|
+
}>;
|
|
25
|
+
export type ControlplaneErrorEnvelope = Readonly<{
|
|
26
|
+
error: Readonly<{
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
export declare class ControlplaneRequestError extends Error {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
readonly code: string;
|
|
34
|
+
readonly responseBody: unknown;
|
|
35
|
+
constructor(args: Readonly<{
|
|
36
|
+
status: number;
|
|
37
|
+
message: string;
|
|
38
|
+
code?: string;
|
|
39
|
+
responseBody?: unknown;
|
|
40
|
+
}>);
|
|
41
|
+
}
|
|
42
|
+
export declare const DEFAULT_CONNECT_ARTIFACT_PATH = "/v1/connect/artifact";
|
|
43
|
+
export declare const DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH = "/v1/connect/artifact/entry";
|
|
44
|
+
export declare function buildControlplaneURL(baseUrl: string | undefined, path: string): string;
|
|
45
|
+
export declare function requestControlplaneJSON(url: string, init: RequestInit & Readonly<{
|
|
46
|
+
fetch?: FetchLike;
|
|
47
|
+
}>): Promise<unknown>;
|
|
48
|
+
export declare function requestConnectArtifact(config: RequestConnectArtifactInput): Promise<ConnectArtifact>;
|
|
49
|
+
export declare function requestEntryConnectArtifact(config: RequestEntryConnectArtifactInput): Promise<ConnectArtifact>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { assertConnectArtifact } from "../connect/artifact.js";
|
|
2
|
+
export class ControlplaneRequestError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
responseBody;
|
|
6
|
+
constructor(args) {
|
|
7
|
+
super(args.message);
|
|
8
|
+
this.name = "ControlplaneRequestError";
|
|
9
|
+
this.status = args.status;
|
|
10
|
+
this.code = String(args.code ?? "").trim();
|
|
11
|
+
this.responseBody = args.responseBody;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const DEFAULT_CONNECT_ARTIFACT_PATH = "/v1/connect/artifact";
|
|
15
|
+
export const DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH = "/v1/connect/artifact/entry";
|
|
16
|
+
const DEFAULT_MAX_CONTROLPLANE_RESPONSE_BYTES = 1 << 20;
|
|
17
|
+
class ControlplaneResponseTooLargeError extends Error {
|
|
18
|
+
constructor(maxBytes) {
|
|
19
|
+
super(`controlplane response exceeded ${maxBytes} bytes`);
|
|
20
|
+
this.name = "ControlplaneResponseTooLargeError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function resolveFetch(fetchImpl) {
|
|
24
|
+
if (fetchImpl)
|
|
25
|
+
return fetchImpl;
|
|
26
|
+
if (typeof globalThis.fetch === "function")
|
|
27
|
+
return globalThis.fetch.bind(globalThis);
|
|
28
|
+
throw new Error("global fetch is not available");
|
|
29
|
+
}
|
|
30
|
+
export function buildControlplaneURL(baseUrl, path) {
|
|
31
|
+
const base = String(baseUrl ?? "").trim();
|
|
32
|
+
if (base === "")
|
|
33
|
+
return path;
|
|
34
|
+
return `${base.replace(/\/+$/, "")}${path}`;
|
|
35
|
+
}
|
|
36
|
+
function parseMaybeJSON(bodyText) {
|
|
37
|
+
const trimmed = String(bodyText ?? "").trim();
|
|
38
|
+
if (trimmed === "")
|
|
39
|
+
return "";
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(trimmed);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function parseContentLength(headerValue) {
|
|
48
|
+
const raw = String(headerValue ?? "").trim();
|
|
49
|
+
if (raw === "")
|
|
50
|
+
return null;
|
|
51
|
+
if (!/^[0-9]+$/.test(raw))
|
|
52
|
+
return null;
|
|
53
|
+
const parsed = Number(raw);
|
|
54
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
55
|
+
return null;
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
async function readControlplaneText(response, maxBytes) {
|
|
59
|
+
const contentLength = parseContentLength(response.headers.get("Content-Length"));
|
|
60
|
+
if (contentLength !== null && contentLength > maxBytes) {
|
|
61
|
+
throw new ControlplaneResponseTooLargeError(maxBytes);
|
|
62
|
+
}
|
|
63
|
+
if (!response.body) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
const reader = response.body.getReader();
|
|
67
|
+
const decoder = new TextDecoder();
|
|
68
|
+
let totalBytes = 0;
|
|
69
|
+
let text = "";
|
|
70
|
+
while (true) {
|
|
71
|
+
const chunk = await reader.read();
|
|
72
|
+
if (chunk.done)
|
|
73
|
+
break;
|
|
74
|
+
totalBytes += chunk.value.byteLength;
|
|
75
|
+
if (totalBytes > maxBytes) {
|
|
76
|
+
try {
|
|
77
|
+
await reader.cancel();
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
throw new ControlplaneResponseTooLargeError(maxBytes);
|
|
81
|
+
}
|
|
82
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
83
|
+
}
|
|
84
|
+
text += decoder.decode();
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
function decodeErrorMessage(status, responseBody) {
|
|
88
|
+
let message = `controlplane request failed: ${status}`;
|
|
89
|
+
let code = "";
|
|
90
|
+
if (responseBody && typeof responseBody === "object") {
|
|
91
|
+
const error = responseBody.error;
|
|
92
|
+
if (error && typeof error === "object") {
|
|
93
|
+
const nextMessage = String(error.message ?? "").trim();
|
|
94
|
+
if (nextMessage !== "") {
|
|
95
|
+
message = nextMessage;
|
|
96
|
+
}
|
|
97
|
+
code = String(error.code ?? "").trim();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (typeof responseBody === "string" && responseBody !== "") {
|
|
101
|
+
message = responseBody;
|
|
102
|
+
}
|
|
103
|
+
return { code, message };
|
|
104
|
+
}
|
|
105
|
+
export async function requestControlplaneJSON(url, init) {
|
|
106
|
+
const runFetch = resolveFetch(init.fetch);
|
|
107
|
+
const response = await runFetch(url, {
|
|
108
|
+
...init,
|
|
109
|
+
cache: "no-store",
|
|
110
|
+
});
|
|
111
|
+
let rawBody = "";
|
|
112
|
+
try {
|
|
113
|
+
rawBody = await readControlplaneText(response, DEFAULT_MAX_CONTROLPLANE_RESPONSE_BYTES);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (err instanceof ControlplaneResponseTooLargeError && !response.ok) {
|
|
117
|
+
throw new ControlplaneRequestError({
|
|
118
|
+
status: response.status,
|
|
119
|
+
message: err.message,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
const responseBody = parseMaybeJSON(rawBody);
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const error = decodeErrorMessage(response.status, responseBody);
|
|
127
|
+
throw new ControlplaneRequestError({
|
|
128
|
+
status: response.status,
|
|
129
|
+
message: error.message,
|
|
130
|
+
code: error.code,
|
|
131
|
+
responseBody,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (typeof responseBody === "string") {
|
|
135
|
+
if (responseBody === "") {
|
|
136
|
+
throw new Error("Invalid controlplane response: expected JSON body");
|
|
137
|
+
}
|
|
138
|
+
throw new Error("Invalid controlplane response: expected JSON body");
|
|
139
|
+
}
|
|
140
|
+
return responseBody;
|
|
141
|
+
}
|
|
142
|
+
function buildConnectArtifactPayload(config) {
|
|
143
|
+
const body = {
|
|
144
|
+
endpoint_id: String(config.endpointId ?? "").trim(),
|
|
145
|
+
};
|
|
146
|
+
if (body.endpoint_id === "")
|
|
147
|
+
throw new Error("endpointId is required");
|
|
148
|
+
if (config.payload !== undefined)
|
|
149
|
+
body.payload = { ...config.payload };
|
|
150
|
+
const traceId = String(config.correlation?.traceId ?? "").trim();
|
|
151
|
+
if (traceId !== "") {
|
|
152
|
+
body.correlation = { trace_id: traceId };
|
|
153
|
+
}
|
|
154
|
+
return body;
|
|
155
|
+
}
|
|
156
|
+
function createJSONHeaders(input) {
|
|
157
|
+
const headers = new Headers(input);
|
|
158
|
+
if (!headers.has("Content-Type")) {
|
|
159
|
+
headers.set("Content-Type", "application/json");
|
|
160
|
+
}
|
|
161
|
+
return headers;
|
|
162
|
+
}
|
|
163
|
+
export async function requestConnectArtifact(config) {
|
|
164
|
+
const data = (await requestControlplaneJSON(buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_CONNECT_ARTIFACT_PATH), {
|
|
165
|
+
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
166
|
+
method: "POST",
|
|
167
|
+
credentials: config.credentials ?? "omit",
|
|
168
|
+
headers: createJSONHeaders(config.headers),
|
|
169
|
+
body: JSON.stringify(buildConnectArtifactPayload(config)),
|
|
170
|
+
...(config.signal === undefined ? {} : { signal: config.signal }),
|
|
171
|
+
}));
|
|
172
|
+
if (!data?.connect_artifact) {
|
|
173
|
+
throw new Error("Invalid controlplane response: missing `connect_artifact`");
|
|
174
|
+
}
|
|
175
|
+
return assertConnectArtifact(data.connect_artifact);
|
|
176
|
+
}
|
|
177
|
+
export async function requestEntryConnectArtifact(config) {
|
|
178
|
+
const entryTicket = String(config.entryTicket ?? "").trim();
|
|
179
|
+
if (entryTicket === "")
|
|
180
|
+
throw new Error("entryTicket is required");
|
|
181
|
+
const headers = createJSONHeaders(config.headers);
|
|
182
|
+
headers.set("Authorization", `Bearer ${entryTicket}`);
|
|
183
|
+
const data = (await requestControlplaneJSON(buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH), {
|
|
184
|
+
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
185
|
+
method: "POST",
|
|
186
|
+
credentials: config.credentials ?? "omit",
|
|
187
|
+
headers,
|
|
188
|
+
body: JSON.stringify(buildConnectArtifactPayload(config)),
|
|
189
|
+
...(config.signal === undefined ? {} : { signal: config.signal }),
|
|
190
|
+
}));
|
|
191
|
+
if (!data?.connect_artifact) {
|
|
192
|
+
throw new Error("Invalid controlplane response: missing `connect_artifact`");
|
|
193
|
+
}
|
|
194
|
+
return assertConnectArtifact(data.connect_artifact);
|
|
195
|
+
}
|
|
@@ -68,8 +68,6 @@ export class SecureChannel {
|
|
|
68
68
|
// read resolves with the next plaintext chunk or throws on errors/close.
|
|
69
69
|
async read() {
|
|
70
70
|
while (true) {
|
|
71
|
-
if (this.readErr != null)
|
|
72
|
-
throw this.readErr;
|
|
73
71
|
if (this.recvQueueHead < this.recvQueue.length) {
|
|
74
72
|
const b = this.recvQueue[this.recvQueueHead];
|
|
75
73
|
this.recvQueueHead++;
|
|
@@ -80,6 +78,8 @@ export class SecureChannel {
|
|
|
80
78
|
this.recvQueueBytes -= b.length;
|
|
81
79
|
return b;
|
|
82
80
|
}
|
|
81
|
+
if (this.readErr != null)
|
|
82
|
+
throw this.readErr;
|
|
83
83
|
if (this.closed)
|
|
84
84
|
throw new Error("closed");
|
|
85
85
|
await new Promise((resolve) => this.recvWaiters.push(resolve));
|
|
@@ -94,9 +94,6 @@ export class SecureChannel {
|
|
|
94
94
|
this.rejectQueuedSenders(this.sendErr ?? new Error("closed"));
|
|
95
95
|
this.wakeSendWaiters();
|
|
96
96
|
this.transport.close();
|
|
97
|
-
this.recvQueue.length = 0;
|
|
98
|
-
this.recvQueueHead = 0;
|
|
99
|
-
this.recvQueueBytes = 0;
|
|
100
97
|
const ws = this.recvWaiters;
|
|
101
98
|
this.recvWaiters = [];
|
|
102
99
|
for (const w of ws)
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
2
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
3
|
+
export type { DirectNodeReconnectConfig, NodeReconnectConfig, TunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
4
|
+
export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
|
|
3
5
|
export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
|
|
4
6
|
export { assertConnectArtifact } from "../connect/artifact.js";
|