@floegence/flowersec-core 2.3.10 → 2.4.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/README.md CHANGED
@@ -29,7 +29,7 @@ The root type exports are:
29
29
 
30
30
  Retry ownership belongs to `ConnectionController`; applications do not classify error text or run a parallel retry scheduler. Public failures remain redacted and reveal no carrier, candidate, URL, credential, stage, key, or diagnostic details.
31
31
 
32
- `RpcResult<Response>` is a discriminated union. `RpcPeer.call(...)` requires a decoder for successful payloads, so the typed success value has passed application validation before it is returned. Check `result.ok` before reading either the typed success `payload` or bounded application `error`; a result cannot contain both. RPC call and notify are portable across SDKs. TypeScript `RpcPeer.onNotify(...)` receives peer outbound notifications through the local Session's inbound reserved RPC stream.
32
+ `RpcResult<Response>` is a discriminated union. `RpcPeer.call(...)` requires a decoder for successful payloads, so the typed success value has passed application validation before it is returned. Check `result.ok` before reading either the typed success `payload` or bounded application `error`; a result cannot contain both. RPC call and notify accept only `JsonValue` payloads and reject values that cannot be represented on the wire before sending. TypeScript `RpcPeer.onNotify(typeId, decoder, handler)` receives peer outbound notifications through the local Session's inbound reserved RPC stream. A notification reaches the handler only after its decoder succeeds; decoder and handler failures are isolated from RPC serving.
33
33
 
34
34
  When connector options omit a connection timeout, browser and Node.js connectors use the shared ten-second default.
35
35
 
@@ -42,9 +42,9 @@ export type RpcResult<Response = unknown> = Readonly<{
42
42
  }>;
43
43
  }>;
44
44
  export interface RpcPeer {
45
- call<Request = unknown, Response = unknown>(typeId: number, payload: Request, decodeResponse: (payload: JsonValue) => Response, options?: OperationOptions): Promise<RpcResult<Response>>;
46
- notify<Payload = unknown>(typeId: number, payload: Payload, options?: OperationOptions): Promise<void>;
47
- onNotify<Payload = unknown>(typeId: number, handler: (payload: Payload) => void): () => void;
45
+ call<Request extends JsonValue = JsonValue, Response = unknown>(typeId: number, payload: Request, decodeResponse: (payload: JsonValue) => Response, options?: OperationOptions): Promise<RpcResult<Response>>;
46
+ notify<Payload extends JsonValue = JsonValue>(typeId: number, payload: Payload, options?: OperationOptions): Promise<void>;
47
+ onNotify<Payload>(typeId: number, decodePayload: (payload: JsonValue) => Payload, handler: (payload: Payload) => void | Promise<void>): () => void;
48
48
  }
49
49
  export interface ByteStream {
50
50
  readonly kind: string;
@@ -2,8 +2,18 @@ import { SessionError } from "./contract.js";
2
2
  import { createStreamMetadataV2, streamMetadataValuesV2 } from "./streamMetadata.js";
3
3
  /** @internal */
4
4
  export function projectSessionV2(session) {
5
- const rpc = projectRpcPeerV2(session.rpc);
5
+ const notificationOwner = {
6
+ subscriptions: new Set(),
7
+ closed: false,
8
+ };
9
+ const clearNotificationSubscriptions = () => {
10
+ notificationOwner.closed = true;
11
+ for (const unsubscribe of [...notificationOwner.subscriptions])
12
+ unsubscribe();
13
+ };
14
+ const rpc = projectRpcPeerV2(session.rpc, notificationOwner);
6
15
  const unreliable = session.unreliableMessages;
16
+ void session.termination.then(clearNotificationSubscriptions, clearNotificationSubscriptions);
7
17
  return Object.freeze({
8
18
  rpc,
9
19
  ...(unreliable === undefined ? {} : { unreliableMessages: unreliable }),
@@ -64,6 +74,9 @@ export function projectSessionV2(session) {
64
74
  catch (error) {
65
75
  throw redactSessionError(error);
66
76
  }
77
+ finally {
78
+ clearNotificationSubscriptions();
79
+ }
67
80
  },
68
81
  });
69
82
  }
@@ -115,14 +128,16 @@ function projectByteStreamV2(stream) {
115
128
  },
116
129
  });
117
130
  }
118
- function projectRpcPeerV2(peer) {
131
+ function projectRpcPeerV2(peer, notificationOwner) {
119
132
  return Object.freeze({
120
133
  async call(typeId, payload, decodeResponse, options) {
121
134
  try {
135
+ assertJsonValue(payload);
122
136
  const result = await peer.call(typeId, payload, options?.signal);
123
137
  if (result.error !== undefined) {
124
138
  return Object.freeze({ ok: false, error: Object.freeze({ ...result.error }) });
125
139
  }
140
+ assertJsonValue(result.payload);
126
141
  return Object.freeze({ ok: true, payload: decodeResponse(result.payload) });
127
142
  }
128
143
  catch (error) {
@@ -131,6 +146,7 @@ function projectRpcPeerV2(peer) {
131
146
  },
132
147
  async notify(typeId, payload, options) {
133
148
  try {
149
+ assertJsonValue(payload);
134
150
  if (options?.signal?.aborted)
135
151
  throw options.signal.reason ?? new DOMException("The operation was aborted", "AbortError");
136
152
  await raceWithSignal(peer.notify(typeId, payload), options?.signal);
@@ -139,11 +155,75 @@ function projectRpcPeerV2(peer) {
139
155
  throw redactSessionError(error);
140
156
  }
141
157
  },
142
- onNotify(typeId, handler) {
143
- return peer.onNotify(typeId, (payload) => handler(payload));
158
+ onNotify(typeId, decodePayload, handler) {
159
+ if (notificationOwner.closed)
160
+ return () => undefined;
161
+ const unsubscribe = peer.onNotify(typeId, (payload) => {
162
+ let decoded;
163
+ try {
164
+ assertJsonValue(payload);
165
+ decoded = decodePayload(payload);
166
+ }
167
+ catch {
168
+ return;
169
+ }
170
+ try {
171
+ void Promise.resolve(handler(decoded)).catch(() => undefined);
172
+ }
173
+ catch {
174
+ // Notification handlers are isolated from RPC serving.
175
+ }
176
+ });
177
+ let subscribed = true;
178
+ const cancel = () => {
179
+ if (!subscribed)
180
+ return;
181
+ subscribed = false;
182
+ notificationOwner.subscriptions.delete(cancel);
183
+ unsubscribe();
184
+ };
185
+ notificationOwner.subscriptions.add(cancel);
186
+ return cancel;
144
187
  },
145
188
  });
146
189
  }
190
+ function assertJsonValue(value) {
191
+ validateJsonValue(value, new Set());
192
+ }
193
+ function validateJsonValue(value, ancestors) {
194
+ if (value === null || typeof value === "string" || typeof value === "boolean")
195
+ return;
196
+ if (typeof value === "number") {
197
+ if (Number.isFinite(value))
198
+ return;
199
+ throw new TypeError("RPC payload contains a non-finite number");
200
+ }
201
+ if (typeof value !== "object")
202
+ throw new TypeError("RPC payload is not a JSON value");
203
+ if (ancestors.has(value))
204
+ throw new TypeError("RPC payload contains a cycle");
205
+ ancestors.add(value);
206
+ try {
207
+ if (Array.isArray(value)) {
208
+ for (let index = 0; index < value.length; index += 1) {
209
+ if (!(index in value))
210
+ throw new TypeError("RPC payload contains a sparse array");
211
+ validateJsonValue(value[index], ancestors);
212
+ }
213
+ return;
214
+ }
215
+ const prototype = Object.getPrototypeOf(value);
216
+ if (prototype !== Object.prototype && prototype !== null) {
217
+ throw new TypeError("RPC payload contains a non-JSON object");
218
+ }
219
+ for (const key of Object.keys(value)) {
220
+ validateJsonValue(value[key], ancestors);
221
+ }
222
+ }
223
+ finally {
224
+ ancestors.delete(value);
225
+ }
226
+ }
147
227
  async function raceWithSignal(operation, signal) {
148
228
  if (signal === undefined)
149
229
  return await operation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floegence/flowersec-core",
3
- "version": "2.3.10",
3
+ "version": "2.4.0",
4
4
  "description": "Flowersec core TypeScript library for carrier-neutral encrypted sessions and multiplexed streams.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -77,7 +77,7 @@
77
77
  "ws": "^8.21.2"
78
78
  },
79
79
  "optionalDependencies": {
80
- "@floegence/flowersec-node-native": "2.3.10"
80
+ "@floegence/flowersec-node-native": "2.4.0"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@playwright/test": "1.62.1",
@@ -95,5 +95,5 @@
95
95
  "vite": "^8.2.1",
96
96
  "vitest": "4.1.10"
97
97
  },
98
- "flowersecSourceCommit": "67e5ff521e77982e4f57e8ba5eb858df3e48c886"
98
+ "flowersecSourceCommit": "0849589d3516564cc2f32ce51f15a1bba4aa3aa6"
99
99
  }
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:2ff806a3-4e32-5ae7-867b-3dc94826ed55",
4
+ "serialNumber": "urn:uuid:c502284d-2e30-5f97-8204-2cdd20d4483a",
5
5
  "version": 1,
6
6
  "metadata": {
7
7
  "component": {
8
8
  "type": "library",
9
9
  "name": "@floegence/flowersec-core",
10
- "version": "2.3.10",
11
- "purl": "pkg:npm/%40floegence/flowersec-core@2.3.10",
12
- "bom-ref": "pkg:npm/%40floegence/flowersec-core@2.3.10"
10
+ "version": "2.4.0",
11
+ "purl": "pkg:npm/%40floegence/flowersec-core@2.4.0",
12
+ "bom-ref": "pkg:npm/%40floegence/flowersec-core@2.4.0"
13
13
  },
14
14
  "properties": [
15
15
  {
16
16
  "name": "flowersec:source-inventory-sha256",
17
- "value": "36475d4f06355a764285462713f086777546ed34fe2ea5911961d9bb9a3037f1"
17
+ "value": "1c377fd1fb5ea77eadd418a3990b5ae9585edf1a740a95901287df743cc2ec9c"
18
18
  }
19
19
  ]
20
20
  },
@@ -190,7 +190,7 @@
190
190
  ],
191
191
  "dependencies": [
192
192
  {
193
- "ref": "pkg:npm/%40floegence/flowersec-core@2.3.10",
193
+ "ref": "pkg:npm/%40floegence/flowersec-core@2.4.0",
194
194
  "dependsOn": [
195
195
  "pkg:npm/%40noble/ciphers@2.3.0",
196
196
  "pkg:npm/%40noble/curves@2.3.0",
package/sbom/spdx.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
5
  "name": "flowersec-ts",
6
- "documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/36475d4f06355a764285462713f086777546ed34fe2ea5911961d9bb9a3037f1",
6
+ "documentNamespace": "https://github.com/floegence/flowersec/sbom/flowersec-ts/1c377fd1fb5ea77eadd418a3990b5ae9585edf1a740a95901287df743cc2ec9c",
7
7
  "creationInfo": {
8
8
  "created": "1970-01-01T00:00:00Z",
9
9
  "creators": [
@@ -13,19 +13,19 @@
13
13
  "packages": [
14
14
  {
15
15
  "name": "@floegence/flowersec-core",
16
- "SPDXID": "SPDXRef-Package-bd5ff7d2005890830d32",
17
- "versionInfo": "2.3.10",
16
+ "SPDXID": "SPDXRef-Package-d5c0c7bb3c27968de167",
17
+ "versionInfo": "2.4.0",
18
18
  "downloadLocation": "NOASSERTION",
19
19
  "filesAnalyzed": false,
20
20
  "licenseConcluded": "NOASSERTION",
21
21
  "licenseDeclared": "NOASSERTION",
22
22
  "copyrightText": "NOASSERTION",
23
- "comment": "Flowersec source inventory SHA-256: 36475d4f06355a764285462713f086777546ed34fe2ea5911961d9bb9a3037f1",
23
+ "comment": "Flowersec source inventory SHA-256: 1c377fd1fb5ea77eadd418a3990b5ae9585edf1a740a95901287df743cc2ec9c",
24
24
  "externalRefs": [
25
25
  {
26
26
  "referenceCategory": "PACKAGE-MANAGER",
27
27
  "referenceType": "purl",
28
- "referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.3.10"
28
+ "referenceLocator": "pkg:npm/%40floegence/flowersec-core@2.4.0"
29
29
  }
30
30
  ]
31
31
  },
@@ -136,30 +136,30 @@
136
136
  {
137
137
  "spdxElementId": "SPDXRef-DOCUMENT",
138
138
  "relationshipType": "DESCRIBES",
139
- "relatedSpdxElement": "SPDXRef-Package-bd5ff7d2005890830d32"
139
+ "relatedSpdxElement": "SPDXRef-Package-d5c0c7bb3c27968de167"
140
140
  },
141
141
  {
142
- "spdxElementId": "SPDXRef-Package-bd5ff7d2005890830d32",
142
+ "spdxElementId": "SPDXRef-Package-d5c0c7bb3c27968de167",
143
143
  "relationshipType": "DEPENDS_ON",
144
144
  "relatedSpdxElement": "SPDXRef-Package-5ce913a03239b02770fb"
145
145
  },
146
146
  {
147
- "spdxElementId": "SPDXRef-Package-bd5ff7d2005890830d32",
147
+ "spdxElementId": "SPDXRef-Package-d5c0c7bb3c27968de167",
148
148
  "relationshipType": "DEPENDS_ON",
149
149
  "relatedSpdxElement": "SPDXRef-Package-01009adf60db02c13634"
150
150
  },
151
151
  {
152
- "spdxElementId": "SPDXRef-Package-bd5ff7d2005890830d32",
152
+ "spdxElementId": "SPDXRef-Package-d5c0c7bb3c27968de167",
153
153
  "relationshipType": "DEPENDS_ON",
154
154
  "relatedSpdxElement": "SPDXRef-Package-8815b117c8a1d5f7eeb0"
155
155
  },
156
156
  {
157
- "spdxElementId": "SPDXRef-Package-bd5ff7d2005890830d32",
157
+ "spdxElementId": "SPDXRef-Package-d5c0c7bb3c27968de167",
158
158
  "relationshipType": "DEPENDS_ON",
159
159
  "relatedSpdxElement": "SPDXRef-Package-f8b45a289df643042b94"
160
160
  },
161
161
  {
162
- "spdxElementId": "SPDXRef-Package-bd5ff7d2005890830d32",
162
+ "spdxElementId": "SPDXRef-Package-d5c0c7bb3c27968de167",
163
163
  "relationshipType": "DEPENDS_ON",
164
164
  "relatedSpdxElement": "SPDXRef-Package-b779412f685822663496"
165
165
  },