@nodefony/realtime 10.0.0-beta.4 → 10.0.0-beta.6

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.
Files changed (78) hide show
  1. package/README.md +79 -10
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +79 -12
  4. package/dist/nodefony/config/config.js +7 -0
  5. package/dist/nodefony/decorators/realtimeDecorators.js +125 -19
  6. package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
  7. package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
  8. package/dist/nodefony/src/backplane/RedisBackplane.js +143 -31
  9. package/dist/nodefony/src/backplane/controlOrder.js +57 -0
  10. package/dist/nodefony/src/backplane/envelope.js +113 -12
  11. package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
  12. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
  13. package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
  14. package/dist/nodefony/src/server/RealtimeConnection.js +110 -0
  15. package/dist/nodefony/src/server/RealtimeController.js +344 -85
  16. package/dist/nodefony/src/server/RealtimeHub.js +604 -61
  17. package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
  18. package/dist/nodefony/src/server/admissionBudget.js +176 -0
  19. package/dist/nodefony/src/server/channelPolicyRegistry.js +73 -0
  20. package/dist/nodefony/src/server/connectionAdmission.js +242 -0
  21. package/dist/nodefony/src/server/contractRoutes.js +85 -0
  22. package/dist/nodefony/src/server/deniedDetail.js +10 -3
  23. package/dist/nodefony/src/server/frameAdmission.js +168 -0
  24. package/dist/nodefony/src/server/subscribedAck.js +10 -1
  25. package/dist/nodefony/src/server/syslogUplink.js +3 -4
  26. package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
  27. package/dist/nodefony/src/service/RealtimeService.js +42 -1
  28. package/dist/nodefony/src/service/publicOrigin.js +40 -0
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
  30. package/dist/nodefony/testing/backplaneConformance.js +271 -0
  31. package/dist/nodefony/testing/index.js +21 -4
  32. package/dist/types/index.d.ts +34 -7
  33. package/dist/types/nodefony/config/config.d.ts +9 -0
  34. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +86 -6
  35. package/dist/types/nodefony/interfaces/IBackplane.d.ts +34 -50
  36. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
  37. package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
  38. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
  39. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
  40. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
  41. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  43. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
  44. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +79 -16
  45. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +52 -0
  46. package/dist/types/nodefony/src/backplane/envelope.d.ts +34 -0
  47. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
  48. package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
  49. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +17 -0
  50. package/dist/types/nodefony/src/server/RealtimeController.d.ts +93 -13
  51. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +321 -24
  52. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
  53. package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
  54. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +34 -0
  55. package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
  56. package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
  57. package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
  58. package/dist/types/nodefony/src/server/frameAdmission.d.ts +70 -0
  59. package/dist/types/nodefony/src/server/subscribedAck.d.ts +7 -0
  60. package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
  61. package/dist/types/nodefony/src/service/RealtimeService.d.ts +10 -2
  62. package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
  63. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
  64. package/dist/types/nodefony/testing/backplaneConformance.d.ts +66 -0
  65. package/dist/types/nodefony/testing/index.d.ts +17 -4
  66. package/docs/actions.md +442 -19
  67. package/docs/architecture.md +83 -8
  68. package/docs/cadence-et-contre-pression.md +87 -11
  69. package/docs/canaux-a-membres.md +267 -0
  70. package/docs/cas-d-usage.md +1702 -0
  71. package/docs/configuration.md +137 -17
  72. package/docs/cookbook-chat.md +333 -85
  73. package/docs/index.md +11 -1
  74. package/docs/observabilite.md +71 -14
  75. package/docs/protocole.md +133 -24
  76. package/docs/securite.md +294 -33
  77. package/docs/vocabulaire.md +13 -12
  78. package/package.json +14 -11
@@ -0,0 +1,168 @@
1
+ // nodefony/src/server/frameAdmission.ts
2
+ /**
3
+ * Verdicts du verrou retenus par connexion, au plus (ADR-0022 §6, décision 1 ter).
4
+ *
5
+ * Borne la table qu'un client remplit en poussant sur des canaux entrants : au-delà,
6
+ * le verrou est consulté à chaque trame, il n'est jamais contourné. Une admission
7
+ * par `allow` n'y est pas soumise — elle est déjà bornée par le plafond de canaux
8
+ * de la connexion.
9
+ */
10
+ const MAX_RETAINED_FRAME_VERDICTS = 64;
11
+ /**
12
+ * Méthodes du protocole : leurs trames ne sont JAMAIS admises d'office, même si un
13
+ * canal entrant porte ce nom. `subscribe` et `api.request` restent sur le verrou ;
14
+ * `ping` et `unsubscribe` passent sans règle (le verrou n'a rien à en dire).
15
+ */
16
+ const PROTOCOL_METHODS = /* @__PURE__ */ new Set([
17
+ "subscribe",
18
+ "unsubscribe",
19
+ "ping",
20
+ "api.request"
21
+ ]);
22
+ /**
23
+ * La connexion peut-elle admettre d'office les trames de ce nom ?
24
+ *
25
+ * Seulement un canal ENTRANT déclaré — une clé PROPRE de la table, jamais une
26
+ * héritée de son prototype —, qui n'est ni une méthode du protocole ni une
27
+ * action : la lecture admise ne regarde que le nom, et une requête d'action du
28
+ * même nom passerait sinon sans le verrou. Cold path (admission, jamais par trame).
29
+ */
30
+ function canAdmit(state, inbound, name) {
31
+ return Object.hasOwn(inbound, name) && !PROTOCOL_METHODS.has(name) && !state.peer.methods.includes(name);
32
+ }
33
+ /**
34
+ * Table des admis de la connexion, créée à la première admission.
35
+ *
36
+ * Mêmes clés, dans le même ordre, que la table entrante, toutes à `false` :
37
+ * toutes les connexions d'un contrôleur partagent ainsi UNE forme d'objet, et la
38
+ * lecture chaude reste monomorphe. Mesuré sur la trame réelle (médiane de 9) :
39
+ * objet de forme commune ≈ 0 ns, objets de formes diverses +15 ns, `Set` +26 à
40
+ * +30 ns. Une admission passe une clé existante à `true`, un retrait à `false` —
41
+ * jamais d'ajout ni de `delete`, qui changeraient la forme.
42
+ */
43
+ function admittedTable(state, inbound) {
44
+ let admitted = state.admitted;
45
+ if (admitted === null) {
46
+ admitted = {};
47
+ for (const name of Object.keys(inbound)) admitted[name] = false;
48
+ state.admitted = admitted;
49
+ }
50
+ return admitted;
51
+ }
52
+ /**
53
+ * Admet les trames entrantes d'un canal que sa règle `allow` vient d'ouvrir à la connexion.
54
+ *
55
+ * Pas de plafond propre : le nombre de canaux ouverts l'est déjà. Sans effet si le
56
+ * canal n'accepte pas d'entrée client.
57
+ *
58
+ * @param state - état de la connexion.
59
+ * @param channel - canal ouvert par `allow`.
60
+ */
61
+ function admitByRule(state, channel) {
62
+ const inbound = state.inbound;
63
+ if (inbound === null || !canAdmit(state, inbound, channel)) return;
64
+ admittedTable(state, inbound)[channel] = true;
65
+ }
66
+ /**
67
+ * Retire TOUTE admission d'un canal à la connexion — marque de sa règle ET verdict retenu du verrou — quand le serveur l'en éjecte ou le termine (ADR-0022 §6 décision 3).
68
+ *
69
+ * Plus fort que {@link releaseRuleAdmission}, qui garde le verdict du verrou :
70
+ * après une éjection, une trame entrante sur ce canal repasse par le verrou.
71
+ *
72
+ * @param state - état de la connexion.
73
+ * @param channel - canal dont la connexion est retirée.
74
+ */
75
+ function revokeAdmission(state, channel) {
76
+ const retained = state.retained;
77
+ if (retained !== null) {
78
+ const at = retained.indexOf(channel);
79
+ if (at !== -1) {
80
+ retained.splice(at, 1);
81
+ if (retained.length === 0) state.retained = null;
82
+ }
83
+ }
84
+ const admitted = state.admitted;
85
+ if (admitted?.[channel] === true) admitted[channel] = false;
86
+ }
87
+ /**
88
+ * Retire l'admission d'un canal à la fin de son abonnement — celle de sa règle
89
+ * `allow`, que rien ne justifie plus. Un verdict du verrou, lui, ne tient qu'à
90
+ * l'identité et survit au désabonnement.
91
+ *
92
+ * @param state - état de la connexion.
93
+ * @param channel - canal quitté.
94
+ */
95
+ function releaseRuleAdmission(state, channel) {
96
+ const admitted = state.admitted;
97
+ if (admitted === null || admitted[channel] !== true) return;
98
+ if (state.retained?.includes(channel) === true) return;
99
+ admitted[channel] = false;
100
+ }
101
+ /**
102
+ * Retient le « oui » du verrou pour un canal entrant à politique fixe (jamais un refus).
103
+ *
104
+ * Au plus {@link MAX_RETAINED_FRAME_VERDICTS} par connexion. La première retenue
105
+ * inscrit la connexion auprès du hub, qui pourra la vider si une politique arrive
106
+ * après coup.
107
+ */
108
+ function retainVerdict(state, method, hub) {
109
+ const retained = state.retained;
110
+ if (retained !== null && retained.length >= 64) return;
111
+ const inbound = state.inbound;
112
+ if (inbound === null || !canAdmit(state, inbound, method)) return;
113
+ admittedTable(state, inbound)[method] = true;
114
+ if (retained === null) {
115
+ state.retained = [method];
116
+ hub.trackRetainedVerdicts(state);
117
+ } else retained.push(method);
118
+ }
119
+ /**
120
+ * Oublie les verdicts du verrou retenus par la connexion — leur politique vient
121
+ * de changer. Les admissions par `allow` restent : elles valent autant que
122
+ * l'abonnement qui les porte.
123
+ *
124
+ * @param state - état de la connexion.
125
+ */
126
+ function dropRetainedVerdicts(state) {
127
+ const retained = state.retained;
128
+ if (retained === null) return;
129
+ const admitted = state.admitted;
130
+ if (admitted !== null) for (const name of retained) admitted[name] = false;
131
+ state.retained = null;
132
+ }
133
+ /**
134
+ * Juge une trame entrante que la connexion n'a pas admise d'office (ADR-0022 §6, décision 1 ter).
135
+ *
136
+ * Dans l'ordre :
137
+ * - une REQUÊTE (action, `api.request`) passe par le verrou, toujours ;
138
+ * - `ping` et `unsubscribe` passent sans règle ;
139
+ * - `subscribe` passe par le verrou (`allow` est jugé après, à l'ouverture) ;
140
+ * - un nom hérité du prototype de la table entrante est refusé ;
141
+ * - un canal entrant à `allow` que l'abonnement n'a pas admis est refusé ;
142
+ * - un canal entrant à politique fixe passe par le verrou, dont le « oui » est retenu ;
143
+ * - toute autre notification passe par le verrou.
144
+ *
145
+ * Le premier test du contrôleur (`admitted[method] === true`) a déjà écarté les trames
146
+ * admises : rien ici n'est sur le chemin d'une trame chaude.
147
+ *
148
+ * @param state - état de la connexion.
149
+ * @param frame - trame déjà classée par le pair.
150
+ * @param hub - hub qui porte le verrou et les règles.
151
+ * @returns `true` si la trame peut être distribuée.
152
+ */
153
+ function gateFrame(state, frame, hub) {
154
+ if ("id" in frame) return hub.runAuthorizer(frame, state.peer);
155
+ const method = frame.method;
156
+ if (method === "ping" || method === "unsubscribe") return true;
157
+ const inbound = state.inbound;
158
+ if (method === "subscribe" || inbound === null) return hub.runAuthorizer(frame, state.peer);
159
+ if (!Object.hasOwn(inbound, method)) {
160
+ if (method in inbound) return false;
161
+ return hub.runAuthorizer(frame, state.peer);
162
+ }
163
+ if (hub.resolveChannelAdmission(method) !== null) return false;
164
+ if (!hub.runAuthorizer(frame, state.peer)) return false;
165
+ retainVerdict(state, method, hub);
166
+ return true;
167
+ }
168
+ export { MAX_RETAINED_FRAME_VERDICTS, admitByRule, dropRetainedVerdicts, gateFrame, releaseRuleAdmission, revokeAdmission };
@@ -26,4 +26,13 @@ const SUBSCRIBED_FRAME_PREFIX = "{\"jsonrpc\":\"2.0\",\"method\":\"realtime:subs
26
26
  function subscribedAckFrame(channel) {
27
27
  return SUBSCRIBED_FRAME_PREFIX + JSON.stringify(channel) + "}}";
28
28
  }
29
- export { subscribedAckFrame };
29
+ /**
30
+ * Sérialise `realtime:subscribed { channel, imposed: true }` — l'acquittement d'un abonnement imposé par le serveur.
31
+ *
32
+ * @param channel - le canal imposé (échappé par `JSON.stringify`).
33
+ * @returns la trame JSON-RPC 2.0 prête à écrire sur le transport.
34
+ */
35
+ function imposedSubscribedAckFrame(channel) {
36
+ return SUBSCRIBED_FRAME_PREFIX + JSON.stringify(channel) + ",\"imposed\":true}}";
37
+ }
38
+ export { imposedSubscribedAckFrame, subscribedAckFrame };
@@ -75,11 +75,10 @@ function createSyslogUplinkHandler(opts) {
75
75
  let windowStart = 0;
76
76
  let windowCount = 0;
77
77
  return function onSyslogUplink(params) {
78
- if (params === null || typeof params !== "object") return;
79
- const batch = params;
80
- const entries = batch.entries;
78
+ if (!isRecord(params)) return;
79
+ const entries = params.entries;
81
80
  if (!Array.isArray(entries) || entries.length === 0) return;
82
- const pageId = safeRequestId(batch.pageId, maxRequestIdLength);
81
+ const pageId = safeRequestId(params.pageId, maxRequestIdLength);
83
82
  if (pageId === void 0) return;
84
83
  const now = Date.now();
85
84
  if (now - windowStart > windowMs) {
@@ -0,0 +1,37 @@
1
+ // nodefony/src/server/welcomeIdentity.ts
2
+ /**
3
+ * Vue PUBLIQUE de l'identité d'une connexion, annoncée au client par `realtime:welcome` — tirée de l'identité d'admission quand le jeton en produit une.
4
+ *
5
+ * Un objet NEUF, champ par champ, et jamais l'identité d'admission elle-même :
6
+ * celle-ci porte `issuer` et `account`, qui restent au serveur — la sérialiser
7
+ * telle quelle les enverrait au client (ADR-0021 §S10 point 1). Les tableaux
8
+ * de rôles et de scopes, eux, sont repris par RÉFÉRENCE : gelés une fois à la
9
+ * construction de l'identité, ils ne se recopient pas.
10
+ *
11
+ * Un jeton sans `getIdentity` (authentificateur tiers) annonce `id: null` : son
12
+ * identifiant stable n'est pas connu, et le déduire du login serait le défaut
13
+ * même que `id` corrige.
14
+ *
15
+ * @param token - jeton figé à la poignée de main.
16
+ * @returns l'identité à placer dans le `welcome`.
17
+ */
18
+ function welcomeIdentityOf(token) {
19
+ const who = token.getIdentity?.();
20
+ if (who !== void 0) return {
21
+ type: who.type,
22
+ authenticated: who.authenticated,
23
+ id: who.id,
24
+ userIdentifier: who.userIdentifier,
25
+ roles: who.roles,
26
+ scopes: who.scopes
27
+ };
28
+ return {
29
+ type: token.type,
30
+ authenticated: token.isAuthenticated(),
31
+ id: null,
32
+ userIdentifier: token.getUserIdentifier(),
33
+ roles: token.getRoles(),
34
+ scopes: token.getScopes()
35
+ };
36
+ }
37
+ export { welcomeIdentityOf };
@@ -1,8 +1,11 @@
1
- import { isRealtimeConfig } from "../../config/defineModuleConfig.js";
2
1
  import { RESERVED_SYSTEM_PREFIXES, getRealtimeHub } from "../server/RealtimeHub.js";
2
+ import { realtimeConnectSource } from "./publicOrigin.js";
3
3
  import { applyDeclaredBroadcastPrefixes } from "../../decorators/realtimeDecorators.js";
4
+ import { isRealtimeConfig } from "../../config/defineModuleConfig.js";
4
5
  import { Service } from "nodefony";
5
6
  // nodefony/src/service/RealtimeService.ts
7
+ /** Nom sous lequel ce module contribue à la politique de contenu. */
8
+ const CSP_CONTRIBUTOR = "realtime";
6
9
  const serviceName = "realtimeService";
7
10
  /**
8
11
  * Façade DI publique de la **socket Nodefony** côté serveur.
@@ -70,7 +73,17 @@ var RealtimeService = class extends Service {
70
73
  }
71
74
  hub.setOriginGuard(buildOriginGuard(this.#config));
72
75
  hub.setMaxChannelsPerConnection(this.#config.limits.maxChannelsPerConnection);
76
+ hub.setAdmissionLimits({
77
+ authenticateTimeoutMs: this.#config.authenticateTimeoutMs,
78
+ allowTimeoutMs: this.#config.allowTimeoutMs,
79
+ maxPendingAllowsPerConnection: this.#config.maxPendingAllowsPerConnection,
80
+ maxPendingAllowsTotal: this.#config.maxPendingAllowsTotal
81
+ });
73
82
  hub.setSlowConsumerBytes(this.#config.slowConsumer.bytes);
83
+ if (this.#config.codec === "cbor") {
84
+ const { CBOR_FRAME_CODEC } = await import("nodefony/cbor");
85
+ hub.setFrameCodec(CBOR_FRAME_CODEC);
86
+ } else hub.setFrameCodec();
74
87
  hub.setClientLogsLimits(this.#config.clientLogs.enabled ? {
75
88
  maxEntriesPerBatch: this.#config.clientLogs.maxEntriesPerBatch,
76
89
  maxEntriesPerWindow: this.#config.clientLogs.maxEntriesPerWindow,
@@ -78,8 +91,26 @@ var RealtimeService = class extends Service {
78
91
  maxStringLength: this.#config.clientLogs.maxStringLength
79
92
  } : null);
80
93
  applyDeclaredBroadcastPrefixes(hub);
94
+ if (this.#config.publicOrigin !== void 0) this.#declareCspOrigin(this.#config.publicOrigin);
81
95
  return this;
82
96
  }
97
+ /**
98
+ * Déclare au pare-feu l'origine publique de la socket dans `connect-src`.
99
+ *
100
+ * Différé à `onReady` : au moment de `init`, le module de sécurité peut ne pas
101
+ * avoir encore enregistré son pare-feu (ordre des modules). Sans pare-feu
102
+ * (application sans `@nodefony/security`), aucune politique n'est posée :
103
+ * rien à déclarer.
104
+ *
105
+ * @param publicOrigin - origine nue validée par le schéma
106
+ */
107
+ #declareCspOrigin(publicOrigin) {
108
+ const source = realtimeConnectSource(publicOrigin);
109
+ if (source === null) return;
110
+ this.kernel?.once("onReady", () => {
111
+ this.get("firewall")?.registerCspOrigins?.(CSP_CONTRIBUTOR, { "connect-src": [source] });
112
+ });
113
+ }
83
114
  /** Configuration realtime gelée (lue par Studio / introspection). */
84
115
  getConfig() {
85
116
  if (!this.#config) throw new Error(`${serviceName}: not initialized`);
@@ -89,6 +120,14 @@ var RealtimeService = class extends Service {
89
120
  getHub() {
90
121
  return this.#hub ??= getRealtimeHub();
91
122
  }
123
+ /**
124
+ * Le codec texte des connexions socket, lu PAR NOM par le REST du plan
125
+ * d'administration (`AdminApiController`, framework ne dépend pas de realtime) :
126
+ * les octets d'une réponse `GET` y prennent la même forme que sur la socket.
127
+ */
128
+ get textFrameCodec() {
129
+ return this.getHub().textFrameCodec;
130
+ }
92
131
  /** Backplane actuellement branché sur le hub (ou null = Loopback implicite). */
93
132
  getBackplane() {
94
133
  return this.getHub().backplane;
@@ -96,6 +135,8 @@ var RealtimeService = class extends Service {
96
135
  /**
97
136
  * Publie une charge sur un canal — fan-out local + propagation backplane si
98
137
  * canal broadcast. Délégué à `RealtimeHub.publish`.
138
+ *
139
+ * @throws TypeError si la charge est un scalaire ou `null` (cf `isRealtimePayload`).
99
140
  */
100
141
  publish(channel, payload) {
101
142
  this.getHub().publish(channel, payload);
@@ -0,0 +1,40 @@
1
+ import { parseBareOrigin } from "nodefony";
2
+ // nodefony/src/service/publicOrigin.ts
3
+ /**
4
+ * Schémas qu'accepte `realtime.publicOrigin` : celui de la socket (`ws:`,
5
+ * `wss:`) ou celui de l'hôte qui la sert (`http:`, `https:`), ramené au
6
+ * premier.
7
+ */
8
+ const REALTIME_ORIGIN_PROTOCOLS = [
9
+ "ws:",
10
+ "wss:",
11
+ "http:",
12
+ "https:"
13
+ ];
14
+ /** Le schéma de socket qui correspond à chaque schéma accepté. */
15
+ const SOCKET_SCHEME = {
16
+ "ws:": "ws:",
17
+ "wss:": "wss:",
18
+ "http:": "ws:",
19
+ "https:": "wss:"
20
+ };
21
+ /**
22
+ * Rend la source `connect-src` qui autorise la socket servie à `publicOrigin`,
23
+ * toujours sous son schéma de socket.
24
+ *
25
+ * Une source `wss://hôte` couvre `wss:` ET `https:` du même hôte (CSP Level 3,
26
+ * « scheme-part match ») ; une source `https://hôte` ne couvrirait PAS la
27
+ * socket. D'où la conversion `https:` → `wss:`, `http:` → `ws:`.
28
+ *
29
+ * @param publicOrigin - origine nue où la socket est servie
30
+ * @returns la source à déclarer (`wss://rt.example.com:8443`), ou `null` si la
31
+ * valeur n'est pas une origine nue d'un schéma accepté
32
+ */
33
+ function realtimeConnectSource(publicOrigin) {
34
+ const url = parseBareOrigin(publicOrigin, REALTIME_ORIGIN_PROTOCOLS);
35
+ if (url === null) return null;
36
+ const scheme = SOCKET_SCHEME[url.protocol];
37
+ if (scheme === void 0) return null;
38
+ return `${scheme}//${url.host}`;
39
+ }
40
+ export { REALTIME_ORIGIN_PROTOCOLS, realtimeConnectSource };
@@ -24,6 +24,16 @@ import { decideSend } from "@nodefony/http";
24
24
  * la fermeture est signalée par `fireClose()` (hook `onFinish` du Context). Le
25
25
  * `connect`/`onOpen` sont des no-op (la connexion est déjà ouverte au handshake).
26
26
  */
27
+ /**
28
+ * Vrai si les octets reposent sur un `ArrayBuffer` ordinaire — ce que le pair
29
+ * et ses codecs reçoivent (`RealtimeFrameData`), jamais un `SharedArrayBuffer`.
30
+ *
31
+ * @param bytes - les octets reçus.
32
+ * @returns `true` si la vue porte sur un `ArrayBuffer`.
33
+ */
34
+ function isOwnedBytes(bytes) {
35
+ return bytes.buffer instanceof ArrayBuffer;
36
+ }
27
37
  var WsConnectionTransport = class {
28
38
  conn;
29
39
  _onMessage = null;
@@ -78,7 +88,7 @@ var WsConnectionTransport = class {
78
88
  * Compte une trame ENTRANTE écartée et rend le cumul de sa nature — `1` dit
79
89
  * « première », le seul instant où l'appelant journalise.
80
90
  *
81
- * @param kind - `binary` (sans lecteur) ou `malformed` (texte non JSON).
91
+ * @param kind - `binary` (sans lecteur) ou `malformed` (illisible par le codec).
82
92
  * @returns le cumul de cette nature, après incrément.
83
93
  */
84
94
  discard(kind) {
@@ -88,7 +98,7 @@ var WsConnectionTransport = class {
88
98
  get binaryDiscarded() {
89
99
  return this._binaryDiscarded;
90
100
  }
91
- /** Cumul de trames TEXTE reçues et écartées parce que non JSON — monotone. */
101
+ /** Cumul de trames reçues et écartées parce qu'illisibles par le codec de la connexion — monotone. */
92
102
  get malformedDiscarded() {
93
103
  return this._malformedDiscarded;
94
104
  }
@@ -122,9 +132,22 @@ var WsConnectionTransport = class {
122
132
  this._onClose = cb;
123
133
  }
124
134
  onError() {}
125
- /** Pump entrant : le pipeline framework pousse ici chaque message reçu. */
135
+ /**
136
+ * Pump entrant : le pipeline framework pousse ici chaque frame reçue — texte,
137
+ * ou octets d'une connexion qui a négocié un codec binaire (ADR-0022 §2).
138
+ *
139
+ * Une vue sur un `SharedArrayBuffer` n'est pas une trame : `ws` livre
140
+ * toujours un `Buffer` sur un `ArrayBuffer`, le type de Node l'admet
141
+ * seulement. Elle est comptée comme illisible, jamais transmise au pair.
142
+ *
143
+ * @param raw - la trame telle que reçue.
144
+ */
126
145
  feed(raw) {
127
- this._onMessage?.(raw);
146
+ if (typeof raw === "string" || isOwnedBytes(raw)) {
147
+ this._onMessage?.(raw);
148
+ return;
149
+ }
150
+ this._malformedDiscarded += 1;
128
151
  }
129
152
  /** Fermeture : appelée par le owner sur `onFinish` (close WS). */
130
153
  fireClose(code = 1e3, reason = "") {