@floegence/flowersec-core 0.17.1 → 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 +5 -2
|
@@ -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
|
+
}
|
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 {
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/node/connect.d.ts
CHANGED
|
@@ -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>;
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { createNodeWsFactory } from "./wsFactory.js";
|
|
2
2
|
export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
|
|
3
|
+
export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
|
|
4
|
+
export { assertConnectArtifact } from "../connect/artifact.js";
|
package/dist/node/index.js
CHANGED
|
@@ -8,6 +8,27 @@ export type HandshakeReason = "auth_tag_mismatch" | "handshake_failed" | "invali
|
|
|
8
8
|
export type WsCloseKind = "local" | "peer_or_error";
|
|
9
9
|
export type WsErrorReason = "error" | "recv_buffer_exceeded" | "unexpected_text_frame" | "unexpected_message_type";
|
|
10
10
|
export type RpcCallResult = "ok" | "rpc_error" | "handler_not_found" | "transport_error" | "canceled";
|
|
11
|
+
export type DiagnosticEvent = Readonly<{
|
|
12
|
+
v: 1;
|
|
13
|
+
namespace: "connect";
|
|
14
|
+
path: ClientPath | "auto";
|
|
15
|
+
stage: "validate" | "normalize" | "scope" | "connect" | "attach" | "handshake" | "close" | "reconnect";
|
|
16
|
+
code_domain: "error" | "event";
|
|
17
|
+
code: string;
|
|
18
|
+
result: "ok" | "fail" | "retry" | "skip";
|
|
19
|
+
elapsed_ms: number;
|
|
20
|
+
attempt_seq: number;
|
|
21
|
+
trace_id?: string;
|
|
22
|
+
session_id?: string;
|
|
23
|
+
}>;
|
|
24
|
+
type ObserverContext = Readonly<{
|
|
25
|
+
path?: ClientPath | "auto";
|
|
26
|
+
traceId?: string;
|
|
27
|
+
sessionId?: string;
|
|
28
|
+
attemptSeq?: number;
|
|
29
|
+
attemptStartMs?: number;
|
|
30
|
+
maxQueuedItems?: number;
|
|
31
|
+
}>;
|
|
11
32
|
export type ClientObserver = {
|
|
12
33
|
onConnect(path: ClientPath, result: ConnectResult, reason: ConnectReason | undefined, elapsedSeconds: number): void;
|
|
13
34
|
onAttach(result: AttachResult, reason: AttachReason | undefined): void;
|
|
@@ -16,8 +37,13 @@ export type ClientObserver = {
|
|
|
16
37
|
onWsError(reason: WsErrorReason): void;
|
|
17
38
|
onRpcCall(result: RpcCallResult, elapsedSeconds: number): void;
|
|
18
39
|
onRpcNotify(): void;
|
|
40
|
+
onDiagnosticEvent(event: DiagnosticEvent): void;
|
|
19
41
|
};
|
|
20
42
|
export type ClientObserverLike = Partial<ClientObserver>;
|
|
43
|
+
type DiagnosticEventInput = Omit<DiagnosticEvent, "v" | "namespace" | "elapsed_ms" | "attempt_seq" | "trace_id" | "session_id" | "path"> & Partial<Pick<DiagnosticEvent, "path">>;
|
|
21
44
|
export declare const NoopObserver: ClientObserver;
|
|
22
|
-
export declare function
|
|
45
|
+
export declare function withObserverContext(observer: ClientObserverLike | undefined, context: ObserverContext): ClientObserverLike | undefined;
|
|
46
|
+
export declare function emitObserverDiagnostic(observer: ClientObserverLike | undefined, event: DiagnosticEventInput): void;
|
|
47
|
+
export declare function normalizeObserver(observer?: ClientObserverLike, context?: ObserverContext): ClientObserver;
|
|
23
48
|
export declare function nowSeconds(): number;
|
|
49
|
+
export {};
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const OBSERVER_CONTEXT = Symbol.for("floegence.flowersec.observer_context");
|
|
2
|
+
const NORMALIZED_OBSERVER = Symbol.for("floegence.flowersec.normalized_observer");
|
|
3
|
+
const DEFAULT_MAX_QUEUED_ITEMS = 64;
|
|
1
4
|
export const NoopObserver = {
|
|
2
5
|
onConnect: () => { },
|
|
3
6
|
onAttach: () => { },
|
|
@@ -5,24 +8,277 @@ export const NoopObserver = {
|
|
|
5
8
|
onWsClose: () => { },
|
|
6
9
|
onWsError: () => { },
|
|
7
10
|
onRpcCall: () => { },
|
|
8
|
-
onRpcNotify: () => { }
|
|
11
|
+
onRpcNotify: () => { },
|
|
12
|
+
onDiagnosticEvent: () => { },
|
|
9
13
|
};
|
|
10
|
-
|
|
14
|
+
function getObserverContext(observer) {
|
|
15
|
+
const context = observer?.[OBSERVER_CONTEXT];
|
|
16
|
+
if (context == null || typeof context !== "object")
|
|
17
|
+
return {};
|
|
18
|
+
return context;
|
|
19
|
+
}
|
|
20
|
+
function safeInvoke(fn) {
|
|
21
|
+
if (fn == null)
|
|
22
|
+
return;
|
|
23
|
+
try {
|
|
24
|
+
fn();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Best effort only; observability must not affect connect semantics.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function hasAnyHandlers(observer) {
|
|
11
31
|
if (observer == null)
|
|
12
|
-
return
|
|
32
|
+
return false;
|
|
33
|
+
return (observer.onConnect != null ||
|
|
34
|
+
observer.onAttach != null ||
|
|
35
|
+
observer.onHandshake != null ||
|
|
36
|
+
observer.onWsClose != null ||
|
|
37
|
+
observer.onWsError != null ||
|
|
38
|
+
observer.onRpcCall != null ||
|
|
39
|
+
observer.onRpcNotify != null ||
|
|
40
|
+
observer.onDiagnosticEvent != null);
|
|
41
|
+
}
|
|
42
|
+
function buildDiagnosticEvent(context, event) {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
v: 1,
|
|
45
|
+
namespace: "connect",
|
|
46
|
+
path: event.path ?? context.path ?? "auto",
|
|
47
|
+
stage: event.stage,
|
|
48
|
+
code_domain: event.code_domain,
|
|
49
|
+
code: event.code,
|
|
50
|
+
result: event.result,
|
|
51
|
+
elapsed_ms: Math.max(0, Math.floor(nowMilliseconds() - (context.attemptStartMs ?? nowMilliseconds()))),
|
|
52
|
+
attempt_seq: Math.max(1, Math.floor(context.attemptSeq ?? 1)),
|
|
53
|
+
...(context.traceId === undefined ? {} : { trace_id: context.traceId }),
|
|
54
|
+
...(context.sessionId === undefined ? {} : { session_id: context.sessionId }),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function mapConnectDiagnostic(path, result, reason) {
|
|
58
|
+
if (result === "ok") {
|
|
59
|
+
return { path, stage: "connect", code_domain: "event", code: "connect_ok", result: "ok" };
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
path,
|
|
63
|
+
stage: "connect",
|
|
64
|
+
code_domain: "error",
|
|
65
|
+
code: reason === "timeout" || reason === "canceled" ? reason : "dial_failed",
|
|
66
|
+
result: "fail",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function mapAttachDiagnostic(result, reason, path) {
|
|
70
|
+
if (result === "ok") {
|
|
71
|
+
return { path, stage: "attach", code_domain: "event", code: "attach_ok", result: "ok" };
|
|
72
|
+
}
|
|
13
73
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
onRpcCall: observer.onRpcCall ?? NoopObserver.onRpcCall,
|
|
20
|
-
onRpcNotify: observer.onRpcNotify ?? NoopObserver.onRpcNotify
|
|
74
|
+
path,
|
|
75
|
+
stage: "attach",
|
|
76
|
+
code_domain: "error",
|
|
77
|
+
code: reason === "send_failed" ? "attach_failed" : (reason ?? "attach_failed"),
|
|
78
|
+
result: "fail",
|
|
21
79
|
};
|
|
22
80
|
}
|
|
81
|
+
function mapHandshakeDiagnostic(path, result, reason) {
|
|
82
|
+
if (result === "ok") {
|
|
83
|
+
return { path, stage: "handshake", code_domain: "event", code: "handshake_ok", result: "ok" };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
path,
|
|
87
|
+
stage: "handshake",
|
|
88
|
+
code_domain: "error",
|
|
89
|
+
code: reason ?? "handshake_failed",
|
|
90
|
+
result: "fail",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function mapWsCloseDiagnostic(kind, path) {
|
|
94
|
+
return {
|
|
95
|
+
path,
|
|
96
|
+
stage: "close",
|
|
97
|
+
code_domain: "event",
|
|
98
|
+
code: kind === "local" ? "ws_close_local" : "ws_close_peer_or_error",
|
|
99
|
+
result: "skip",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function mapWsErrorDiagnostic(path) {
|
|
103
|
+
return {
|
|
104
|
+
path,
|
|
105
|
+
stage: "close",
|
|
106
|
+
code_domain: "event",
|
|
107
|
+
code: "ws_error",
|
|
108
|
+
result: "skip",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
class ObserverDispatcher {
|
|
112
|
+
[NORMALIZED_OBSERVER] = true;
|
|
113
|
+
[OBSERVER_CONTEXT];
|
|
114
|
+
queue = [];
|
|
115
|
+
observer;
|
|
116
|
+
maxQueuedItems;
|
|
117
|
+
draining = false;
|
|
118
|
+
overflowQueued = false;
|
|
119
|
+
constructor(observer, context) {
|
|
120
|
+
this.observer = observer;
|
|
121
|
+
this[OBSERVER_CONTEXT] = context;
|
|
122
|
+
this.maxQueuedItems = Math.max(4, Math.floor(context.maxQueuedItems ?? DEFAULT_MAX_QUEUED_ITEMS));
|
|
123
|
+
}
|
|
124
|
+
onConnect(path, result, reason, elapsedSeconds) {
|
|
125
|
+
const diagnostic = mapConnectDiagnostic(path, result, reason);
|
|
126
|
+
this.enqueueCombined(() => this.observer.onConnect?.(path, result, reason, elapsedSeconds), diagnostic, result === "fail");
|
|
127
|
+
}
|
|
128
|
+
onAttach(result, reason) {
|
|
129
|
+
const diagnostic = mapAttachDiagnostic(result, reason, this[OBSERVER_CONTEXT].path ?? "auto");
|
|
130
|
+
this.enqueueCombined(() => this.observer.onAttach?.(result, reason), diagnostic, result === "fail");
|
|
131
|
+
}
|
|
132
|
+
onHandshake(path, result, reason, elapsedSeconds) {
|
|
133
|
+
const diagnostic = mapHandshakeDiagnostic(path, result, reason);
|
|
134
|
+
this.enqueueCombined(() => this.observer.onHandshake?.(path, result, reason, elapsedSeconds), diagnostic, result === "fail");
|
|
135
|
+
}
|
|
136
|
+
onWsClose(kind, code) {
|
|
137
|
+
const diagnostic = mapWsCloseDiagnostic(kind, this[OBSERVER_CONTEXT].path ?? "auto");
|
|
138
|
+
this.enqueueCombined(() => this.observer.onWsClose?.(kind, code), diagnostic, false);
|
|
139
|
+
}
|
|
140
|
+
onWsError(reason) {
|
|
141
|
+
const diagnostic = mapWsErrorDiagnostic(this[OBSERVER_CONTEXT].path ?? "auto");
|
|
142
|
+
this.enqueueCombined(() => this.observer.onWsError?.(reason), diagnostic, false);
|
|
143
|
+
}
|
|
144
|
+
onRpcCall(result, elapsedSeconds) {
|
|
145
|
+
this.enqueueCombined(() => this.observer.onRpcCall?.(result, elapsedSeconds), undefined, false);
|
|
146
|
+
}
|
|
147
|
+
onRpcNotify() {
|
|
148
|
+
this.enqueueCombined(() => this.observer.onRpcNotify?.(), undefined, false);
|
|
149
|
+
}
|
|
150
|
+
onDiagnosticEvent(event) {
|
|
151
|
+
this.enqueueCombined(() => this.observer.onDiagnosticEvent?.(event), undefined, event.result === "fail");
|
|
152
|
+
}
|
|
153
|
+
emitDiagnosticEvent(event) {
|
|
154
|
+
const diagnostic = buildDiagnosticEvent(this[OBSERVER_CONTEXT], event);
|
|
155
|
+
this.enqueue({
|
|
156
|
+
kind: event.code === "diagnostics_overflow" ? "overflow" : event.result === "fail" ? "terminal" : "normal",
|
|
157
|
+
deliver: () => this.observer.onDiagnosticEvent?.(diagnostic),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
enqueueCombined(callback, diagnostic, terminal) {
|
|
161
|
+
if (callback == null && diagnostic == null)
|
|
162
|
+
return;
|
|
163
|
+
const event = diagnostic == null ? undefined : buildDiagnosticEvent(this[OBSERVER_CONTEXT], diagnostic);
|
|
164
|
+
this.enqueue({
|
|
165
|
+
kind: terminal ? "terminal" : "normal",
|
|
166
|
+
deliver: () => {
|
|
167
|
+
safeInvoke(callback);
|
|
168
|
+
if (event != null) {
|
|
169
|
+
safeInvoke(() => this.observer.onDiagnosticEvent?.(event));
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
enqueue(item) {
|
|
175
|
+
if (this.queue.length >= this.maxQueuedItems) {
|
|
176
|
+
if (!this.makeRoom(item.kind)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (item.kind === "overflow") {
|
|
181
|
+
if (this.overflowQueued)
|
|
182
|
+
return;
|
|
183
|
+
this.overflowQueued = true;
|
|
184
|
+
}
|
|
185
|
+
this.queue.push(item);
|
|
186
|
+
this.scheduleDrain();
|
|
187
|
+
}
|
|
188
|
+
makeRoom(kind) {
|
|
189
|
+
const removableIndex = this.queue.findIndex((entry) => entry.kind === "normal");
|
|
190
|
+
if (removableIndex >= 0) {
|
|
191
|
+
this.queue.splice(removableIndex, 1);
|
|
192
|
+
if (kind === "normal") {
|
|
193
|
+
this.queueOverflowEvent();
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
if (kind === "normal") {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (this.queue.length > 0) {
|
|
201
|
+
const shifted = this.queue.shift();
|
|
202
|
+
if (shifted?.kind === "overflow")
|
|
203
|
+
this.overflowQueued = false;
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
queueOverflowEvent() {
|
|
209
|
+
if (this.overflowQueued || this.observer.onDiagnosticEvent == null)
|
|
210
|
+
return;
|
|
211
|
+
this.enqueue({
|
|
212
|
+
kind: "overflow",
|
|
213
|
+
deliver: () => {
|
|
214
|
+
this.overflowQueued = false;
|
|
215
|
+
safeInvoke(() => this.observer.onDiagnosticEvent?.(buildDiagnosticEvent(this[OBSERVER_CONTEXT], {
|
|
216
|
+
stage: "reconnect",
|
|
217
|
+
code_domain: "event",
|
|
218
|
+
code: "diagnostics_overflow",
|
|
219
|
+
result: "skip",
|
|
220
|
+
})));
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
scheduleDrain() {
|
|
225
|
+
if (this.draining)
|
|
226
|
+
return;
|
|
227
|
+
this.draining = true;
|
|
228
|
+
queueMicrotask(() => this.drainOne());
|
|
229
|
+
}
|
|
230
|
+
drainOne() {
|
|
231
|
+
this.draining = false;
|
|
232
|
+
const item = this.queue.shift();
|
|
233
|
+
if (item == null)
|
|
234
|
+
return;
|
|
235
|
+
if (item.kind === "overflow") {
|
|
236
|
+
this.overflowQueued = false;
|
|
237
|
+
}
|
|
238
|
+
safeInvoke(item.deliver);
|
|
239
|
+
if (this.queue.length > 0) {
|
|
240
|
+
this.scheduleDrain();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function withObserverContext(observer, context) {
|
|
245
|
+
if (observer == null && Object.keys(context).length === 0)
|
|
246
|
+
return observer;
|
|
247
|
+
const previous = getObserverContext(observer);
|
|
248
|
+
return Object.assign({}, observer ?? {}, {
|
|
249
|
+
[OBSERVER_CONTEXT]: {
|
|
250
|
+
...previous,
|
|
251
|
+
...context,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
export function emitObserverDiagnostic(observer, event) {
|
|
256
|
+
const normalized = normalizeObserver(observer);
|
|
257
|
+
if (normalized.emitDiagnosticEvent) {
|
|
258
|
+
normalized.emitDiagnosticEvent(event);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
safeInvoke(() => normalized.onDiagnosticEvent(buildDiagnosticEvent(getObserverContext(observer), event)));
|
|
262
|
+
}
|
|
263
|
+
export function normalizeObserver(observer, context = {}) {
|
|
264
|
+
if (observer?.[NORMALIZED_OBSERVER] === true) {
|
|
265
|
+
return observer;
|
|
266
|
+
}
|
|
267
|
+
if (!hasAnyHandlers(observer))
|
|
268
|
+
return NoopObserver;
|
|
269
|
+
return new ObserverDispatcher(observer ?? {}, {
|
|
270
|
+
attemptSeq: 1,
|
|
271
|
+
attemptStartMs: nowMilliseconds(),
|
|
272
|
+
...getObserverContext(observer),
|
|
273
|
+
...context,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
23
276
|
export function nowSeconds() {
|
|
277
|
+
return nowMilliseconds() / 1000;
|
|
278
|
+
}
|
|
279
|
+
function nowMilliseconds() {
|
|
24
280
|
if (typeof performance !== "undefined" && typeof performance.now === "function") {
|
|
25
|
-
return performance.now()
|
|
281
|
+
return performance.now();
|
|
26
282
|
}
|
|
27
|
-
return Date.now()
|
|
283
|
+
return Date.now();
|
|
28
284
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Client } from "../client.js";
|
|
2
2
|
import type { TunnelConnectBrowserOptions } from "../browser/connect.js";
|
|
3
3
|
import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
4
|
-
import { type ProxyIntegrationPlugin, type
|
|
4
|
+
import { type ProxyIntegrationPlugin, type RegisterProxyIntegrationOptions, type ProxyIntegrationServiceWorkerOptions } from "./integration.js";
|
|
5
5
|
import { type RegisterProxyControllerWindowOptions } from "./controllerWindow.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ProxyPresetInput } from "./preset.js";
|
|
7
7
|
import { type ProxyRuntime } from "./runtime.js";
|
|
8
8
|
export type ConnectTunnelProxyBrowserOptions = Readonly<{
|
|
9
9
|
connect?: TunnelConnectBrowserOptions;
|
|
10
|
-
|
|
10
|
+
preset?: ProxyPresetInput;
|
|
11
11
|
runtimeGlobalKey?: string;
|
|
12
12
|
runtime?: RegisterProxyIntegrationOptions["runtime"];
|
|
13
13
|
serviceWorker: ProxyIntegrationServiceWorkerOptions;
|
package/dist/proxy/bootstrap.js
CHANGED
|
@@ -4,10 +4,12 @@ import { registerProxyControllerWindow } from "./controllerWindow.js";
|
|
|
4
4
|
import { createProxyRuntime } from "./runtime.js";
|
|
5
5
|
export async function connectTunnelProxyBrowser(grant, opts) {
|
|
6
6
|
const client = await connectTunnelBrowser(grant, opts.connect ?? {});
|
|
7
|
+
const compat = opts;
|
|
7
8
|
const integrationInput = {
|
|
8
9
|
client,
|
|
9
10
|
serviceWorker: opts.serviceWorker,
|
|
10
|
-
...(opts.
|
|
11
|
+
...(opts.preset === undefined ? {} : { preset: opts.preset }),
|
|
12
|
+
...(compat.profile === undefined ? {} : { profile: compat.profile }),
|
|
11
13
|
...(opts.runtimeGlobalKey === undefined ? {} : { runtimeGlobalKey: opts.runtimeGlobalKey }),
|
|
12
14
|
...(opts.runtime === undefined ? {} : { runtime: opts.runtime }),
|
|
13
15
|
...(opts.plugins === undefined ? {} : { plugins: opts.plugins }),
|