@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.
- package/dist/browser/connect.d.ts +2 -1
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +1 -1
- package/dist/client-connect/connectCore.d.ts +1 -1
- package/dist/connect/artifact.d.ts +0 -1
- package/dist/connect/artifact.js +0 -4
- package/dist/connect/{internalNormalize.d.ts → resolveArtifact.d.ts} +8 -8
- package/dist/connect/resolveArtifact.js +87 -0
- package/dist/e2ee/record.js +8 -4
- package/dist/e2ee/secureChannel.d.ts +1 -0
- package/dist/e2ee/secureChannel.js +18 -4
- package/dist/facade.d.ts +0 -3
- package/dist/facade.js +4 -10
- package/dist/node/connect.d.ts +0 -2
- package/dist/tunnel-client/connect.d.ts +1 -1
- package/dist/tunnel-client/connect.js +1 -11
- package/package.json +4 -6
- package/dist/browser/controlplane.d.ts +0 -15
- package/dist/browser/controlplane.js +0 -64
- package/dist/connect/internalNormalize.js +0 -173
|
@@ -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:
|
|
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>;
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
export {
|
|
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";
|
package/dist/browser/index.js
CHANGED
|
@@ -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 {
|
|
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/
|
|
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;
|
package/dist/connect/artifact.js
CHANGED
|
@@ -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
|
|
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
|
|
7
|
+
type ResolveOptions = Readonly<{
|
|
6
8
|
observer?: ClientObserverLike;
|
|
7
9
|
scopeResolvers?: ConnectScopeResolverMap;
|
|
8
10
|
relaxedOptionalScopeValidation?: boolean;
|
|
9
11
|
}>;
|
|
10
|
-
export type
|
|
12
|
+
export type ResolvedConnectArtifact = Readonly<{
|
|
11
13
|
kind: "tunnel";
|
|
12
|
-
input:
|
|
13
|
-
correlation?: CorrelationContext;
|
|
14
|
+
input: ChannelInitGrant;
|
|
14
15
|
observer?: ClientObserverLike;
|
|
15
16
|
}> | Readonly<{
|
|
16
17
|
kind: "direct";
|
|
17
|
-
input:
|
|
18
|
-
correlation?: CorrelationContext;
|
|
18
|
+
input: DirectConnectInfo;
|
|
19
19
|
observer?: ClientObserverLike;
|
|
20
20
|
}>;
|
|
21
|
-
export declare function
|
|
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
|
+
}
|
package/dist/e2ee/record.js
CHANGED
|
@@ -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
|
|
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(
|
|
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 (
|
|
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.
|
|
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) {
|
|
@@ -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 {
|
|
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
|
|
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);
|
package/dist/node/connect.d.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
}
|