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