@nodefony/realtime 10.0.0-beta.6 → 10.0.0-beta.7
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 +5 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +15 -3
- package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
- package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
- package/dist/nodefony/src/backplane/controlOrder.js +34 -5
- package/dist/nodefony/src/backplane/envelope.js +422 -45
- package/dist/nodefony/src/backplane/ingress.js +93 -0
- package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
- package/dist/nodefony/src/backplane/publishQueue.js +97 -3
- package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
- package/dist/nodefony/src/server/RealtimeController.js +87 -44
- package/dist/nodefony/src/server/RealtimeHub.js +631 -127
- package/dist/nodefony/src/server/admissionBudget.js +8 -8
- package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/contractRoutes.js +3 -13
- package/dist/nodefony/src/server/frameAdmission.js +27 -35
- package/dist/nodefony/src/server/presenceBus.js +486 -0
- package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
- package/dist/nodefony/src/server/remotePresence.js +541 -0
- package/dist/nodefony/src/server/subscribedAck.js +3 -0
- package/dist/nodefony/src/service/RealtimeService.js +9 -0
- package/dist/nodefony/testing/backplaneConformance.js +79 -3
- package/dist/nodefony/testing/index.js +22 -8
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
- package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
- package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
- package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
- package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
- package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
- package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
- package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
- package/dist/types/nodefony/testing/index.d.ts +1 -1
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +1 -0
- package/docs/architecture.md +21 -6
- package/docs/canaux-a-membres.md +13 -3
- package/docs/cas-d-usage.md +4 -3
- package/docs/configuration.md +38 -2
- package/docs/cookbook-chat.md +400 -152
- package/docs/observabilite.md +28 -9
- package/docs/protocole.md +10 -10
- package/docs/securite.md +64 -10
- package/package.json +12 -12
|
@@ -12,9 +12,38 @@ import { ownField } from "nodefony";
|
|
|
12
12
|
*/
|
|
13
13
|
/** Longueur maximale d'un champ texte d'un ordre (nom de canal, identifiant de compte). */
|
|
14
14
|
const MAX_CONTROL_FIELD_LENGTH = 1024;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Un texte venu du bus est-il recevable — chaîne non vide d'au plus {@link MAX_CONTROL_FIELD_LENGTH} caractères ?
|
|
17
|
+
*
|
|
18
|
+
* Seule borne des textes du bus (nom de canal, identifiant de compte), lue par
|
|
19
|
+
* les ordres de contrôle ET par les messages de présence (ADR-0025 P2).
|
|
20
|
+
*
|
|
21
|
+
* @param value - champ reçu (forme inconnue).
|
|
22
|
+
*/
|
|
23
|
+
function isBoundedBusText(value) {
|
|
16
24
|
return typeof value === "string" && value.length > 0 && value.length <= 1024;
|
|
17
25
|
}
|
|
26
|
+
/** Les types d'ordre que cette version sait appliquer — liste fermée. */
|
|
27
|
+
function isKnownOp(op) {
|
|
28
|
+
return op === "close" || op === "evictUser";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* La charge est-elle un ordre d'un TYPE que cette version ne connaît pas — `op` lisible, mais hors de la liste fermée ?
|
|
32
|
+
*
|
|
33
|
+
* C'est ce qu'émet un serveur d'une version plus récente pendant un
|
|
34
|
+
* déploiement progressif (ADR-0025, « Règle de lecture du bus ») : l'ordre
|
|
35
|
+
* n'est pas appliqué, mais il ne se compte pas comme une injection. Rien
|
|
36
|
+
* d'autre que `op` n'est lu — ses autres champs appartiennent à une version
|
|
37
|
+
* qui n'est pas celle-ci. Une charge sans `op` textuel, ou d'un type CONNU de
|
|
38
|
+
* forme fausse, n'est pas « inconnue » : elle est invalide.
|
|
39
|
+
*
|
|
40
|
+
* @param payload - la charge du message reçu (forme inconnue).
|
|
41
|
+
* @returns `true` si le type d'ordre est inconnu de cette version.
|
|
42
|
+
*/
|
|
43
|
+
function isUnknownControlOrder(payload) {
|
|
44
|
+
const op = ownField(payload, "op");
|
|
45
|
+
return isBoundedBusText(op) && !isKnownOp(op);
|
|
46
|
+
}
|
|
18
47
|
/**
|
|
19
48
|
* Lit un ordre de contrôle dans une charge venue du bus — `null` si la forme n'est pas celle d'un ordre connu.
|
|
20
49
|
*
|
|
@@ -31,15 +60,15 @@ function isBoundedText(value) {
|
|
|
31
60
|
*/
|
|
32
61
|
function readControlOrder(payload) {
|
|
33
62
|
const channel = ownField(payload, "channel");
|
|
34
|
-
if (!
|
|
63
|
+
if (!isBoundedBusText(channel)) return null;
|
|
35
64
|
const op = ownField(payload, "op");
|
|
65
|
+
if (!isKnownOp(op)) return null;
|
|
36
66
|
if (op === "close") return {
|
|
37
67
|
op,
|
|
38
68
|
channel
|
|
39
69
|
};
|
|
40
|
-
if (op !== "evictUser") return null;
|
|
41
70
|
const user = ownField(payload, "user");
|
|
42
|
-
if (!
|
|
71
|
+
if (!isBoundedBusText(user)) return null;
|
|
43
72
|
const generation = ownField(payload, "generation");
|
|
44
73
|
if (generation === void 0) return {
|
|
45
74
|
op,
|
|
@@ -54,4 +83,4 @@ function readControlOrder(payload) {
|
|
|
54
83
|
generation
|
|
55
84
|
};
|
|
56
85
|
}
|
|
57
|
-
export { MAX_CONTROL_FIELD_LENGTH, readControlOrder };
|
|
86
|
+
export { MAX_CONTROL_FIELD_LENGTH, isBoundedBusText, isUnknownControlOrder, readControlOrder };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ownField } from "nodefony";
|
|
2
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createHmac, createSecretKey, timingSafeEqual } from "node:crypto";
|
|
3
3
|
// nodefony/src/backplane/envelope.ts
|
|
4
4
|
/**
|
|
5
5
|
* **Sceau d'enveloppe backplane** — authenticité des messages transportés sur un
|
|
@@ -37,23 +37,200 @@ import { createHmac, timingSafeEqual } from "node:crypto";
|
|
|
37
37
|
* captés), parade réservée par le champ optionnel `generation` de l'ordre. Un
|
|
38
38
|
* anti-rejeu par horodatage exigerait des horloges alignées entre pods.
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* **Aucune reconversion de la charge** (ADR-0021 S6) : à l'envoi, la charge est
|
|
41
|
+
* encodée UNE fois et l'enveloppe assemblée par fragments ; à la réception, le
|
|
42
|
+
* sceau se vérifie sur la TRANCHE reçue de la charge, et l'écho d'un pod se
|
|
43
|
+
* reconnaît à la fin du message, sans le décoder. Une forme différente (producteur
|
|
44
|
+
* tiers : clés dans un autre ordre, espaces, nombres non canoniques) retombe sur la
|
|
45
|
+
* vérification par re-sérialisation : on accepte strictement plus qu'avant, jamais
|
|
46
|
+
* moins.
|
|
47
|
+
*
|
|
48
|
+
* Pourquoi lire la tranche reçue est sûr : le sceau valide un triplet (origine,
|
|
49
|
+
* canal, texte de la charge) émis par un pair ; la forme vérifiée autour de la
|
|
50
|
+
* tranche est exactement celle qu'il écrit, donc le message reçu EST ce message
|
|
51
|
+
* émis, octet pour octet — et son décodage, déterministe, rend la charge signée.
|
|
42
52
|
*/
|
|
43
53
|
/** Nom du champ portant le sceau dans l'enveloppe sérialisée. */
|
|
44
54
|
const SEAL_FIELD = "sig";
|
|
55
|
+
/** Longueur d'un sceau : HMAC-SHA256 (32 octets) en base64url, sans remplissage. */
|
|
56
|
+
const SEAL_CHARS = 43;
|
|
57
|
+
const TEXT_HEAD = "{\"channel\":";
|
|
58
|
+
const TEXT_PAYLOAD_KEY = ",\"payload\":";
|
|
59
|
+
const TEXT_ORIGIN_KEY = ",\"originId\":";
|
|
60
|
+
const TEXT_SEAL_KEY = `,"${SEAL_FIELD}":"`;
|
|
61
|
+
const TEXT_SEAL_END = "\"}";
|
|
62
|
+
/**
|
|
63
|
+
* Guillemet fermant du canal suivi de la clé de charge. Cette suite ne peut
|
|
64
|
+
* apparaître DANS une chaîne JSON — un `"` y est toujours échappé — donc sa
|
|
65
|
+
* première occurrence après l'ouverture du canal en marque la fin.
|
|
66
|
+
*/
|
|
67
|
+
const TEXT_CHANNEL_CLOSE = `"${TEXT_PAYLOAD_KEY}`;
|
|
68
|
+
const TEXT_HEAD_LEN = 11;
|
|
69
|
+
const TEXT_PAYLOAD_KEY_LEN = 11;
|
|
70
|
+
const TEXT_ORIGIN_KEY_LEN = 12;
|
|
71
|
+
const TEXT_SEAL_KEY_LEN = TEXT_SEAL_KEY.length;
|
|
72
|
+
const TEXT_SEAL_END_LEN = 2;
|
|
73
|
+
const TEXT_CHANNEL_CLOSE_LEN = TEXT_CHANNEL_CLOSE.length;
|
|
74
|
+
/** `"` — ouvre la chaîne JSON du canal. */
|
|
75
|
+
const QUOTE = 34;
|
|
76
|
+
/** En-tête d'une map CBOR de 3 entrées (enveloppe nue). */
|
|
77
|
+
const CBOR_MAP_3 = 163;
|
|
78
|
+
/** En-tête d'une map CBOR de 4 entrées (enveloppe scellée). */
|
|
79
|
+
const CBOR_MAP_4 = 164;
|
|
80
|
+
/** Type majeur 3 (chaîne de texte), longueur dans l'octet initial (< 24). */
|
|
81
|
+
const CBOR_TEXT = 96;
|
|
82
|
+
/** Chaîne de texte, longueur sur 1 / 2 / 4 octets qui suivent. */
|
|
83
|
+
const CBOR_TEXT_1 = 120;
|
|
84
|
+
const CBOR_TEXT_2 = 121;
|
|
85
|
+
const CBOR_TEXT_4 = 122;
|
|
86
|
+
/** Taille de l'en-tête CBOR d'une chaîne de texte de `len` octets (forme la plus courte). */
|
|
87
|
+
function cborTextHeadSize(len) {
|
|
88
|
+
if (len < 24) return 1;
|
|
89
|
+
if (len < 256) return 2;
|
|
90
|
+
if (len < 65536) return 3;
|
|
91
|
+
return 5;
|
|
92
|
+
}
|
|
93
|
+
/** Écrit l'en-tête CBOR d'une chaîne de `len` octets ; rend la position suivante. */
|
|
94
|
+
function writeCborTextHead(out, at, len) {
|
|
95
|
+
if (len < 24) {
|
|
96
|
+
out[at] = CBOR_TEXT + len;
|
|
97
|
+
return at + 1;
|
|
98
|
+
}
|
|
99
|
+
if (len < 256) {
|
|
100
|
+
out[at] = CBOR_TEXT_1;
|
|
101
|
+
out[at + 1] = len;
|
|
102
|
+
return at + 2;
|
|
103
|
+
}
|
|
104
|
+
if (len < 65536) {
|
|
105
|
+
out[at] = CBOR_TEXT_2;
|
|
106
|
+
out[at + 1] = len >>> 8;
|
|
107
|
+
out[at + 2] = len & 255;
|
|
108
|
+
return at + 3;
|
|
109
|
+
}
|
|
110
|
+
out[at] = CBOR_TEXT_4;
|
|
111
|
+
out[at + 1] = len >>> 24;
|
|
112
|
+
out[at + 2] = len >>> 16 & 255;
|
|
113
|
+
out[at + 3] = len >>> 8 & 255;
|
|
114
|
+
out[at + 4] = len & 255;
|
|
115
|
+
return at + 5;
|
|
116
|
+
}
|
|
117
|
+
/** L'en-tête CBOR d'une chaîne de `len` octets se trouve-t-il à `at` (forme la plus courte) ? */
|
|
118
|
+
function cborTextHeadAt(raw, at, len) {
|
|
119
|
+
if (len < 24) return raw[at] === CBOR_TEXT + len;
|
|
120
|
+
if (len < 256) return raw[at] === CBOR_TEXT_1 && raw[at + 1] === len;
|
|
121
|
+
if (len < 65536) return raw[at] === CBOR_TEXT_2 && raw[at + 1] === len >>> 8 && raw[at + 2] === (len & 255);
|
|
122
|
+
return raw[at] === CBOR_TEXT_4 && raw[at + 1] === len >>> 24 && raw[at + 2] === (len >>> 16 & 255) && raw[at + 3] === (len >>> 8 & 255) && raw[at + 4] === (len & 255);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Longueur de la chaîne CBOR dont l'en-tête est à `at`, et taille de cet en-tête,
|
|
126
|
+
* écrites dans `into` (`[longueur, taille]`) ; `false` si ce n'est pas une chaîne
|
|
127
|
+
* de texte à longueur définie.
|
|
128
|
+
*/
|
|
129
|
+
function readCborTextHead(raw, at, into) {
|
|
130
|
+
const b = raw[at];
|
|
131
|
+
if (b === void 0) return false;
|
|
132
|
+
if (b >= CBOR_TEXT && b < CBOR_TEXT_1) {
|
|
133
|
+
into[0] = b - CBOR_TEXT;
|
|
134
|
+
into[1] = 1;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (b === CBOR_TEXT_1 && at + 1 < raw.length) {
|
|
138
|
+
into[0] = raw[at + 1] ?? 0;
|
|
139
|
+
into[1] = 2;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (b === CBOR_TEXT_2 && at + 2 < raw.length) {
|
|
143
|
+
into[0] = (raw[at + 1] ?? 0) << 8 | (raw[at + 2] ?? 0);
|
|
144
|
+
into[1] = 3;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (b === CBOR_TEXT_4 && at + 4 < raw.length) {
|
|
148
|
+
into[0] = ((raw[at + 1] ?? 0) << 24 >>> 0) + ((raw[at + 2] ?? 0) << 16 | (raw[at + 3] ?? 0) << 8 | (raw[at + 4] ?? 0));
|
|
149
|
+
into[1] = 5;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
/** Chaîne CBOR complète (en-tête + octets UTF-8) — pour les fragments fixes. */
|
|
155
|
+
function cborText(value) {
|
|
156
|
+
const len = Buffer.byteLength(value, "utf8");
|
|
157
|
+
const out = new Uint8Array(cborTextHeadSize(len) + len);
|
|
158
|
+
Buffer.from(out.buffer, out.byteOffset, out.byteLength).write(value, writeCborTextHead(out, 0, len), "utf8");
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
const CBOR_KEY_CHANNEL = cborText("channel");
|
|
162
|
+
const CBOR_KEY_PAYLOAD = cborText("payload");
|
|
163
|
+
const CBOR_KEY_ORIGIN = cborText("originId");
|
|
164
|
+
const CBOR_KEY_SEAL = cborText(SEAL_FIELD);
|
|
165
|
+
/** En-tête de la chaîne du sceau : 43 caractères ASCII. */
|
|
166
|
+
const CBOR_SEAL_HEAD = Uint8Array.of(CBOR_TEXT_1, SEAL_CHARS);
|
|
167
|
+
/** Octets fixes de la queue scellée, après la chaîne de l'origine. */
|
|
168
|
+
const CBOR_SEAL_TAIL_LEN = CBOR_KEY_SEAL.length + CBOR_SEAL_HEAD.length + SEAL_CHARS;
|
|
169
|
+
/** `frag` est-il présent tel quel dans `raw` à la position `at` ? Aucune allocation. */
|
|
170
|
+
function bytesAt(raw, at, frag) {
|
|
171
|
+
if (at < 0 || at + frag.length > raw.length) return false;
|
|
172
|
+
for (let i = 0; i < frag.length; i += 1) if (raw[at + i] !== frag[i]) return false;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/** Vue `Buffer` sur les mêmes octets (aucune copie) — pour les lectures de chaînes. */
|
|
176
|
+
function asBuffer(raw) {
|
|
177
|
+
return Buffer.isBuffer(raw) ? raw : Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Début de la chaîne canonique : `originId\nchannel\n`, fragments JSON. Les deux
|
|
181
|
+
* premiers sont des chaînes échappées : aucun décalage de frontière n'est
|
|
182
|
+
* possible, et la charge, en dernier, n'a pas de frontière à garder.
|
|
183
|
+
*/
|
|
184
|
+
function sealHead(originIdJson, channelJson) {
|
|
185
|
+
return `${originIdJson}\n${channelJson}\n`;
|
|
186
|
+
}
|
|
187
|
+
/** Dernière clé préparée et son secret (un serveur n'en a qu'un : la case unique suffit). */
|
|
188
|
+
let preparedSecret = null;
|
|
189
|
+
let preparedKey = null;
|
|
190
|
+
/**
|
|
191
|
+
* Clé HMAC préparée UNE fois par secret : une chaîne passée à `createHmac` est
|
|
192
|
+
* réencodée à chaque appel (mesuré : ~150 ns par sceau). Mêmes octets de clé — le
|
|
193
|
+
* secret en UTF-8, comme `createHmac` l'aurait fait.
|
|
194
|
+
*/
|
|
195
|
+
function keyOf(secret) {
|
|
196
|
+
if (secret !== preparedSecret || preparedKey === null) {
|
|
197
|
+
preparedKey = createSecretKey(Buffer.from(secret, "utf8"));
|
|
198
|
+
preparedSecret = secret;
|
|
199
|
+
}
|
|
200
|
+
return preparedKey;
|
|
201
|
+
}
|
|
45
202
|
/**
|
|
46
|
-
* Sceau HMAC-SHA256
|
|
47
|
-
* trois fragments JSON : les deux premiers sont des chaînes échappées, donc
|
|
48
|
-
* aucun décalage de frontière n'est possible entre les parties.
|
|
203
|
+
* Sceau (HMAC-SHA256 en base64url) de la chaîne canonique — un seul calcul.
|
|
49
204
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
205
|
+
* Une charge TEXTE est concaténée à l'en-tête : un seul passage vers le moteur de
|
|
206
|
+
* hachage coûte moins qu'une concaténation (mesuré : ~250 ns de moins) ; des
|
|
207
|
+
* OCTETS s'ajoutent tels quels, sans copie. Rendu en base64url, jamais en
|
|
208
|
+
* `Buffer` brut : allouer le `Buffer` du résultat coûte plus que de l'encoder
|
|
209
|
+
* (mesuré : ~900 ns).
|
|
54
210
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
211
|
+
function sealText(head, payload, secret) {
|
|
212
|
+
const hmac = createHmac("sha256", keyOf(secret));
|
|
213
|
+
return (typeof payload === "string" ? hmac.update(head + payload) : hmac.update(head).update(payload)).digest("base64url");
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Le sceau fourni correspond-il au sceau calculé ? À temps constant.
|
|
217
|
+
*
|
|
218
|
+
* Voie courante : les deux chaînes base64url comparées caractère par caractère,
|
|
219
|
+
* SANS sortie anticipée (le OU accumulé ne dépend que de la longueur, publique :
|
|
220
|
+
* celle de SHA-256) et sans allocation. Repli : un sceau écrit sous une autre forme
|
|
221
|
+
* base64 (remplissage, alphabet standard) est décodé et comparé par
|
|
222
|
+
* `timingSafeEqual` — on accepte ce qu'on acceptait avant. Le repli ne se prend
|
|
223
|
+
* que si la voie courante échoue : il ne révèle rien du secret.
|
|
224
|
+
*/
|
|
225
|
+
function sealMatches(provided, expected) {
|
|
226
|
+
if (provided.length === expected.length) {
|
|
227
|
+
let diff = 0;
|
|
228
|
+
for (let i = 0; i < expected.length; i += 1) diff |= provided.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
229
|
+
if (diff === 0) return true;
|
|
230
|
+
}
|
|
231
|
+
const a = Buffer.from(provided, "base64url");
|
|
232
|
+
const b = Buffer.from(expected, "base64url");
|
|
233
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
57
234
|
}
|
|
58
235
|
/**
|
|
59
236
|
* Sérialise un message backplane, scellé si un secret est fourni.
|
|
@@ -71,14 +248,26 @@ function sealBackplaneEnvelope(msg, secret) {
|
|
|
71
248
|
const channelJson = JSON.stringify(msg.channel);
|
|
72
249
|
const originIdJson = JSON.stringify(msg.originId);
|
|
73
250
|
const payloadJson = JSON.stringify(msg.payload) ?? "null";
|
|
74
|
-
const sig =
|
|
75
|
-
return
|
|
251
|
+
const sig = sealText(sealHead(originIdJson, channelJson), payloadJson, secret);
|
|
252
|
+
return TEXT_HEAD + channelJson + TEXT_PAYLOAD_KEY + payloadJson + TEXT_ORIGIN_KEY + originIdJson + TEXT_SEAL_KEY + sig + TEXT_SEAL_END;
|
|
76
253
|
}
|
|
77
254
|
/** Type-guard d'enveloppe — narrowing sûr d'un message brut venu du bus. */
|
|
78
255
|
function isEnvelope(m) {
|
|
79
256
|
return typeof ownField(m, "channel") === "string" && typeof ownField(m, "originId") === "string";
|
|
80
257
|
}
|
|
81
258
|
/**
|
|
259
|
+
* Tranche de la charge dans une enveloppe texte reçue, si elle a EXACTEMENT la
|
|
260
|
+
* forme qu'écrit {@link sealBackplaneEnvelope} pour ce canal, cette origine et ce
|
|
261
|
+
* sceau ; `null` sinon (forme tierce → repli).
|
|
262
|
+
*/
|
|
263
|
+
function receivedPayloadText(raw, channelJson, originIdJson, provided) {
|
|
264
|
+
const start = TEXT_HEAD_LEN + channelJson.length + TEXT_PAYLOAD_KEY_LEN;
|
|
265
|
+
const originAt = raw.length - TEXT_SEAL_END_LEN - provided.length - TEXT_SEAL_KEY_LEN - originIdJson.length - TEXT_ORIGIN_KEY_LEN;
|
|
266
|
+
if (originAt <= start) return null;
|
|
267
|
+
const sealAt = originAt + TEXT_ORIGIN_KEY_LEN + originIdJson.length + TEXT_SEAL_KEY_LEN;
|
|
268
|
+
return raw.startsWith(TEXT_HEAD) && raw.startsWith(channelJson, TEXT_HEAD_LEN) && raw.startsWith(TEXT_PAYLOAD_KEY, start - TEXT_PAYLOAD_KEY_LEN) && raw.startsWith(TEXT_ORIGIN_KEY, originAt) && raw.startsWith(originIdJson, originAt + TEXT_ORIGIN_KEY_LEN) && raw.startsWith(TEXT_SEAL_KEY, sealAt - TEXT_SEAL_KEY_LEN) && raw.startsWith(provided, sealAt) && raw.startsWith(TEXT_SEAL_END, sealAt + provided.length) ? raw.slice(start, originAt) : null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
82
271
|
* Parse et **vérifie** une enveloppe reçue du bus partagé.
|
|
83
272
|
*
|
|
84
273
|
* Fail-closed strict quand un secret est posé : une enveloppe sans sceau ou au
|
|
@@ -86,6 +275,10 @@ function isEnvelope(m) {
|
|
|
86
275
|
* `sig`. Sans secret, l'enveloppe nue est acceptée (compat ; l'alerte de boot qui
|
|
87
276
|
* signale ce mode non authentifié est portée par le wiring du driver).
|
|
88
277
|
*
|
|
278
|
+
* Le sceau se vérifie sur la tranche REÇUE de la charge quand l'enveloppe a la
|
|
279
|
+
* forme qu'écrit {@link sealBackplaneEnvelope} — aucune re-sérialisation ; une
|
|
280
|
+
* autre forme retombe sur la re-sérialisation de la charge lue.
|
|
281
|
+
*
|
|
89
282
|
* @param raw - message brut lu sur le bus.
|
|
90
283
|
* @param secret - secret partagé attendu, ou `null`/`undefined` si aucun.
|
|
91
284
|
* @returns le message vérifié, ou `null` s'il doit être ignoré (malformé, non
|
|
@@ -107,35 +300,109 @@ function openBackplaneEnvelope(raw, secret) {
|
|
|
107
300
|
if (!secret) return msg;
|
|
108
301
|
const provided = parsed[SEAL_FIELD];
|
|
109
302
|
if (typeof provided !== "string") return null;
|
|
110
|
-
const
|
|
111
|
-
|
|
303
|
+
const channelJson = JSON.stringify(msg.channel);
|
|
304
|
+
const originIdJson = JSON.stringify(msg.originId);
|
|
305
|
+
const head = sealHead(originIdJson, channelJson);
|
|
306
|
+
const slice = receivedPayloadText(raw, channelJson, originIdJson, provided);
|
|
307
|
+
if (slice !== null && sealMatches(provided, sealText(head, slice, secret))) return msg;
|
|
308
|
+
return sealMatches(provided, sealText(head, JSON.stringify(msg.payload) ?? "null", secret)) ? msg : null;
|
|
309
|
+
}
|
|
310
|
+
/** Encode par le codec, et refuse un codec texte : le sujet binaire porte des octets. */
|
|
311
|
+
function encodeBytes(codec, value) {
|
|
312
|
+
const out = codec.encode(value);
|
|
313
|
+
if (typeof out === "string") throw new TypeError(`backplane : le codec « ${codec.protocol} » rend du texte — le sujet binaire du bus exige un codec binaire`);
|
|
314
|
+
return out;
|
|
112
315
|
}
|
|
113
316
|
/**
|
|
114
|
-
*
|
|
317
|
+
* Assemble l'enveloppe binaire scellée par fragments : la charge, déjà encodée,
|
|
318
|
+
* est recopiée telle quelle — jamais ré-encodée. Une seule zone mémoire.
|
|
115
319
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
320
|
+
* Zone PROPRE (`new Uint8Array`), jamais le pool partagé de `Buffer.allocUnsafe` :
|
|
321
|
+
* une charge décodée de ces octets rend des vues, qui retiendraient tout le bloc du
|
|
322
|
+
* pool. Un `Uint8Array`, comme le codec : un `Buffer` y ferait décoder des
|
|
323
|
+
* `Buffer`. Les chaînes ASCII (le cas courant : canal, origine, sceau) s'écrivent
|
|
324
|
+
* par une boucle, sans objet intermédiaire.
|
|
118
325
|
*/
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
326
|
+
function frameSealedBytes(channel, originId, payloadBytes, sig) {
|
|
327
|
+
const channelLen = Buffer.byteLength(channel, "utf8");
|
|
328
|
+
const originLen = Buffer.byteLength(originId, "utf8");
|
|
329
|
+
const out = new Uint8Array(1 + CBOR_KEY_CHANNEL.length + cborTextHeadSize(channelLen) + channelLen + CBOR_KEY_PAYLOAD.length + payloadBytes.byteLength + CBOR_KEY_ORIGIN.length + cborTextHeadSize(originLen) + originLen + CBOR_SEAL_TAIL_LEN);
|
|
330
|
+
out[0] = CBOR_MAP_4;
|
|
331
|
+
let at = 1;
|
|
332
|
+
out.set(CBOR_KEY_CHANNEL, at);
|
|
333
|
+
at = writeCborTextHead(out, at + CBOR_KEY_CHANNEL.length, channelLen);
|
|
334
|
+
at = writeUtf8(out, at, channel, channelLen);
|
|
335
|
+
out.set(CBOR_KEY_PAYLOAD, at);
|
|
336
|
+
at += CBOR_KEY_PAYLOAD.length;
|
|
337
|
+
out.set(payloadBytes, at);
|
|
338
|
+
at += payloadBytes.byteLength;
|
|
339
|
+
out.set(CBOR_KEY_ORIGIN, at);
|
|
340
|
+
at = writeCborTextHead(out, at + CBOR_KEY_ORIGIN.length, originLen);
|
|
341
|
+
at = writeUtf8(out, at, originId, originLen);
|
|
342
|
+
out.set(CBOR_KEY_SEAL, at);
|
|
343
|
+
at += CBOR_KEY_SEAL.length;
|
|
344
|
+
out.set(CBOR_SEAL_HEAD, at);
|
|
345
|
+
writeUtf8(out, at + CBOR_SEAL_HEAD.length, sig, SEAL_CHARS);
|
|
346
|
+
return out;
|
|
123
347
|
}
|
|
348
|
+
/** Encodeur UTF-8 partagé, pour les chaînes non ASCII. */
|
|
349
|
+
const UTF8 = new TextEncoder();
|
|
124
350
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* Les deux premiers fragments restent des chaînes JSON échappées : aucun
|
|
128
|
-
* décalage de frontière n'est possible, et la charge, en dernier, n'a pas de
|
|
129
|
-
* frontière à garder.
|
|
351
|
+
* Écrit `value` en UTF-8 à `at` ; rend la position suivante. `byteLen` égal au
|
|
352
|
+
* nombre de caractères = chaîne ASCII : boucle directe, sans vue intermédiaire.
|
|
130
353
|
*/
|
|
131
|
-
function
|
|
132
|
-
|
|
354
|
+
function writeUtf8(out, at, value, byteLen) {
|
|
355
|
+
if (byteLen === value.length) for (let i = 0; i < byteLen; i += 1) out[at + i] = value.charCodeAt(i);
|
|
356
|
+
else UTF8.encodeInto(value, out.subarray(at, at + byteLen));
|
|
357
|
+
return at + byteLen;
|
|
133
358
|
}
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
359
|
+
/** Verdicts déjà CONSTATÉS, par codec (un serveur n'en a qu'un : la case unique suffit). */
|
|
360
|
+
let framedCodec = null;
|
|
361
|
+
let framedVerdict = false;
|
|
362
|
+
const framedVerdicts = /* @__PURE__ */ new WeakMap();
|
|
363
|
+
/**
|
|
364
|
+
* Le codec écrit-il le CBOR le plus court, celui que {@link frameSealedBytes}
|
|
365
|
+
* assemble ? CONSTATÉ, jamais déduit de son nom : une enveloppe d'épreuve est
|
|
366
|
+
* encodée par le codec et comparée à l'assemblage par fragments, une fois par
|
|
367
|
+
* codec. Faux → le codec garde la voie générique (double encodage), qui reste
|
|
368
|
+
* correcte pour n'importe quel format.
|
|
369
|
+
*/
|
|
370
|
+
function framesAsCbor(codec) {
|
|
371
|
+
if (codec === framedCodec) return framedVerdict;
|
|
372
|
+
let verdict = framedVerdicts.get(codec);
|
|
373
|
+
if (verdict === void 0) {
|
|
374
|
+
verdict = probeFraming(codec);
|
|
375
|
+
framedVerdicts.set(codec, verdict);
|
|
376
|
+
}
|
|
377
|
+
framedCodec = codec;
|
|
378
|
+
framedVerdict = verdict;
|
|
379
|
+
return verdict;
|
|
380
|
+
}
|
|
381
|
+
/** Épreuve de {@link framesAsCbor} : chaque longueur d'en-tête utile, octets compris. */
|
|
382
|
+
function probeFraming(codec) {
|
|
383
|
+
const channel = "é".repeat(20);
|
|
384
|
+
const originId = "pod";
|
|
385
|
+
const payload = {
|
|
386
|
+
seq: 7,
|
|
387
|
+
list: [
|
|
388
|
+
1,
|
|
389
|
+
-2.5,
|
|
390
|
+
null,
|
|
391
|
+
"x"
|
|
392
|
+
],
|
|
393
|
+
raw: new Uint8Array([0, 255])
|
|
394
|
+
};
|
|
395
|
+
const sig = "A".repeat(SEAL_CHARS);
|
|
396
|
+
try {
|
|
397
|
+
return asBuffer(encodeBytes(codec, {
|
|
398
|
+
channel,
|
|
399
|
+
payload,
|
|
400
|
+
originId,
|
|
401
|
+
sig
|
|
402
|
+
})).equals(asBuffer(frameSealedBytes(channel, originId, encodeBytes(codec, payload), sig)));
|
|
403
|
+
} catch {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
139
406
|
}
|
|
140
407
|
/**
|
|
141
408
|
* Encode un message backplane pour le **sujet binaire** du bus (ADR-0021 S6) :
|
|
@@ -143,9 +410,10 @@ function encodeBytes(codec, value) {
|
|
|
143
410
|
* binaire du serveur, octets natifs dans `payload`, scellée si un secret est
|
|
144
411
|
* fourni.
|
|
145
412
|
*
|
|
146
|
-
* Scellée, la charge est encodée
|
|
147
|
-
* l'enveloppe
|
|
148
|
-
*
|
|
413
|
+
* Scellée, la charge est encodée UNE fois : le sceau se calcule sur ses octets,
|
|
414
|
+
* et l'enveloppe s'assemble autour d'eux. Un codec qui n'écrit pas le CBOR le plus
|
|
415
|
+
* court (constaté, cf {@link framesAsCbor}) encode l'enveloppe entière une seconde
|
|
416
|
+
* fois — correct pour tout format, plus coûteux.
|
|
149
417
|
*
|
|
150
418
|
* @param msg - message à transporter (canal, charge, origine).
|
|
151
419
|
* @param codec - le codec binaire du serveur (`CBOR_FRAME_CODEC` de `nodefony/cbor`).
|
|
@@ -160,7 +428,9 @@ function sealBackplaneEnvelopeBytes(msg, codec, secret) {
|
|
|
160
428
|
payload,
|
|
161
429
|
originId: msg.originId
|
|
162
430
|
});
|
|
163
|
-
const
|
|
431
|
+
const payloadBytes = encodeBytes(codec, payload);
|
|
432
|
+
const sig = sealText(sealHead(JSON.stringify(msg.originId), JSON.stringify(msg.channel)), payloadBytes, secret);
|
|
433
|
+
if (framesAsCbor(codec)) return frameSealedBytes(msg.channel, msg.originId, payloadBytes, sig);
|
|
164
434
|
return encodeBytes(codec, {
|
|
165
435
|
channel: msg.channel,
|
|
166
436
|
payload,
|
|
@@ -169,14 +439,32 @@ function sealBackplaneEnvelopeBytes(msg, codec, secret) {
|
|
|
169
439
|
});
|
|
170
440
|
}
|
|
171
441
|
/**
|
|
442
|
+
* Tranche de la charge dans une enveloppe binaire reçue, si elle a EXACTEMENT la
|
|
443
|
+
* forme qu'assemble {@link frameSealedBytes} pour ce canal, cette origine et ce
|
|
444
|
+
* sceau ; `null` sinon (forme tierce → repli). Aucune copie : une vue.
|
|
445
|
+
*/
|
|
446
|
+
function receivedPayloadBytes(raw, channel, originId, provided) {
|
|
447
|
+
if (provided.length !== SEAL_CHARS || raw[0] !== CBOR_MAP_4) return null;
|
|
448
|
+
const channelLen = Buffer.byteLength(channel, "utf8");
|
|
449
|
+
const originLen = Buffer.byteLength(originId, "utf8");
|
|
450
|
+
const channelHeadAt = 1 + CBOR_KEY_CHANNEL.length;
|
|
451
|
+
const payloadKeyAt = channelHeadAt + cborTextHeadSize(channelLen) + channelLen;
|
|
452
|
+
const start = payloadKeyAt + CBOR_KEY_PAYLOAD.length;
|
|
453
|
+
const originAt = raw.length - CBOR_SEAL_TAIL_LEN - originLen - cborTextHeadSize(originLen) - CBOR_KEY_ORIGIN.length;
|
|
454
|
+
if (originAt <= start) return null;
|
|
455
|
+
const originHeadAt = originAt + CBOR_KEY_ORIGIN.length;
|
|
456
|
+
const sealKeyAt = originHeadAt + cborTextHeadSize(originLen) + originLen;
|
|
457
|
+
return bytesAt(raw, 1, CBOR_KEY_CHANNEL) && cborTextHeadAt(raw, channelHeadAt, channelLen) && bytesAt(raw, payloadKeyAt, CBOR_KEY_PAYLOAD) && bytesAt(raw, originAt, CBOR_KEY_ORIGIN) && cborTextHeadAt(raw, originHeadAt, originLen) && bytesAt(raw, sealKeyAt, CBOR_KEY_SEAL) && bytesAt(raw, sealKeyAt + CBOR_KEY_SEAL.length, CBOR_SEAL_HEAD) ? raw.subarray(start, originAt) : null;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
172
460
|
* Décode et **vérifie** une enveloppe reçue sur le sujet binaire du bus — même
|
|
173
461
|
* règle que {@link openBackplaneEnvelope} : fail-closed strict sous un secret,
|
|
174
462
|
* aucun downgrade en retirant `sig`.
|
|
175
463
|
*
|
|
176
|
-
* Le sceau se
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* octets.
|
|
464
|
+
* Le sceau se vérifie sur la tranche REÇUE de la charge quand l'enveloppe a la
|
|
465
|
+
* forme qu'assemble l'émetteur — aucun ré-encodage ; une autre forme retombe sur
|
|
466
|
+
* le ré-encodage de la charge décodée (le codec CBOR du cœur rend la forme la plus
|
|
467
|
+
* courte de chaque valeur, donc émetteur et récepteur signent les mêmes octets).
|
|
180
468
|
*
|
|
181
469
|
* @param raw - octets lus sur le sujet binaire.
|
|
182
470
|
* @param codec - le codec binaire du serveur.
|
|
@@ -199,12 +487,101 @@ function openBackplaneEnvelopeBytes(raw, codec, secret) {
|
|
|
199
487
|
if (!secret) return msg;
|
|
200
488
|
const provided = parsed[SEAL_FIELD];
|
|
201
489
|
if (typeof provided !== "string") return null;
|
|
490
|
+
const head = sealHead(JSON.stringify(msg.originId), JSON.stringify(msg.channel));
|
|
491
|
+
if (framesAsCbor(codec)) {
|
|
492
|
+
const slice = receivedPayloadBytes(raw, msg.channel, msg.originId, provided);
|
|
493
|
+
if (slice !== null && sealMatches(provided, sealText(head, slice, secret))) return msg;
|
|
494
|
+
}
|
|
202
495
|
let payloadBytes;
|
|
203
496
|
try {
|
|
204
497
|
payloadBytes = encodeBytes(codec, parsed.payload ?? null);
|
|
205
498
|
} catch {
|
|
206
499
|
return null;
|
|
207
500
|
}
|
|
208
|
-
return
|
|
501
|
+
return sealMatches(provided, sealText(head, payloadBytes, secret)) ? msg : null;
|
|
209
502
|
}
|
|
210
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Reconnaît l'**écho** d'un pod — ce que le bus lui renvoie de ses propres
|
|
505
|
+
* publications — à la FIN du message, sans le décoder (ADR-0021 S6, ADR-0025).
|
|
506
|
+
*
|
|
507
|
+
* Un bus pub/sub comme Redis rend à l'émetteur chacune de ses publications : sur un
|
|
508
|
+
* pod chargé, c'est un message reçu sur deux, et la présence répartie en fait un
|
|
509
|
+
* signal (le bus est vivant tant que l'écho revient). Le reconnaître coûte, sous un
|
|
510
|
+
* secret, UN HMAC sur la tranche de la charge : ni `JSON.parse`, ni décodage CBOR.
|
|
511
|
+
*
|
|
512
|
+
* Sous un secret, l'écho est VÉRIFIÉ avant d'être compté : un tiers qui forgerait
|
|
513
|
+
* l'origine de ce pod pour simuler un bus vivant échoue au sceau. Sans secret, rien
|
|
514
|
+
* ne peut l'être — le bus est ouvert, et le wiring l'annonce au démarrage.
|
|
515
|
+
*
|
|
516
|
+
* Un message que ce lecteur ne reconnaît pas (forme tierce, sceau faux) n'est PAS
|
|
517
|
+
* jeté : il suit la voie ordinaire, qui le vérifie en entier.
|
|
518
|
+
*/
|
|
519
|
+
var BackplaneEcho = class {
|
|
520
|
+
#secret;
|
|
521
|
+
/** `originId\n` en JSON : début de la chaîne canonique de CE pod. */
|
|
522
|
+
#sealHeadStart;
|
|
523
|
+
/** Texte, scellé : `,"originId":<origine>,"sig":"`. */
|
|
524
|
+
#textTail;
|
|
525
|
+
/** Texte, nu : `,"originId":<origine>}`. */
|
|
526
|
+
#textBareTail;
|
|
527
|
+
/** CBOR : clé `originId` puis la chaîne de l'origine. */
|
|
528
|
+
#bytesOrigin;
|
|
529
|
+
/** Lecture d'un en-tête de chaîne CBOR, réutilisée (aucune allocation par message). */
|
|
530
|
+
#head = [0, 0];
|
|
531
|
+
/**
|
|
532
|
+
* @param originId - identité de CE pod sur le bus.
|
|
533
|
+
* @param secret - secret partagé, ou `null` sur un bus non scellé.
|
|
534
|
+
*/
|
|
535
|
+
constructor(originId, secret) {
|
|
536
|
+
this.#secret = secret;
|
|
537
|
+
const originIdJson = JSON.stringify(originId);
|
|
538
|
+
this.#sealHeadStart = `${originIdJson}\n`;
|
|
539
|
+
this.#textTail = TEXT_ORIGIN_KEY + originIdJson + TEXT_SEAL_KEY;
|
|
540
|
+
this.#textBareTail = `${TEXT_ORIGIN_KEY}${originIdJson}}`;
|
|
541
|
+
const origin = cborText(originId);
|
|
542
|
+
this.#bytesOrigin = new Uint8Array(CBOR_KEY_ORIGIN.length + origin.length);
|
|
543
|
+
this.#bytesOrigin.set(CBOR_KEY_ORIGIN, 0);
|
|
544
|
+
this.#bytesOrigin.set(origin, CBOR_KEY_ORIGIN.length);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Ce message texte est-il l'écho de ce pod (sceau vérifié sous un secret) ?
|
|
548
|
+
*
|
|
549
|
+
* @param raw - message brut lu sur le sujet texte.
|
|
550
|
+
*/
|
|
551
|
+
text(raw) {
|
|
552
|
+
const secret = this.#secret;
|
|
553
|
+
if (secret === null) return raw.endsWith(this.#textBareTail);
|
|
554
|
+
const tail = this.#textTail;
|
|
555
|
+
const tailAt = raw.length - TEXT_SEAL_END_LEN - SEAL_CHARS - tail.length;
|
|
556
|
+
if (tailAt <= TEXT_HEAD_LEN || !raw.startsWith(tail, tailAt) || !raw.endsWith(TEXT_SEAL_END) || !raw.startsWith(TEXT_HEAD) || raw.charCodeAt(TEXT_HEAD_LEN) !== QUOTE) return false;
|
|
557
|
+
const close = raw.indexOf(TEXT_CHANNEL_CLOSE, 12);
|
|
558
|
+
const start = close + TEXT_CHANNEL_CLOSE_LEN;
|
|
559
|
+
if (close < 0 || start >= tailAt) return false;
|
|
560
|
+
const sealAt = tailAt + tail.length;
|
|
561
|
+
return sealMatches(raw.slice(sealAt, sealAt + SEAL_CHARS), sealText(`${this.#sealHeadStart}${raw.slice(TEXT_HEAD_LEN, close + 1)}\n`, raw.slice(start, tailAt), secret));
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Ce message binaire est-il l'écho de ce pod (sceau vérifié sous un secret) ?
|
|
565
|
+
*
|
|
566
|
+
* @param raw - octets lus sur le sujet binaire.
|
|
567
|
+
* @param codec - le codec binaire du serveur ; un codec qui n'écrit pas le CBOR
|
|
568
|
+
* attendu (cf {@link framesAsCbor}) laisse tout message à la voie ordinaire.
|
|
569
|
+
*/
|
|
570
|
+
bytes(raw, codec) {
|
|
571
|
+
if (!framesAsCbor(codec)) return false;
|
|
572
|
+
const origin = this.#bytesOrigin;
|
|
573
|
+
const secret = this.#secret;
|
|
574
|
+
if (secret === null) return raw[0] === CBOR_MAP_3 && bytesAt(raw, raw.length - origin.length, origin);
|
|
575
|
+
const originAt = raw.length - CBOR_SEAL_TAIL_LEN - origin.length;
|
|
576
|
+
const sealKeyAt = originAt + origin.length;
|
|
577
|
+
const head = this.#head;
|
|
578
|
+
if (raw[0] !== CBOR_MAP_4 || !bytesAt(raw, originAt, origin) || !bytesAt(raw, sealKeyAt, CBOR_KEY_SEAL) || !bytesAt(raw, sealKeyAt + CBOR_KEY_SEAL.length, CBOR_SEAL_HEAD) || !bytesAt(raw, 1, CBOR_KEY_CHANNEL) || !readCborTextHead(raw, 1 + CBOR_KEY_CHANNEL.length, head)) return false;
|
|
579
|
+
const channelAt = 1 + CBOR_KEY_CHANNEL.length + head[1];
|
|
580
|
+
const channelEnd = channelAt + head[0];
|
|
581
|
+
const start = channelEnd + CBOR_KEY_PAYLOAD.length;
|
|
582
|
+
if (start >= originAt || !bytesAt(raw, channelEnd, CBOR_KEY_PAYLOAD)) return false;
|
|
583
|
+
const view = asBuffer(raw);
|
|
584
|
+
return sealMatches(view.toString("latin1", raw.length - SEAL_CHARS, raw.length), sealText(`${this.#sealHeadStart}${JSON.stringify(view.toString("utf8", channelAt, channelEnd))}\n`, raw.subarray(start, originAt), secret));
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
export { BackplaneEcho, openBackplaneEnvelope, openBackplaneEnvelopeBytes, sealBackplaneEnvelope, sealBackplaneEnvelopeBytes };
|