@floegence/flowersec-core 0.2.2 → 0.3.1

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.
@@ -9,7 +9,7 @@ export declare class WsFactoryRequiredError extends Error {
9
9
  }
10
10
  export declare function createWebSocket(url: string, origin: string, wsFactory: ((url: string, origin: string) => WebSocketLike) | undefined): WebSocketLike;
11
11
  export declare function classifyConnectError(err: unknown): "websocket_error" | "websocket_closed" | "timeout" | "canceled";
12
- export declare function classifyHandshakeError(err: unknown): "auth_tag_mismatch" | "handshake_failed" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
12
+ export declare function classifyHandshakeError(err: unknown): "auth_tag_mismatch" | "handshake_failed" | "invalid_suite" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
13
13
  export declare function withAbortAndTimeout<T>(p: Promise<T>, opts: Readonly<{
14
14
  signal?: AbortSignal;
15
15
  timeoutMs?: number;
@@ -1,3 +1,3 @@
1
- export declare const tunnelAttachCloseReasons: readonly ["too_many_connections", "expected_attach", "invalid_attach", "invalid_token", "channel_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"];
2
2
  export type TunnelAttachCloseReason = (typeof tunnelAttachCloseReasons)[number];
3
3
  export declare function isTunnelAttachCloseReason(v: string | undefined): v is TunnelAttachCloseReason;
@@ -4,6 +4,8 @@ export const tunnelAttachCloseReasons = [
4
4
  "invalid_attach",
5
5
  "invalid_token",
6
6
  "channel_mismatch",
7
+ "init_exp_mismatch",
8
+ "idle_timeout_mismatch",
7
9
  "role_mismatch",
8
10
  "token_replay",
9
11
  "replace_rate_limited",
@@ -5,6 +5,9 @@ import { connectCore } from "../client-connect/connectCore.js";
5
5
  function isRecord(v) {
6
6
  return typeof v === "object" && v != null && !Array.isArray(v);
7
7
  }
8
+ function hasOwn(o, key) {
9
+ return Object.prototype.hasOwnProperty.call(o, key);
10
+ }
8
11
  // connectDirect connects to a direct websocket endpoint and returns an RPC-ready session.
9
12
  export async function connectDirect(info, opts) {
10
13
  const endpointInstanceId = opts?.endpointInstanceId;
@@ -20,6 +23,35 @@ export async function connectDirect(info, opts) {
20
23
  throw new FlowersecError({ stage: "validate", code: "missing_connect_info", path: "direct", message: "missing connect info" });
21
24
  }
22
25
  if (isRecord(info)) {
26
+ // Align missing/invalid field codes with Go: missing fields map to specific stable codes.
27
+ const okTypes = (!hasOwn(info, "ws_url") || typeof info["ws_url"] === "string") &&
28
+ (!hasOwn(info, "channel_id") || typeof info["channel_id"] === "string") &&
29
+ (!hasOwn(info, "channel_init_expire_at_unix_s") ||
30
+ (typeof info["channel_init_expire_at_unix_s"] === "number" && Number.isSafeInteger(info["channel_init_expire_at_unix_s"]))) &&
31
+ (!hasOwn(info, "e2ee_psk_b64u") || typeof info["e2ee_psk_b64u"] === "string") &&
32
+ (!hasOwn(info, "default_suite") || (typeof info["default_suite"] === "number" && Number.isSafeInteger(info["default_suite"])));
33
+ if (okTypes) {
34
+ if (!hasOwn(info, "ws_url")) {
35
+ throw new FlowersecError({ stage: "validate", code: "missing_ws_url", path: "direct", message: "missing ws_url" });
36
+ }
37
+ if (!hasOwn(info, "channel_id")) {
38
+ throw new FlowersecError({ stage: "validate", code: "missing_channel_id", path: "direct", message: "missing channel_id" });
39
+ }
40
+ if (!hasOwn(info, "channel_init_expire_at_unix_s")) {
41
+ throw new FlowersecError({
42
+ stage: "validate",
43
+ code: "missing_init_exp",
44
+ path: "direct",
45
+ message: "missing channel_init_expire_at_unix_s",
46
+ });
47
+ }
48
+ if (!hasOwn(info, "e2ee_psk_b64u")) {
49
+ throw new FlowersecError({ stage: "validate", code: "invalid_psk", path: "direct", message: "missing e2ee_psk_b64u" });
50
+ }
51
+ if (!hasOwn(info, "default_suite")) {
52
+ throw new FlowersecError({ stage: "validate", code: "invalid_suite", path: "direct", message: "missing default_suite" });
53
+ }
54
+ }
23
55
  const suite = info["default_suite"];
24
56
  // Keep "invalid_suite" as the stable error code even when the IDL validator rejects the enum value.
25
57
  if (typeof suite === "number" && Number.isSafeInteger(suite) && suite !== 1 && suite !== 2) {
@@ -1,4 +1,4 @@
1
- export type E2EEHandshakeErrorCode = "auth_tag_mismatch" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew";
1
+ export type E2EEHandshakeErrorCode = "auth_tag_mismatch" | "invalid_suite" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew";
2
2
  export declare class E2EEHandshakeError extends Error {
3
3
  readonly code: E2EEHandshakeErrorCode;
4
4
  constructor(code: E2EEHandshakeErrorCode, message: string);
@@ -242,7 +242,7 @@ export async function serverHandshake(transport, cache, opts) {
242
242
  throw new Error("bad channel_id");
243
243
  const suite = init.suite;
244
244
  if (suite !== opts.suite)
245
- throw new Error("bad suite");
245
+ throw new E2EEHandshakeError("invalid_suite", "bad suite");
246
246
  const clientPub = base64urlDecode(init.client_eph_pub_b64u);
247
247
  const nonceC = base64urlDecode(init.nonce_c_b64u);
248
248
  if (nonceC.length !== 32)
@@ -2,9 +2,9 @@ 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" | "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";
6
6
  export type HandshakeResult = "ok" | "fail";
7
- export type HandshakeReason = "auth_tag_mismatch" | "handshake_failed" | "invalid_version" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "timeout" | "canceled";
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";
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";
@@ -7,14 +7,16 @@ import { connectCore } from "../client-connect/connectCore.js";
7
7
  function isRecord(v) {
8
8
  return typeof v === "object" && v != null && !Array.isArray(v);
9
9
  }
10
+ function hasOwn(o, key) {
11
+ return Object.prototype.hasOwnProperty.call(o, key);
12
+ }
10
13
  function unwrapGrant(v) {
11
- if (v == null || typeof v !== "object")
14
+ if (!isRecord(v))
12
15
  return v;
13
- const o = v;
14
- if (o["grant_client"] != null)
15
- return o["grant_client"];
16
- if (o["grant_server"] != null)
17
- return o["grant_server"];
16
+ if (hasOwn(v, "grant_client"))
17
+ return v["grant_client"];
18
+ if (hasOwn(v, "grant_server"))
19
+ return v["grant_server"];
18
20
  return v;
19
21
  }
20
22
  // connectTunnel attaches to a tunnel and returns an RPC-ready session.
@@ -24,6 +26,39 @@ export async function connectTunnel(grant, opts) {
24
26
  throw new FlowersecError({ stage: "validate", code: "missing_grant", path: "tunnel", message: "missing grant" });
25
27
  }
26
28
  if (isRecord(input)) {
29
+ // Align missing/invalid field codes with Go: missing fields map to specific stable codes.
30
+ const okTypes = (!hasOwn(input, "role") || (typeof input["role"] === "number" && Number.isSafeInteger(input["role"]))) &&
31
+ (!hasOwn(input, "tunnel_url") || typeof input["tunnel_url"] === "string") &&
32
+ (!hasOwn(input, "channel_id") || typeof input["channel_id"] === "string") &&
33
+ (!hasOwn(input, "token") || typeof input["token"] === "string") &&
34
+ (!hasOwn(input, "channel_init_expire_at_unix_s") ||
35
+ (typeof input["channel_init_expire_at_unix_s"] === "number" && Number.isSafeInteger(input["channel_init_expire_at_unix_s"]))) &&
36
+ (!hasOwn(input, "e2ee_psk_b64u") || typeof input["e2ee_psk_b64u"] === "string") &&
37
+ (!hasOwn(input, "default_suite") || (typeof input["default_suite"] === "number" && Number.isSafeInteger(input["default_suite"])));
38
+ if (okTypes) {
39
+ const role = input["role"];
40
+ if (role === undefined || role !== ControlRole.Role_client) {
41
+ throw new FlowersecError({ stage: "validate", code: "role_mismatch", path: "tunnel", message: "expected role=client" });
42
+ }
43
+ if (!hasOwn(input, "tunnel_url")) {
44
+ throw new FlowersecError({ stage: "validate", code: "missing_tunnel_url", path: "tunnel", message: "missing tunnel_url" });
45
+ }
46
+ if (!hasOwn(input, "channel_id")) {
47
+ throw new FlowersecError({ stage: "validate", code: "missing_channel_id", path: "tunnel", message: "missing channel_id" });
48
+ }
49
+ if (!hasOwn(input, "token")) {
50
+ throw new FlowersecError({ stage: "validate", code: "missing_token", path: "tunnel", message: "missing token" });
51
+ }
52
+ if (!hasOwn(input, "channel_init_expire_at_unix_s")) {
53
+ throw new FlowersecError({ stage: "validate", code: "missing_init_exp", path: "tunnel", message: "missing channel_init_expire_at_unix_s" });
54
+ }
55
+ if (!hasOwn(input, "e2ee_psk_b64u")) {
56
+ throw new FlowersecError({ stage: "validate", code: "invalid_psk", path: "tunnel", message: "missing e2ee_psk_b64u" });
57
+ }
58
+ if (!hasOwn(input, "default_suite")) {
59
+ throw new FlowersecError({ stage: "validate", code: "invalid_suite", path: "tunnel", message: "missing default_suite" });
60
+ }
61
+ }
27
62
  const suite = input["default_suite"];
28
63
  // Keep "invalid_suite" as the stable error code even when the IDL validator rejects the enum value.
29
64
  if (typeof suite === "number" && Number.isSafeInteger(suite) && suite !== 1 && suite !== 2) {
@@ -6,7 +6,7 @@ export declare class AbortError extends Error {
6
6
  }
7
7
  export type FlowersecPath = "auto" | "tunnel" | "direct";
8
8
  export type FlowersecStage = "validate" | "connect" | "attach" | "handshake" | "secure" | "yamux" | "rpc" | "close";
9
- export type FlowersecErrorCode = "timeout" | "canceled" | "invalid_version" | "invalid_input" | "invalid_option" | "invalid_endpoint_instance_id" | "invalid_psk" | "invalid_suite" | "missing_grant" | "missing_connect_info" | "missing_conn" | "missing_handler" | "missing_stream_kind" | "role_mismatch" | "missing_tunnel_url" | "missing_ws_url" | "missing_origin" | "missing_channel_id" | "missing_token" | "missing_init_exp" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "auth_tag_mismatch" | "resolve_failed" | "random_failed" | "upgrade_failed" | "dial_failed" | "attach_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "token_replay" | "replace_rate_limited" | "handshake_failed" | "ping_failed" | "mux_failed" | "accept_stream_failed" | "open_stream_failed" | "stream_hello_failed" | "not_connected";
9
+ export type FlowersecErrorCode = "timeout" | "canceled" | "invalid_version" | "invalid_input" | "invalid_option" | "invalid_endpoint_instance_id" | "invalid_psk" | "invalid_suite" | "missing_grant" | "missing_connect_info" | "missing_conn" | "missing_handler" | "missing_stream_kind" | "role_mismatch" | "missing_tunnel_url" | "missing_ws_url" | "missing_origin" | "missing_channel_id" | "missing_token" | "missing_init_exp" | "timestamp_after_init_exp" | "timestamp_out_of_skew" | "auth_tag_mismatch" | "resolve_failed" | "random_failed" | "upgrade_failed" | "dial_failed" | "attach_failed" | "too_many_connections" | "expected_attach" | "invalid_attach" | "invalid_token" | "channel_mismatch" | "init_exp_mismatch" | "idle_timeout_mismatch" | "token_replay" | "replace_rate_limited" | "handshake_failed" | "ping_failed" | "mux_failed" | "accept_stream_failed" | "open_stream_failed" | "stream_hello_failed" | "not_connected";
10
10
  export declare class FlowersecError extends Error {
11
11
  readonly code: FlowersecErrorCode;
12
12
  readonly stage: FlowersecStage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Flowersec core TypeScript library (browser-friendly E2EE + multiplexing over WebSocket).",
5
5
  "license": "MIT",
6
6
  "repository": {