@floegence/flowersec-core 0.18.0 → 0.19.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @floegence/flowersec-core
2
2
 
3
- Flowersec core TypeScript library for building an end-to-end encrypted, multiplexed connection over WebSocket (browser-friendly).
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
- ## Usage
13
+ ## Recommended usage
14
14
 
15
- Browser (recommended):
15
+ Browser:
16
16
 
17
17
  ```ts
18
- import { connectBrowser, requestConnectArtifact } from "@floegence/flowersec-core/browser";
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 (recommended):
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 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());
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(artifactEnvelope.connect_artifact, {
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 { type ConnectArtifact } from "../connect/artifact.js";
3
- type FetchLike = typeof fetch;
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 = BaseControlplaneRequestConfig & Readonly<{
17
- path?: string;
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 { assertConnectArtifact } from "../connect/artifact.js";
3
- export class ControlplaneRequestError extends Error {
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
- const runFetch = resolveFetch(init.fetch);
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 ConnectArtifactRequestConfig, type ControlplaneConfig, type EntryConnectArtifactRequestConfig } from "./controlplane.js";
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?: ConnectArtifactRequestConfig | EntryConnectArtifactRequestConfig;
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?: ConnectArtifactRequestConfig | EntryConnectArtifactRequestConfig;
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, requestConnectArtifact, requestEntryConnectArtifact, } from "./controlplane.js";
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,136 @@
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
+ function resolveFetch(fetchImpl) {
17
+ if (fetchImpl)
18
+ return fetchImpl;
19
+ if (typeof globalThis.fetch === "function")
20
+ return globalThis.fetch.bind(globalThis);
21
+ throw new Error("global fetch is not available");
22
+ }
23
+ export function buildControlplaneURL(baseUrl, path) {
24
+ const base = String(baseUrl ?? "").trim();
25
+ if (base === "")
26
+ return path;
27
+ return `${base.replace(/\/+$/, "")}${path}`;
28
+ }
29
+ function parseMaybeJSON(bodyText) {
30
+ const trimmed = String(bodyText ?? "").trim();
31
+ if (trimmed === "")
32
+ return "";
33
+ try {
34
+ return JSON.parse(trimmed);
35
+ }
36
+ catch {
37
+ return trimmed;
38
+ }
39
+ }
40
+ function decodeErrorMessage(status, responseBody) {
41
+ let message = `controlplane request failed: ${status}`;
42
+ let code = "";
43
+ if (responseBody && typeof responseBody === "object") {
44
+ const error = responseBody.error;
45
+ if (error && typeof error === "object") {
46
+ const nextMessage = String(error.message ?? "").trim();
47
+ if (nextMessage !== "") {
48
+ message = nextMessage;
49
+ }
50
+ code = String(error.code ?? "").trim();
51
+ }
52
+ }
53
+ else if (typeof responseBody === "string" && responseBody !== "") {
54
+ message = responseBody;
55
+ }
56
+ return { code, message };
57
+ }
58
+ export async function requestControlplaneJSON(url, init) {
59
+ const runFetch = resolveFetch(init.fetch);
60
+ const response = await runFetch(url, {
61
+ ...init,
62
+ cache: "no-store",
63
+ });
64
+ const rawBody = await response.text();
65
+ const responseBody = parseMaybeJSON(rawBody);
66
+ if (!response.ok) {
67
+ const error = decodeErrorMessage(response.status, responseBody);
68
+ throw new ControlplaneRequestError({
69
+ status: response.status,
70
+ message: error.message,
71
+ code: error.code,
72
+ responseBody,
73
+ });
74
+ }
75
+ if (typeof responseBody === "string") {
76
+ if (responseBody === "") {
77
+ throw new Error("Invalid controlplane response: expected JSON body");
78
+ }
79
+ throw new Error("Invalid controlplane response: expected JSON body");
80
+ }
81
+ return responseBody;
82
+ }
83
+ function buildConnectArtifactPayload(config) {
84
+ const body = {
85
+ endpoint_id: String(config.endpointId ?? "").trim(),
86
+ };
87
+ if (body.endpoint_id === "")
88
+ throw new Error("endpointId is required");
89
+ if (config.payload !== undefined)
90
+ body.payload = { ...config.payload };
91
+ const traceId = String(config.correlation?.traceId ?? "").trim();
92
+ if (traceId !== "") {
93
+ body.correlation = { trace_id: traceId };
94
+ }
95
+ return body;
96
+ }
97
+ function createJSONHeaders(input) {
98
+ const headers = new Headers(input);
99
+ if (!headers.has("Content-Type")) {
100
+ headers.set("Content-Type", "application/json");
101
+ }
102
+ return headers;
103
+ }
104
+ export async function requestConnectArtifact(config) {
105
+ const data = (await requestControlplaneJSON(buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_CONNECT_ARTIFACT_PATH), {
106
+ ...(config.fetch === undefined ? {} : { fetch: config.fetch }),
107
+ method: "POST",
108
+ credentials: config.credentials ?? "omit",
109
+ headers: createJSONHeaders(config.headers),
110
+ body: JSON.stringify(buildConnectArtifactPayload(config)),
111
+ ...(config.signal === undefined ? {} : { signal: config.signal }),
112
+ }));
113
+ if (!data?.connect_artifact) {
114
+ throw new Error("Invalid controlplane response: missing `connect_artifact`");
115
+ }
116
+ return assertConnectArtifact(data.connect_artifact);
117
+ }
118
+ export async function requestEntryConnectArtifact(config) {
119
+ const entryTicket = String(config.entryTicket ?? "").trim();
120
+ if (entryTicket === "")
121
+ throw new Error("entryTicket is required");
122
+ const headers = createJSONHeaders(config.headers);
123
+ headers.set("Authorization", `Bearer ${entryTicket}`);
124
+ const data = (await requestControlplaneJSON(buildControlplaneURL(config.baseUrl, config.path ?? DEFAULT_ENTRY_CONNECT_ARTIFACT_PATH), {
125
+ ...(config.fetch === undefined ? {} : { fetch: config.fetch }),
126
+ method: "POST",
127
+ credentials: config.credentials ?? "omit",
128
+ headers,
129
+ body: JSON.stringify(buildConnectArtifactPayload(config)),
130
+ ...(config.signal === undefined ? {} : { signal: config.signal }),
131
+ }));
132
+ if (!data?.connect_artifact) {
133
+ throw new Error("Invalid controlplane response: missing `connect_artifact`");
134
+ }
135
+ return assertConnectArtifact(data.connect_artifact);
136
+ }
@@ -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)
@@ -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";
@@ -1,3 +1,4 @@
1
1
  export { createNodeWsFactory } from "./wsFactory.js";
2
2
  export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
3
+ export { createDirectNodeReconnectConfig, createNodeReconnectConfig, createTunnelNodeReconnectConfig, } from "./reconnectConfig.js";
3
4
  export { assertConnectArtifact } from "../connect/artifact.js";
@@ -0,0 +1,42 @@
1
+ import type { ConnectArtifact } from "../connect/artifact.js";
2
+ import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
3
+ import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
4
+ import type { ClientObserverLike } from "../observability/observer.js";
5
+ import type { DirectConnectOptions } from "../direct-client/connect.js";
6
+ import type { TunnelConnectOptions } from "../tunnel-client/connect.js";
7
+ import type { AutoReconnectConfig, ConnectConfig as ReconnectConnectConfig } from "../reconnect/index.js";
8
+ import { type ArtifactAwareReconnectConfig, type ArtifactFactoryArgs } from "../reconnect/artifactControlplane.js";
9
+ import type { RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/index.js";
10
+ type SharedReconnectOptions = Readonly<{
11
+ observer?: ClientObserverLike;
12
+ autoReconnect?: AutoReconnectConfig;
13
+ }>;
14
+ type TunnelReconnectConnectOptions = Omit<TunnelConnectOptions, "observer" | "signal">;
15
+ type DirectReconnectConnectOptions = Omit<DirectConnectOptions, "observer" | "signal">;
16
+ type ArtifactAwareTunnelReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
17
+ artifact?: ConnectArtifact;
18
+ getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
19
+ artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
20
+ }>;
21
+ type ArtifactAwareDirectReconnectConfig = ArtifactAwareReconnectConfig & Readonly<{
22
+ artifact?: ConnectArtifact;
23
+ getArtifact?: (args: ArtifactFactoryArgs) => Promise<ConnectArtifact>;
24
+ artifactControlplane?: RequestConnectArtifactInput | RequestEntryConnectArtifactInput;
25
+ }>;
26
+ export type TunnelNodeReconnectConfig = SharedReconnectOptions & ArtifactAwareTunnelReconnectConfig & Readonly<{
27
+ mode?: "tunnel";
28
+ connect?: TunnelReconnectConnectOptions;
29
+ grant?: ChannelInitGrant;
30
+ getGrant?: () => Promise<ChannelInitGrant>;
31
+ }>;
32
+ export type DirectNodeReconnectConfig = SharedReconnectOptions & ArtifactAwareDirectReconnectConfig & Readonly<{
33
+ mode: "direct";
34
+ connect?: DirectReconnectConnectOptions;
35
+ directInfo?: DirectConnectInfo;
36
+ getDirectInfo?: () => Promise<DirectConnectInfo>;
37
+ }>;
38
+ export type NodeReconnectConfig = TunnelNodeReconnectConfig | DirectNodeReconnectConfig;
39
+ export declare function createTunnelNodeReconnectConfig(config: TunnelNodeReconnectConfig): ReconnectConnectConfig;
40
+ export declare function createDirectNodeReconnectConfig(config: DirectNodeReconnectConfig): ReconnectConnectConfig;
41
+ export declare function createNodeReconnectConfig(config: NodeReconnectConfig): ReconnectConnectConfig;
42
+ export {};