@floegence/flowersec-core 0.11.1 → 0.13.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.
|
@@ -8,6 +8,7 @@ import { base64urlDecode } from "../utils/base64url.js";
|
|
|
8
8
|
import { AbortError, FlowersecError, throwIfAborted } from "../utils/errors.js";
|
|
9
9
|
import { WebSocketBinaryTransport, WsCloseError } from "../ws-client/binaryTransport.js";
|
|
10
10
|
import { OriginMismatchError, WsFactoryRequiredError, classifyConnectError, classifyHandshakeError, createWebSocket, waitOpen, withAbortAndTimeout, } from "./common.js";
|
|
11
|
+
import { prepareChannelId } from "./contract.js";
|
|
11
12
|
import { isTunnelAttachCloseReason } from "./tunnelAttachCloseReason.js";
|
|
12
13
|
export async function connectCore(args) {
|
|
13
14
|
const observer = normalizeObserver(args.opts.observer);
|
|
@@ -65,10 +66,7 @@ export async function connectCore(args) {
|
|
|
65
66
|
if (!Number.isSafeInteger(maxWsQueuedBytes) || maxWsQueuedBytes < 0) {
|
|
66
67
|
invalidOption("maxWsQueuedBytes must be a non-negative integer");
|
|
67
68
|
}
|
|
68
|
-
const channelId =
|
|
69
|
-
if (channelId === "") {
|
|
70
|
-
throw new FlowersecError({ path: args.path, stage: "validate", code: "missing_channel_id", message: "missing channel_id" });
|
|
71
|
-
}
|
|
69
|
+
const channelId = prepareChannelId(args.channelId, args.path);
|
|
72
70
|
let psk;
|
|
73
71
|
try {
|
|
74
72
|
const pskB64u = typeof args.e2eePskB64u === "string" ? args.e2eePskB64u.trim() : "";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChannelInitGrant, Role as ControlRole } from "../gen/flowersec/controlplane/v1.gen.js";
|
|
2
|
+
import type { DirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
3
|
+
import { type FlowersecPath } from "../utils/errors.js";
|
|
4
|
+
export declare const CHANNEL_ID_MAX_BYTES = 256;
|
|
5
|
+
export declare function prepareChannelId(raw: string, path: Exclude<FlowersecPath, "auto">): string;
|
|
6
|
+
export declare function assertTunnelGrantContract(grant: ChannelInitGrant, expectedRole: ControlRole): void;
|
|
7
|
+
export declare function assertDirectConnectContract(info: DirectConnectInfo): void;
|
|
8
|
+
export declare function assertValidPSK(raw: string, path: Exclude<FlowersecPath, "auto">): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { base64urlDecode } from "../utils/base64url.js";
|
|
2
|
+
import { FlowersecError } from "../utils/errors.js";
|
|
3
|
+
const textEncoder = new TextEncoder();
|
|
4
|
+
export const CHANNEL_ID_MAX_BYTES = 256;
|
|
5
|
+
export function prepareChannelId(raw, path) {
|
|
6
|
+
const channelId = typeof raw === "string" ? raw.trim() : "";
|
|
7
|
+
if (channelId === "") {
|
|
8
|
+
throw new FlowersecError({ path, stage: "validate", code: "missing_channel_id", message: "missing channel_id" });
|
|
9
|
+
}
|
|
10
|
+
if (textEncoder.encode(channelId).length > CHANNEL_ID_MAX_BYTES) {
|
|
11
|
+
throw new FlowersecError({ path, stage: "validate", code: "invalid_input", message: "channel_id too long" });
|
|
12
|
+
}
|
|
13
|
+
return channelId;
|
|
14
|
+
}
|
|
15
|
+
export function assertTunnelGrantContract(grant, expectedRole) {
|
|
16
|
+
if (grant.role !== expectedRole) {
|
|
17
|
+
throw new FlowersecError({ stage: "validate", code: "role_mismatch", path: "tunnel", message: `expected role=${expectedRole === 1 ? "client" : "server"}` });
|
|
18
|
+
}
|
|
19
|
+
const allowedSuites = Array.isArray(grant.allowed_suites) ? grant.allowed_suites : [];
|
|
20
|
+
if (allowedSuites.length === 0) {
|
|
21
|
+
throw new FlowersecError({ stage: "validate", code: "invalid_suite", path: "tunnel", message: "allowed_suites must be non-empty" });
|
|
22
|
+
}
|
|
23
|
+
for (const suite of allowedSuites) {
|
|
24
|
+
assertSupportedSuite(suite, "tunnel");
|
|
25
|
+
}
|
|
26
|
+
assertSupportedSuite(grant.default_suite, "tunnel");
|
|
27
|
+
if (!allowedSuites.includes(grant.default_suite)) {
|
|
28
|
+
throw new FlowersecError({ stage: "validate", code: "invalid_suite", path: "tunnel", message: "default_suite must be included in allowed_suites" });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function assertDirectConnectContract(info) {
|
|
32
|
+
assertSupportedSuite(info.default_suite, "direct");
|
|
33
|
+
}
|
|
34
|
+
export function assertValidPSK(raw, path) {
|
|
35
|
+
const pskB64u = typeof raw === "string" ? raw.trim() : "";
|
|
36
|
+
try {
|
|
37
|
+
const psk = base64urlDecode(pskB64u);
|
|
38
|
+
if (psk.length !== 32) {
|
|
39
|
+
throw new Error("psk must be 32 bytes");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
throw new FlowersecError({ stage: "validate", code: "invalid_psk", path, message: "invalid e2ee_psk_b64u", cause: e });
|
|
44
|
+
}
|
|
45
|
+
return pskB64u;
|
|
46
|
+
}
|
|
47
|
+
function assertSupportedSuite(suite, path) {
|
|
48
|
+
if (suite !== 1 && suite !== 2) {
|
|
49
|
+
throw new FlowersecError({ stage: "validate", code: "invalid_suite", path, message: "invalid suite" });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assertDirectConnectInfo } from "../gen/flowersec/direct/v1.gen.js";
|
|
2
|
-
import { base64urlDecode } from "../utils/base64url.js";
|
|
3
2
|
import { FlowersecError } from "../utils/errors.js";
|
|
4
3
|
import { connectCore } from "../client-connect/connectCore.js";
|
|
4
|
+
import { assertDirectConnectContract, assertValidPSK, prepareChannelId } from "../client-connect/contract.js";
|
|
5
5
|
function isRecord(v) {
|
|
6
6
|
return typeof v === "object" && v != null && !Array.isArray(v);
|
|
7
7
|
}
|
|
@@ -68,9 +68,7 @@ export async function connectDirect(info, opts) {
|
|
|
68
68
|
if (ready.ws_url === "") {
|
|
69
69
|
throw new FlowersecError({ stage: "validate", code: "missing_ws_url", path: "direct", message: "missing ws_url" });
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
throw new FlowersecError({ stage: "validate", code: "missing_channel_id", path: "direct", message: "missing channel_id" });
|
|
73
|
-
}
|
|
71
|
+
const channelId = prepareChannelId(ready.channel_id, "direct");
|
|
74
72
|
if (ready.channel_init_expire_at_unix_s <= 0) {
|
|
75
73
|
throw new FlowersecError({
|
|
76
74
|
stage: "validate",
|
|
@@ -79,19 +77,12 @@ export async function connectDirect(info, opts) {
|
|
|
79
77
|
message: "missing channel_init_expire_at_unix_s",
|
|
80
78
|
});
|
|
81
79
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (psk.length !== 32) {
|
|
85
|
-
throw new Error("psk must be 32 bytes");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
throw new FlowersecError({ stage: "validate", code: "invalid_psk", path: "direct", message: "invalid e2ee_psk_b64u", cause: e });
|
|
90
|
-
}
|
|
80
|
+
assertDirectConnectContract(ready);
|
|
81
|
+
assertValidPSK(ready.e2ee_psk_b64u, "direct");
|
|
91
82
|
return await connectCore({
|
|
92
83
|
path: "direct",
|
|
93
84
|
wsUrl: ready.ws_url,
|
|
94
|
-
channelId
|
|
85
|
+
channelId,
|
|
95
86
|
e2eePskB64u: ready.e2ee_psk_b64u,
|
|
96
87
|
defaultSuite: ready.default_suite,
|
|
97
88
|
opts,
|
|
@@ -4,6 +4,7 @@ import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
|
|
|
4
4
|
import { FlowersecError } from "../utils/errors.js";
|
|
5
5
|
import { randomBytes } from "../client-connect/common.js";
|
|
6
6
|
import { connectCore } from "../client-connect/connectCore.js";
|
|
7
|
+
import { assertTunnelGrantContract, assertValidPSK, prepareChannelId } from "../client-connect/contract.js";
|
|
7
8
|
function isRecord(v) {
|
|
8
9
|
return typeof v === "object" && v != null && !Array.isArray(v);
|
|
9
10
|
}
|
|
@@ -84,10 +85,7 @@ export async function connectTunnel(grant, opts) {
|
|
|
84
85
|
if (tunnelUrl === "") {
|
|
85
86
|
throw new FlowersecError({ stage: "validate", code: "missing_tunnel_url", path: "tunnel", message: "missing tunnel_url" });
|
|
86
87
|
}
|
|
87
|
-
const channelId = checkedGrant.channel_id
|
|
88
|
-
if (channelId === "") {
|
|
89
|
-
throw new FlowersecError({ stage: "validate", code: "missing_channel_id", path: "tunnel", message: "missing channel_id" });
|
|
90
|
-
}
|
|
88
|
+
const channelId = prepareChannelId(checkedGrant.channel_id, "tunnel");
|
|
91
89
|
const token = checkedGrant.token.trim();
|
|
92
90
|
if (token === "") {
|
|
93
91
|
throw new FlowersecError({ stage: "validate", code: "missing_token", path: "tunnel", message: "missing token" });
|
|
@@ -100,20 +98,9 @@ export async function connectTunnel(grant, opts) {
|
|
|
100
98
|
message: "missing channel_init_expire_at_unix_s",
|
|
101
99
|
});
|
|
102
100
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const psk = base64urlDecode(e2eePskB64u);
|
|
106
|
-
if (psk.length !== 32) {
|
|
107
|
-
throw new Error("psk must be 32 bytes");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch (e) {
|
|
111
|
-
throw new FlowersecError({ stage: "validate", code: "invalid_psk", path: "tunnel", message: "invalid e2ee_psk_b64u", cause: e });
|
|
112
|
-
}
|
|
101
|
+
assertTunnelGrantContract(checkedGrant, ControlRole.Role_client);
|
|
102
|
+
const e2eePskB64u = assertValidPSK(checkedGrant.e2ee_psk_b64u, "tunnel");
|
|
113
103
|
const idleTimeoutSeconds = checkedGrant.idle_timeout_seconds;
|
|
114
|
-
if (checkedGrant.role !== ControlRole.Role_client) {
|
|
115
|
-
throw new FlowersecError({ stage: "validate", code: "role_mismatch", path: "tunnel", message: "expected role=client" });
|
|
116
|
-
}
|
|
117
104
|
const endpointInstanceId = opts.endpointInstanceId ?? base64urlEncode(randomBytes(24));
|
|
118
105
|
let eidBytes;
|
|
119
106
|
try {
|