@floegence/flowersec-core 0.26.0 → 0.27.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.
@@ -2,11 +2,12 @@ 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
4
  import { type ConnectOptions } from "../facade.js";
5
+ import type { ConnectArtifact } from "../connect/artifact.js";
5
6
  import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
6
7
  import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
7
8
  export type TunnelConnectBrowserOptions = Omit<TunnelConnectOptions, "origin" | "wsFactory">;
8
9
  export type DirectConnectBrowserOptions = Omit<DirectConnectOptions, "origin" | "wsFactory">;
9
10
  export type ConnectBrowserOptions = Omit<ConnectOptions, "origin" | "wsFactory">;
10
- export declare function connectBrowser(input: unknown, opts?: ConnectBrowserOptions): Promise<Client>;
11
+ export declare function connectBrowser(input: ConnectArtifact, opts?: ConnectBrowserOptions): Promise<Client>;
11
12
  export declare function connectTunnelBrowser(grant: ChannelInitGrant, opts?: TunnelConnectBrowserOptions): Promise<Client>;
12
13
  export declare function connectDirectBrowser(info: DirectConnectInfo, opts?: DirectConnectBrowserOptions): Promise<Client>;
@@ -4,7 +4,7 @@ export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskA
4
4
  export type { TransportSecurityPolicy, TransportSecurityPolicyInput, TransportSecurityPolicyPreset, NetworkPlaintextPolicyOptions, } from "../client-connect/transportSecurity.js";
5
5
  export type { ConnectArtifact, CorrelationContext, CorrelationKV, DirectClientConnectArtifact, ScopeMetadataEntry, TunnelClientConnectArtifact, } from "../connect/artifact.js";
6
6
  export { assertConnectArtifact } from "../connect/artifact.js";
7
- export type { ControlplaneConfig, EntryControlplaneConfig } from "./controlplane.js";
8
- export { requestChannelGrant, requestEntryChannelGrant, } from "./controlplane.js";
7
+ export type { RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/request.js";
8
+ export { requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.js";
9
9
  export type { BrowserReconnectConfig, DirectBrowserReconnectConfig, TunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
10
10
  export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
@@ -1,5 +1,5 @@
1
1
  export { connectBrowser, connectDirectBrowser, connectTunnelBrowser } from "./connect.js";
2
2
  export { AllowPlaintextForLoopback, createNetworkPlaintextPolicy, PlaintextRiskAcceptance, RequireTLS, } from "../client-connect/transportSecurity.js";
3
3
  export { assertConnectArtifact } from "../connect/artifact.js";
4
- export { requestChannelGrant, requestEntryChannelGrant, } from "./controlplane.js";
4
+ export { requestConnectArtifact, requestEntryConnectArtifact, } from "../controlplane/request.js";
5
5
  export { createBrowserReconnectConfig, createDirectBrowserReconnectConfig, createTunnelBrowserReconnectConfig, } from "./reconnectConfig.js";
@@ -2,7 +2,7 @@ import { type YamuxLimits } from "../yamux/session.js";
2
2
  import { type ClientObserverLike } from "../observability/observer.js";
3
3
  import { type WebSocketLike, type WebSocketLimits } from "../ws-client/binaryTransport.js";
4
4
  import type { ClientInternal } from "../client.js";
5
- import type { ConnectScopeResolverMap } from "../connect/internalNormalize.js";
5
+ import type { ConnectScopeResolverMap } from "../connect/resolveArtifact.js";
6
6
  import { type TransportSecurityPolicy } from "./transportSecurity.js";
7
7
  export type LivenessOptions = Readonly<{
8
8
  intervalMs?: number;
@@ -33,5 +33,4 @@ export type DirectClientConnectArtifact = Readonly<{
33
33
  correlation?: CorrelationContext;
34
34
  }>;
35
35
  export type ConnectArtifact = TunnelClientConnectArtifact | DirectClientConnectArtifact;
36
- export declare function hasArtifactOnlyFields(value: Record<string, unknown>): boolean;
37
36
  export declare function assertConnectArtifact(value: unknown): ConnectArtifact;
@@ -6,7 +6,6 @@ const CORRELATION_ID_RE = /^[A-Za-z0-9._~-]{8,128}$/;
6
6
  const encoder = new TextEncoder();
7
7
  const TUNNEL_ARTIFACT_KEYS = new Set(["v", "transport", "tunnel_grant", "scoped", "correlation"]);
8
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
9
  function isRecord(v) {
11
10
  return typeof v === "object" && v != null && !Array.isArray(v);
12
11
  }
@@ -177,9 +176,6 @@ function assertArtifactTransport(value) {
177
176
  throw new Error("bad ConnectArtifact.transport");
178
177
  return value;
179
178
  }
180
- export function hasArtifactOnlyFields(value) {
181
- return Object.keys(value).some((key) => ARTIFACT_ONLY_KEYS.has(key));
182
- }
183
179
  export function assertConnectArtifact(value) {
184
180
  const record = assertArtifactObject(value);
185
181
  if (record.v !== 1)
@@ -1,22 +1,22 @@
1
+ import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
2
+ import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
1
3
  import { type ClientObserverLike } from "../observability/observer.js";
2
- import { type CorrelationContext, type ScopeMetadataEntry } from "./artifact.js";
4
+ import { type ConnectArtifact, type ScopeMetadataEntry } from "./artifact.js";
3
5
  export type ConnectScopeResolver = (entry: ScopeMetadataEntry) => void | Promise<void>;
4
6
  export type ConnectScopeResolverMap = Readonly<Record<string, ConnectScopeResolver>>;
5
- type NormalizeOptions = Readonly<{
7
+ type ResolveOptions = Readonly<{
6
8
  observer?: ClientObserverLike;
7
9
  scopeResolvers?: ConnectScopeResolverMap;
8
10
  relaxedOptionalScopeValidation?: boolean;
9
11
  }>;
10
- export type NormalizedConnectInput = Readonly<{
12
+ export type ResolvedConnectArtifact = Readonly<{
11
13
  kind: "tunnel";
12
- input: unknown;
13
- correlation?: CorrelationContext;
14
+ input: ChannelInitGrant;
14
15
  observer?: ClientObserverLike;
15
16
  }> | Readonly<{
16
17
  kind: "direct";
17
- input: unknown;
18
- correlation?: CorrelationContext;
18
+ input: DirectConnectInfo;
19
19
  observer?: ClientObserverLike;
20
20
  }>;
21
- export declare function normalizeConnectInput(input: unknown, opts?: NormalizeOptions): Promise<NormalizedConnectInput>;
21
+ export declare function resolveConnectArtifact(input: ConnectArtifact, opts?: ResolveOptions): Promise<ResolvedConnectArtifact>;
22
22
  export {};
@@ -0,0 +1,87 @@
1
+ import { emitObserverDiagnostic, normalizeObserver, withObserverContext } from "../observability/observer.js";
2
+ import { FlowersecError } from "../utils/errors.js";
3
+ import { assertConnectArtifact, } from "./artifact.js";
4
+ async function validateArtifactScopes(artifact, opts, observer) {
5
+ const scoped = artifact.scoped ?? [];
6
+ if (scoped.length === 0)
7
+ return;
8
+ const path = artifact.transport;
9
+ for (const entry of scoped) {
10
+ const resolver = opts.scopeResolvers?.[entry.scope];
11
+ if (resolver == null) {
12
+ if (entry.critical) {
13
+ throw new FlowersecError({
14
+ path,
15
+ stage: "validate",
16
+ code: "resolve_failed",
17
+ message: `missing scope resolver for ${entry.scope}@${entry.scope_version}`,
18
+ });
19
+ }
20
+ emitObserverDiagnostic(observer, {
21
+ path,
22
+ stage: "scope",
23
+ code_domain: "event",
24
+ code: "scope_ignored_missing_resolver",
25
+ result: "skip",
26
+ });
27
+ continue;
28
+ }
29
+ try {
30
+ await resolver(entry);
31
+ }
32
+ catch (error) {
33
+ if (!entry.critical && opts.relaxedOptionalScopeValidation === true) {
34
+ emitObserverDiagnostic(observer, {
35
+ path,
36
+ stage: "scope",
37
+ code_domain: "event",
38
+ code: "scope_ignored_relaxed_validation",
39
+ result: "skip",
40
+ });
41
+ continue;
42
+ }
43
+ throw new FlowersecError({
44
+ path,
45
+ stage: "validate",
46
+ code: "resolve_failed",
47
+ message: `scope validation failed for ${entry.scope}@${entry.scope_version}`,
48
+ cause: error,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ export async function resolveConnectArtifact(input, opts = {}) {
54
+ let artifact;
55
+ try {
56
+ artifact = assertConnectArtifact(input);
57
+ }
58
+ catch (error) {
59
+ throw new FlowersecError({
60
+ path: "auto",
61
+ stage: "validate",
62
+ code: "invalid_input",
63
+ message: "invalid ConnectArtifact",
64
+ cause: error,
65
+ });
66
+ }
67
+ const observer = opts.observer == null
68
+ ? undefined
69
+ : normalizeObserver(withObserverContext(opts.observer, {
70
+ path: artifact.transport,
71
+ ...(artifact.correlation?.trace_id === undefined ? {} : { traceId: artifact.correlation.trace_id }),
72
+ ...(artifact.correlation?.session_id === undefined ? {} : { sessionId: artifact.correlation.session_id }),
73
+ }), { path: artifact.transport });
74
+ await validateArtifactScopes(artifact, opts, observer);
75
+ if (artifact.transport === "direct") {
76
+ return {
77
+ kind: "direct",
78
+ input: artifact.direct_info,
79
+ ...(observer === undefined ? {} : { observer }),
80
+ };
81
+ }
82
+ return {
83
+ kind: "tunnel",
84
+ input: artifact.tunnel_grant,
85
+ ...(observer === undefined ? {} : { observer }),
86
+ };
87
+ }
@@ -1,7 +1,7 @@
1
1
  import { gcm } from "@noble/ciphers/aes";
2
2
  import { concatBytes, readU32be, readU64be, u32be, u64be } from "../utils/bin.js";
3
3
  import { PROTOCOL_VERSION, RECORD_MAGIC, RECORD_FLAG_APP, RECORD_FLAG_PING, RECORD_FLAG_REKEY } from "./constants.js";
4
- const te = new TextEncoder();
4
+ const recordMagicBytes = new TextEncoder().encode(RECORD_MAGIC);
5
5
  // RecordError marks record parsing or cryptographic failures.
6
6
  export class RecordError extends Error {
7
7
  }
@@ -28,7 +28,7 @@ export function encryptRecord(key, noncePrefix, flags, seq, plaintext, maxRecord
28
28
  if (cipherLen > 0xffffffff)
29
29
  throw new RecordError("record too large");
30
30
  const header = new Uint8Array(4 + 1 + 1 + 8 + 4);
31
- header.set(te.encode(RECORD_MAGIC), 0);
31
+ header.set(recordMagicBytes, 0);
32
32
  header[4] = PROTOCOL_VERSION;
33
33
  header[5] = flags & 0xff;
34
34
  header.set(u64be(seq), 6);
@@ -51,8 +51,12 @@ export function decryptRecord(key, noncePrefix, frame, expectSeq, maxRecordBytes
51
51
  throw new RecordError("record too large");
52
52
  if (frame.length < headerLen)
53
53
  throw new RecordError("record too short");
54
- if (new TextDecoder().decode(frame.slice(0, 4)) !== RECORD_MAGIC)
54
+ if (frame[0] !== recordMagicBytes[0] ||
55
+ frame[1] !== recordMagicBytes[1] ||
56
+ frame[2] !== recordMagicBytes[2] ||
57
+ frame[3] !== recordMagicBytes[3]) {
55
58
  throw new RecordError("bad record magic");
59
+ }
56
60
  if (frame[4] !== PROTOCOL_VERSION)
57
61
  throw new RecordError("bad record version");
58
62
  const flags = frame[5];
@@ -67,7 +71,7 @@ export function decryptRecord(key, noncePrefix, frame, expectSeq, maxRecordBytes
67
71
  throw new RecordError("length mismatch");
68
72
  const nonce = concatBytes([noncePrefix, u64be(seq)]);
69
73
  try {
70
- const plaintext = gcm(key, nonce, frame.slice(0, headerLen)).decrypt(frame.slice(headerLen));
74
+ const plaintext = gcm(key, nonce, frame.subarray(0, headerLen)).decrypt(frame.subarray(headerLen));
71
75
  return { flags, seq, plaintext };
72
76
  }
73
77
  catch (e) {
@@ -88,5 +88,6 @@ export declare class SecureChannel {
88
88
  private reserveSendSeq;
89
89
  private sendLoop;
90
90
  private readLoop;
91
+ private clearKeyMaterial;
91
92
  }
92
93
  export {};
@@ -62,11 +62,11 @@ export class SecureChannel {
62
62
  throw new RangeError("maxOutboundBufferedBytes must be a non-negative safe integer");
63
63
  }
64
64
  this.maxOutboundBufferedBytes = maxOutboundBufferedBytes === 0 ? SDK_DEFAULTS.e2ee.maxOutboundBufferedBytes : maxOutboundBufferedBytes;
65
- this.sendKey = args.sendKey;
66
- this.recvKey = args.recvKey;
65
+ this.sendKey = args.sendKey.slice();
66
+ this.recvKey = args.recvKey.slice();
67
67
  this.sendNoncePrefix = args.sendNoncePrefix;
68
68
  this.recvNoncePrefix = args.recvNoncePrefix;
69
- this.rekeyBase = args.rekeyBase;
69
+ this.rekeyBase = args.rekeyBase.slice();
70
70
  this.transcriptHash = args.transcriptHash;
71
71
  this.sendDir = args.sendDir;
72
72
  this.recvDir = args.recvDir;
@@ -113,11 +113,16 @@ export class SecureChannel {
113
113
  this.sendClosed = true;
114
114
  this.rejectQueuedSenders(this.sendErr ?? new Error("closed"));
115
115
  this.wakeSendWaiters();
116
- this.transport.close();
117
116
  const ws = this.recvWaiters;
118
117
  this.recvWaiters = [];
119
118
  for (const w of ws)
120
119
  w();
120
+ try {
121
+ this.transport.close();
122
+ }
123
+ finally {
124
+ this.clearKeyMaterial();
125
+ }
121
126
  }
122
127
  // sendPing emits a keepalive record.
123
128
  async sendPing() {
@@ -254,6 +259,8 @@ export class SecureChannel {
254
259
  if (req.kind === "app") {
255
260
  const payload = req.payload ?? new Uint8Array();
256
261
  for (let offset = 0; offset < payload.length; offset += this.outboundRecordChunkBytes) {
262
+ if (this.closed || this.sendClosed)
263
+ throw new Error("closed");
257
264
  const chunk = payload.subarray(offset, Math.min(payload.length, offset + this.outboundRecordChunkBytes));
258
265
  const seq = this.reserveSendSeq();
259
266
  frame = encryptRecord(this.sendKey, this.sendNoncePrefix, RECORD_FLAG_APP, seq, chunk, this.maxRecordBytes);
@@ -290,6 +297,8 @@ export class SecureChannel {
290
297
  try {
291
298
  while (!this.closed) {
292
299
  const frame = await this.transport.readBinary();
300
+ if (this.closed)
301
+ return;
293
302
  const { flags, seq, plaintext } = decryptRecord(this.recvKey, this.recvNoncePrefix, frame, this.recvSeq, this.maxRecordBytes);
294
303
  if (seq >= maxRecordSeq) {
295
304
  throw new RecordSeqExhaustedError();
@@ -325,4 +334,9 @@ export class SecureChannel {
325
334
  this.close();
326
335
  }
327
336
  }
337
+ clearKeyMaterial() {
338
+ this.sendKey.fill(0);
339
+ this.recvKey.fill(0);
340
+ this.rekeyBase.fill(0);
341
+ }
328
342
  }
package/dist/facade.d.ts CHANGED
@@ -24,7 +24,4 @@ export type { DirectConnectOptions } from "./direct-client/connect.js";
24
24
  export type ConnectOptions = TunnelConnectOptions | DirectConnectOptions;
25
25
  export declare function connectTunnel(grant: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
26
26
  export declare function connectDirect(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
27
- export declare function connect(input: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
28
- export declare function connect(input: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
29
27
  export declare function connect(input: ConnectArtifact, opts: ConnectOptions): Promise<Client>;
30
- export declare function connect(input: unknown, opts: ConnectOptions): Promise<Client>;
package/dist/facade.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { connectDirect as connectDirectInternal } from "./direct-client/connect.js";
2
2
  import { connectTunnel as connectTunnelInternal } from "./tunnel-client/connect.js";
3
- import { normalizeConnectInput } from "./connect/internalNormalize.js";
4
- import { withObserverContext } from "./observability/observer.js";
3
+ import { resolveConnectArtifact } from "./connect/resolveArtifact.js";
5
4
  export { assertChannelInitGrant } from "./gen/flowersec/controlplane/v1.gen.js";
6
5
  export { assertDirectConnectInfo } from "./gen/flowersec/direct/v1.gen.js";
7
6
  export { assertConnectArtifact } from "./connect/artifact.js";
@@ -13,15 +12,10 @@ export async function connectTunnel(grant, opts) {
13
12
  export async function connectDirect(info, opts) {
14
13
  return await connectDirectInternal(info, opts);
15
14
  }
15
+ // connect resolves an artifact to its explicit direct or tunnel transport.
16
16
  export async function connect(input, opts) {
17
- const normalized = await normalizeConnectInput(input, opts);
18
- const nextObserver = normalized.observer ??
19
- (normalized.correlation == null
20
- ? opts.observer
21
- : withObserverContext(opts.observer, {
22
- ...(normalized.correlation.trace_id === undefined ? {} : { traceId: normalized.correlation.trace_id }),
23
- ...(normalized.correlation.session_id === undefined ? {} : { sessionId: normalized.correlation.session_id }),
24
- }));
17
+ const normalized = await resolveConnectArtifact(input, opts);
18
+ const nextObserver = normalized.observer ?? opts.observer;
25
19
  const nextOpts = (nextObserver === opts.observer ? opts : { ...opts, observer: nextObserver });
26
20
  if (normalized.kind === "direct") {
27
21
  return await connectDirectInternal(normalized.input, nextOpts);
@@ -5,8 +5,6 @@ import { type ConnectOptions } from "../facade.js";
5
5
  import type { ConnectArtifact } from "../connect/artifact.js";
6
6
  import type { ChannelInitGrant } from "../gen/flowersec/controlplane/v1.gen.js";
7
7
  import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
8
- export declare function connectNode(input: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
9
- export declare function connectNode(input: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
10
8
  export declare function connectNode(input: ConnectArtifact, opts: ConnectOptions): Promise<Client>;
11
9
  export declare function connectTunnelNode(grant: ChannelInitGrant, opts: TunnelConnectOptions): Promise<Client>;
12
10
  export declare function connectDirectNode(info: DirectConnectInfo, opts: DirectConnectOptions): Promise<Client>;
@@ -6,4 +6,4 @@ export type TunnelConnectOptions = ConnectOptionsBase & Readonly<{
6
6
  /** Optional caller-provided endpoint instance ID (base64url). */
7
7
  endpointInstanceId?: string;
8
8
  }>;
9
- export declare function connectTunnel(grant: unknown, opts: TunnelConnectOptions): Promise<ClientInternal>;
9
+ export declare function connectTunnel(input: unknown, opts: TunnelConnectOptions): Promise<ClientInternal>;
@@ -11,18 +11,8 @@ function isRecord(v) {
11
11
  function hasOwn(o, key) {
12
12
  return Object.prototype.hasOwnProperty.call(o, key);
13
13
  }
14
- function unwrapGrant(v) {
15
- if (!isRecord(v))
16
- return v;
17
- if (hasOwn(v, "grant_client"))
18
- return v["grant_client"];
19
- if (hasOwn(v, "grant_server"))
20
- return v["grant_server"];
21
- return v;
22
- }
23
14
  // connectTunnel attaches to a tunnel and returns an RPC-ready session.
24
- export async function connectTunnel(grant, opts) {
25
- const input = unwrapGrant(grant);
15
+ export async function connectTunnel(input, opts) {
26
16
  if (input == null) {
27
17
  throw new FlowersecError({ stage: "validate", code: "missing_grant", path: "tunnel", message: "missing grant" });
28
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -103,10 +103,6 @@
103
103
  "./gen/flowersec/tunnel/*": {
104
104
  "types": "./dist/gen/flowersec/tunnel/*.d.ts",
105
105
  "default": "./dist/gen/flowersec/tunnel/*.js"
106
- },
107
- "./internal": {
108
- "types": "./dist/index.d.ts",
109
- "default": "./dist/index.js"
110
106
  }
111
107
  },
112
108
  "engines": {
@@ -122,7 +118,9 @@
122
118
  "bench": "vitest bench --run",
123
119
  "test": "npm run build && vitest run",
124
120
  "test:browser": "npm run build && playwright test",
125
- "ensure:browser": "node ./scripts/ensure-playwright-chromium.mjs",
121
+ "test:browser:chromium": "npm run build && playwright test --project=chromium",
122
+ "test:browser:webkit": "npm run build && playwright test --project=webkit-smoke",
123
+ "ensure:browser": "node ./scripts/ensure-playwright-browsers.mjs",
126
124
  "test:coverage": "npm run build && vitest run --coverage",
127
125
  "lint": "eslint .",
128
126
  "verify:package": "node ./scripts/verify-package-exports.mjs",
@@ -1,15 +0,0 @@
1
- import { type ChannelInitGrant } from "../facade.js";
2
- import type { ControlplaneBaseConfig as SharedControlplaneBaseConfig, RequestConnectArtifactInput, RequestEntryConnectArtifactInput } from "../controlplane/request.js";
3
- type BaseControlplaneRequestConfig = SharedControlplaneBaseConfig & Readonly<{
4
- endpointId: string;
5
- payload?: Record<string, unknown>;
6
- }>;
7
- export type ControlplaneConfig = BaseControlplaneRequestConfig;
8
- export type EntryControlplaneConfig = BaseControlplaneRequestConfig & Readonly<{
9
- entryTicket: string;
10
- }>;
11
- export type ConnectArtifactRequestConfig = RequestConnectArtifactInput;
12
- export type EntryConnectArtifactRequestConfig = RequestEntryConnectArtifactInput;
13
- export declare function requestChannelGrant(config: ControlplaneConfig): Promise<ChannelInitGrant>;
14
- export declare function requestEntryChannelGrant(config: EntryControlplaneConfig): Promise<ChannelInitGrant>;
15
- export {};
@@ -1,64 +0,0 @@
1
- import { assertChannelInitGrant } from "../facade.js";
2
- import { requestControlplaneJSON, } from "../controlplane/request.js";
3
- function buildURL(baseUrl, path) {
4
- const base = String(baseUrl ?? "").trim();
5
- if (base === "")
6
- return path;
7
- return `${base.replace(/\/+$/, "")}${path}`;
8
- }
9
- function buildPayload(endpointId, payload) {
10
- const id = String(endpointId ?? "").trim();
11
- if (id === "")
12
- throw new Error("endpointId is required");
13
- const out = { ...(payload ?? {}) };
14
- const raw = out.endpoint_id;
15
- if (raw !== undefined && String(raw ?? "").trim() !== id) {
16
- throw new Error("payload.endpoint_id must match endpointId");
17
- }
18
- out.endpoint_id = id;
19
- return out;
20
- }
21
- async function requestGrant(url, init) {
22
- return await requestControlplaneJSON(url, init);
23
- }
24
- export async function requestChannelGrant(config) {
25
- const headers = new Headers(config.headers);
26
- if (!headers.has("Content-Type")) {
27
- headers.set("Content-Type", "application/json");
28
- }
29
- const data = (await requestGrant(buildURL(config.baseUrl, "/v1/channel/init"), {
30
- ...(config.fetch === undefined ? {} : { fetch: config.fetch }),
31
- method: "POST",
32
- credentials: config.credentials ?? "omit",
33
- headers,
34
- body: JSON.stringify(buildPayload(config.endpointId, config.payload)),
35
- ...(config.signal === undefined ? {} : { signal: config.signal }),
36
- }));
37
- if (!data?.grant_client) {
38
- throw new Error("Invalid controlplane response: missing `grant_client`");
39
- }
40
- return assertChannelInitGrant(data.grant_client);
41
- }
42
- export async function requestEntryChannelGrant(config) {
43
- const entryTicket = String(config.entryTicket ?? "").trim();
44
- if (entryTicket === "")
45
- throw new Error("entryTicket is required");
46
- const endpointId = String(config.endpointId ?? "").trim();
47
- const headers = new Headers(config.headers);
48
- headers.set("Authorization", `Bearer ${entryTicket}`);
49
- if (!headers.has("Content-Type")) {
50
- headers.set("Content-Type", "application/json");
51
- }
52
- const data = (await requestGrant(buildURL(config.baseUrl, `/v1/channel/init/entry?endpoint_id=${encodeURIComponent(endpointId)}`), {
53
- ...(config.fetch === undefined ? {} : { fetch: config.fetch }),
54
- method: "POST",
55
- credentials: config.credentials ?? "omit",
56
- headers,
57
- body: JSON.stringify(buildPayload(endpointId, config.payload)),
58
- ...(config.signal === undefined ? {} : { signal: config.signal }),
59
- }));
60
- if (!data?.grant_client) {
61
- throw new Error("Invalid controlplane response: missing `grant_client`");
62
- }
63
- return assertChannelInitGrant(data.grant_client);
64
- }
@@ -1,173 +0,0 @@
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
- }