@floegence/flowersec-core 0.17.2 → 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.
Files changed (48) hide show
  1. package/README.md +34 -9
  2. package/dist/browser/controlplane.d.ts +5 -18
  3. package/dist/browser/controlplane.js +17 -67
  4. package/dist/browser/index.d.ts +4 -2
  5. package/dist/browser/index.js +2 -1
  6. package/dist/browser/reconnectConfig.d.ts +15 -2
  7. package/dist/browser/reconnectConfig.js +43 -12
  8. package/dist/client-connect/connectCore.d.ts +5 -0
  9. package/dist/client-connect/connectCore.js +1 -1
  10. package/dist/client-connect/tunnelAttachCloseReason.d.ts +1 -1
  11. package/dist/client-connect/tunnelAttachCloseReason.js +4 -1
  12. package/dist/connect/artifact.d.ts +37 -0
  13. package/dist/connect/artifact.js +217 -0
  14. package/dist/connect/internalNormalize.d.ts +22 -0
  15. package/dist/connect/internalNormalize.js +173 -0
  16. package/dist/controlplane/index.d.ts +2 -0
  17. package/dist/controlplane/index.js +1 -0
  18. package/dist/controlplane/request.d.ts +50 -0
  19. package/dist/controlplane/request.js +136 -0
  20. package/dist/e2ee/secureChannel.js +2 -5
  21. package/dist/facade.d.ts +4 -0
  22. package/dist/facade.js +15 -47
  23. package/dist/node/connect.d.ts +3 -0
  24. package/dist/node/index.d.ts +4 -0
  25. package/dist/node/index.js +2 -0
  26. package/dist/node/reconnectConfig.d.ts +42 -0
  27. package/dist/node/reconnectConfig.js +78 -0
  28. package/dist/observability/observer.d.ts +28 -2
  29. package/dist/observability/observer.js +296 -13
  30. package/dist/proxy/bootstrap.d.ts +22 -4
  31. package/dist/proxy/bootstrap.js +51 -6
  32. package/dist/proxy/index.d.ts +3 -1
  33. package/dist/proxy/index.js +2 -1
  34. package/dist/proxy/integration.d.ts +3 -3
  35. package/dist/proxy/integration.js +21 -10
  36. package/dist/proxy/preset.d.ts +31 -0
  37. package/dist/proxy/preset.js +133 -0
  38. package/dist/proxy/profiles.d.ts +2 -0
  39. package/dist/proxy/profiles.js +40 -17
  40. package/dist/proxy/runtimeScope.d.ts +49 -0
  41. package/dist/proxy/runtimeScope.js +223 -0
  42. package/dist/reconnect/artifactControlplane.d.ts +13 -0
  43. package/dist/reconnect/artifactControlplane.js +34 -0
  44. package/dist/reconnect/index.d.ts +1 -1
  45. package/dist/reconnect/index.js +13 -6
  46. package/dist/utils/errors.d.ts +1 -1
  47. package/dist/utils/errors.js +3 -1
  48. package/package.json +7 -3
@@ -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 {};
@@ -0,0 +1,173 @@
1
+ import { Role as ControlRole } from "../gen/flowersec/controlplane/v1.gen.js";
2
+ import { emitObserverDiagnostic, normalizeObserver, withObserverContext } from "../observability/observer.js";
3
+ import { FlowersecError } from "../utils/errors.js";
4
+ import { assertConnectArtifact, hasArtifactOnlyFields, } from "./artifact.js";
5
+ function maybeParseJSON(input) {
6
+ if (typeof input !== "string")
7
+ return input;
8
+ const s = input.trim();
9
+ if (s === "")
10
+ return input;
11
+ if (s[0] !== "{" && s[0] !== "[")
12
+ return input;
13
+ try {
14
+ return JSON.parse(s);
15
+ }
16
+ catch (e) {
17
+ throw new FlowersecError({
18
+ path: "auto",
19
+ stage: "validate",
20
+ code: "invalid_input",
21
+ message: "invalid JSON string",
22
+ cause: e,
23
+ });
24
+ }
25
+ }
26
+ async function validateArtifactScopes(artifact, opts, observer) {
27
+ const scoped = artifact.scoped ?? [];
28
+ if (scoped.length === 0)
29
+ return;
30
+ const path = artifact.transport;
31
+ for (const entry of scoped) {
32
+ const resolver = opts.scopeResolvers?.[entry.scope];
33
+ if (resolver == null) {
34
+ if (entry.critical) {
35
+ throw new FlowersecError({
36
+ path,
37
+ stage: "validate",
38
+ code: "resolve_failed",
39
+ message: `missing scope resolver for ${entry.scope}@${entry.scope_version}`,
40
+ });
41
+ }
42
+ emitObserverDiagnostic(observer, {
43
+ path,
44
+ stage: "scope",
45
+ code_domain: "event",
46
+ code: "scope_ignored_missing_resolver",
47
+ result: "skip",
48
+ });
49
+ continue;
50
+ }
51
+ try {
52
+ await resolver(entry);
53
+ }
54
+ catch (e) {
55
+ if (!entry.critical && opts.relaxedOptionalScopeValidation === true) {
56
+ emitObserverDiagnostic(observer, {
57
+ path,
58
+ stage: "scope",
59
+ code_domain: "event",
60
+ code: "scope_ignored_relaxed_validation",
61
+ result: "skip",
62
+ });
63
+ continue;
64
+ }
65
+ throw new FlowersecError({
66
+ path,
67
+ stage: "validate",
68
+ code: "resolve_failed",
69
+ message: `scope validation failed for ${entry.scope}@${entry.scope_version}`,
70
+ cause: e,
71
+ });
72
+ }
73
+ }
74
+ }
75
+ export async function normalizeConnectInput(input, opts = {}) {
76
+ const v = maybeParseJSON(input);
77
+ if (v == null || typeof v !== "object") {
78
+ throw new FlowersecError({
79
+ path: "auto",
80
+ stage: "validate",
81
+ code: "invalid_input",
82
+ message: "invalid input: expected an object or a JSON string",
83
+ });
84
+ }
85
+ const o = v;
86
+ const hasWsUrl = Object.prototype.hasOwnProperty.call(o, "ws_url");
87
+ const hasTunnelUrl = Object.prototype.hasOwnProperty.call(o, "tunnel_url");
88
+ const hasGrantClient = Object.prototype.hasOwnProperty.call(o, "grant_client");
89
+ const hasGrantServer = Object.prototype.hasOwnProperty.call(o, "grant_server");
90
+ const isArtifactCandidate = hasArtifactOnlyFields(o);
91
+ if ((hasWsUrl && (hasTunnelUrl || hasGrantClient || hasGrantServer)) || (hasTunnelUrl && (hasGrantClient || hasGrantServer))) {
92
+ throw new FlowersecError({
93
+ path: "auto",
94
+ stage: "validate",
95
+ code: "invalid_input",
96
+ message: "hybrid connect input is not allowed",
97
+ });
98
+ }
99
+ if (isArtifactCandidate && (hasWsUrl || hasTunnelUrl || hasGrantClient || hasGrantServer)) {
100
+ throw new FlowersecError({
101
+ path: "auto",
102
+ stage: "validate",
103
+ code: "invalid_input",
104
+ message: "artifact fields cannot be mixed with legacy connect inputs",
105
+ });
106
+ }
107
+ if (hasGrantServer) {
108
+ throw new FlowersecError({
109
+ path: "tunnel",
110
+ stage: "validate",
111
+ code: "role_mismatch",
112
+ message: "expected role=client",
113
+ });
114
+ }
115
+ if (hasGrantClient)
116
+ return { kind: "tunnel", input: v };
117
+ if (hasWsUrl)
118
+ return { kind: "direct", input: v };
119
+ if (hasTunnelUrl) {
120
+ if (typeof o.role === "number" && Number.isSafeInteger(o.role) && o.role === ControlRole.Role_server) {
121
+ throw new FlowersecError({
122
+ path: "tunnel",
123
+ stage: "validate",
124
+ code: "role_mismatch",
125
+ message: "expected role=client",
126
+ });
127
+ }
128
+ return { kind: "tunnel", input: v };
129
+ }
130
+ if (isArtifactCandidate) {
131
+ let artifact;
132
+ try {
133
+ artifact = assertConnectArtifact(v);
134
+ }
135
+ catch (e) {
136
+ throw new FlowersecError({
137
+ path: "auto",
138
+ stage: "validate",
139
+ code: "invalid_input",
140
+ message: "invalid ConnectArtifact",
141
+ cause: e,
142
+ });
143
+ }
144
+ const observer = opts.observer == null
145
+ ? undefined
146
+ : normalizeObserver(withObserverContext(opts.observer, {
147
+ path: artifact.transport,
148
+ ...(artifact.correlation?.trace_id === undefined ? {} : { traceId: artifact.correlation.trace_id }),
149
+ ...(artifact.correlation?.session_id === undefined ? {} : { sessionId: artifact.correlation.session_id }),
150
+ }), { path: artifact.transport });
151
+ await validateArtifactScopes(artifact, opts, observer);
152
+ if (artifact.transport === "direct") {
153
+ return {
154
+ kind: "direct",
155
+ input: artifact.direct_info,
156
+ ...(artifact.correlation === undefined ? {} : { correlation: artifact.correlation }),
157
+ ...(observer === undefined ? {} : { observer }),
158
+ };
159
+ }
160
+ return {
161
+ kind: "tunnel",
162
+ input: artifact.tunnel_grant,
163
+ ...(artifact.correlation === undefined ? {} : { correlation: artifact.correlation }),
164
+ ...(observer === undefined ? {} : { observer }),
165
+ };
166
+ }
167
+ throw new FlowersecError({
168
+ path: "auto",
169
+ stage: "validate",
170
+ code: "invalid_input",
171
+ message: "invalid input: expected DirectConnectInfo, ChannelInitGrant, wrapper, or ConnectArtifact",
172
+ });
173
+ }
@@ -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)
package/dist/facade.d.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  import type { Client } from "./client.js";
2
2
  import type { DirectConnectOptions } from "./direct-client/connect.js";
3
3
  import type { TunnelConnectOptions } from "./tunnel-client/connect.js";
4
+ import { type ConnectArtifact, type CorrelationContext, type CorrelationKV, type DirectClientConnectArtifact, type ScopeMetadataEntry, type TunnelClientConnectArtifact } from "./connect/artifact.js";
4
5
  import type { ChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
5
6
  import type { DirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
6
7
  export type { ChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
7
8
  export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
8
9
  export type { DirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
9
10
  export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
11
+ export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, };
12
+ export { assertConnectArtifact } from "./connect/artifact.js";
10
13
  export type { ClientObserverLike } from "./observability/observer.js";
11
14
  export type { Client, ClientPath } from "./client.js";
12
15
  export type { FlowersecErrorCode, FlowersecPath, FlowersecStage } from "./utils/errors.js";
@@ -18,4 +21,5 @@ export declare function connectTunnel(grant: ChannelInitGrant, opts: TunnelConne
18
21
  export declare function connectDirect(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
19
22
  export declare function connect(input: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
20
23
  export declare function connect(input: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
24
+ export declare function connect(input: ConnectArtifact, opts: ConnectOptions): Promise<Client>;
21
25
  export declare function connect(input: unknown, opts: ConnectOptions): Promise<Client>;
package/dist/facade.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { connectDirect as connectDirectInternal } from "./direct-client/connect.js";
2
2
  import { connectTunnel as connectTunnelInternal } from "./tunnel-client/connect.js";
3
- import { FlowersecError } from "./utils/errors.js";
3
+ import { normalizeConnectInput } from "./connect/internalNormalize.js";
4
+ import { withObserverContext } from "./observability/observer.js";
4
5
  export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
5
6
  export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
7
+ export { assertConnectArtifact } from "./connect/artifact.js";
6
8
  export { FlowersecError } from "./utils/errors.js";
7
9
  export async function connectTunnel(grant, opts) {
8
10
  return await connectTunnelInternal(grant, opts);
@@ -10,52 +12,18 @@ export async function connectTunnel(grant, opts) {
10
12
  export async function connectDirect(info, opts) {
11
13
  return await connectDirectInternal(info, opts);
12
14
  }
13
- function maybeParseJSON(input) {
14
- if (typeof input !== "string")
15
- return input;
16
- const s = input.trim();
17
- if (s === "")
18
- return input;
19
- if (s[0] !== "{" && s[0] !== "[")
20
- return input;
21
- try {
22
- return JSON.parse(s);
23
- }
24
- catch (e) {
25
- throw new FlowersecError({
26
- path: "auto",
27
- stage: "validate",
28
- code: "invalid_input",
29
- message: "invalid JSON string",
30
- cause: e,
31
- });
32
- }
33
- }
34
15
  export async function connect(input, opts) {
35
- const v = maybeParseJSON(input);
36
- if (v != null && typeof v === "object") {
37
- const o = v;
38
- if (o["ws_url"] !== undefined)
39
- return await connectDirectInternal(v, opts);
40
- if (o["grant_client"] !== undefined)
41
- return await connectTunnelInternal(v, opts);
42
- if (o["grant_server"] !== undefined)
43
- return await connectTunnelInternal(v, opts);
44
- if (o["tunnel_url"] !== undefined)
45
- return await connectTunnelInternal(v, opts);
46
- if (o["token"] !== undefined || o["role"] !== undefined)
47
- return await connectTunnelInternal(v, opts);
48
- throw new FlowersecError({
49
- path: "auto",
50
- stage: "validate",
51
- code: "invalid_input",
52
- message: "invalid input: expected DirectConnectInfo (ws_url) or ChannelInitGrant (tunnel_url, grant_client, or grant_server)",
53
- });
16
+ const normalized = await normalizeConnectInput(input, opts);
17
+ const nextObserver = normalized.observer ??
18
+ (normalized.correlation == null
19
+ ? opts.observer
20
+ : withObserverContext(opts.observer, {
21
+ ...(normalized.correlation.trace_id === undefined ? {} : { traceId: normalized.correlation.trace_id }),
22
+ ...(normalized.correlation.session_id === undefined ? {} : { sessionId: normalized.correlation.session_id }),
23
+ }));
24
+ const nextOpts = (nextObserver === opts.observer ? opts : { ...opts, observer: nextObserver });
25
+ if (normalized.kind === "direct") {
26
+ return await connectDirectInternal(normalized.input, nextOpts);
54
27
  }
55
- throw new FlowersecError({
56
- path: "auto",
57
- stage: "validate",
58
- code: "invalid_input",
59
- message: "invalid input: expected an object or a JSON string",
60
- });
28
+ return await connectTunnelInternal(normalized.input, nextOpts);
61
29
  }
@@ -1,9 +1,12 @@
1
1
  import type { Client } from "../client.js";
2
2
  import type { DirectConnectOptions } from "../direct-client/connect.js";
3
3
  import type { TunnelConnectOptions } from "../tunnel-client/connect.js";
4
+ import { type ConnectOptions } from "../facade.js";
5
+ import type { ConnectArtifact } from "../connect/artifact.js";
4
6
  import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
5
7
  import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
6
8
  export declare function connectNode(input: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
7
9
  export declare function connectNode(input: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
10
+ export declare function connectNode(input: ConnectArtifact, opts: ConnectOptions): Promise<Client>;
8
11
  export declare function connectTunnelNode(grant: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
9
12
  export declare function connectDirectNode(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
@@ -1,2 +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";
5
+ export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
6
+ export { assertConnectArtifact } from "../connect/artifact.js";
@@ -1,2 +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";
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 {};
@@ -0,0 +1,78 @@
1
+ import { resolveConnectArtifact, updateTraceId, } from "../reconnect/artifactControlplane.js";
2
+ import { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
3
+ async function resolveTunnelGrant(config) {
4
+ if (config.getGrant)
5
+ return await config.getGrant();
6
+ if (config.grant)
7
+ return config.grant;
8
+ throw new Error("Tunnel reconnect config requires `getGrant` or `grant`");
9
+ }
10
+ async function resolveDirectInfo(config) {
11
+ if (config.getDirectInfo)
12
+ return await config.getDirectInfo();
13
+ if (config.directInfo)
14
+ return config.directInfo;
15
+ throw new Error("Direct reconnect config requires `getDirectInfo` or `directInfo`");
16
+ }
17
+ export function createTunnelNodeReconnectConfig(config) {
18
+ let traceId = config.artifact?.correlation?.trace_id;
19
+ return {
20
+ ...(config.observer === undefined ? {} : { observer: config.observer }),
21
+ ...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
22
+ connectOnce: async ({ signal, observer }) => {
23
+ if (config.getArtifact || config.artifact || config.artifactControlplane) {
24
+ const artifact = await resolveConnectArtifact(config, traceId, signal);
25
+ if (artifact.transport !== "tunnel") {
26
+ throw new Error("Tunnel reconnect config requires a tunnel ConnectArtifact");
27
+ }
28
+ traceId = updateTraceId(traceId, artifact);
29
+ const connectOptions = {
30
+ ...(config.connect === undefined ? {} : config.connect),
31
+ signal,
32
+ observer,
33
+ };
34
+ return await connectNode(artifact, connectOptions);
35
+ }
36
+ const connectOptions = {
37
+ ...(config.connect === undefined ? {} : config.connect),
38
+ signal,
39
+ observer,
40
+ };
41
+ return await connectTunnelNode(await resolveTunnelGrant(config), connectOptions);
42
+ },
43
+ };
44
+ }
45
+ export function createDirectNodeReconnectConfig(config) {
46
+ let traceId = config.artifact?.correlation?.trace_id;
47
+ return {
48
+ ...(config.observer === undefined ? {} : { observer: config.observer }),
49
+ ...(config.autoReconnect === undefined ? {} : { autoReconnect: config.autoReconnect }),
50
+ connectOnce: async ({ signal, observer }) => {
51
+ if (config.getArtifact || config.artifact || config.artifactControlplane) {
52
+ const artifact = await resolveConnectArtifact(config, traceId, signal);
53
+ if (artifact.transport !== "direct") {
54
+ throw new Error("Direct reconnect config requires a direct ConnectArtifact");
55
+ }
56
+ traceId = updateTraceId(traceId, artifact);
57
+ const connectOptions = {
58
+ ...(config.connect === undefined ? {} : config.connect),
59
+ signal,
60
+ observer,
61
+ };
62
+ return await connectNode(artifact, connectOptions);
63
+ }
64
+ const connectOptions = {
65
+ ...(config.connect === undefined ? {} : config.connect),
66
+ signal,
67
+ observer,
68
+ };
69
+ return await connectDirectNode(await resolveDirectInfo(config), connectOptions);
70
+ },
71
+ };
72
+ }
73
+ export function createNodeReconnectConfig(config) {
74
+ if (config.mode === "direct") {
75
+ return createDirectNodeReconnectConfig(config);
76
+ }
77
+ return createTunnelNodeReconnectConfig(config);
78
+ }