@reclaimprotocol/attestor-core 5.0.1-beta.21 → 5.0.1-beta.23
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/browser/resources/attestor-browser.min.mjs +9 -9
- package/lib/avs/abis/avsDirectoryABI.js +340 -0
- package/lib/avs/abis/delegationABI.js +1 -0
- package/lib/avs/abis/registryABI.js +725 -0
- package/lib/avs/client/create-claim-on-avs.js +140 -0
- package/lib/avs/config.js +20 -0
- package/lib/avs/contracts/factories/ReclaimServiceManager__factory.js +1166 -0
- package/lib/avs/contracts/factories/index.js +4 -0
- package/lib/avs/contracts/index.js +2 -0
- package/lib/avs/utils/contracts.js +33 -0
- package/lib/avs/utils/register.js +79 -0
- package/lib/avs/utils/tasks.js +41 -0
- package/lib/client/create-claim.js +432 -0
- package/lib/client/index.js +3 -0
- package/lib/client/tunnels/make-rpc-tcp-tunnel.js +51 -0
- package/lib/client/tunnels/make-rpc-tls-tunnel.js +131 -0
- package/lib/client/utils/attestor-pool.js +25 -0
- package/lib/client/utils/client-socket.js +97 -0
- package/lib/client/utils/message-handler.js +87 -0
- package/lib/config/index.js +44 -0
- package/lib/external-rpc/benchmark.js +69 -0
- package/lib/external-rpc/event-bus.js +14 -0
- package/lib/external-rpc/handle-incoming-msg.js +232 -0
- package/lib/external-rpc/index.js +3 -10399
- package/lib/external-rpc/jsc-polyfills/1.js +82 -0
- package/lib/external-rpc/jsc-polyfills/2.js +20 -0
- package/lib/external-rpc/jsc-polyfills/event.js +14 -0
- package/lib/external-rpc/jsc-polyfills/index.js +2 -0
- package/lib/external-rpc/jsc-polyfills/ws.js +81 -0
- package/lib/external-rpc/setup-browser.js +33 -0
- package/lib/external-rpc/setup-jsc.js +22 -0
- package/lib/external-rpc/types.d.ts +0 -1
- package/lib/external-rpc/utils.js +100 -0
- package/lib/external-rpc/zk.js +63 -0
- package/lib/index.js +9 -8326
- package/lib/mechain/abis/governanceABI.js +458 -0
- package/lib/mechain/abis/taskABI.js +509 -0
- package/lib/mechain/client/create-claim-on-mechain.js +28 -0
- package/lib/mechain/client/index.js +1 -0
- package/lib/mechain/constants/index.js +3 -0
- package/lib/mechain/index.js +2 -0
- package/lib/mechain/types/index.js +1 -0
- package/lib/proto/api.js +4363 -0
- package/lib/proto/tee-bundle.js +1316 -0
- package/lib/providers/http/index.js +653 -0
- package/lib/providers/http/patch-parse5-tree.js +32 -0
- package/lib/providers/http/utils.js +324 -0
- package/lib/providers/index.js +4 -0
- package/lib/server/create-server.js +103 -0
- package/lib/server/handlers/claimTeeBundle.js +252 -0
- package/lib/server/handlers/claimTunnel.js +73 -0
- package/lib/server/handlers/completeClaimOnChain.js +24 -0
- package/lib/server/handlers/createClaimOnChain.js +26 -0
- package/lib/server/handlers/createTaskOnMechain.js +47 -0
- package/lib/server/handlers/createTunnel.js +93 -0
- package/lib/server/handlers/disconnectTunnel.js +5 -0
- package/lib/server/handlers/fetchCertificateBytes.js +41 -0
- package/lib/server/handlers/index.js +22 -0
- package/lib/server/handlers/init.js +32 -0
- package/lib/server/handlers/toprf.js +16 -0
- package/lib/server/index.js +4 -0
- package/lib/server/socket.js +109 -0
- package/lib/server/tunnels/make-tcp-tunnel.js +177 -0
- package/lib/server/utils/apm.js +36 -0
- package/lib/server/utils/assert-valid-claim-request.js +325 -0
- package/lib/server/utils/config-env.js +4 -0
- package/lib/server/utils/dns.js +18 -0
- package/lib/server/utils/gcp-attestation.js +289 -0
- package/lib/server/utils/generics.d.ts +1 -1
- package/lib/server/utils/generics.js +51 -0
- package/lib/server/utils/iso.js +256 -0
- package/lib/server/utils/keep-alive.js +38 -0
- package/lib/server/utils/nitro-attestation.js +324 -0
- package/lib/server/utils/oprf-raw.js +54 -0
- package/lib/server/utils/process-handshake.js +215 -0
- package/lib/server/utils/proxy-session.js +6 -0
- package/lib/server/utils/tee-oprf-mpc-verification.js +90 -0
- package/lib/server/utils/tee-oprf-verification.js +174 -0
- package/lib/server/utils/tee-transcript-reconstruction.js +187 -0
- package/lib/server/utils/tee-verification.js +421 -0
- package/lib/server/utils/validation.js +38 -0
- package/lib/types/bgp.js +1 -0
- package/lib/types/claims.js +1 -0
- package/lib/types/client.js +1 -0
- package/lib/types/general.js +1 -0
- package/lib/types/handlers.js +1 -0
- package/lib/types/index.js +10 -0
- package/lib/types/providers.d.ts +3 -2
- package/lib/types/providers.gen.js +10 -0
- package/lib/types/providers.js +1 -0
- package/lib/types/rpc.js +1 -0
- package/lib/types/signatures.d.ts +1 -2
- package/lib/types/signatures.js +1 -0
- package/lib/types/tunnel.js +1 -0
- package/lib/types/zk.js +1 -0
- package/lib/utils/auth.js +59 -0
- package/lib/utils/b64-json.js +17 -0
- package/lib/utils/bgp-listener.js +119 -0
- package/lib/utils/claims.js +98 -0
- package/lib/utils/env.js +15 -0
- package/lib/utils/error.js +50 -0
- package/lib/utils/generics.js +317 -0
- package/lib/utils/http-parser.js +246 -0
- package/lib/utils/index.js +13 -0
- package/lib/utils/logger.js +91 -0
- package/lib/utils/prepare-packets.js +71 -0
- package/lib/utils/redactions.js +177 -0
- package/lib/utils/retries.js +24 -0
- package/lib/utils/signatures/eth.js +32 -0
- package/lib/utils/signatures/index.js +7 -0
- package/lib/utils/socket-base.js +92 -0
- package/lib/utils/tls.js +58 -0
- package/lib/utils/ws.js +22 -0
- package/lib/utils/zk.js +585 -0
- package/package.json +5 -3
- package/lib/scripts/check-avs-registration.d.ts +0 -1
- package/lib/scripts/fallbacks/crypto.d.ts +0 -1
- package/lib/scripts/fallbacks/empty.d.ts +0 -3
- package/lib/scripts/fallbacks/re2.d.ts +0 -1
- package/lib/scripts/fallbacks/snarkjs.d.ts +0 -1
- package/lib/scripts/fallbacks/stwo.d.ts +0 -6
- package/lib/scripts/generate-provider-types.d.ts +0 -5
- package/lib/scripts/generate-receipt.d.ts +0 -9
- package/lib/scripts/jsc-cli-rpc.d.ts +0 -1
- package/lib/scripts/register-avs-operator.d.ts +0 -1
- package/lib/scripts/start-server.d.ts +0 -1
- package/lib/scripts/update-avs-metadata.d.ts +0 -1
- package/lib/scripts/utils.d.ts +0 -1
- package/lib/scripts/whitelist-operator.d.ts +0 -1
- /package/lib/{scripts/build-browser.d.ts → avs/contracts/ReclaimServiceManager.js} +0 -0
- /package/lib/{scripts/build-jsc.d.ts → avs/contracts/common.js} +0 -0
- /package/lib/{scripts/build-lib.d.ts → avs/types/index.js} +0 -0
- /package/lib/{scripts/generate-toprf-keys.d.ts → external-rpc/types.js} +0 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// noinspection ExceptionCaughtLocallyJS
|
|
2
|
+
import "./patch-parse5-tree.js";
|
|
3
|
+
import { concatenateUint8Arrays } from '@reclaimprotocol/tls';
|
|
4
|
+
import { ArrayExpression, ExpressionStatement, ObjectExpression, parseScript, Property, Syntax } from 'esprima-next';
|
|
5
|
+
import { JSONPath } from 'jsonpath-plus';
|
|
6
|
+
import { parse } from 'parse5';
|
|
7
|
+
import { adapter as htmlAdapter, } from 'parse5-htmlparser2-tree-adapter';
|
|
8
|
+
import RE2 from 're2';
|
|
9
|
+
import xpath from 'xpath';
|
|
10
|
+
import { getHttpRequestDataFromTranscript, isApplicationData, makeHttpResponseParser, REDACTION_CHAR_CODE } from "../../utils/index.js";
|
|
11
|
+
/**
|
|
12
|
+
* Returns only first extracted element
|
|
13
|
+
* @param html
|
|
14
|
+
* @param xpathExpression
|
|
15
|
+
* @param contentsOnly
|
|
16
|
+
*/
|
|
17
|
+
export function extractHTMLElement(html, xpathExpression, contentsOnly) {
|
|
18
|
+
const { start, end } = extractHTMLElementIndex(html, xpathExpression, contentsOnly);
|
|
19
|
+
return html.slice(start, end);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns all extracted elements
|
|
23
|
+
* @param html
|
|
24
|
+
* @param xpathExpression
|
|
25
|
+
* @param contentsOnly
|
|
26
|
+
*/
|
|
27
|
+
export function extractHTMLElements(html, xpathExpression, contentsOnly) {
|
|
28
|
+
const indexes = extractHTMLElementsIndexes(html, xpathExpression, contentsOnly);
|
|
29
|
+
const res = [];
|
|
30
|
+
for (const { start, end } of indexes) {
|
|
31
|
+
res.push(html.slice(start, end));
|
|
32
|
+
}
|
|
33
|
+
return res;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* returns a single index of extracted element
|
|
37
|
+
* @param html
|
|
38
|
+
* @param xpathExpression
|
|
39
|
+
* @param contentsOnly
|
|
40
|
+
*/
|
|
41
|
+
export function extractHTMLElementIndex(html, xpathExpression, contentsOnly) {
|
|
42
|
+
return extractHTMLElementsIndexes(html, xpathExpression, contentsOnly)[0];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns indexes of all extracted elements
|
|
46
|
+
* @param html
|
|
47
|
+
* @param xpathExpression
|
|
48
|
+
* @param contentsOnly indices of the start and end of the element's contents only,
|
|
49
|
+
* not the whole tag
|
|
50
|
+
*/
|
|
51
|
+
export function extractHTMLElementsIndexes(html, xpathExpression, contentsOnly) {
|
|
52
|
+
return extractHTMLElementIndexesParse5(html, xpathExpression, contentsOnly);
|
|
53
|
+
}
|
|
54
|
+
function extractHTMLElementIndexesParse5(html, xpathExpression, contentsOnly) {
|
|
55
|
+
const domLight = parse(html, { treeAdapter: htmlAdapter, sourceCodeLocationInfo: true });
|
|
56
|
+
// lets xpath identify this as a node
|
|
57
|
+
domLight['name'] = 'root';
|
|
58
|
+
const parsedPath = xpath.parse(xpathExpression);
|
|
59
|
+
const nodes = parsedPath.select({
|
|
60
|
+
node: domLight,
|
|
61
|
+
allowAnyNamespaceForNoPrefix: true,
|
|
62
|
+
});
|
|
63
|
+
if (!nodes.length) {
|
|
64
|
+
throw new Error(`Failed to find XPath: "${xpathExpression}"`);
|
|
65
|
+
}
|
|
66
|
+
return nodes.map(node => getNodeRange(node, contentsOnly));
|
|
67
|
+
}
|
|
68
|
+
function getNodeRange(node, contentsOnly) {
|
|
69
|
+
if (!contentsOnly) {
|
|
70
|
+
return { start: node.startIndex, end: node.endIndex };
|
|
71
|
+
}
|
|
72
|
+
if (!('firstChild' in node) || !node.firstChild) {
|
|
73
|
+
throw new Error(`Node "${node['name']}" has no children`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
start: node.firstChild.startIndex,
|
|
77
|
+
end: node.lastChild.endIndex
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function extractJSONValueIndex(json, jsonPath) {
|
|
81
|
+
return extractJSONValueIndexes(json, jsonPath)[0];
|
|
82
|
+
}
|
|
83
|
+
export function extractJSONValueIndexes(json, jsonPath) {
|
|
84
|
+
const pointers = JSONPath({
|
|
85
|
+
path: jsonPath,
|
|
86
|
+
json: JSON.parse(json),
|
|
87
|
+
wrap: false,
|
|
88
|
+
resultType: 'pointer',
|
|
89
|
+
eval: 'safe',
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
ignoreEvalErrors: true
|
|
92
|
+
});
|
|
93
|
+
if (!pointers) {
|
|
94
|
+
throw new Error('jsonPath not found');
|
|
95
|
+
}
|
|
96
|
+
//wrap in parentheses for esprima to parse
|
|
97
|
+
const tree = parseScript('(' + json + ')', { range: true });
|
|
98
|
+
if (tree.body[0] instanceof ExpressionStatement
|
|
99
|
+
&& (tree.body[0].expression instanceof ObjectExpression || tree.body[0].expression instanceof ArrayExpression)) {
|
|
100
|
+
const traversePointers = Array.isArray(pointers) ? pointers : [pointers];
|
|
101
|
+
const res = [];
|
|
102
|
+
for (const pointer of traversePointers) {
|
|
103
|
+
const index = traverse(tree.body[0].expression, '', [pointer]);
|
|
104
|
+
if (index) {
|
|
105
|
+
res.push({
|
|
106
|
+
start: index.start - 1, //account for '('
|
|
107
|
+
end: index.end - 1,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return res;
|
|
112
|
+
}
|
|
113
|
+
throw new Error('jsonPath not found');
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* recursively go through AST tree and build a JSON path while it's not equal to the one we search for
|
|
117
|
+
* @param o - esprima expression for root object
|
|
118
|
+
* @param path - path that is being built
|
|
119
|
+
* @param pointers - JSON pointers to compare to
|
|
120
|
+
*/
|
|
121
|
+
function traverse(o, path, pointers) {
|
|
122
|
+
if (o instanceof ObjectExpression) {
|
|
123
|
+
for (const p of o.properties) {
|
|
124
|
+
if (!(p instanceof Property)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const localPath = p.key.type === Syntax.Literal
|
|
128
|
+
? path + '/' + p.key.value
|
|
129
|
+
: path;
|
|
130
|
+
if (pointers.includes(localPath) && 'range' in p && Array.isArray(p.range)) {
|
|
131
|
+
return {
|
|
132
|
+
start: p.range[0],
|
|
133
|
+
end: p.range[1],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (p.value instanceof ObjectExpression
|
|
137
|
+
|| p.value instanceof ArrayExpression) {
|
|
138
|
+
const res = traverse(p.value, localPath, pointers);
|
|
139
|
+
if (res) {
|
|
140
|
+
return res;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (o instanceof ArrayExpression) {
|
|
146
|
+
for (let i = 0; i < o.elements.length; i++) {
|
|
147
|
+
const element = o.elements[i];
|
|
148
|
+
if (!element) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const localPath = path + '/' + i;
|
|
152
|
+
if (pointers.includes(localPath) &&
|
|
153
|
+
'range' in element &&
|
|
154
|
+
Array.isArray(element.range)) {
|
|
155
|
+
return {
|
|
156
|
+
start: element.range[0],
|
|
157
|
+
end: element.range[1],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (element instanceof ObjectExpression) {
|
|
161
|
+
const res = traverse(element, localPath, pointers);
|
|
162
|
+
if (res) {
|
|
163
|
+
return res;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (element instanceof ArrayExpression) {
|
|
167
|
+
const res = traverse(element, localPath, pointers);
|
|
168
|
+
if (res) {
|
|
169
|
+
return res;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
export function buildHeaders(input) {
|
|
177
|
+
const headers = [];
|
|
178
|
+
for (const [key, value] of Object.entries(input || {})) {
|
|
179
|
+
headers.push(`${key}: ${value}`);
|
|
180
|
+
}
|
|
181
|
+
return headers;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Converts position in HTTP response body to an absolute position in TLS transcript considering chunked encoding
|
|
185
|
+
* @param pos
|
|
186
|
+
* @param bodyStartIdx
|
|
187
|
+
* @param chunks
|
|
188
|
+
*/
|
|
189
|
+
export function convertResponsePosToAbsolutePos(pos, bodyStartIdx, chunks) {
|
|
190
|
+
if (chunks?.length) {
|
|
191
|
+
let chunkBodyStart = 0;
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
const chunkSize = chunk.toIndex - chunk.fromIndex;
|
|
194
|
+
if (pos >= chunkBodyStart && pos <= (chunkBodyStart + chunkSize)) {
|
|
195
|
+
return pos - chunkBodyStart + chunk.fromIndex;
|
|
196
|
+
}
|
|
197
|
+
chunkBodyStart += chunkSize;
|
|
198
|
+
}
|
|
199
|
+
throw new Error('position out of range');
|
|
200
|
+
}
|
|
201
|
+
return bodyStartIdx + pos;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* If this reveal spans the boundary of two chunks, we'll
|
|
205
|
+
*
|
|
206
|
+
*/
|
|
207
|
+
export function getRedactionsForChunkHeaders(from, to, chunks) {
|
|
208
|
+
const res = [];
|
|
209
|
+
if (!chunks?.length) {
|
|
210
|
+
return res;
|
|
211
|
+
}
|
|
212
|
+
for (let i = 1; i < chunks?.length; i++) {
|
|
213
|
+
const chunk = chunks[i];
|
|
214
|
+
if (chunk.fromIndex > from && chunk.fromIndex < to) {
|
|
215
|
+
res.push({
|
|
216
|
+
fromIndex: chunks[i - 1].toIndex,
|
|
217
|
+
toIndex: chunk.fromIndex,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return res;
|
|
222
|
+
}
|
|
223
|
+
export function parseHttpResponse(buff) {
|
|
224
|
+
const parser = makeHttpResponseParser();
|
|
225
|
+
parser.onChunk(buff);
|
|
226
|
+
parser.streamEnded();
|
|
227
|
+
return parser.res;
|
|
228
|
+
}
|
|
229
|
+
export function makeRegex(str) {
|
|
230
|
+
return RE2(str, 'sgiu');
|
|
231
|
+
}
|
|
232
|
+
const TEMPLATE_START_CHARCODE = '{'.charCodeAt(0);
|
|
233
|
+
const TEMPLATE_END_CHARCODE = '}'.charCodeAt(0);
|
|
234
|
+
/**
|
|
235
|
+
* Try to match strings that contain templates like {{param}}
|
|
236
|
+
* against redacted string that has *** instead of that param
|
|
237
|
+
*/
|
|
238
|
+
export function matchRedactedStrings(templateString, redactedString) {
|
|
239
|
+
if (templateString.length === 0 && redactedString?.length === 0) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (!redactedString) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
let ts = -1;
|
|
246
|
+
let rs = -1;
|
|
247
|
+
while (ts < templateString.length && rs < redactedString.length) {
|
|
248
|
+
let ct = getTChar();
|
|
249
|
+
let cr = getRChar();
|
|
250
|
+
if (ct !== cr) {
|
|
251
|
+
// only valid if param contains "{" & redacted contains "*"
|
|
252
|
+
if (ct === TEMPLATE_START_CHARCODE && cr === REDACTION_CHAR_CODE) {
|
|
253
|
+
//check that the char after first "{" is also "{"
|
|
254
|
+
if (getTChar() !== TEMPLATE_START_CHARCODE) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
//look for first closing bracket
|
|
258
|
+
while (((ct = getTChar()) !== TEMPLATE_END_CHARCODE) && ct !== -1) {
|
|
259
|
+
}
|
|
260
|
+
//look for second closing bracket
|
|
261
|
+
while (((ct = getTChar()) !== TEMPLATE_END_CHARCODE) && ct !== -1) {
|
|
262
|
+
}
|
|
263
|
+
if (ct === -1) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
//find the end of redaction
|
|
267
|
+
while (((cr = getRChar()) === REDACTION_CHAR_CODE) && cr !== -1) {
|
|
268
|
+
}
|
|
269
|
+
if (cr === -1) {
|
|
270
|
+
//if there's nothing after template too then both ended at the end of strings
|
|
271
|
+
return getTChar() === -1;
|
|
272
|
+
}
|
|
273
|
+
//rewind redacted string position back 1 char because we read one extra
|
|
274
|
+
rs--;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function getTChar() {
|
|
282
|
+
ts++;
|
|
283
|
+
if (ts < templateString.length) {
|
|
284
|
+
return templateString[ts];
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
return -1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function getRChar() {
|
|
291
|
+
if (!redactedString) {
|
|
292
|
+
return -1;
|
|
293
|
+
}
|
|
294
|
+
rs++;
|
|
295
|
+
if (rs < redactedString.length) {
|
|
296
|
+
return redactedString[rs];
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
return -1;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return ts === templateString.length && rs === redactedString.length;
|
|
303
|
+
}
|
|
304
|
+
export function generateRequstAndResponseFromTranscript(transcript, tlsVersion) {
|
|
305
|
+
const allPackets = transcript;
|
|
306
|
+
const packets = [];
|
|
307
|
+
for (const b of allPackets) {
|
|
308
|
+
if (b.message.type !== 'ciphertext'
|
|
309
|
+
|| !isApplicationData(b.message, tlsVersion)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const plaintext = tlsVersion === 'TLS1_3'
|
|
313
|
+
? b.message.plaintext.slice(0, -1)
|
|
314
|
+
: b.message.plaintext;
|
|
315
|
+
packets.push({
|
|
316
|
+
message: plaintext,
|
|
317
|
+
sender: b.sender
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const req = getHttpRequestDataFromTranscript(packets);
|
|
321
|
+
const responsePackets = concatenateUint8Arrays(packets.filter(p => p.sender === 'server').map(p => p.message).filter(b => !b.every(b => b === REDACTION_CHAR_CODE)));
|
|
322
|
+
const res = parseHttpResponse(responsePackets);
|
|
323
|
+
return { req, res };
|
|
324
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createServer as createHttpServer } from 'http';
|
|
2
|
+
import serveStatic from 'serve-static';
|
|
3
|
+
import { WebSocketServer } from 'ws';
|
|
4
|
+
import { API_SERVER_PORT, ATTESTOR_ADDRESS_PATHNAME, BROWSER_RPC_PATHNAME, WS_PATHNAME } from "../config/index.js";
|
|
5
|
+
import { AttestorServerSocket } from "./socket.js";
|
|
6
|
+
import { getAttestorAddress } from "./utils/generics.js";
|
|
7
|
+
import { addKeepAlive } from "./utils/keep-alive.js";
|
|
8
|
+
import { createBgpListener } from "../utils/bgp-listener.js";
|
|
9
|
+
import { getEnvVariable } from "../utils/env.js";
|
|
10
|
+
import { logger as LOGGER } from "../utils/index.js";
|
|
11
|
+
import { SelectedServiceSignatureType } from "../utils/signatures/index.js";
|
|
12
|
+
import { promisifySend } from "../utils/ws.js";
|
|
13
|
+
const PORT = +(getEnvVariable('PORT') || API_SERVER_PORT);
|
|
14
|
+
const DISABLE_BGP_CHECKS = getEnvVariable('DISABLE_BGP_CHECKS') === '1';
|
|
15
|
+
const ATTESTOR_ADDRESS_JSON_RES = JSON.stringify({
|
|
16
|
+
address: getAttestorAddress(SelectedServiceSignatureType),
|
|
17
|
+
signatureType: SelectedServiceSignatureType
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Creates the WebSocket API server,
|
|
21
|
+
* creates a fileserver to serve the browser RPC client,
|
|
22
|
+
* and listens on the given port.
|
|
23
|
+
*/
|
|
24
|
+
export async function createServer(port = PORT) {
|
|
25
|
+
const http = createHttpServer();
|
|
26
|
+
const serveBrowserRpc = serveStatic('browser', {
|
|
27
|
+
index: ['index.html'],
|
|
28
|
+
setHeaders(res) {
|
|
29
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const bgpListener = !DISABLE_BGP_CHECKS
|
|
33
|
+
? createBgpListener(LOGGER.child({ service: 'bgp-listener' }))
|
|
34
|
+
: undefined;
|
|
35
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
36
|
+
http.on('upgrade', handleUpgrade.bind(wss));
|
|
37
|
+
http.on('request', (req, res) => {
|
|
38
|
+
const url = URL.parse(req.url || '', 'http://localhost');
|
|
39
|
+
if (!url) {
|
|
40
|
+
res.statusCode = 422;
|
|
41
|
+
res.end('Invalid URL');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (url.pathname === ATTESTOR_ADDRESS_PATHNAME) {
|
|
45
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
46
|
+
res.end(ATTESTOR_ADDRESS_JSON_RES);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// simple way to serve files at the browser RPC path
|
|
50
|
+
if (!url.pathname?.startsWith(BROWSER_RPC_PATHNAME)) {
|
|
51
|
+
res.statusCode = 404;
|
|
52
|
+
res.end('Not found');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
req.url = req.url.slice(BROWSER_RPC_PATHNAME.length) || '/';
|
|
56
|
+
serveBrowserRpc(req, res, (err) => {
|
|
57
|
+
if (err) {
|
|
58
|
+
LOGGER.error({ err, url: req.url }, 'Failed to serve file');
|
|
59
|
+
}
|
|
60
|
+
res.statusCode = err?.statusCode ?? 404;
|
|
61
|
+
res.end(err?.message ?? 'Not found');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// wait for us to start listening
|
|
65
|
+
http.listen(port);
|
|
66
|
+
await new Promise((resolve, reject) => {
|
|
67
|
+
http.once('listening', () => resolve());
|
|
68
|
+
http.once('error', reject);
|
|
69
|
+
});
|
|
70
|
+
wss.on('connection', (ws, req) => handleNewClient(ws, req, bgpListener));
|
|
71
|
+
LOGGER.info({
|
|
72
|
+
port,
|
|
73
|
+
apiPath: WS_PATHNAME,
|
|
74
|
+
browserRpcPath: BROWSER_RPC_PATHNAME,
|
|
75
|
+
signerAddress: getAttestorAddress(SelectedServiceSignatureType)
|
|
76
|
+
}, 'WS server listening');
|
|
77
|
+
const wssClose = wss.close.bind(wss);
|
|
78
|
+
wss.close = (cb) => {
|
|
79
|
+
wssClose(() => http.close(cb));
|
|
80
|
+
bgpListener?.close();
|
|
81
|
+
};
|
|
82
|
+
return wss;
|
|
83
|
+
}
|
|
84
|
+
async function handleNewClient(ws, req, bgpListener) {
|
|
85
|
+
promisifySend(ws);
|
|
86
|
+
const client = await AttestorServerSocket.acceptConnection(ws, { req, bgpListener, logger: LOGGER });
|
|
87
|
+
// if initialisation fails, don't store the client
|
|
88
|
+
if (!client) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
ws.serverSocket = client;
|
|
92
|
+
addKeepAlive(ws, LOGGER.child({ sessionId: client.sessionId }));
|
|
93
|
+
}
|
|
94
|
+
function handleUpgrade(request, socket, head) {
|
|
95
|
+
const { pathname } = new URL(request.url, 'wss://base.url');
|
|
96
|
+
if (pathname === WS_PATHNAME) {
|
|
97
|
+
this.handleUpgrade(request, socket, head, (ws) => {
|
|
98
|
+
this.emit('connection', ws, request);
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
socket.destroy();
|
|
103
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEE Bundle Claim Handler
|
|
3
|
+
* Handles ClaimTeeBundleRequest by verifying TEE attestations and reconstructing TLS transcript
|
|
4
|
+
*/
|
|
5
|
+
import { ClaimTeeBundleResponse } from "../../proto/api.js";
|
|
6
|
+
import { VerificationBundle } from "../../proto/tee-bundle.js";
|
|
7
|
+
import { substituteParamValues } from "../../providers/http/index.js";
|
|
8
|
+
import { assertValidProviderTranscript } from "../utils/assert-valid-claim-request.js";
|
|
9
|
+
import { getAttestorAddress, niceParseJsonObject, signAsAttestor } from "../utils/generics.js";
|
|
10
|
+
import { verifyOprfMpcOutputs } from "../utils/tee-oprf-mpc-verification.js";
|
|
11
|
+
import { verifyOprfProofs } from "../utils/tee-oprf-verification.js";
|
|
12
|
+
import { reconstructTlsTranscript } from "../utils/tee-transcript-reconstruction.js";
|
|
13
|
+
import { verifyTeeBundle } from "../utils/tee-verification.js";
|
|
14
|
+
import { AttestorError } from "../../utils/error.js";
|
|
15
|
+
import { createSignDataForClaim, getIdentifierFromClaimInfo } from "../../utils/index.js";
|
|
16
|
+
export const claimTeeBundle = async (teeBundleRequest, { logger, client }) => {
|
|
17
|
+
const { verificationBundle, data } = teeBundleRequest;
|
|
18
|
+
// Initialize response
|
|
19
|
+
const res = ClaimTeeBundleResponse.create({ request: teeBundleRequest });
|
|
20
|
+
// 1. Verify TEE bundle (attestations + signatures) - this includes timestamp validation
|
|
21
|
+
logger.info('Starting TEE bundle verification');
|
|
22
|
+
const teeData = await verifyTeeBundle(verificationBundle, logger);
|
|
23
|
+
// 2. Extract timestampS from TEE_K bundle for claim signing
|
|
24
|
+
const timestampS = Math.floor(teeData.kOutputPayload.timestampMs / 1000);
|
|
25
|
+
// 3. Verify OPRF proofs first (before transcript reconstruction)
|
|
26
|
+
logger.info('Verifying OPRF proofs');
|
|
27
|
+
// Parse the verification bundle to get OPRF verifications
|
|
28
|
+
const bundle = VerificationBundle.decode(verificationBundle);
|
|
29
|
+
const zkOprfResults = await verifyOprfProofs({ ...teeData, oprfVerifications: bundle.oprfVerifications }, logger);
|
|
30
|
+
// 4. Verify OPRF MPC outputs (TEE-to-TEE computed OPRF)
|
|
31
|
+
logger.info('Verifying OPRF MPC outputs');
|
|
32
|
+
const oprfMpcResults = verifyOprfMpcOutputs(teeData.kOutputPayload, teeData.tOutputPayload, logger);
|
|
33
|
+
// 5. Combine ZK and OPRF MPC results for transcript reconstruction
|
|
34
|
+
const allOprfResults = validateAndCombineOprfResults(zkOprfResults, oprfMpcResults, logger);
|
|
35
|
+
// 6. Reconstruct TLS transcript with all OPRF replacements applied
|
|
36
|
+
logger.info('Starting TLS transcript reconstruction with OPRF replacements');
|
|
37
|
+
const transcriptData = await reconstructTlsTranscript(teeData, logger, allOprfResults);
|
|
38
|
+
// 7. Create plaintext transcript for provider validation (OPRF already applied)
|
|
39
|
+
logger.info('Creating plaintext transcript from TEE data');
|
|
40
|
+
const plaintextTranscript = createPlaintextTranscriptFromTeeData(transcriptData, logger);
|
|
41
|
+
// 8. Direct provider validation
|
|
42
|
+
logger.info('Running direct provider validation on TEE reconstructed data');
|
|
43
|
+
if (!data) {
|
|
44
|
+
throw new AttestorError('ERROR_INVALID_CLAIM', 'No claim data provided in TEE bundle request');
|
|
45
|
+
}
|
|
46
|
+
const validatedClaim = await validateTeeProviderReceipt(plaintextTranscript, data, logger, { version: client.metadata.clientVersion }, transcriptData.certificateInfo);
|
|
47
|
+
const ctx = niceParseJsonObject(validatedClaim.context, 'context');
|
|
48
|
+
// eslint-disable-next-line camelcase
|
|
49
|
+
ctx.pcr0_k = teeData.teekPcr0;
|
|
50
|
+
// eslint-disable-next-line camelcase
|
|
51
|
+
ctx.pcr0_t = teeData.teetPcr0;
|
|
52
|
+
// eslint-disable-next-line camelcase
|
|
53
|
+
ctx.tee_session_id = teeData.teeSessionId;
|
|
54
|
+
validatedClaim.context = JSON.stringify(ctx);
|
|
55
|
+
res.claim = {
|
|
56
|
+
...validatedClaim,
|
|
57
|
+
identifier: getIdentifierFromClaimInfo(validatedClaim),
|
|
58
|
+
// Use timestampS from TEE_K bundle for claim signing
|
|
59
|
+
timestampS,
|
|
60
|
+
// hardcode for compatibility with V1 claims
|
|
61
|
+
epoch: 1
|
|
62
|
+
};
|
|
63
|
+
logger.info({ claim: res.claim }, 'TEE bundle claim validation successful');
|
|
64
|
+
// 9. Sign the response
|
|
65
|
+
res.signatures = {
|
|
66
|
+
attestorAddress: getAttestorAddress(client.metadata.signatureType),
|
|
67
|
+
claimSignature: res.claim
|
|
68
|
+
? await signAsAttestor(createSignDataForClaim(res.claim), client.metadata.signatureType)
|
|
69
|
+
: new Uint8Array(),
|
|
70
|
+
resultSignature: await signAsAttestor(ClaimTeeBundleResponse.encode(res).finish(), client.metadata.signatureType)
|
|
71
|
+
};
|
|
72
|
+
logger.info('TEE bundle claim processing completed');
|
|
73
|
+
return res;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Creates a plaintext transcript from TEE reconstructed data
|
|
77
|
+
* This converts the TEE transcript data into the format expected by provider validation
|
|
78
|
+
* NEW: Uses consolidated response instead of individual packets for simplicity
|
|
79
|
+
*/
|
|
80
|
+
function createPlaintextTranscriptFromTeeData(transcriptData, logger) {
|
|
81
|
+
const transcript = [];
|
|
82
|
+
// Add reconstructed request (client -> server)
|
|
83
|
+
if (transcriptData.revealedRequest && transcriptData.revealedRequest.length > 0) {
|
|
84
|
+
transcript.push({
|
|
85
|
+
sender: 'client',
|
|
86
|
+
message: transcriptData.revealedRequest
|
|
87
|
+
});
|
|
88
|
+
logger.debug('Added TEE revealed request to plaintext transcript', {
|
|
89
|
+
length: transcriptData.revealedRequest.length
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Add consolidated reconstructed response (server -> client)
|
|
93
|
+
if (transcriptData.reconstructedResponse && transcriptData.reconstructedResponse.length > 0) {
|
|
94
|
+
transcript.push({
|
|
95
|
+
sender: 'server',
|
|
96
|
+
message: transcriptData.reconstructedResponse
|
|
97
|
+
});
|
|
98
|
+
logger.debug('Added TEE consolidated response to plaintext transcript', {
|
|
99
|
+
length: transcriptData.reconstructedResponse.length
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Log certificate validation info if available
|
|
103
|
+
if (transcriptData.certificateInfo) {
|
|
104
|
+
logger.info('Certificate information available for validation', {
|
|
105
|
+
commonName: transcriptData.certificateInfo.commonName,
|
|
106
|
+
issuerCommonName: transcriptData.certificateInfo.issuerCommonName,
|
|
107
|
+
dnsNames: transcriptData.certificateInfo.dnsNames,
|
|
108
|
+
notBefore: new Date(transcriptData.certificateInfo.notBeforeUnix * 1000).toISOString(),
|
|
109
|
+
notAfter: new Date(transcriptData.certificateInfo.notAfterUnix * 1000).toISOString()
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
logger.info('Created plaintext transcript from TEE data', {
|
|
113
|
+
totalMessages: transcript.length,
|
|
114
|
+
hasRequest: !!transcriptData.revealedRequest?.length,
|
|
115
|
+
hasResponse: !!transcriptData.reconstructedResponse?.length,
|
|
116
|
+
hasCertificateInfo: !!transcriptData.certificateInfo
|
|
117
|
+
});
|
|
118
|
+
return transcript;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Validates TEE provider receipt directly without signature validation
|
|
122
|
+
* This is essentially assertValidProviderTranscript but for TEE data
|
|
123
|
+
* NEW: Includes certificate validation for domain authentication
|
|
124
|
+
*/
|
|
125
|
+
async function validateTeeProviderReceipt(plaintextTranscript, claimInfo, logger, providerCtx, certificateInfo) {
|
|
126
|
+
logger.info('Starting direct TEE provider validation', {
|
|
127
|
+
provider: claimInfo.provider,
|
|
128
|
+
transcriptMessages: plaintextTranscript.length,
|
|
129
|
+
hasCertificateInfo: !!certificateInfo
|
|
130
|
+
});
|
|
131
|
+
// Validate certificate if available
|
|
132
|
+
if (certificateInfo) {
|
|
133
|
+
validateTlsCertificate(claimInfo, certificateInfo, logger);
|
|
134
|
+
}
|
|
135
|
+
// Use the existing provider validation logic directly
|
|
136
|
+
const validatedClaim = await assertValidProviderTranscript(plaintextTranscript, claimInfo, logger, providerCtx);
|
|
137
|
+
logger.info('TEE provider validation completed successfully', {
|
|
138
|
+
provider: validatedClaim.provider,
|
|
139
|
+
owner: validatedClaim.owner || 'unknown'
|
|
140
|
+
});
|
|
141
|
+
return validatedClaim;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Checks if a hostname matches a certificate name (with wildcard support)
|
|
145
|
+
* @param hostname - The hostname to check
|
|
146
|
+
* @param certName - The certificate name
|
|
147
|
+
* @returns true if the hostname is valid for this certificate name
|
|
148
|
+
*/
|
|
149
|
+
function isHostnameValidForCertificate(hostname, certName) {
|
|
150
|
+
// Exact match
|
|
151
|
+
if (hostname === certName) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
// Wildcard match
|
|
155
|
+
if (certName.startsWith('*.')) {
|
|
156
|
+
// Extract the domain from wildcard
|
|
157
|
+
const wildcardDomain = certName.slice(2);
|
|
158
|
+
// Check if hostname ends with the wildcard domain
|
|
159
|
+
if (hostname.endsWith(wildcardDomain)) {
|
|
160
|
+
// Ensure we're matching a subdomain, not partial domain
|
|
161
|
+
const subdomainPart = hostname.slice(0, -(wildcardDomain.length));
|
|
162
|
+
// Valid if:
|
|
163
|
+
// 1. The subdomain part ends with a dot (proper subdomain boundary)
|
|
164
|
+
// 2. The subdomain part doesn't contain additional dots (single-level wildcard)
|
|
165
|
+
if (subdomainPart.endsWith('.')) {
|
|
166
|
+
const subdomain = subdomainPart.slice(0, -1);
|
|
167
|
+
// Wildcard only matches single level, not multiple subdomains
|
|
168
|
+
return !subdomain.includes('.');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Validates that the TLS certificate is valid for the domain being claimed
|
|
176
|
+
*/
|
|
177
|
+
function validateTlsCertificate(claimInfo, certificateInfo, logger) {
|
|
178
|
+
// Extract hostname from the claim (this varies by provider)
|
|
179
|
+
let claimedHostname;
|
|
180
|
+
const paramsWithTemplates = niceParseJsonObject(claimInfo.parameters, 'params');
|
|
181
|
+
const params = substituteParamValues(paramsWithTemplates, undefined, true).newParams;
|
|
182
|
+
// Different providers store hostname in different places
|
|
183
|
+
if ('url' in params && typeof params.url === 'string') {
|
|
184
|
+
claimedHostname = new URL(params.url).hostname;
|
|
185
|
+
}
|
|
186
|
+
if (!claimedHostname) {
|
|
187
|
+
logger.warn('Could not extract hostname from claim for certificate validation', {
|
|
188
|
+
provider: claimInfo.provider
|
|
189
|
+
});
|
|
190
|
+
throw new AttestorError('ERROR_INVALID_CLAIM', 'Certificate validation failed: hostname not found');
|
|
191
|
+
}
|
|
192
|
+
logger.info('Validating TLS certificate for claimed hostname', {
|
|
193
|
+
claimedHostname,
|
|
194
|
+
certificateCommonName: certificateInfo.commonName,
|
|
195
|
+
certificateDnsNames: certificateInfo.dnsNames
|
|
196
|
+
});
|
|
197
|
+
// Check if claimed hostname matches certificate (including wildcard support)
|
|
198
|
+
const isValidForHostname = isHostnameValidForCertificate(claimedHostname, certificateInfo.commonName) ||
|
|
199
|
+
certificateInfo.dnsNames.some(name => isHostnameValidForCertificate(claimedHostname, name));
|
|
200
|
+
if (!isValidForHostname) {
|
|
201
|
+
throw new AttestorError('ERROR_INVALID_CLAIM', `Certificate validation failed: hostname '${claimedHostname}' not valid for certificate (CN: ${certificateInfo.commonName}, SANs: ${certificateInfo.dnsNames.join(', ')})`);
|
|
202
|
+
}
|
|
203
|
+
// Check certificate validity period
|
|
204
|
+
const now = Date.now() / 1000; // Current time in Unix seconds
|
|
205
|
+
if (now < certificateInfo.notBeforeUnix || now > certificateInfo.notAfterUnix) {
|
|
206
|
+
throw new AttestorError('ERROR_INVALID_CLAIM', `Certificate validation failed: certificate not valid at current time (valid from ${new Date(certificateInfo.notBeforeUnix * 1000).toISOString()} to ${new Date(certificateInfo.notAfterUnix * 1000).toISOString()})`);
|
|
207
|
+
}
|
|
208
|
+
logger.info('TLS certificate validation passed', {
|
|
209
|
+
claimedHostname,
|
|
210
|
+
validatedAgainst: isHostnameValidForCertificate(claimedHostname, certificateInfo.commonName) ?
|
|
211
|
+
`CommonName: ${certificateInfo.commonName}` :
|
|
212
|
+
`SAN: ${certificateInfo.dnsNames.find(name => isHostnameValidForCertificate(claimedHostname, name))}`
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Validates OPRF results have no overlapping ranges and combines them
|
|
217
|
+
* SECURITY: Prevents position collisions between ZK and OPRF MPC results
|
|
218
|
+
*/
|
|
219
|
+
function validateAndCombineOprfResults(zkOprfResults, oprfMpcResults, logger) {
|
|
220
|
+
const allOprfResults = [...zkOprfResults, ...oprfMpcResults];
|
|
221
|
+
if (allOprfResults.length === 0) {
|
|
222
|
+
return allOprfResults;
|
|
223
|
+
}
|
|
224
|
+
logger.info(`Combined ${zkOprfResults.length} ZK OPRF + ${oprfMpcResults.length} OPRF MPC results`);
|
|
225
|
+
// Check for overlapping ranges (position collision detection)
|
|
226
|
+
const seen = {};
|
|
227
|
+
for (const result of zkOprfResults) {
|
|
228
|
+
seen[result.position] = { length: result.length, source: 'zk' };
|
|
229
|
+
}
|
|
230
|
+
for (const result of oprfMpcResults) {
|
|
231
|
+
const existing = seen[result.position];
|
|
232
|
+
if (existing) {
|
|
233
|
+
// Exact duplicate at same position - verify they match
|
|
234
|
+
if (existing.length !== result.length) {
|
|
235
|
+
throw new AttestorError('ERROR_INVALID_CLAIM', `OPRF range conflict at position ${result.position}: ZK length ${existing.length} vs MPC length ${result.length}`);
|
|
236
|
+
}
|
|
237
|
+
logger.warn(`Duplicate OPRF range at position ${result.position} from both ZK and MPC - using MPC result`);
|
|
238
|
+
}
|
|
239
|
+
// Check for overlapping (but not identical) ranges
|
|
240
|
+
for (const [pos, data] of Object.entries(seen)) {
|
|
241
|
+
const position = Number(pos);
|
|
242
|
+
const existingEnd = position + data.length;
|
|
243
|
+
const newEnd = result.position + result.length;
|
|
244
|
+
const overlaps = (result.position < existingEnd && newEnd > position) && result.position !== position;
|
|
245
|
+
if (overlaps) {
|
|
246
|
+
throw new AttestorError('ERROR_INVALID_CLAIM', `Overlapping OPRF ranges: [${position}:${existingEnd}] (${data.source}) and [${result.position}:${newEnd}] (mpc)`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
seen[result.position] = { length: result.length, source: 'mpc' };
|
|
250
|
+
}
|
|
251
|
+
return allOprfResults;
|
|
252
|
+
}
|