@floegence/flowersec-core 0.17.2 → 0.18.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.
- package/README.md +15 -5
- package/dist/browser/controlplane.d.ts +12 -0
- package/dist/browser/controlplane.js +66 -9
- package/dist/browser/index.d.ts +4 -2
- package/dist/browser/index.js +2 -1
- package/dist/browser/reconnectConfig.d.ts +17 -3
- package/dist/browser/reconnectConfig.js +68 -12
- package/dist/client-connect/connectCore.d.ts +5 -0
- package/dist/client-connect/connectCore.js +1 -1
- package/dist/connect/artifact.d.ts +37 -0
- package/dist/connect/artifact.js +217 -0
- package/dist/connect/internalNormalize.d.ts +22 -0
- package/dist/connect/internalNormalize.js +173 -0
- package/dist/facade.d.ts +4 -0
- package/dist/facade.js +15 -47
- package/dist/node/connect.d.ts +3 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +1 -0
- package/dist/observability/observer.d.ts +27 -1
- package/dist/observability/observer.js +268 -12
- package/dist/proxy/bootstrap.d.ts +3 -3
- package/dist/proxy/bootstrap.js +3 -1
- package/dist/proxy/index.d.ts +2 -1
- package/dist/proxy/index.js +1 -1
- package/dist/proxy/integration.d.ts +3 -3
- package/dist/proxy/integration.js +21 -10
- package/dist/proxy/preset.d.ts +31 -0
- package/dist/proxy/preset.js +133 -0
- package/dist/proxy/profiles.d.ts +2 -0
- package/dist/proxy/profiles.js +40 -17
- package/dist/reconnect/index.d.ts +1 -1
- package/dist/reconnect/index.js +13 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,10 +15,13 @@ npm install @floegence/flowersec-core
|
|
|
15
15
|
Browser (recommended):
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import { connectBrowser } from "@floegence/flowersec-core/browser";
|
|
18
|
+
import { connectBrowser, requestConnectArtifact } from "@floegence/flowersec-core/browser";
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
20
|
+
const artifact = await requestConnectArtifact({
|
|
21
|
+
endpointId: "env_demo",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const client = await connectBrowser(artifact);
|
|
22
25
|
await client.ping();
|
|
23
26
|
client.close();
|
|
24
27
|
```
|
|
@@ -28,8 +31,15 @@ Node.js (recommended):
|
|
|
28
31
|
```ts
|
|
29
32
|
import { connectNode } from "@floegence/flowersec-core/node";
|
|
30
33
|
|
|
31
|
-
const
|
|
32
|
-
|
|
34
|
+
const artifactEnvelope = await fetch("https://your-app.example/api/flowersec/connect/artifact", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ endpoint_id: "env_demo" }),
|
|
38
|
+
}).then((r) => r.json());
|
|
39
|
+
|
|
40
|
+
const client = await connectNode(artifactEnvelope.connect_artifact, {
|
|
41
|
+
origin: "https://your-app.example",
|
|
42
|
+
});
|
|
33
43
|
await client.ping();
|
|
34
44
|
client.close();
|
|
35
45
|
```
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChannelInitGrant } from "../facade.js";
|
|
2
|
+
import { type ConnectArtifact } from "../connect/artifact.js";
|
|
2
3
|
type FetchLike = typeof fetch;
|
|
3
4
|
type BaseControlplaneRequestConfig = Readonly<{
|
|
4
5
|
baseUrl?: string;
|
|
@@ -12,6 +13,15 @@ export type ControlplaneConfig = BaseControlplaneRequestConfig;
|
|
|
12
13
|
export type EntryControlplaneConfig = BaseControlplaneRequestConfig & Readonly<{
|
|
13
14
|
entryTicket: string;
|
|
14
15
|
}>;
|
|
16
|
+
export type ConnectArtifactRequestConfig = BaseControlplaneRequestConfig & Readonly<{
|
|
17
|
+
path?: string;
|
|
18
|
+
correlation?: Readonly<{
|
|
19
|
+
traceId?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}>;
|
|
22
|
+
export type EntryConnectArtifactRequestConfig = ConnectArtifactRequestConfig & Readonly<{
|
|
23
|
+
entryTicket: string;
|
|
24
|
+
}>;
|
|
15
25
|
export declare class ControlplaneRequestError extends Error {
|
|
16
26
|
readonly status: number;
|
|
17
27
|
readonly code: string;
|
|
@@ -25,4 +35,6 @@ export declare class ControlplaneRequestError extends Error {
|
|
|
25
35
|
}
|
|
26
36
|
export declare function requestChannelGrant(config: ControlplaneConfig): Promise<ChannelInitGrant>;
|
|
27
37
|
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>;
|
|
28
40
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertChannelInitGrant } from "../facade.js";
|
|
2
|
+
import { assertConnectArtifact } from "../connect/artifact.js";
|
|
2
3
|
export class ControlplaneRequestError extends Error {
|
|
3
4
|
status;
|
|
4
5
|
code;
|
|
@@ -76,24 +77,24 @@ async function requestGrant(url, init) {
|
|
|
76
77
|
responseBody,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
if (!data?.grant_client) {
|
|
81
|
-
throw new Error("Invalid controlplane response: missing `grant_client`");
|
|
82
|
-
}
|
|
83
|
-
return assertChannelInitGrant(data.grant_client);
|
|
80
|
+
return await response.json();
|
|
84
81
|
}
|
|
85
82
|
export async function requestChannelGrant(config) {
|
|
86
83
|
const headers = new Headers(config.headers);
|
|
87
84
|
if (!headers.has("Content-Type")) {
|
|
88
85
|
headers.set("Content-Type", "application/json");
|
|
89
86
|
}
|
|
90
|
-
|
|
87
|
+
const data = (await requestGrant(buildURL(config.baseUrl, "/v1/channel/init"), {
|
|
91
88
|
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
92
89
|
method: "POST",
|
|
93
90
|
credentials: config.credentials ?? "omit",
|
|
94
91
|
headers,
|
|
95
92
|
body: JSON.stringify(buildPayload(config.endpointId, config.payload)),
|
|
96
|
-
});
|
|
93
|
+
}));
|
|
94
|
+
if (!data?.grant_client) {
|
|
95
|
+
throw new Error("Invalid controlplane response: missing `grant_client`");
|
|
96
|
+
}
|
|
97
|
+
return assertChannelInitGrant(data.grant_client);
|
|
97
98
|
}
|
|
98
99
|
export async function requestEntryChannelGrant(config) {
|
|
99
100
|
const entryTicket = String(config.entryTicket ?? "").trim();
|
|
@@ -105,11 +106,67 @@ export async function requestEntryChannelGrant(config) {
|
|
|
105
106
|
if (!headers.has("Content-Type")) {
|
|
106
107
|
headers.set("Content-Type", "application/json");
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
const data = (await requestGrant(buildURL(config.baseUrl, `/v1/channel/init/entry?endpoint_id=${encodeURIComponent(endpointId)}`), {
|
|
109
110
|
...(config.fetch === undefined ? {} : { fetch: config.fetch }),
|
|
110
111
|
method: "POST",
|
|
111
112
|
credentials: config.credentials ?? "omit",
|
|
112
113
|
headers,
|
|
113
114
|
body: JSON.stringify(buildPayload(endpointId, config.payload)),
|
|
114
|
-
});
|
|
115
|
+
}));
|
|
116
|
+
if (!data?.grant_client) {
|
|
117
|
+
throw new Error("Invalid controlplane response: missing `grant_client`");
|
|
118
|
+
}
|
|
119
|
+
return assertChannelInitGrant(data.grant_client);
|
|
120
|
+
}
|
|
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);
|
|
115
172
|
}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type { ConnectBrowserOptions, DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
|
|
2
2
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
3
|
-
export type {
|
|
4
|
-
export {
|
|
3
|
+
export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
|
|
4
|
+
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
5
|
+
export type { ConnectArtifactRequestConfig, ControlplaneConfig, EntryConnectArtifactRequestConfig, EntryControlplaneConfig, } from "./controlplane.js";
|
|
6
|
+
export { ControlplaneRequestError, requestChannelGrant, requestConnectArtifact, requestEntryChannelGrant, requestEntryConnectArtifact, } from "./controlplane.js";
|
|
5
7
|
export type { BrowserReconnectConfig, DirectBrowserReconnectConfig, TunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
|
6
8
|
export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
package/dist/browser/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
-
export {
|
|
2
|
+
export { assertConnectArtifact } from "../connect/artifact.js";
|
|
3
|
+
export { ControlplaneRequestError, requestChannelGrant, requestConnectArtifact, requestEntryChannelGrant, requestEntryConnectArtifact, } from "./controlplane.js";
|
|
3
4
|
export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
|
|
@@ -1,23 +1,37 @@
|
|
|
1
|
+
import type { ConnectArtifact } from "../connect/artifact.js";
|
|
1
2
|
import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
2
3
|
import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
3
4
|
import type { ClientObserverLike } from "../observability/observer.js";
|
|
4
5
|
import type { AutoReconnectConfig, ConnectConfig as ReconnectConnectConfig } from "../reconnect/index.js";
|
|
5
6
|
import type { DirectConnectBrowserOptions, TunnelConnectBrowserOptions } from "./connect.js";
|
|
6
|
-
import { type ControlplaneConfig } from "./controlplane.js";
|
|
7
|
+
import { type ConnectArtifactRequestConfig, type ControlplaneConfig, type EntryConnectArtifactRequestConfig } from "./controlplane.js";
|
|
7
8
|
type SharedReconnectOptions = Readonly<{
|
|
8
9
|
observer?: ClientObserverLike;
|
|
9
10
|
autoReconnect?: AutoReconnectConfig;
|
|
10
11
|
}>;
|
|
12
|
+
type ArtifactFactoryArgs = Readonly<{
|
|
13
|
+
traceId?: string;
|
|
14
|
+
}>;
|
|
11
15
|
type TunnelReconnectConnectOptions = Omit<TunnelConnectBrowserOptions, "observer" | "signal">;
|
|
12
16
|
type DirectReconnectConnectOptions = Omit<DirectConnectBrowserOptions, "observer" | "signal">;
|
|
13
|
-
|
|
17
|
+
type ArtifactAwareTunnelReconnectConfig = Readonly<{
|
|
18
|
+
artifact?: ConnectArtifact;
|
|
19
|
+
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
20
|
+
artifactControlplane?: ConnectArtifactRequestConfig | EntryConnectArtifactRequestConfig;
|
|
21
|
+
}>;
|
|
22
|
+
type ArtifactAwareDirectReconnectConfig = Readonly<{
|
|
23
|
+
artifact?: ConnectArtifact;
|
|
24
|
+
getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
|
|
25
|
+
artifactControlplane?: ConnectArtifactRequestConfig | EntryConnectArtifactRequestConfig;
|
|
26
|
+
}>;
|
|
27
|
+
export type TunnelBrowserReconnectConfig = SharedReconnectOptions & ArtifactAwareTunnelReconnectConfig & Readonly<{
|
|
14
28
|
mode?: "tunnel";
|
|
15
29
|
connect?: TunnelReconnectConnectOptions;
|
|
16
30
|
grant?: ChannelInitGrant;
|
|
17
31
|
getGrant?: () => Promise<ChannelInitGrant>;
|
|
18
32
|
controlplane?: ControlplaneConfig;
|
|
19
33
|
}>;
|
|
20
|
-
export type DirectBrowserReconnectConfig = SharedReconnectOptions & Readonly<{
|
|
34
|
+
export type DirectBrowserReconnectConfig = SharedReconnectOptions & ArtifactAwareDirectReconnectConfig & Readonly<{
|
|
21
35
|
mode: "direct";
|
|
22
36
|
connect?: DirectReconnectConnectOptions;
|
|
23
37
|
directInfo?: DirectConnectInfo;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
-
import { requestChannelGrant } from "./controlplane.js";
|
|
1
|
+
import { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
|
|
2
|
+
import { requestChannelGrant, requestConnectArtifact, requestEntryConnectArtifact, } from "./controlplane.js";
|
|
3
3
|
async function resolveTunnelGrant(config) {
|
|
4
4
|
if (config.getGrant)
|
|
5
5
|
return await config.getGrant();
|
|
@@ -16,26 +16,82 @@ async function resolveDirectInfo(config) {
|
|
|
16
16
|
return config.directInfo;
|
|
17
17
|
throw new Error("Direct reconnect config requires `getDirectInfo` or `directInfo`");
|
|
18
18
|
}
|
|
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
|
+
}
|
|
19
45
|
export function createTunnelBrowserReconnectConfig(config) {
|
|
46
|
+
let traceId = config.artifact?.correlation?.trace_id;
|
|
20
47
|
return {
|
|
21
48
|
...(config.observer === undefined ? {} : { observer: config.observer }),
|
|
22
49
|
...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
|
|
23
|
-
connectOnce: async ({ signal, observer }) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
connectOnce: async ({ signal, observer }) => {
|
|
51
|
+
if (config.getArtifact || config.artifact || config.artifactControlplane) {
|
|
52
|
+
const artifact = await resolveConnectArtifact(config, traceId);
|
|
53
|
+
if (artifact.transport !== "tunnel") {
|
|
54
|
+
throw new Error("Tunnel reconnect config requires a tunnel ConnectArtifact");
|
|
55
|
+
}
|
|
56
|
+
traceId = updateTraceId(traceId, artifact);
|
|
57
|
+
return await connectBrowser(artifact, {
|
|
58
|
+
...(config.connect ?? {}),
|
|
59
|
+
signal,
|
|
60
|
+
observer,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return await connectTunnelBrowser(await resolveTunnelGrant(config), {
|
|
64
|
+
...(config.connect ?? {}),
|
|
65
|
+
signal,
|
|
66
|
+
observer,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
28
69
|
};
|
|
29
70
|
}
|
|
30
71
|
export function createDirectBrowserReconnectConfig(config) {
|
|
72
|
+
let traceId = config.artifact?.correlation?.trace_id;
|
|
31
73
|
return {
|
|
32
74
|
...(config.observer === undefined ? {} : { observer: config.observer }),
|
|
33
75
|
...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
|
|
34
|
-
connectOnce: async ({ signal, observer }) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
76
|
+
connectOnce: async ({ signal, observer }) => {
|
|
77
|
+
if (config.getArtifact || config.artifact || config.artifactControlplane) {
|
|
78
|
+
const artifact = await resolveConnectArtifact(config, traceId);
|
|
79
|
+
if (artifact.transport !== "direct") {
|
|
80
|
+
throw new Error("Direct reconnect config requires a direct ConnectArtifact");
|
|
81
|
+
}
|
|
82
|
+
traceId = updateTraceId(traceId, artifact);
|
|
83
|
+
return await connectBrowser(artifact, {
|
|
84
|
+
...(config.connect ?? {}),
|
|
85
|
+
signal,
|
|
86
|
+
observer,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return await connectDirectBrowser(await resolveDirectInfo(config), {
|
|
90
|
+
...(config.connect ?? {}),
|
|
91
|
+
signal,
|
|
92
|
+
observer,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
39
95
|
};
|
|
40
96
|
}
|
|
41
97
|
export function createBrowserReconnectConfig(config) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ClientObserverLike } from "../observability/observer.js";
|
|
2
2
|
import { type WebSocketLike } from "../ws-client/binaryTransport.js";
|
|
3
3
|
import type { ClientInternal } from "../client.js";
|
|
4
|
+
import type { ConnectScopeResolverMap } from "../connect/internalNormalize.js";
|
|
4
5
|
export type ConnectOptionsBase = Readonly<{
|
|
5
6
|
/** Explicit Origin value (required). In browsers this must match window.location.origin. */
|
|
6
7
|
origin: string;
|
|
@@ -24,6 +25,10 @@ export type ConnectOptionsBase = Readonly<{
|
|
|
24
25
|
wsFactory?: (url: string, origin: string) => WebSocketLike;
|
|
25
26
|
/** Optional observer for client metrics. */
|
|
26
27
|
observer?: ClientObserverLike;
|
|
28
|
+
/** Experimental scope validators keyed by scope name. */
|
|
29
|
+
scopeResolvers?: ConnectScopeResolverMap;
|
|
30
|
+
/** Experimental migration switch for optional scope failures. */
|
|
31
|
+
relaxedOptionalScopeValidation?: boolean;
|
|
27
32
|
/** Encrypted keepalive ping interval in milliseconds (0 disables). */
|
|
28
33
|
keepaliveIntervalMs?: number;
|
|
29
34
|
}>;
|
|
@@ -11,7 +11,7 @@ import { OriginMismatchError, WsFactoryRequiredError, classifyConnectError, clas
|
|
|
11
11
|
import { prepareChannelId } from "./contract.js";
|
|
12
12
|
import { isTunnelAttachCloseReason } from "./tunnelAttachCloseReason.js";
|
|
13
13
|
export async function connectCore(args) {
|
|
14
|
-
const observer = normalizeObserver(args.opts.observer);
|
|
14
|
+
const observer = normalizeObserver(args.opts.observer, { path: args.path });
|
|
15
15
|
const signal = args.opts.signal;
|
|
16
16
|
const connectStart = nowSeconds();
|
|
17
17
|
let attachState = args.path === "tunnel" ? "not_started" : "accepted";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
2
|
+
import { type DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
3
|
+
export type ConnectArtifactTransport = "tunnel" | "direct";
|
|
4
|
+
export type CorrelationKV = Readonly<{
|
|
5
|
+
key: string;
|
|
6
|
+
value: string;
|
|
7
|
+
}>;
|
|
8
|
+
export type CorrelationContext = Readonly<{
|
|
9
|
+
v: 1;
|
|
10
|
+
trace_id?: string;
|
|
11
|
+
session_id?: string;
|
|
12
|
+
tags: readonly CorrelationKV[];
|
|
13
|
+
}>;
|
|
14
|
+
export type ScopePayload = Record<string, unknown>;
|
|
15
|
+
export type ScopeMetadataEntry = Readonly<{
|
|
16
|
+
scope: string;
|
|
17
|
+
scope_version: number;
|
|
18
|
+
critical: boolean;
|
|
19
|
+
payload: ScopePayload;
|
|
20
|
+
}>;
|
|
21
|
+
export type TunnelClientConnectArtifact = Readonly<{
|
|
22
|
+
v: 1;
|
|
23
|
+
transport: "tunnel";
|
|
24
|
+
tunnel_grant: ChannelInitGrant;
|
|
25
|
+
scoped?: readonly ScopeMetadataEntry[];
|
|
26
|
+
correlation?: CorrelationContext;
|
|
27
|
+
}>;
|
|
28
|
+
export type DirectClientConnectArtifact = Readonly<{
|
|
29
|
+
v: 1;
|
|
30
|
+
transport: "direct";
|
|
31
|
+
direct_info: DirectConnectInfo;
|
|
32
|
+
scoped?: readonly ScopeMetadataEntry[];
|
|
33
|
+
correlation?: CorrelationContext;
|
|
34
|
+
}>;
|
|
35
|
+
export type ConnectArtifact = TunnelClientConnectArtifact | DirectClientConnectArtifact;
|
|
36
|
+
export declare function hasArtifactOnlyFields(value: Record<string, unknown>): boolean;
|
|
37
|
+
export declare function assertConnectArtifact(value: unknown): ConnectArtifact;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { assertChannelInitGrant, Role as ControlRole } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
2
|
+
import { assertDirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
3
|
+
const SCOPE_NAME_RE = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
4
|
+
const TAG_KEY_RE = /^[a-z][a-z0-9._-]{0,31}$/;
|
|
5
|
+
const CORRELATION_ID_RE = /^[A-Za-z0-9._~-]{8,128}$/;
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const TUNNEL_ARTIFACT_KEYS = new Set(["v", "transport", "tunnel_grant", "scoped", "correlation"]);
|
|
8
|
+
const DIRECT_ARTIFACT_KEYS = new Set(["v", "transport", "direct_info", "scoped", "correlation"]);
|
|
9
|
+
const ARTIFACT_ONLY_KEYS = new Set(["v", "transport", "tunnel_grant", "direct_info", "scoped", "correlation"]);
|
|
10
|
+
function isRecord(v) {
|
|
11
|
+
return typeof v === "object" && v != null && !Array.isArray(v);
|
|
12
|
+
}
|
|
13
|
+
function hasOwn(o, key) {
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(o, key);
|
|
15
|
+
}
|
|
16
|
+
function assertNoUnknownFields(kind, obj, allowed) {
|
|
17
|
+
for (const key of Object.keys(obj)) {
|
|
18
|
+
if (!allowed.has(key))
|
|
19
|
+
throw new Error(`bad ${kind}.${key}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function assertPositiveInt(name, value, min, max) {
|
|
23
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value))
|
|
24
|
+
throw new Error(`bad ${name}`);
|
|
25
|
+
if (value < min || value > max)
|
|
26
|
+
throw new Error(`bad ${name}`);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function utf8Len(value) {
|
|
30
|
+
return encoder.encode(value).length;
|
|
31
|
+
}
|
|
32
|
+
function normalizedJSONForSize(value) {
|
|
33
|
+
return serializeNormalizedJSON(normalizeJSONValue(value));
|
|
34
|
+
}
|
|
35
|
+
function normalizeJSONValue(value) {
|
|
36
|
+
if (value === null)
|
|
37
|
+
return null;
|
|
38
|
+
switch (typeof value) {
|
|
39
|
+
case "string":
|
|
40
|
+
case "boolean":
|
|
41
|
+
return value;
|
|
42
|
+
case "number":
|
|
43
|
+
if (!Number.isFinite(value))
|
|
44
|
+
throw new Error("bad payload.number");
|
|
45
|
+
return value;
|
|
46
|
+
case "object": {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return value.map((entry) => normalizeJSONValue(entry));
|
|
49
|
+
}
|
|
50
|
+
if (!isRecord(value))
|
|
51
|
+
throw new Error("bad payload.object");
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const key of Object.keys(value).sort()) {
|
|
54
|
+
out[key] = normalizeJSONValue(value[key]);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
default:
|
|
59
|
+
throw new Error("bad payload.value");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function serializeNormalizedJSON(value) {
|
|
63
|
+
return JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
function maxContainerDepth(value) {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
let best = 1;
|
|
68
|
+
for (const entry of value)
|
|
69
|
+
best = Math.max(best, 1 + maxContainerDepth(entry));
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
if (isRecord(value)) {
|
|
73
|
+
let best = 1;
|
|
74
|
+
for (const entry of Object.values(value))
|
|
75
|
+
best = Math.max(best, 1 + maxContainerDepth(entry));
|
|
76
|
+
return best;
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
function sanitizeCorrelationID(value) {
|
|
81
|
+
if (typeof value !== "string")
|
|
82
|
+
return undefined;
|
|
83
|
+
const trimmed = value.trim();
|
|
84
|
+
if (!CORRELATION_ID_RE.test(trimmed))
|
|
85
|
+
return undefined;
|
|
86
|
+
return trimmed;
|
|
87
|
+
}
|
|
88
|
+
function assertCorrelationKV(value) {
|
|
89
|
+
if (!isRecord(value))
|
|
90
|
+
throw new Error("bad CorrelationKV");
|
|
91
|
+
assertNoUnknownFields("CorrelationKV", value, new Set(["key", "value"]));
|
|
92
|
+
if (typeof value.key !== "string" || !TAG_KEY_RE.test(value.key))
|
|
93
|
+
throw new Error("bad CorrelationKV.key");
|
|
94
|
+
if (typeof value.value !== "string")
|
|
95
|
+
throw new Error("bad CorrelationKV.value");
|
|
96
|
+
if (utf8Len(value.key) > 32)
|
|
97
|
+
throw new Error("bad CorrelationKV.key");
|
|
98
|
+
if (utf8Len(value.value) > 128)
|
|
99
|
+
throw new Error("bad CorrelationKV.value");
|
|
100
|
+
return Object.freeze({ key: value.key, value: value.value });
|
|
101
|
+
}
|
|
102
|
+
function assertCorrelationContext(value) {
|
|
103
|
+
if (!isRecord(value))
|
|
104
|
+
throw new Error("bad CorrelationContext");
|
|
105
|
+
assertNoUnknownFields("CorrelationContext", value, new Set(["v", "trace_id", "session_id", "tags"]));
|
|
106
|
+
if (value.v !== 1)
|
|
107
|
+
throw new Error("bad CorrelationContext.v");
|
|
108
|
+
const rawTags = value.tags;
|
|
109
|
+
if (rawTags !== undefined && !Array.isArray(rawTags))
|
|
110
|
+
throw new Error("bad CorrelationContext.tags");
|
|
111
|
+
const tags = (rawTags ?? []).map((entry) => assertCorrelationKV(entry));
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
for (const tag of tags) {
|
|
114
|
+
if (seen.has(tag.key))
|
|
115
|
+
throw new Error("bad CorrelationContext.tags");
|
|
116
|
+
seen.add(tag.key);
|
|
117
|
+
}
|
|
118
|
+
if (tags.length > 8)
|
|
119
|
+
throw new Error("bad CorrelationContext.tags");
|
|
120
|
+
const traceId = sanitizeCorrelationID(value.trace_id);
|
|
121
|
+
const sessionId = sanitizeCorrelationID(value.session_id);
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
v: 1,
|
|
124
|
+
...(traceId === undefined ? {} : { trace_id: traceId }),
|
|
125
|
+
...(sessionId === undefined ? {} : { session_id: sessionId }),
|
|
126
|
+
tags: Object.freeze(tags),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function assertPayloadObject(value) {
|
|
130
|
+
if (!isRecord(value))
|
|
131
|
+
throw new Error("bad ScopeMetadataEntry.payload");
|
|
132
|
+
const normalized = normalizedJSONForSize(value);
|
|
133
|
+
if (utf8Len(normalized) > 8192)
|
|
134
|
+
throw new Error("bad ScopeMetadataEntry.payload");
|
|
135
|
+
if (maxContainerDepth(value) > 8)
|
|
136
|
+
throw new Error("bad ScopeMetadataEntry.payload");
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
function assertScopeMetadataEntry(value) {
|
|
140
|
+
if (!isRecord(value))
|
|
141
|
+
throw new Error("bad ScopeMetadataEntry");
|
|
142
|
+
assertNoUnknownFields("ScopeMetadataEntry", value, new Set(["scope", "scope_version", "critical", "payload"]));
|
|
143
|
+
if (typeof value.scope !== "string" || !SCOPE_NAME_RE.test(value.scope))
|
|
144
|
+
throw new Error("bad ScopeMetadataEntry.scope");
|
|
145
|
+
if (typeof value.critical !== "boolean")
|
|
146
|
+
throw new Error("bad ScopeMetadataEntry.critical");
|
|
147
|
+
const scopeVersion = assertPositiveInt("ScopeMetadataEntry.scope_version", value.scope_version, 1, 65535);
|
|
148
|
+
const payload = assertPayloadObject(value.payload);
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
scope: value.scope,
|
|
151
|
+
scope_version: scopeVersion,
|
|
152
|
+
critical: value.critical,
|
|
153
|
+
payload,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function assertScopedEntries(value) {
|
|
157
|
+
if (!Array.isArray(value))
|
|
158
|
+
throw new Error("bad ConnectArtifact.scoped");
|
|
159
|
+
if (value.length > 8)
|
|
160
|
+
throw new Error("bad ConnectArtifact.scoped");
|
|
161
|
+
const out = value.map((entry) => assertScopeMetadataEntry(entry));
|
|
162
|
+
const seen = new Set();
|
|
163
|
+
for (const entry of out) {
|
|
164
|
+
if (seen.has(entry.scope))
|
|
165
|
+
throw new Error("bad ConnectArtifact.scoped");
|
|
166
|
+
seen.add(entry.scope);
|
|
167
|
+
}
|
|
168
|
+
return Object.freeze(out);
|
|
169
|
+
}
|
|
170
|
+
function assertArtifactObject(value) {
|
|
171
|
+
if (!isRecord(value))
|
|
172
|
+
throw new Error("bad ConnectArtifact");
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function assertArtifactTransport(value) {
|
|
176
|
+
if (value !== "tunnel" && value !== "direct")
|
|
177
|
+
throw new Error("bad ConnectArtifact.transport");
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
export function hasArtifactOnlyFields(value) {
|
|
181
|
+
return Object.keys(value).some((key) => ARTIFACT_ONLY_KEYS.has(key));
|
|
182
|
+
}
|
|
183
|
+
export function assertConnectArtifact(value) {
|
|
184
|
+
const record = assertArtifactObject(value);
|
|
185
|
+
if (record.v !== 1)
|
|
186
|
+
throw new Error("bad ConnectArtifact.v");
|
|
187
|
+
const transport = assertArtifactTransport(record.transport);
|
|
188
|
+
const scoped = record.scoped === undefined ? undefined : assertScopedEntries(record.scoped);
|
|
189
|
+
const correlation = record.correlation === undefined ? undefined : assertCorrelationContext(record.correlation);
|
|
190
|
+
if (transport === "tunnel") {
|
|
191
|
+
assertNoUnknownFields("TunnelClientConnectArtifact", record, TUNNEL_ARTIFACT_KEYS);
|
|
192
|
+
if (!hasOwn(record, "tunnel_grant"))
|
|
193
|
+
throw new Error("bad TunnelClientConnectArtifact.tunnel_grant");
|
|
194
|
+
const tunnelGrant = assertChannelInitGrant(record.tunnel_grant);
|
|
195
|
+
if (tunnelGrant.role !== ControlRole.Role_client) {
|
|
196
|
+
throw new Error("bad TunnelClientConnectArtifact.tunnel_grant.role");
|
|
197
|
+
}
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
v: 1,
|
|
200
|
+
transport,
|
|
201
|
+
tunnel_grant: tunnelGrant,
|
|
202
|
+
...(scoped === undefined ? {} : { scoped }),
|
|
203
|
+
...(correlation === undefined ? {} : { correlation }),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
assertNoUnknownFields("DirectClientConnectArtifact", record, DIRECT_ARTIFACT_KEYS);
|
|
207
|
+
if (!hasOwn(record, "direct_info"))
|
|
208
|
+
throw new Error("bad DirectClientConnectArtifact.direct_info");
|
|
209
|
+
const directInfo = assertDirectConnectInfo(record.direct_info);
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
v: 1,
|
|
212
|
+
transport,
|
|
213
|
+
direct_info: directInfo,
|
|
214
|
+
...(scoped === undefined ? {} : { scoped }),
|
|
215
|
+
...(correlation === undefined ? {} : { correlation }),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ClientObserverLike } from "../observability/observer.js";
|
|
2
|
+
import { type CorrelationContext, type ScopeMetadataEntry } from "./artifact.js";
|
|
3
|
+
export type ConnectScopeResolver = (entry: ScopeMetadataEntry) => void | Promise<void>;
|
|
4
|
+
export type ConnectScopeResolverMap = Readonly<Record<string, ConnectScopeResolver>>;
|
|
5
|
+
type NormalizeOptions = Readonly<{
|
|
6
|
+
observer?: ClientObserverLike;
|
|
7
|
+
scopeResolvers?: ConnectScopeResolverMap;
|
|
8
|
+
relaxedOptionalScopeValidation?: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
export type NormalizedConnectInput = Readonly<{
|
|
11
|
+
kind: "tunnel";
|
|
12
|
+
input: unknown;
|
|
13
|
+
correlation?: CorrelationContext;
|
|
14
|
+
observer?: ClientObserverLike;
|
|
15
|
+
}> | Readonly<{
|
|
16
|
+
kind: "direct";
|
|
17
|
+
input: unknown;
|
|
18
|
+
correlation?: CorrelationContext;
|
|
19
|
+
observer?: ClientObserverLike;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function normalizeConnectInput(input: unknown, opts?: NormalizeOptions): Promise<NormalizedConnectInput>;
|
|
22
|
+
export {};
|