@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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1208 -0
  3. package/dist/Logging/ILogger.d.ts +7 -0
  4. package/dist/Logging/ILogger.d.ts.map +1 -0
  5. package/dist/Logging/ILogger.js +2 -0
  6. package/dist/Logging/ILogger.js.map +1 -0
  7. package/dist/NodeRpcServer.d.ts +55 -0
  8. package/dist/NodeRpcServer.d.ts.map +1 -0
  9. package/dist/NodeRpcServer.js +46 -0
  10. package/dist/NodeRpcServer.js.map +1 -0
  11. package/dist/RPC/Auth.d.ts +98 -0
  12. package/dist/RPC/Auth.d.ts.map +1 -0
  13. package/dist/RPC/Auth.js +14 -0
  14. package/dist/RPC/Auth.js.map +1 -0
  15. package/dist/RPC/Codec.d.ts +24 -0
  16. package/dist/RPC/Codec.d.ts.map +1 -0
  17. package/dist/RPC/Codec.js +21 -0
  18. package/dist/RPC/Codec.js.map +1 -0
  19. package/dist/RPC/Compatibility.d.ts +39 -0
  20. package/dist/RPC/Compatibility.d.ts.map +1 -0
  21. package/dist/RPC/Compatibility.js +195 -0
  22. package/dist/RPC/Compatibility.js.map +1 -0
  23. package/dist/RPC/Core.d.ts +188 -0
  24. package/dist/RPC/Core.d.ts.map +1 -0
  25. package/dist/RPC/Core.js +354 -0
  26. package/dist/RPC/Core.js.map +1 -0
  27. package/dist/RPC/Expose.d.ts +90 -0
  28. package/dist/RPC/Expose.d.ts.map +1 -0
  29. package/dist/RPC/Expose.js +110 -0
  30. package/dist/RPC/Expose.js.map +1 -0
  31. package/dist/RPC/Idempotency.d.ts +90 -0
  32. package/dist/RPC/Idempotency.d.ts.map +1 -0
  33. package/dist/RPC/Idempotency.js +46 -0
  34. package/dist/RPC/Idempotency.js.map +1 -0
  35. package/dist/RPC/Introspection.d.ts +92 -0
  36. package/dist/RPC/Introspection.d.ts.map +1 -0
  37. package/dist/RPC/Introspection.js +188 -0
  38. package/dist/RPC/Introspection.js.map +1 -0
  39. package/dist/RPC/Introspection.types.json +1093 -0
  40. package/dist/RPC/Messages.d.ts +108 -0
  41. package/dist/RPC/Messages.d.ts.map +1 -0
  42. package/dist/RPC/Messages.js +30 -0
  43. package/dist/RPC/Messages.js.map +1 -0
  44. package/dist/RPC/Rpc.d.ts +51 -0
  45. package/dist/RPC/Rpc.d.ts.map +1 -0
  46. package/dist/RPC/Rpc.js +54 -0
  47. package/dist/RPC/Rpc.js.map +1 -0
  48. package/dist/RPC/RpcClientHandler.d.ts +129 -0
  49. package/dist/RPC/RpcClientHandler.d.ts.map +1 -0
  50. package/dist/RPC/RpcClientHandler.js +267 -0
  51. package/dist/RPC/RpcClientHandler.js.map +1 -0
  52. package/dist/RPC/RpcServerHandler.d.ts +226 -0
  53. package/dist/RPC/RpcServerHandler.d.ts.map +1 -0
  54. package/dist/RPC/RpcServerHandler.js +676 -0
  55. package/dist/RPC/RpcServerHandler.js.map +1 -0
  56. package/dist/RPC/Schema.d.ts +150 -0
  57. package/dist/RPC/Schema.d.ts.map +1 -0
  58. package/dist/RPC/Schema.js +198 -0
  59. package/dist/RPC/Schema.js.map +1 -0
  60. package/dist/RPC/Signing.d.ts +118 -0
  61. package/dist/RPC/Signing.d.ts.map +1 -0
  62. package/dist/RPC/Signing.js +171 -0
  63. package/dist/RPC/Signing.js.map +1 -0
  64. package/dist/RPC/Tokens.d.ts +49 -0
  65. package/dist/RPC/Tokens.d.ts.map +1 -0
  66. package/dist/RPC/Tokens.js +45 -0
  67. package/dist/RPC/Tokens.js.map +1 -0
  68. package/dist/RpcClient.d.ts +91 -0
  69. package/dist/RpcClient.d.ts.map +1 -0
  70. package/dist/RpcClient.js +145 -0
  71. package/dist/RpcClient.js.map +1 -0
  72. package/dist/RpcServer.d.ts +191 -0
  73. package/dist/RpcServer.d.ts.map +1 -0
  74. package/dist/RpcServer.js +293 -0
  75. package/dist/RpcServer.js.map +1 -0
  76. package/dist/Transports/Mqtt5Frame.d.ts +107 -0
  77. package/dist/Transports/Mqtt5Frame.d.ts.map +1 -0
  78. package/dist/Transports/Mqtt5Frame.js +162 -0
  79. package/dist/Transports/Mqtt5Frame.js.map +1 -0
  80. package/dist/Transports/MqttTransport.d.ts +279 -0
  81. package/dist/Transports/MqttTransport.d.ts.map +1 -0
  82. package/dist/Transports/MqttTransport.js +843 -0
  83. package/dist/Transports/MqttTransport.js.map +1 -0
  84. package/dist/Transports/Presence.d.ts +68 -0
  85. package/dist/Transports/Presence.d.ts.map +1 -0
  86. package/dist/Transports/Presence.js +21 -0
  87. package/dist/Transports/Presence.js.map +1 -0
  88. package/dist/Transports/SocketIoClientTransport.d.ts +89 -0
  89. package/dist/Transports/SocketIoClientTransport.d.ts.map +1 -0
  90. package/dist/Transports/SocketIoClientTransport.js +280 -0
  91. package/dist/Transports/SocketIoClientTransport.js.map +1 -0
  92. package/dist/Transports/SocketIoServerTransport.d.ts +135 -0
  93. package/dist/Transports/SocketIoServerTransport.d.ts.map +1 -0
  94. package/dist/Transports/SocketIoServerTransport.js +505 -0
  95. package/dist/Transports/SocketIoServerTransport.js.map +1 -0
  96. package/dist/Utilities/Converters.d.ts +22 -0
  97. package/dist/Utilities/Converters.d.ts.map +1 -0
  98. package/dist/Utilities/Converters.js +58 -0
  99. package/dist/Utilities/Converters.js.map +1 -0
  100. package/dist/Utilities/Filter.d.ts +7 -0
  101. package/dist/Utilities/Filter.d.ts.map +1 -0
  102. package/dist/Utilities/Filter.js +14 -0
  103. package/dist/Utilities/Filter.js.map +1 -0
  104. package/dist/Utilities/ReadableName.d.ts +38 -0
  105. package/dist/Utilities/ReadableName.d.ts.map +1 -0
  106. package/dist/Utilities/ReadableName.js +58 -0
  107. package/dist/Utilities/ReadableName.js.map +1 -0
  108. package/dist/Utilities/Switch.d.ts +21 -0
  109. package/dist/Utilities/Switch.d.ts.map +1 -0
  110. package/dist/Utilities/Switch.js +70 -0
  111. package/dist/Utilities/Switch.js.map +1 -0
  112. package/dist/Utilities/TryCatch.d.ts +11 -0
  113. package/dist/Utilities/TryCatch.d.ts.map +1 -0
  114. package/dist/Utilities/TryCatch.js +13 -0
  115. package/dist/Utilities/TryCatch.js.map +1 -0
  116. package/dist/index-web.d.ts +25 -0
  117. package/dist/index-web.d.ts.map +1 -0
  118. package/dist/index-web.js +28 -0
  119. package/dist/index-web.js.map +1 -0
  120. package/dist/index.d.ts +28 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +30 -0
  123. package/dist/index.js.map +1 -0
  124. 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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"}