@floegence/flowersec-core 0.4.0 → 0.6.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.
@@ -13,11 +13,20 @@ export async function connectCore(args) {
13
13
  const observer = normalizeObserver(args.opts.observer);
14
14
  const signal = args.opts.signal;
15
15
  const connectStart = nowSeconds();
16
- const origin = args.opts.origin;
17
- if (origin == null || origin === "") {
16
+ const origin = typeof args.opts.origin === "string" ? args.opts.origin.trim() : "";
17
+ if (origin === "") {
18
18
  throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_origin", message: "missing origin" });
19
19
  }
20
- if (args.wsUrl == null || args.wsUrl === "") {
20
+ if (args.path === "tunnel" && args.attach == null) {
21
+ throw new FlowersecError({
22
+ path: args.path,
23
+ stage: "validate",
24
+ code: "invalid_option",
25
+ message: "missing attach payload",
26
+ });
27
+ }
28
+ const wsUrl = typeof args.wsUrl === "string" ? args.wsUrl.trim() : "";
29
+ if (wsUrl === "") {
21
30
  const code = args.path === "tunnel" ? "missing_tunnel_url" : "missing_ws_url";
22
31
  throw new FlowersecError({ path: args.path, stage: "validate", code, message: "missing websocket url" });
23
32
  }
@@ -56,9 +65,28 @@ export async function connectCore(args) {
56
65
  if (!Number.isSafeInteger(maxWsQueuedBytes) || maxWsQueuedBytes < 0) {
57
66
  invalidOption("maxWsQueuedBytes must be a non-negative integer");
58
67
  }
68
+ const channelId = typeof args.channelId === "string" ? args.channelId.trim() : "";
69
+ if (channelId === "") {
70
+ throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_channel_id", message: "missing channel_id" });
71
+ }
72
+ let psk;
73
+ try {
74
+ const pskB64u = typeof args.e2eePskB64u === "string" ? args.e2eePskB64u.trim() : "";
75
+ psk = base64urlDecode(pskB64u);
76
+ }
77
+ catch (e) {
78
+ throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_psk", message: "invalid e2ee_psk_b64u", cause: e });
79
+ }
80
+ if (psk.length !== 32) {
81
+ throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_psk", message: "psk must be 32 bytes" });
82
+ }
83
+ const suite = args.defaultSuite;
84
+ if (suite !== 1 && suite !== 2) {
85
+ throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_suite", message: "invalid suite" });
86
+ }
59
87
  let ws;
60
88
  try {
61
- ws = createWebSocket(args.wsUrl, origin, args.opts.wsFactory);
89
+ ws = createWebSocket(wsUrl, origin, args.opts.wsFactory);
62
90
  }
63
91
  catch (e) {
64
92
  if (e instanceof OriginMismatchError) {
@@ -103,14 +131,6 @@ export async function connectCore(args) {
103
131
  }
104
132
  throwIfAborted(signal, "connect aborted");
105
133
  if (args.path === "tunnel") {
106
- if (args.attach == null) {
107
- throw new FlowersecError({
108
- path: args.path,
109
- stage: "validate",
110
- code: "invalid_option",
111
- message: "missing attach payload",
112
- });
113
- }
114
134
  try {
115
135
  ws.send(args.attach.attachJson);
116
136
  }
@@ -125,32 +145,11 @@ export async function connectCore(args) {
125
145
  throw new FlowersecError({ path: args.path, stage: "attach", code: "attach_failed", message: "attach failed", cause: err });
126
146
  }
127
147
  }
128
- let psk;
129
- try {
130
- psk = base64urlDecode(args.e2eePskB64u);
131
- }
132
- catch (e) {
133
- transport.close();
134
- throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_psk", message: "invalid e2ee_psk_b64u", cause: e });
135
- }
136
- if (psk.length !== 32) {
137
- transport.close();
138
- throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_psk", message: "psk must be 32 bytes" });
139
- }
140
- if (args.channelId == null || args.channelId === "") {
141
- transport.close();
142
- throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_channel_id", message: "missing channel_id" });
143
- }
144
- const suite = args.defaultSuite;
145
- if (suite !== 1 && suite !== 2) {
146
- transport.close();
147
- throw new FlowersecError({ path: args.path, stage: "validate", code: "invalid_suite", message: "invalid suite" });
148
- }
149
148
  const handshakeStart = nowSeconds();
150
149
  let secure;
151
150
  try {
152
151
  secure = await withAbortAndTimeout(clientHandshake(transport, {
153
- channelId: args.channelId,
152
+ channelId,
154
153
  suite,
155
154
  psk,
156
155
  clientFeatures,
@@ -386,6 +385,11 @@ export async function connectCore(args) {
386
385
  cause: err,
387
386
  });
388
387
  }
388
+ if (signal != null && abortListener != null) {
389
+ // AbortSignal is only used to cancel the open + StreamHello phase.
390
+ // After the stream is ready, callers should close/reset it explicitly.
391
+ signal.removeEventListener("abort", abortListener);
392
+ }
389
393
  return s;
390
394
  },
391
395
  close: closeAll,
@@ -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"];
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"];
2
2
  export type TunnelAttachCloseReason = (typeof tunnelAttachCloseReasons)[number];
3
3
  export declare function isTunnelAttachCloseReason(v: string | undefined): v is TunnelAttachCloseReason;
@@ -9,7 +9,9 @@ export const tunnelAttachCloseReasons = [
9
9
  "role_mismatch",
10
10
  "token_replay",
11
11
  "replace_rate_limited",
12
- "attach_failed"
12
+ "attach_failed",
13
+ "timeout",
14
+ "canceled"
13
15
  ];
14
16
  export function isTunnelAttachCloseReason(v) {
15
17
  if (v == null)
@@ -2,7 +2,7 @@ import type { ClientPath } from "../client.js";
2
2
  export type ConnectResult = "ok" | "fail";
3
3
  export type ConnectReason = "websocket_error" | "websocket_closed" | "timeout" | "canceled";
4
4
  export type AttachResult = "ok" | "fail";
5
- export type AttachReason = "send_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "role_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "replace_rate_limited" | "attach_failed";
5
+ export type AttachReason = "send_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "role_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "replace_rate_limited" | "attach_failed" | "timeout" | "canceled";
6
6
  export type HandshakeResult = "ok" | "fail";
7
7
  export type HandshakeReason = "auth_tag_mismatch" | "handshake_failed" | "invalid_suite" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
8
8
  export type WsCloseKind = "local" | "peer_or_error";
@@ -80,13 +80,16 @@ export async function connectTunnel(grant, opts) {
80
80
  catch (e) {
81
81
  throw new FlowersecError({ stage: "validate", code: "invalid_input", path: "tunnel", message: "invalid ChannelInitGrant", cause: e });
82
82
  }
83
- if (checkedGrant.tunnel_url === "") {
83
+ const tunnelUrl = checkedGrant.tunnel_url.trim();
84
+ if (tunnelUrl === "") {
84
85
  throw new FlowersecError({ stage: "validate", code: "missing_tunnel_url", path: "tunnel", message: "missing tunnel_url" });
85
86
  }
86
- if (checkedGrant.channel_id === "") {
87
+ const channelId = checkedGrant.channel_id.trim();
88
+ if (channelId === "") {
87
89
  throw new FlowersecError({ stage: "validate", code: "missing_channel_id", path: "tunnel", message: "missing channel_id" });
88
90
  }
89
- if (checkedGrant.token === "") {
91
+ const token = checkedGrant.token.trim();
92
+ if (token === "") {
90
93
  throw new FlowersecError({ stage: "validate", code: "missing_token", path: "tunnel", message: "missing token" });
91
94
  }
92
95
  if (checkedGrant.channel_init_expire_at_unix_s <= 0) {
@@ -97,8 +100,9 @@ export async function connectTunnel(grant, opts) {
97
100
  message: "missing channel_init_expire_at_unix_s",
98
101
  });
99
102
  }
103
+ const e2eePskB64u = checkedGrant.e2ee_psk_b64u.trim();
100
104
  try {
101
- const psk = base64urlDecode(checkedGrant.e2ee_psk_b64u);
105
+ const psk = base64urlDecode(e2eePskB64u);
102
106
  if (psk.length !== 32) {
103
107
  throw new Error("psk must be 32 bytes");
104
108
  }
@@ -134,18 +138,18 @@ export async function connectTunnel(grant, opts) {
134
138
  }
135
139
  const attach = {
136
140
  v: 1,
137
- channel_id: checkedGrant.channel_id,
141
+ channel_id: channelId,
138
142
  role: TunnelRole.Role_client,
139
- token: checkedGrant.token,
143
+ token,
140
144
  endpoint_instance_id: endpointInstanceId
141
145
  };
142
146
  const attachJson = JSON.stringify(attach);
143
147
  const keepaliveIntervalMs = opts.keepaliveIntervalMs ?? defaultKeepaliveIntervalMs(idleTimeoutSeconds);
144
148
  return await connectCore({
145
149
  path: "tunnel",
146
- wsUrl: checkedGrant.tunnel_url,
147
- channelId: checkedGrant.channel_id,
148
- e2eePskB64u: checkedGrant.e2ee_psk_b64u,
150
+ wsUrl: tunnelUrl,
151
+ channelId,
152
+ e2eePskB64u,
149
153
  defaultSuite: checkedGrant.default_suite,
150
154
  opts: { ...opts, keepaliveIntervalMs },
151
155
  attach: { attachJson, endpointInstanceId }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {