@source-repo/rpc 3.0.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/LICENSE +21 -0
- package/README.md +1208 -0
- package/dist/Logging/ILogger.d.ts +7 -0
- package/dist/Logging/ILogger.d.ts.map +1 -0
- package/dist/Logging/ILogger.js +2 -0
- package/dist/Logging/ILogger.js.map +1 -0
- package/dist/NodeRpcServer.d.ts +55 -0
- package/dist/NodeRpcServer.d.ts.map +1 -0
- package/dist/NodeRpcServer.js +46 -0
- package/dist/NodeRpcServer.js.map +1 -0
- package/dist/RPC/Auth.d.ts +98 -0
- package/dist/RPC/Auth.d.ts.map +1 -0
- package/dist/RPC/Auth.js +14 -0
- package/dist/RPC/Auth.js.map +1 -0
- package/dist/RPC/Codec.d.ts +24 -0
- package/dist/RPC/Codec.d.ts.map +1 -0
- package/dist/RPC/Codec.js +21 -0
- package/dist/RPC/Codec.js.map +1 -0
- package/dist/RPC/Compatibility.d.ts +39 -0
- package/dist/RPC/Compatibility.d.ts.map +1 -0
- package/dist/RPC/Compatibility.js +195 -0
- package/dist/RPC/Compatibility.js.map +1 -0
- package/dist/RPC/Core.d.ts +188 -0
- package/dist/RPC/Core.d.ts.map +1 -0
- package/dist/RPC/Core.js +354 -0
- package/dist/RPC/Core.js.map +1 -0
- package/dist/RPC/Expose.d.ts +90 -0
- package/dist/RPC/Expose.d.ts.map +1 -0
- package/dist/RPC/Expose.js +110 -0
- package/dist/RPC/Expose.js.map +1 -0
- package/dist/RPC/Idempotency.d.ts +90 -0
- package/dist/RPC/Idempotency.d.ts.map +1 -0
- package/dist/RPC/Idempotency.js +46 -0
- package/dist/RPC/Idempotency.js.map +1 -0
- package/dist/RPC/Introspection.d.ts +92 -0
- package/dist/RPC/Introspection.d.ts.map +1 -0
- package/dist/RPC/Introspection.js +188 -0
- package/dist/RPC/Introspection.js.map +1 -0
- package/dist/RPC/Introspection.types.json +1093 -0
- package/dist/RPC/Messages.d.ts +108 -0
- package/dist/RPC/Messages.d.ts.map +1 -0
- package/dist/RPC/Messages.js +30 -0
- package/dist/RPC/Messages.js.map +1 -0
- package/dist/RPC/Rpc.d.ts +51 -0
- package/dist/RPC/Rpc.d.ts.map +1 -0
- package/dist/RPC/Rpc.js +54 -0
- package/dist/RPC/Rpc.js.map +1 -0
- package/dist/RPC/RpcClientHandler.d.ts +129 -0
- package/dist/RPC/RpcClientHandler.d.ts.map +1 -0
- package/dist/RPC/RpcClientHandler.js +267 -0
- package/dist/RPC/RpcClientHandler.js.map +1 -0
- package/dist/RPC/RpcServerHandler.d.ts +226 -0
- package/dist/RPC/RpcServerHandler.d.ts.map +1 -0
- package/dist/RPC/RpcServerHandler.js +676 -0
- package/dist/RPC/RpcServerHandler.js.map +1 -0
- package/dist/RPC/Schema.d.ts +150 -0
- package/dist/RPC/Schema.d.ts.map +1 -0
- package/dist/RPC/Schema.js +198 -0
- package/dist/RPC/Schema.js.map +1 -0
- package/dist/RPC/Signing.d.ts +118 -0
- package/dist/RPC/Signing.d.ts.map +1 -0
- package/dist/RPC/Signing.js +171 -0
- package/dist/RPC/Signing.js.map +1 -0
- package/dist/RPC/Tokens.d.ts +49 -0
- package/dist/RPC/Tokens.d.ts.map +1 -0
- package/dist/RPC/Tokens.js +45 -0
- package/dist/RPC/Tokens.js.map +1 -0
- package/dist/RpcClient.d.ts +91 -0
- package/dist/RpcClient.d.ts.map +1 -0
- package/dist/RpcClient.js +145 -0
- package/dist/RpcClient.js.map +1 -0
- package/dist/RpcServer.d.ts +191 -0
- package/dist/RpcServer.d.ts.map +1 -0
- package/dist/RpcServer.js +293 -0
- package/dist/RpcServer.js.map +1 -0
- package/dist/Transports/Mqtt5Frame.d.ts +107 -0
- package/dist/Transports/Mqtt5Frame.d.ts.map +1 -0
- package/dist/Transports/Mqtt5Frame.js +162 -0
- package/dist/Transports/Mqtt5Frame.js.map +1 -0
- package/dist/Transports/MqttTransport.d.ts +279 -0
- package/dist/Transports/MqttTransport.d.ts.map +1 -0
- package/dist/Transports/MqttTransport.js +843 -0
- package/dist/Transports/MqttTransport.js.map +1 -0
- package/dist/Transports/Presence.d.ts +68 -0
- package/dist/Transports/Presence.d.ts.map +1 -0
- package/dist/Transports/Presence.js +21 -0
- package/dist/Transports/Presence.js.map +1 -0
- package/dist/Transports/SocketIoClientTransport.d.ts +89 -0
- package/dist/Transports/SocketIoClientTransport.d.ts.map +1 -0
- package/dist/Transports/SocketIoClientTransport.js +280 -0
- package/dist/Transports/SocketIoClientTransport.js.map +1 -0
- package/dist/Transports/SocketIoServerTransport.d.ts +135 -0
- package/dist/Transports/SocketIoServerTransport.d.ts.map +1 -0
- package/dist/Transports/SocketIoServerTransport.js +505 -0
- package/dist/Transports/SocketIoServerTransport.js.map +1 -0
- package/dist/Utilities/Converters.d.ts +22 -0
- package/dist/Utilities/Converters.d.ts.map +1 -0
- package/dist/Utilities/Converters.js +58 -0
- package/dist/Utilities/Converters.js.map +1 -0
- package/dist/Utilities/Filter.d.ts +7 -0
- package/dist/Utilities/Filter.d.ts.map +1 -0
- package/dist/Utilities/Filter.js +14 -0
- package/dist/Utilities/Filter.js.map +1 -0
- package/dist/Utilities/ReadableName.d.ts +38 -0
- package/dist/Utilities/ReadableName.d.ts.map +1 -0
- package/dist/Utilities/ReadableName.js +58 -0
- package/dist/Utilities/ReadableName.js.map +1 -0
- package/dist/Utilities/Switch.d.ts +21 -0
- package/dist/Utilities/Switch.d.ts.map +1 -0
- package/dist/Utilities/Switch.js +70 -0
- package/dist/Utilities/Switch.js.map +1 -0
- package/dist/Utilities/TryCatch.d.ts +11 -0
- package/dist/Utilities/TryCatch.d.ts.map +1 -0
- package/dist/Utilities/TryCatch.js +13 -0
- package/dist/Utilities/TryCatch.js.map +1 -0
- package/dist/index-web.d.ts +25 -0
- package/dist/index-web.d.ts.map +1 -0
- package/dist/index-web.js +28 -0
- package/dist/index-web.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { base64ToUint8Array, stringToUint8Array, uint8ArrayToBase64 } from 'uint8array-extras';
|
|
2
|
+
/**
|
|
3
|
+
* Frame signing, for transports that cannot authenticate a connection.
|
|
4
|
+
*
|
|
5
|
+
* MQTT peers connect to a broker, not to each other, so a receiver has no connection to attribute
|
|
6
|
+
* a message to and the source field is only a claim. Signing each frame makes the claim checkable
|
|
7
|
+
* without trusting the broker: a broker operator, or any peer whose ACLs let it publish to another
|
|
8
|
+
* peer's topic, still cannot forge a message from someone else.
|
|
9
|
+
*
|
|
10
|
+
* Built on WebCrypto, which is present in Node and in browsers, so the same code signs on both.
|
|
11
|
+
*/
|
|
12
|
+
const subtle = () => {
|
|
13
|
+
const available = globalThis.crypto?.subtle;
|
|
14
|
+
if (!available)
|
|
15
|
+
throw new Error('source-rpc signing needs WebCrypto (globalThis.crypto.subtle)');
|
|
16
|
+
return available;
|
|
17
|
+
};
|
|
18
|
+
const toBytes = (value) => (typeof value === 'string' ? stringToUint8Array(value) : value);
|
|
19
|
+
/**
|
|
20
|
+
* WebCrypto takes a BufferSource, which recent typings narrow to views over a plain ArrayBuffer.
|
|
21
|
+
* A Uint8Array may sit on a SharedArrayBuffer, so copying is what makes the type honest rather
|
|
22
|
+
* than asserted away. The cost is trivial next to the hashing that follows.
|
|
23
|
+
*/
|
|
24
|
+
const bufferSource = (bytes) => new Uint8Array(bytes);
|
|
25
|
+
/**
|
|
26
|
+
* The exact bytes a signature covers: a JSON array of the header fields, followed by the payload.
|
|
27
|
+
*
|
|
28
|
+
* A JSON array fixes the field order and escapes the values, so no combination of names can be
|
|
29
|
+
* made to look like a different frame. The verifier rebuilds the preamble from the fields it
|
|
30
|
+
* parsed, so it knows where the payload begins without needing a length prefix.
|
|
31
|
+
*/
|
|
32
|
+
export const canonicalSignedBytes = (frame) => {
|
|
33
|
+
const preamble = stringToUint8Array(JSON.stringify([frame.source, frame.target, frame.time, frame.seq, frame.nonce]));
|
|
34
|
+
const result = new Uint8Array(preamble.length + frame.payload.length);
|
|
35
|
+
result.set(preamble, 0);
|
|
36
|
+
result.set(frame.payload, preamble.length);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The MQTT 5 canonical form. The topic is signed rather than a target field, because under this
|
|
41
|
+
* layout the topic is what carries the addressing.
|
|
42
|
+
*
|
|
43
|
+
* Frame version 1 deliberately left contentType out, on the reasoning that it only says how to read
|
|
44
|
+
* bytes that are themselves covered - so altering it could make the payload fail to parse but never
|
|
45
|
+
* change what was authorised. **That reasoning is wrong**, and the counterexample is one byte long:
|
|
46
|
+
* `0x31` decodes as the JSON text `"1"`, which is the number 1, and as a MsgPack positive fixint,
|
|
47
|
+
* which is the number 49. Both parse. Both verify. Flipping one unsigned property therefore changed
|
|
48
|
+
* a signed setpoint from 1 to 49 with the signature still good.
|
|
49
|
+
*
|
|
50
|
+
* The same argument applies to anything else the receiver acts on rather than merely transports: the
|
|
51
|
+
* error code decides what a caller does about a failure, the declared contract version decides
|
|
52
|
+
* whether the call is accepted at all, the response topic decides where the answer is published, the
|
|
53
|
+
* ttl decides whether the method runs at all, and the idempotency key decides whether it runs again.
|
|
54
|
+
* All five are covered.
|
|
55
|
+
*
|
|
56
|
+
* The MQTT message expiry is deliberately **not** covered, because the broker is meant to decrement
|
|
57
|
+
* it in flight and a signature over it would break on the first queued message. Nothing is lost:
|
|
58
|
+
* expiry may only narrow the signed ttl, never extend it, so rewriting it can delay or drop a frame
|
|
59
|
+
* - which anyone able to rewrite it could do anyway - but cannot buy a stale command more time.
|
|
60
|
+
*/
|
|
61
|
+
export const canonicalSignedBytesV5 = (frame) => {
|
|
62
|
+
const preamble = stringToUint8Array(JSON.stringify([
|
|
63
|
+
frame.version,
|
|
64
|
+
frame.topic,
|
|
65
|
+
frame.responseTopic,
|
|
66
|
+
frame.source,
|
|
67
|
+
frame.kind,
|
|
68
|
+
frame.path,
|
|
69
|
+
frame.methodOrEvent,
|
|
70
|
+
frame.correlation,
|
|
71
|
+
frame.contentType,
|
|
72
|
+
frame.code,
|
|
73
|
+
frame.contractVersion,
|
|
74
|
+
frame.ttl,
|
|
75
|
+
frame.idempotencyKey,
|
|
76
|
+
frame.timestamp,
|
|
77
|
+
frame.nonce
|
|
78
|
+
]));
|
|
79
|
+
const result = new Uint8Array(preamble.length + frame.payload.length);
|
|
80
|
+
result.set(preamble, 0);
|
|
81
|
+
result.set(frame.payload, preamble.length);
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
/** A nonce with enough entropy that collisions are not a practical concern. */
|
|
85
|
+
export const createNonce = () => uint8ArrayToBase64(globalThis.crypto.getRandomValues(new Uint8Array(16)));
|
|
86
|
+
/**
|
|
87
|
+
* Rejects frames that are too old and frames whose nonce has been seen before.
|
|
88
|
+
*
|
|
89
|
+
* A signature alone does not stop a captured frame being sent again, which for RPC would mean
|
|
90
|
+
* replaying a command. The freshness window bounds how long a captured frame stays useful and
|
|
91
|
+
* bounds how many nonces have to be remembered to cover it.
|
|
92
|
+
*/
|
|
93
|
+
export class ReplayGuard {
|
|
94
|
+
maxClockSkew;
|
|
95
|
+
maxTrackedNonces;
|
|
96
|
+
seen = new Map();
|
|
97
|
+
constructor(
|
|
98
|
+
/** How far a frame's timestamp may differ from now. Peers need clocks within this of each other. */
|
|
99
|
+
maxClockSkew = 60000,
|
|
100
|
+
/** Hard cap on remembered nonces, so a flood cannot grow this without bound. */
|
|
101
|
+
maxTrackedNonces = 5000) {
|
|
102
|
+
this.maxClockSkew = maxClockSkew;
|
|
103
|
+
this.maxTrackedNonces = maxTrackedNonces;
|
|
104
|
+
}
|
|
105
|
+
/** True if the frame is fresh and previously unseen. Records the nonce as a side effect. */
|
|
106
|
+
accept(nonce, time, now = Date.now()) {
|
|
107
|
+
if (!nonce || !Number.isFinite(time))
|
|
108
|
+
return false;
|
|
109
|
+
if (Math.abs(now - time) > this.maxClockSkew)
|
|
110
|
+
return false;
|
|
111
|
+
if (this.seen.has(nonce))
|
|
112
|
+
return false;
|
|
113
|
+
this.seen.set(nonce, now);
|
|
114
|
+
this.prune(now);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
prune(now) {
|
|
118
|
+
// Insertion order tracks arrival order, so the first entry that is still fresh means the
|
|
119
|
+
// rest are too.
|
|
120
|
+
for (const [nonce, at] of this.seen) {
|
|
121
|
+
if (now - at > this.maxClockSkew || this.seen.size > this.maxTrackedNonces) {
|
|
122
|
+
this.seen.delete(nonce);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
get size() {
|
|
129
|
+
return this.seen.size;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const identityOf = (source, identityFor) => identityFor?.(source) ?? { name: source };
|
|
133
|
+
/**
|
|
134
|
+
* HMAC-SHA256 with a secret per peer. Universally available, but symmetric: whoever can verify a
|
|
135
|
+
* peer's messages can also forge them, so the secret must only be shared with parties allowed to
|
|
136
|
+
* act as that peer. Use Ed25519 when a compromised verifier must not be able to impersonate.
|
|
137
|
+
*/
|
|
138
|
+
export const createHmacSigner = (secret) => {
|
|
139
|
+
let imported;
|
|
140
|
+
const key = () => (imported ??= subtle().importKey('raw', bufferSource(toBytes(secret)), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']));
|
|
141
|
+
return async (canonicalBytes) => uint8ArrayToBase64(new Uint8Array(await subtle().sign('HMAC', await key(), bufferSource(canonicalBytes))));
|
|
142
|
+
};
|
|
143
|
+
export const createHmacVerifier = (resolveSecret, identityFor) => {
|
|
144
|
+
return async (canonicalBytes, signature, { source }) => {
|
|
145
|
+
const secret = await resolveSecret(source);
|
|
146
|
+
if (!secret)
|
|
147
|
+
return undefined;
|
|
148
|
+
const key = await subtle().importKey('raw', bufferSource(toBytes(secret)), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
149
|
+
// subtle.verify compares in constant time, so this does not leak the expected signature.
|
|
150
|
+
const valid = await subtle().verify('HMAC', key, bufferSource(base64ToUint8Array(signature)), bufferSource(canonicalBytes));
|
|
151
|
+
return valid ? identityOf(source, identityFor) : undefined;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Ed25519, taking WebCrypto keys directly so key storage and format stay the caller's concern.
|
|
156
|
+
* Asymmetric: the verifier holds only public keys, so compromising a server does not let anyone
|
|
157
|
+
* forge messages from its peers.
|
|
158
|
+
*/
|
|
159
|
+
export const createEd25519Signer = (privateKey) => {
|
|
160
|
+
return async (canonicalBytes) => uint8ArrayToBase64(new Uint8Array(await subtle().sign('Ed25519', privateKey, bufferSource(canonicalBytes))));
|
|
161
|
+
};
|
|
162
|
+
export const createEd25519Verifier = (resolvePublicKey, identityFor) => {
|
|
163
|
+
return async (canonicalBytes, signature, { source }) => {
|
|
164
|
+
const publicKey = await resolvePublicKey(source);
|
|
165
|
+
if (!publicKey)
|
|
166
|
+
return undefined;
|
|
167
|
+
const valid = await subtle().verify('Ed25519', publicKey, bufferSource(base64ToUint8Array(signature)), bufferSource(canonicalBytes));
|
|
168
|
+
return valid ? identityOf(source, identityFor) : undefined;
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=Signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Signing.js","sourceRoot":"","sources":["../../src/RPC/Signing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RpcAuthenticator } from './Auth.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bearer tokens for a socket.io transport: the smallest thing that turns a bus anyone can join into
|
|
4
|
+
* one only its own peers can.
|
|
5
|
+
*
|
|
6
|
+
* Signing (Signing.ts) answers a different question. It proves a *frame* came from a peer, which is
|
|
7
|
+
* what MQTT needs, because there is no connection for anyone to have authenticated. A socket.io bus
|
|
8
|
+
* does have one, and a token presented once at the handshake pins it - after which the transport
|
|
9
|
+
* refuses any frame whose source is not the name that token resolved to. That check is the point,
|
|
10
|
+
* and it is what a shared password cannot do.
|
|
11
|
+
*
|
|
12
|
+
* So: **one token per peer, not one token for the bus.** A token that maps to a name is evidence of
|
|
13
|
+
* who is calling. A single token everyone knows is evidence only that the caller is inside the
|
|
14
|
+
* fence, and any holder can then claim to be the peer whose commands matter. `createTokenAuthenticator`
|
|
15
|
+
* takes a map for that reason and has no single-secret form.
|
|
16
|
+
*
|
|
17
|
+
* Tokens are bearer credentials: whoever has one is that peer. They belong in a secret store or an
|
|
18
|
+
* environment variable, never in the image, the repository or the command line.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* What a token entitles its holder to be. A bare string is the peer name it may use; the object
|
|
22
|
+
* form adds roles for an `RpcAuthorizer` to read.
|
|
23
|
+
*/
|
|
24
|
+
export type TokenGrant = string | {
|
|
25
|
+
name: string;
|
|
26
|
+
roles?: string[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Accept peers presenting a token this map knows, as the peer that token names.
|
|
30
|
+
*
|
|
31
|
+
* ```typescript
|
|
32
|
+
* new RpcServer({
|
|
33
|
+
* transports: [{ port: defaultWebSocketPort }],
|
|
34
|
+
* authenticate: createTokenAuthenticator({ [process.env.PLANT_TOKEN!]: 'plantServer' })
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* The peer presents it as `credentials: { token }`, and its `name` must be the one the token maps
|
|
39
|
+
* to - the transport drops frames that claim any other source, so a mismatch shows up as every call
|
|
40
|
+
* timing out rather than as a refusal. Worth getting right in configuration rather than debugging.
|
|
41
|
+
*
|
|
42
|
+
* An empty or blank token is refused at construction. It would otherwise sit in the map matching
|
|
43
|
+
* every peer that presented no credentials at all, which is the one mistake here that looks like it
|
|
44
|
+
* is working.
|
|
45
|
+
*/
|
|
46
|
+
export declare const createTokenAuthenticator: (tokens: {
|
|
47
|
+
[token: string]: TokenGrant;
|
|
48
|
+
}) => RpcAuthenticator;
|
|
49
|
+
//# sourceMappingURL=Tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tokens.d.ts","sourceRoot":"","sources":["../../src/RPC/Tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAe,MAAM,WAAW,CAAA;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAUpE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,WAAY;IAAE,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAA;CAAE,KAAG,gBAqBlF,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const grantToIdentity = (grant) => typeof grant === 'string' ? { name: grant } : { name: grant.name, ...(grant.roles ? { roles: grant.roles } : {}) };
|
|
2
|
+
/**
|
|
3
|
+
* Accept peers presenting a token this map knows, as the peer that token names.
|
|
4
|
+
*
|
|
5
|
+
* ```typescript
|
|
6
|
+
* new RpcServer({
|
|
7
|
+
* transports: [{ port: defaultWebSocketPort }],
|
|
8
|
+
* authenticate: createTokenAuthenticator({ [process.env.PLANT_TOKEN!]: 'plantServer' })
|
|
9
|
+
* })
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The peer presents it as `credentials: { token }`, and its `name` must be the one the token maps
|
|
13
|
+
* to - the transport drops frames that claim any other source, so a mismatch shows up as every call
|
|
14
|
+
* timing out rather than as a refusal. Worth getting right in configuration rather than debugging.
|
|
15
|
+
*
|
|
16
|
+
* An empty or blank token is refused at construction. It would otherwise sit in the map matching
|
|
17
|
+
* every peer that presented no credentials at all, which is the one mistake here that looks like it
|
|
18
|
+
* is working.
|
|
19
|
+
*/
|
|
20
|
+
export const createTokenAuthenticator = (tokens) => {
|
|
21
|
+
// Copied into a Map at construction so a later mutation of the caller's object cannot widen who
|
|
22
|
+
// is admitted, and so lookup is by hash of the whole token rather than a comparison that could
|
|
23
|
+
// return early on the first wrong character.
|
|
24
|
+
const grants = new Map();
|
|
25
|
+
for (const [token, grant] of Object.entries(tokens)) {
|
|
26
|
+
if (!token.trim())
|
|
27
|
+
throw new Error('createTokenAuthenticator: a blank token would admit any peer presenting none');
|
|
28
|
+
const identity = grantToIdentity(grant);
|
|
29
|
+
if (!identity.name)
|
|
30
|
+
throw new Error('createTokenAuthenticator: a token must name the peer it admits');
|
|
31
|
+
grants.set(token, identity);
|
|
32
|
+
}
|
|
33
|
+
if (!grants.size)
|
|
34
|
+
throw new Error('createTokenAuthenticator: no tokens, so nothing could ever connect');
|
|
35
|
+
return (credentials) => {
|
|
36
|
+
const presented = credentials?.token;
|
|
37
|
+
if (typeof presented !== 'string')
|
|
38
|
+
return undefined;
|
|
39
|
+
// Returned by value: an authorizer that mutated the identity it was handed would otherwise
|
|
40
|
+
// change what every later connection with the same token is granted.
|
|
41
|
+
const identity = grants.get(presented);
|
|
42
|
+
return identity ? { ...identity, ...(identity.roles ? { roles: [...identity.roles] } : {}) } : undefined;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=Tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tokens.js","sourceRoot":"","sources":["../../src/RPC/Tokens.ts"],"names":[],"mappings":"AA2BA,MAAM,eAAe,GAAG,CAAC,KAAiB,EAAe,EAAE,CACvD,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;AAOtH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,MAAuC,EAAoB,EAAE;IAClG,gGAAgG;IAChG,+FAA+F;IAC/F,6CAA6C;IAC7C,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC7C,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAA;QAClH,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,CAAA;QACvC,IAAI,CAAC,QAAQ,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAA;QACrG,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IAC/B,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAA;IAEvG,OAAO,CAAC,WAAW,EAAE,EAAE;QACnB,MAAM,SAAS,GAAI,WAA4C,EAAE,KAAK,CAAA;QACtE,IAAI,OAAO,SAAS,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAA;QACnD,2FAA2F;QAC3F,qEAAqE;QACrE,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACtC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IAC5G,CAAC,CAAA;AACL,CAAC,CAAA"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { PeerRegistry, Transport } from './RPC/Core.js';
|
|
3
|
+
import { MessageSigner, type TrustedCertificateAuthority } from './RPC/Auth.js';
|
|
4
|
+
import { RpcSchema } from './RPC/Schema.js';
|
|
5
|
+
import { IManageRpc } from './RPC/Rpc.js';
|
|
6
|
+
import { RpcClientHandler, type WithOptions } from './RPC/RpcClientHandler.js';
|
|
7
|
+
export interface RpcClientOptions {
|
|
8
|
+
name: string;
|
|
9
|
+
/** Supply one to take full control of the link. When absent init() builds one from the url. */
|
|
10
|
+
transport?: Transport;
|
|
11
|
+
defaultTarget?: string;
|
|
12
|
+
useMsgPack: boolean;
|
|
13
|
+
/** How long a call waits for a response before rejecting with an RpcError of code 'Timeout'. */
|
|
14
|
+
callTimeout: number;
|
|
15
|
+
/** How long ready() waits for the transport to connect before throwing. 0 waits forever. */
|
|
16
|
+
readyTimeout: number;
|
|
17
|
+
/** Reject in-flight calls as soon as the link drops instead of waiting out their timeouts. */
|
|
18
|
+
failCallsOnDisconnect: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Credentials presented when connecting to a server that authenticates. Passed to socket.io as
|
|
21
|
+
* the handshake `auth` payload, and to MQTT as broker connect options. When the server
|
|
22
|
+
* authenticates, `name` must match the identity these credentials resolve to - the server
|
|
23
|
+
* drops frames whose source does not match.
|
|
24
|
+
*/
|
|
25
|
+
credentials?: unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Sign outgoing frames. Only meaningful for MQTT, where there is no connection for a server to
|
|
28
|
+
* authenticate and the source field would otherwise be an unverifiable claim.
|
|
29
|
+
*/
|
|
30
|
+
sign?: MessageSigner;
|
|
31
|
+
/**
|
|
32
|
+
* The contract this client was built against. Its per-namespace versions are declared on each
|
|
33
|
+
* call so a server can tell a genuinely stale caller from one sending rubbish.
|
|
34
|
+
*/
|
|
35
|
+
schema?: RpcSchema;
|
|
36
|
+
/**
|
|
37
|
+
* Connect to an `https://`, `wss://` or `mqtts://` peer without checking its certificate.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately unsafe, and off: anything able to answer on that address can then read and
|
|
40
|
+
* rewrite everything this client sends, which over this library means industrial commands. It
|
|
41
|
+
* exists for a development server with a self-signed certificate. A plant with its own
|
|
42
|
+
* certificate authority should pass the CA in `credentials` instead, which keeps verification
|
|
43
|
+
* on rather than switching it off.
|
|
44
|
+
*/
|
|
45
|
+
allowInsecureTls?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* A certificate authority to trust, on top of the system ones, when dialling an `https://`,
|
|
48
|
+
* `wss://` or `mqtts://` peer.
|
|
49
|
+
*
|
|
50
|
+
* This is the answer for a plant that issues its own certificates, and it is the one to reach
|
|
51
|
+
* for before `allowInsecureTls`: verification stays on, so a server presenting anything this
|
|
52
|
+
* does not vouch for is still refused.
|
|
53
|
+
*/
|
|
54
|
+
ca?: TrustedCertificateAuthority;
|
|
55
|
+
}
|
|
56
|
+
export interface RpcProxy<T> {
|
|
57
|
+
name: string;
|
|
58
|
+
target?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The remote instance, plus `$with` for the options a caller can attach to a call - an
|
|
61
|
+
* idempotency key, so far. `$with` returns another proxy for the same instance rather than
|
|
62
|
+
* changing this one, so options never leak into calls that did not ask for them.
|
|
63
|
+
*/
|
|
64
|
+
remote?: T & WithOptions<T>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Emits the TransportEvent.connected and TransportEvent.disconnected lifecycle events so an
|
|
68
|
+
* application can show link state instead of inferring it from failed calls.
|
|
69
|
+
*/
|
|
70
|
+
export declare class RpcClient extends EventEmitter {
|
|
71
|
+
url?: string | undefined;
|
|
72
|
+
rpcClient?: RpcClientHandler;
|
|
73
|
+
manageRpc?: IManageRpc;
|
|
74
|
+
readyFlag: boolean;
|
|
75
|
+
/** Peer name -> module, shared by this client's modules and nothing outside them. */
|
|
76
|
+
readonly peers: PeerRegistry;
|
|
77
|
+
options: RpcClientOptions;
|
|
78
|
+
constructor(url?: string | undefined, options?: Partial<RpcClientOptions>);
|
|
79
|
+
/** Why init() failed, rethrown by ready() so the caller sees the cause and not a timeout. */
|
|
80
|
+
private initError?;
|
|
81
|
+
close(): Promise<void>;
|
|
82
|
+
init(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* React to the link coming and going. On reconnect the subscriptions are replayed, which both
|
|
85
|
+
* restores server-side state and re-identifies this client so pushed events can reach it again.
|
|
86
|
+
*/
|
|
87
|
+
private wireTransportLifecycle;
|
|
88
|
+
ready(): Promise<void>;
|
|
89
|
+
proxy<T>(name: string, target?: string): Promise<RpcProxy<T>>;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=RpcClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RpcClient.d.ts","sourceRoot":"","sources":["../src/RpcClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,EAAiB,YAAY,EAAE,SAAS,EAAkB,MAAM,eAAe,CAAA;AACtF,OAAO,EAAE,aAAa,EAAE,KAAK,2BAA2B,EAAE,MAAM,eAAe,CAAA;AAC/E,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,OAAO,EAAwB,UAAU,EAAE,MAAM,cAAc,CAAA;AAC/D,OAAO,EAAsB,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAMlG,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,+FAA+F;IAC/F,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,OAAO,CAAA;IACnB,gGAAgG;IAChG,WAAW,EAAE,MAAM,CAAA;IACnB,4FAA4F;IAC5F,YAAY,EAAE,MAAM,CAAA;IACpB,8FAA8F;IAC9F,qBAAqB,EAAE,OAAO,CAAA;IAC9B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B;;;;;;;OAOG;IACH,EAAE,CAAC,EAAE,2BAA2B,CAAA;CACnC;AAED,MAAM,WAAW,QAAQ,CAAC,CAAC;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;CAC9B;AAED;;;GAGG;AACH,qBAAa,SAAU,SAAQ,YAAY;IAmB5B,GAAG,CAAC,EAAE,MAAM;IAlBvB,SAAS,CAAC,EAAE,gBAAgB,CAAA;IAC5B,SAAS,CAAC,EAAE,UAAU,CAAA;IACtB,SAAS,UAAQ;IACjB,qFAAqF;IACrF,QAAQ,CAAC,KAAK,eAAqB;IAGnC,OAAO,EAAE,gBAAgB,CASxB;IACD,YACW,GAAG,CAAC,EAAE,MAAM,YAAA,EACnB,OAAO,GAAE,OAAO,CAAC,gBAAgB,CAAM,EAY1C;IACD,6FAA6F;IAC7F,OAAO,CAAC,SAAS,CAAC,CAAS;IACrB,KAAK,kBAOV;IACK,IAAI,kBA8CT;IAED;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAexB,KAAK,kBAaV;IACK,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,wBAM3C;CACJ"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { PeerRegistry, TransportEvent } from './RPC/Core.js';
|
|
3
|
+
import { defaultWebSocketPort } from './RPC/Rpc.js';
|
|
4
|
+
import { defaultCallTimeout, RpcClientHandler } from './RPC/RpcClientHandler.js';
|
|
5
|
+
import { SocketIoClientTransport } from './Transports/SocketIoClientTransport.js';
|
|
6
|
+
import { codecFor } from './RPC/Codec.js';
|
|
7
|
+
import { readableName } from './Utilities/ReadableName.js';
|
|
8
|
+
/**
|
|
9
|
+
* Emits the TransportEvent.connected and TransportEvent.disconnected lifecycle events so an
|
|
10
|
+
* application can show link state instead of inferring it from failed calls.
|
|
11
|
+
*/
|
|
12
|
+
export class RpcClient extends EventEmitter {
|
|
13
|
+
url;
|
|
14
|
+
rpcClient;
|
|
15
|
+
manageRpc;
|
|
16
|
+
readyFlag = false;
|
|
17
|
+
/** Peer name -> module, shared by this client's modules and nothing outside them. */
|
|
18
|
+
peers = new PeerRegistry();
|
|
19
|
+
// No transport here: constructing one in a field initialiser opened a socket on every client
|
|
20
|
+
// that init() then replaced and orphaned, leaving it reconnecting forever.
|
|
21
|
+
options = {
|
|
22
|
+
// Readable rather than a UUID: this name is what a peer list shows, what a log line blames
|
|
23
|
+
// and, over MQTT, the broker's client id.
|
|
24
|
+
name: readableName(),
|
|
25
|
+
defaultTarget: '*',
|
|
26
|
+
useMsgPack: true,
|
|
27
|
+
callTimeout: defaultCallTimeout,
|
|
28
|
+
readyTimeout: 30000,
|
|
29
|
+
failCallsOnDisconnect: true
|
|
30
|
+
};
|
|
31
|
+
constructor(url, options = {}) {
|
|
32
|
+
super();
|
|
33
|
+
this.url = url;
|
|
34
|
+
this.options = { ...this.options, ...options };
|
|
35
|
+
// init() is async and the constructor cannot await it, so its rejection was unhandled -
|
|
36
|
+
// and Node ends the process on one of those. A bad url, an unreachable broker or a name the
|
|
37
|
+
// transport refuses took the whole application down from a constructor. Kept instead, so
|
|
38
|
+
// ready() can report the real cause rather than timing out with nothing to say.
|
|
39
|
+
void this.init().catch((e) => {
|
|
40
|
+
this.initError = e;
|
|
41
|
+
this.emit('initError', e);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Why init() failed, rethrown by ready() so the caller sees the cause and not a timeout. */
|
|
45
|
+
initError;
|
|
46
|
+
async close() {
|
|
47
|
+
this.rpcClient?.failPendingCalls('client closed');
|
|
48
|
+
this.rpcClient?.subscriptions.clear();
|
|
49
|
+
await this.rpcClient?.close();
|
|
50
|
+
await this.options.transport?.close();
|
|
51
|
+
this.peers.clear();
|
|
52
|
+
this.readyFlag = false;
|
|
53
|
+
}
|
|
54
|
+
async init() {
|
|
55
|
+
// A caller-supplied transport is honoured. It used to be overwritten unconditionally, so
|
|
56
|
+
// passing one had no effect at all.
|
|
57
|
+
let transport = this.options.transport;
|
|
58
|
+
if (!transport) {
|
|
59
|
+
const socketOptions = {
|
|
60
|
+
...(this.options.credentials ? { auth: this.options.credentials } : {}),
|
|
61
|
+
// The socket.io typings narrow `ca` to a string, while the runtime takes what Node's
|
|
62
|
+
// tls does. Passing the bytes of a PEM is the ordinary thing readFileSync gives you.
|
|
63
|
+
...(this.options.ca ? { ca: this.options.ca } : {})
|
|
64
|
+
};
|
|
65
|
+
if (this.url?.startsWith('http') || this.url?.startsWith('ws'))
|
|
66
|
+
transport = new SocketIoClientTransport(this.options.name, this.url, undefined, socketOptions, true, this.options.allowInsecureTls);
|
|
67
|
+
else if (this.url?.startsWith('mqtt')) {
|
|
68
|
+
// Imported on demand so a browser bundle that only speaks WebSocket does not have
|
|
69
|
+
// to carry the MQTT client. Bundlers split this into a chunk fetched only when an
|
|
70
|
+
// mqtt:// url is actually used.
|
|
71
|
+
const { MqttTransport } = await import('./Transports/MqttTransport.js');
|
|
72
|
+
transport = new MqttTransport(this.options.name, this.url, {
|
|
73
|
+
mqtt: {
|
|
74
|
+
...(this.options.credentials ?? {}),
|
|
75
|
+
...(this.options.ca ? { ca: this.options.ca } : {})
|
|
76
|
+
},
|
|
77
|
+
sign: this.options.sign,
|
|
78
|
+
allowInsecureTls: this.options.allowInsecureTls
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
transport = new SocketIoClientTransport(this.options.name, `http://localhost:${defaultWebSocketPort}`, undefined, socketOptions, true, this.options.allowInsecureTls);
|
|
83
|
+
}
|
|
84
|
+
this.options.transport = transport;
|
|
85
|
+
// The transport encodes, so there is no converter between it and the handler. A structured
|
|
86
|
+
// wire format such as MQTT 5 needs to see the message, not bytes a converter already flattened.
|
|
87
|
+
transport.codec = codecFor(this.options.useMsgPack);
|
|
88
|
+
this.rpcClient = new RpcClientHandler(this.options.name, [transport], this.options.callTimeout);
|
|
89
|
+
if (this.options.schema)
|
|
90
|
+
this.rpcClient.schemaVersions = Object.fromEntries(Object.entries(this.options.schema.namespaces).map(([namespace, described]) => [namespace, described.version]));
|
|
91
|
+
this.rpcClient.pipe(transport);
|
|
92
|
+
for (const module of [transport, this.rpcClient])
|
|
93
|
+
module.usePeerRegistry(this.peers);
|
|
94
|
+
this.wireTransportLifecycle(transport);
|
|
95
|
+
this.readyFlag = true;
|
|
96
|
+
// Built directly instead of via proxy(), which awaits ready(). init() is not awaited by
|
|
97
|
+
// the constructor, so a ready() rejection here would surface as an unhandled rejection.
|
|
98
|
+
// The proxy is inert until a call is made, so there is nothing to wait for.
|
|
99
|
+
this.manageRpc = this.rpcClient.proxy('manageRpc', this.options.defaultTarget);
|
|
100
|
+
await this.options.transport.open();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* React to the link coming and going. On reconnect the subscriptions are replayed, which both
|
|
104
|
+
* restores server-side state and re-identifies this client so pushed events can reach it again.
|
|
105
|
+
*/
|
|
106
|
+
wireTransportLifecycle(transport) {
|
|
107
|
+
transport.on(TransportEvent.disconnected, (reason) => {
|
|
108
|
+
if (this.options.failCallsOnDisconnect)
|
|
109
|
+
this.rpcClient?.failPendingCalls(`transport disconnected: ${reason ?? 'unknown reason'}`);
|
|
110
|
+
this.emit(TransportEvent.disconnected, reason);
|
|
111
|
+
});
|
|
112
|
+
transport.on(TransportEvent.connected, () => {
|
|
113
|
+
// No-op on the first connect, when nothing has been subscribed yet.
|
|
114
|
+
this.rpcClient
|
|
115
|
+
?.resubscribe()
|
|
116
|
+
.then((restored) => this.emit(TransportEvent.connected, { restoredSubscriptions: restored }))
|
|
117
|
+
// Not 'error': an EventEmitter throws on an unhandled 'error' event.
|
|
118
|
+
.catch((e) => this.emit('resubscribeError', e));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async ready() {
|
|
122
|
+
const deadline = Date.now() + this.options.readyTimeout;
|
|
123
|
+
while (!this.options.transport?.readyFlag || !this.readyFlag) {
|
|
124
|
+
// Checked every turn, not just once: init() is still running when ready() is first
|
|
125
|
+
// called, so the failure usually arrives while this loop is already waiting.
|
|
126
|
+
if (this.initError !== undefined)
|
|
127
|
+
throw new Error(`RpcClient '${this.options.name}': could not start: ${this.initError instanceof Error ? this.initError.message : String(this.initError)}`, {
|
|
128
|
+
cause: this.initError
|
|
129
|
+
});
|
|
130
|
+
if (this.options.readyTimeout > 0 && Date.now() > deadline)
|
|
131
|
+
throw new Error(`RpcClient '${this.options.name}': transport not ready within ${this.options.readyTimeout} ms`);
|
|
132
|
+
await new Promise((res) => setTimeout(res, 10));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async proxy(name, target) {
|
|
136
|
+
await this.ready();
|
|
137
|
+
const result = { name };
|
|
138
|
+
if (target)
|
|
139
|
+
result.target = target;
|
|
140
|
+
if (this.rpcClient)
|
|
141
|
+
result.remote = this.rpcClient.proxy(name, target ? target : this.options.defaultTarget);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=RpcClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RpcClient.js","sourceRoot":"","sources":["../src/RpcClient.ts"],"names":[],"mappings":"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"}
|