@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.
@@ -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 { 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,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";
@@ -1,2 +1,3 @@
1
1
  export { createNodeWsFactory } from "./wsFactory.js";
2
2
  export { connectDirectNode, connectNode, connectTunnelNode } from "./connect.js";
3
+ export { assertConnectArtifact } from "../connect/artifact.js";
@@ -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 normalizeObserver(observer?: ClientObserverLike): ClientObserver;
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
- export function normalizeObserver(observer) {
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 NoopObserver;
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
- onConnect: observer.onConnect ?? NoopObserver.onConnect,
15
- onAttach: observer.onAttach ?? NoopObserver.onAttach,
16
- onHandshake: observer.onHandshake ?? NoopObserver.onHandshake,
17
- onWsClose: observer.onWsClose ?? NoopObserver.onWsClose,
18
- onWsError: observer.onWsError ?? NoopObserver.onWsError,
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() / 1000;
281
+ return performance.now();
26
282
  }
27
- return Date.now() / 1000;
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 ProxyIntegrationServiceWorkerOptions, type RegisterProxyIntegrationOptions } from "./integration.js";
4
+ import { type ProxyIntegrationPlugin, type RegisterProxyIntegrationOptions, type ProxyIntegrationServiceWorkerOptions } from "./integration.js";
5
5
  import { type RegisterProxyControllerWindowOptions } from "./controllerWindow.js";
6
- import type { ProxyProfile, ProxyProfileName } from "./profiles.js";
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
- profile?: ProxyProfileName | Partial<ProxyProfile>;
10
+ preset?: ProxyPresetInput;
11
11
  runtimeGlobalKey?: string;
12
12
  runtime?: RegisterProxyIntegrationOptions["runtime"];
13
13
  serviceWorker: ProxyIntegrationServiceWorkerOptions;
@@ -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.profile === undefined ? {} : { profile: opts.profile }),
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 }),