fiftyone.pipeline.did 4.5.46 → 4.5.48
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.
|
@@ -138,6 +138,18 @@
|
|
|
138
138
|
stolen identifier is caught the moment it is presented, with nothing
|
|
139
139
|
stored server side.</p>
|
|
140
140
|
</div>
|
|
141
|
+
|
|
142
|
+
<hr>
|
|
143
|
+
<h3 class="c-eg-page__heading">Find out more</h3>
|
|
144
|
+
<ul>
|
|
145
|
+
<li><a href="https://51degrees.com/documentation/_identifiers__index.html?utm_source=code&utm_medium=example&utm_campaign=pipeline-node&utm_content=fiftyone.pipeline.did-creator-context-web-page.html&utm_term=find-out-more">What a 51Did is, and what the creator context binds it to</a></li>
|
|
146
|
+
<li><a href="https://51degrees.com/contact-us?utm_source=code&utm_medium=example&utm_campaign=pipeline-node&utm_content=fiftyone.pipeline.did-creator-context-web-page.html&utm_term=find-out-more">Talk to 51Degrees about using 51Did</a></li>
|
|
147
|
+
</ul>
|
|
148
|
+
<p class="c-eg-page__lead">The source of this demo and of the code it runs on.</p>
|
|
149
|
+
<ul>
|
|
150
|
+
<li><a href="https://github.com/51Degrees/pipeline-node">https://github.com/51Degrees/pipeline-node</a></li>
|
|
151
|
+
<li><a href="https://github.com/51Degrees/specifications">https://github.com/51Degrees/specifications</a></li>
|
|
152
|
+
</ul>
|
|
141
153
|
</div>
|
|
142
154
|
|
|
143
155
|
<script>
|
|
@@ -280,26 +292,93 @@
|
|
|
280
292
|
tr.appendChild(result);
|
|
281
293
|
}
|
|
282
294
|
|
|
295
|
+
// The name the client script gives its object on the page. It can be
|
|
296
|
+
// set per resource key, and every key used with this demo leaves it at
|
|
297
|
+
// the default.
|
|
298
|
+
var OBJECT_NAME = "fod";
|
|
299
|
+
|
|
300
|
+
// The 51Did is created by the 51Degrees client script, never by a
|
|
301
|
+
// request of our own. The service creates one only once the page has
|
|
302
|
+
// run the snippets it asks for and sent what they collected, so a page
|
|
303
|
+
// that asks for an identifier before then is told the page has not
|
|
304
|
+
// finished and is given nothing at all. The script runs those snippets,
|
|
305
|
+
// sends them with its own request, and calls back through its complete
|
|
306
|
+
// function with the answer, which carries the six identifiers because
|
|
307
|
+
// the resource key is entitled to them. The usage and the email address
|
|
308
|
+
// travel on the script's own address, and the service puts both into
|
|
309
|
+
// the request the script makes.
|
|
310
|
+
function created() {
|
|
311
|
+
return new Promise(function (resolve, reject) {
|
|
312
|
+
var script = document.createElement("script");
|
|
313
|
+
script.async = true;
|
|
314
|
+
script.src = API + RESOURCE + ".js?id.usage=non-marketing"
|
|
315
|
+
+ "&id.email=" + encodeURIComponent("demo@51did.example");
|
|
316
|
+
script.onerror = function () {
|
|
317
|
+
reject(new Error("the 51Degrees client script at " + script.src
|
|
318
|
+
+ " did not load"));
|
|
319
|
+
};
|
|
320
|
+
script.onload = function () {
|
|
321
|
+
var object = window[OBJECT_NAME];
|
|
322
|
+
if (!object || typeof object.complete !== "function") {
|
|
323
|
+
reject(new Error("the 51Degrees client script loaded but left "
|
|
324
|
+
+ "no '" + OBJECT_NAME + "' object with a complete function "
|
|
325
|
+
+ "on the page, so nothing can be created here"));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
object.complete(function (data) { resolve(data || {}); });
|
|
329
|
+
};
|
|
330
|
+
document.head.appendChild(script);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// What the snippets collected, taken from the cookies the client
|
|
335
|
+
// script writes on this page and sent with the verification call. The
|
|
336
|
+
// service compares this browser's device and browser against the
|
|
337
|
+
// creator's from these values, and where a browser needs them and they
|
|
338
|
+
// are absent it answers that the page has not sent them yet rather
|
|
339
|
+
// than giving a verdict. Every name the script writes begins 51D_, so
|
|
340
|
+
// forwarding all of them needs no list here that could fall behind the
|
|
341
|
+
// script.
|
|
342
|
+
function snippetQuery() {
|
|
343
|
+
var query = "";
|
|
344
|
+
(document.cookie || "").split(";").forEach(function (pair) {
|
|
345
|
+
var text = pair.trim();
|
|
346
|
+
var split = text.indexOf("=");
|
|
347
|
+
if (split > 0 && text.slice(0, 4) === "51D_") {
|
|
348
|
+
query += "&" + encodeURIComponent(text.slice(0, split))
|
|
349
|
+
+ "=" + encodeURIComponent(text.slice(split + 1));
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return query;
|
|
353
|
+
}
|
|
354
|
+
|
|
283
355
|
var given = new URLSearchParams(location.search).get("51did");
|
|
284
356
|
|
|
357
|
+
// The client script runs whichever path the page is on, because the
|
|
358
|
+
// service compares this browser against the creator using the values
|
|
359
|
+
// the snippets collect, and it only has them once the script has run.
|
|
360
|
+
var ready = created();
|
|
361
|
+
|
|
285
362
|
// 1. The identifier: created for THIS browser's connection, or
|
|
286
363
|
// taken from the link when demonstrating the transplant.
|
|
287
364
|
var owidPromise = given
|
|
288
|
-
?
|
|
365
|
+
? ready.then(function () { return given; }).then(function (owid) {
|
|
289
366
|
// The row shows the identifier itself, truncated as the table
|
|
290
367
|
// below truncates every identifier, so the reader sees which
|
|
291
368
|
// 51Did the link carried rather than a description of it.
|
|
292
369
|
show("s-create", owid.slice(0, 30) + "…", NONE);
|
|
293
370
|
return owid;
|
|
294
371
|
})
|
|
295
|
-
:
|
|
296
|
-
+ "&id.usage=non-marketing"
|
|
297
|
-
+ "&id.email=" + encodeURIComponent("demo@51did.example")
|
|
298
|
-
+ "&values=FODiD.IdProbGlobal&values=FODiD.IdProbLic"
|
|
299
|
-
+ "&values=FODiD.IdHemGlobal&values=FODiD.IdHemLic"
|
|
300
|
-
+ "&values=FODiD.IdRandGlobal&values=FODiD.IdRandLic")
|
|
301
|
-
.then(asJson)
|
|
372
|
+
: ready
|
|
302
373
|
.then(function (json) {
|
|
374
|
+
if (!json.fodid) {
|
|
375
|
+
// Said here rather than where the script is loaded, because a
|
|
376
|
+
// page checking an identifier from another browser still wants
|
|
377
|
+
// the script for its snippet values and does not need one of
|
|
378
|
+
// its own.
|
|
379
|
+
throw new Error("the client script completed with no 51Did "
|
|
380
|
+
+ "section, so this resource key is not entitled to one");
|
|
381
|
+
}
|
|
303
382
|
var families = [
|
|
304
383
|
["Probabilistic", "Global", "idprobglobal"],
|
|
305
384
|
["Probabilistic", "Licensed", "idproblic"],
|
|
@@ -351,7 +430,7 @@
|
|
|
351
430
|
// 2. Verify from THIS browser, so the cloud observes this live
|
|
352
431
|
// connection. The verdict returns only as an encrypted result.
|
|
353
432
|
return fetch(API + "id/verify-full/" + RESOURCE + "/"
|
|
354
|
-
+ owid + "?challenge=" + CHALLENGE)
|
|
433
|
+
+ owid + "?challenge=" + CHALLENGE + snippetQuery())
|
|
355
434
|
.then(asJson)
|
|
356
435
|
.then(function (verified) {
|
|
357
436
|
if (!verified.result) {
|
|
@@ -28,10 +28,16 @@
|
|
|
28
28
|
* adding the licence key the browser never sees. The page runs the 51Did
|
|
29
29
|
* flow the way production does.
|
|
30
30
|
*
|
|
31
|
-
* 1. Create. The browser
|
|
32
|
-
*
|
|
31
|
+
* 1. Create. The browser loads the 51Degrees client script, which runs
|
|
32
|
+
* the snippets the service asks for, sends what they collected, and
|
|
33
|
+
* hands the page the answer with the 51Did in it, created for the
|
|
34
|
+
* browser's own connection. The service issues an identifier only
|
|
35
|
+
* once those values are in, so a page asking for one by itself is
|
|
36
|
+
* told the page has not finished and is given nothing.
|
|
33
37
|
* 2. Verify. The browser calls verify-full, the first verification
|
|
34
|
-
* step, so the cloud observes the browser's live connection
|
|
38
|
+
* step, so the cloud observes the browser's live connection, and
|
|
39
|
+
* sends what the snippets collected with the call so that this
|
|
40
|
+
* browser can be compared with the creator. Both
|
|
35
41
|
* the signature outcome and the creator context verdict return only
|
|
36
42
|
* as an encrypted result that the browser cannot read or forge. (A
|
|
37
43
|
* deployment holding no context secret answers in the open instead.)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fiftyone.pipeline.did",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.48",
|
|
4
4
|
"description": "Strongly typed reader for the 51Did (51Degrees Identifier) value returned by the 51Degrees Cloud service. Parses the OWID envelope and exposes the usage, licence id, match key and terms document plus the identifier type. Compare match keys, never envelopes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"51degrees",
|
package/readme.md
CHANGED
|
@@ -517,10 +517,16 @@ runs against the code beside it.
|
|
|
517
517
|
`examples/creator-context-web/` holds a small demo web app, `server.js`
|
|
518
518
|
serving `page.html`, that runs the full 51Did flow the way production does.
|
|
519
519
|
|
|
520
|
-
1. **Create.** The browser
|
|
521
|
-
|
|
520
|
+
1. **Create.** The browser loads the 51Degrees client script, which runs the
|
|
521
|
+
snippets the service asks for, sends what they collected, and hands the
|
|
522
|
+
page the answer with the 51Did in it, created for the browser's own
|
|
523
|
+
connection. The service issues an identifier only once those values are
|
|
524
|
+
in, so a page asking for one by itself is told the page has not finished
|
|
525
|
+
and is given nothing.
|
|
522
526
|
2. **Verify.** The browser calls `verify-full`, the first verification step,
|
|
523
|
-
so the cloud observes the browser's live connection
|
|
527
|
+
so the cloud observes the browser's live connection, and sends what the
|
|
528
|
+
snippets collected with the call so that this browser can be compared
|
|
529
|
+
with the creator. Both the signature
|
|
524
530
|
outcome and the creator context verdict return only as an encrypted
|
|
525
531
|
`result` that the browser cannot read or forge. (A deployment holding no
|
|
526
532
|
context secret answers in the open instead.) The page then hands the
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* *********************************************************************
|
|
2
|
+
* This Original Work is copyright of 51 Degrees Mobile Experts Limited.
|
|
3
|
+
* Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
|
|
4
|
+
* Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
|
|
5
|
+
*
|
|
6
|
+
* This Original Work is licensed under the European Union Public Licence
|
|
7
|
+
* (EUPL) v.1.2 and is subject to its terms as set out below.
|
|
8
|
+
*
|
|
9
|
+
* If a copy of the EUPL was not distributed with this file, You can obtain
|
|
10
|
+
* one at https://opensource.org/licenses/EUPL-1.2.
|
|
11
|
+
*
|
|
12
|
+
* The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
|
|
13
|
+
* amended by the European Commission) shall be deemed incompatible for
|
|
14
|
+
* the purposes of the Work and the provisions of the compatibility
|
|
15
|
+
* clause in Article 5 of the EUPL shall not apply.
|
|
16
|
+
*
|
|
17
|
+
* If using the Work as, or as part of, a network application, by
|
|
18
|
+
* including the attribution notice(s) required under Article 5 of the EUPL
|
|
19
|
+
* in the end user terms of the application under an appropriate heading,
|
|
20
|
+
* such notice(s) shall fulfill the requirements of that article.
|
|
21
|
+
* ********************************************************************* */
|
|
22
|
+
|
|
23
|
+
// What the creator context demo page does in a browser, checked by
|
|
24
|
+
// running its script with a small stand in for a browser
|
|
25
|
+
// (creatorContextPageHarness.js). No browser is started and nothing
|
|
26
|
+
// reaches a network.
|
|
27
|
+
//
|
|
28
|
+
// The service creates a 51Did only once the page has run the snippets it
|
|
29
|
+
// asks for and sent what they collected, so a page that asks for one
|
|
30
|
+
// directly is told the page has not finished and is given nothing. The
|
|
31
|
+
// 51Degrees client script is what runs those snippets, so the page has
|
|
32
|
+
// to create through the script and then send what the snippets collected
|
|
33
|
+
// with its verification call as well. These tests pin both, because
|
|
34
|
+
// neither shows up in a unit test of the demo's server.
|
|
35
|
+
|
|
36
|
+
const { execFileSync } = require('child_process');
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const os = require('os');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
const HARNESS = path.join(__dirname, 'creatorContextPageHarness.js');
|
|
42
|
+
const PAGE = path.join(
|
|
43
|
+
__dirname, '..', 'examples', 'creator-context-web', 'page.html');
|
|
44
|
+
|
|
45
|
+
// The licensed probabilistic identifier the stand in client script
|
|
46
|
+
// reports, once the page has made it safe for a URL.
|
|
47
|
+
const CREATED_URL_SAFE = 'prob-lic_value';
|
|
48
|
+
|
|
49
|
+
function runPage (given) {
|
|
50
|
+
const args = [HARNESS, PAGE];
|
|
51
|
+
if (given !== undefined) { args.push(given); }
|
|
52
|
+
const printed = execFileSync(process.execPath, args, {
|
|
53
|
+
encoding: 'utf8', timeout: 60000
|
|
54
|
+
});
|
|
55
|
+
const lines = printed.trim().split(/\r?\n/);
|
|
56
|
+
return JSON.parse(lines[lines.length - 1]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scriptBlock () {
|
|
60
|
+
const page = fs.readFileSync(PAGE, 'utf8');
|
|
61
|
+
// A checkout on Windows has carriage returns in it, so the ends of
|
|
62
|
+
// lines are matched either way.
|
|
63
|
+
const match = page.match(/<script>\r?\n([\s\S]*?)\r?\n<\/script>/);
|
|
64
|
+
expect(match).not.toBeNull();
|
|
65
|
+
return match[1];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('creator context demo page', () => {
|
|
69
|
+
// A page whose script does not parse defines nothing and reports
|
|
70
|
+
// nothing, and the browser says so only in its console. node --check
|
|
71
|
+
// reads JavaScript rather than HTML, so the page's script block is
|
|
72
|
+
// written out on its own and checked.
|
|
73
|
+
test('the page script parses and runs without error', () => {
|
|
74
|
+
const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'page-'));
|
|
75
|
+
const file = path.join(folder, 'page-script.js');
|
|
76
|
+
fs.writeFileSync(file, scriptBlock());
|
|
77
|
+
try {
|
|
78
|
+
execFileSync(process.execPath, ['--check', file], { stdio: 'pipe' });
|
|
79
|
+
} finally {
|
|
80
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
expect(runPage().errors).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// The page asks the cloud for the client script, with the usage and
|
|
86
|
+
// the email address on its address, and takes the identifier from what
|
|
87
|
+
// the script reports.
|
|
88
|
+
test('the identifier comes from the client script', () => {
|
|
89
|
+
const record = runPage();
|
|
90
|
+
expect(record.scripts).toHaveLength(1);
|
|
91
|
+
expect(record.scripts[0]).toContain('TEST-RESOURCE-KEY.js');
|
|
92
|
+
expect(record.scripts[0]).toContain('id.usage=non-marketing');
|
|
93
|
+
expect(record.scripts[0]).toContain('id.email=');
|
|
94
|
+
expect(record.rows['s-create']).toBe('created for this browser');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// A request of the page's own would be made before the snippets had
|
|
98
|
+
// run, and the service would answer it with no identifier at all.
|
|
99
|
+
test('the page does not ask for an identifier itself', () => {
|
|
100
|
+
for (const address of runPage().fetches) {
|
|
101
|
+
expect(address).not.toContain('json?resource=');
|
|
102
|
+
expect(address).not.toContain('/json');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// The identifier the script reported is verified, made safe for a URL,
|
|
107
|
+
// and what the snippets collected goes with it, because the service
|
|
108
|
+
// compares this browser against the creator from those values.
|
|
109
|
+
test('the verification carries the identifier and the snippets', () => {
|
|
110
|
+
const verify = runPage().fetches
|
|
111
|
+
.filter((address) => address.includes('id/verify-full'));
|
|
112
|
+
expect(verify).toHaveLength(1);
|
|
113
|
+
expect(verify[0]).toContain(CREATED_URL_SAFE);
|
|
114
|
+
expect(verify[0].split('?')[0]).not.toContain('+');
|
|
115
|
+
expect(verify[0]).toContain('51D_ScreenPixelsHeight=1080');
|
|
116
|
+
expect(verify[0]).toContain('51D_ProfileIds=1-2-3');
|
|
117
|
+
expect(verify[0]).not.toContain('unrelated=ignored');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// The licence key lives on the server, so the sealed result goes there
|
|
121
|
+
// and the verdict comes back from there.
|
|
122
|
+
test("the result is redeemed on the page's own server", () => {
|
|
123
|
+
const record = runPage();
|
|
124
|
+
const redeem = record.fetches
|
|
125
|
+
.filter((address) => address.startsWith('/redeem?'));
|
|
126
|
+
expect(redeem).toHaveLength(1);
|
|
127
|
+
expect(redeem[0]).toContain('result=sealed-result');
|
|
128
|
+
expect(record.rows['s-signature']).toBe('verified');
|
|
129
|
+
expect(record.rows['s-context']).toBe('verified');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// The page opened with an identifier from another browser checks that
|
|
133
|
+
// identifier, and still needs this browser's snippet values for the
|
|
134
|
+
// comparison, so the script runs on that path too.
|
|
135
|
+
test('a transplanted identifier still runs the client script', () => {
|
|
136
|
+
const record = runPage('given-value');
|
|
137
|
+
expect(record.scripts).toHaveLength(1);
|
|
138
|
+
const verify = record.fetches
|
|
139
|
+
.filter((address) => address.includes('id/verify-full'));
|
|
140
|
+
expect(verify).toHaveLength(1);
|
|
141
|
+
expect(verify[0]).toContain('given-value');
|
|
142
|
+
expect(verify[0]).toContain('51D_ProfileIds=1-2-3');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Someone running the demo is interested in the subject, so the page
|
|
146
|
+
// ends with somewhere to go next. The links to 51degrees.com carry the
|
|
147
|
+
// campaign tags the link lint asks for and the source repositories are
|
|
148
|
+
// given as plain addresses.
|
|
149
|
+
test('the page ends with a Find out more section', () => {
|
|
150
|
+
const page = fs.readFileSync(PAGE, 'utf8');
|
|
151
|
+
expect(page).toContain('Find out more');
|
|
152
|
+
expect(page).toContain('utm_campaign=pipeline-node');
|
|
153
|
+
expect(page).toContain('https://github.com/51Degrees/pipeline-node');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Runs the creator context demo page's script in Node with a small stand
|
|
2
|
+
// in for a browser, and prints what the page did as one line of JSON.
|
|
3
|
+
//
|
|
4
|
+
// Usage: node creator_context_page_harness.js <page.html> [51did]
|
|
5
|
+
//
|
|
6
|
+
// The optional second argument is the identifier the page is opened with,
|
|
7
|
+
// which is the transplant path. Without it the page creates one.
|
|
8
|
+
//
|
|
9
|
+
// The stand in gives the page a document, a window, a location and a
|
|
10
|
+
// fetch. Inserting a script element stands for loading the 51Degrees
|
|
11
|
+
// client script: the harness answers it by putting an object on the
|
|
12
|
+
// window whose complete function hands back a fixed cloud answer with
|
|
13
|
+
// the six 51Did values in it, which is what the real script does once it
|
|
14
|
+
// has run the page's snippets. Nothing here reaches a network.
|
|
15
|
+
//
|
|
16
|
+
// The printed object says which script the page asked for, which
|
|
17
|
+
// addresses it fetched, and what each row of the page ended up saying, so
|
|
18
|
+
// a test can check that the page creates through the client script and
|
|
19
|
+
// verifies the identifier the script reported.
|
|
20
|
+
'use strict';
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const vm = require('vm');
|
|
23
|
+
|
|
24
|
+
const pageFile = process.argv[2];
|
|
25
|
+
const given = process.argv[3];
|
|
26
|
+
|
|
27
|
+
const page = fs.readFileSync(pageFile, 'utf8');
|
|
28
|
+
// A checkout on Windows has carriage returns in it, so the ends of
|
|
29
|
+
// lines are matched either way.
|
|
30
|
+
const match = page.match(/<script>\r?\n([\s\S]*?)\r?\n<\/script>/);
|
|
31
|
+
if (!match) {
|
|
32
|
+
console.log(JSON.stringify({ error: 'no script block in the page' }));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const source = match[1]
|
|
36
|
+
.replace(/__RESOURCE__/g, 'TEST-RESOURCE-KEY')
|
|
37
|
+
.replace(/__CHALLENGE__/g, 'test-challenge')
|
|
38
|
+
.replace(/__API__/g, 'https://cloud.51degrees.com/api/v4/');
|
|
39
|
+
|
|
40
|
+
// The identifiers the stand in client script reports. Base64 with a plus
|
|
41
|
+
// and a slash in it, so a test can see the page make the value safe for a
|
|
42
|
+
// URL before it sends it.
|
|
43
|
+
const IDS = {
|
|
44
|
+
idprobglobal: 'prob+global/value=',
|
|
45
|
+
idproblic: 'prob+lic/value=',
|
|
46
|
+
idhemglobal: 'hem+global/value=',
|
|
47
|
+
idhemlic: 'hem+lic/value=',
|
|
48
|
+
idrandglobal: 'rand+global/value=',
|
|
49
|
+
idrandlic: 'rand+lic/value='
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const record = { scripts: [], fetches: [], rows: {}, revealed: [], errors: [] };
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
*
|
|
56
|
+
* @param id
|
|
57
|
+
*/
|
|
58
|
+
function element (id) {
|
|
59
|
+
return {
|
|
60
|
+
id,
|
|
61
|
+
style: {},
|
|
62
|
+
children: [],
|
|
63
|
+
set textContent (value) { record.rows[this.id] = String(value); },
|
|
64
|
+
get textContent () { return record.rows[this.id] || ''; },
|
|
65
|
+
set className (value) { record.rows[this.id + '.class'] = value; },
|
|
66
|
+
get className () { return record.rows[this.id + '.class'] || ''; },
|
|
67
|
+
appendChild: function (child) { this.children.push(child); },
|
|
68
|
+
onclick: null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const elements = {};
|
|
73
|
+
/**
|
|
74
|
+
*
|
|
75
|
+
* @param id
|
|
76
|
+
*/
|
|
77
|
+
function byId (id) {
|
|
78
|
+
if (!elements[id]) { elements[id] = element(id); }
|
|
79
|
+
return elements[id];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const document = {
|
|
83
|
+
// The page reads this the way a browser gives it, as one string of
|
|
84
|
+
// name and value pairs. The client script stand in writes the two
|
|
85
|
+
// below when it "loads", which is what the real script does.
|
|
86
|
+
cookie: 'unrelated=ignored',
|
|
87
|
+
getElementById: byId,
|
|
88
|
+
createElement: function (tag) {
|
|
89
|
+
const made = element('<' + tag + '>');
|
|
90
|
+
if (tag === 'script') {
|
|
91
|
+
// Loading the client script. The page sets src, then this
|
|
92
|
+
// harness answers as the real script does, by leaving its
|
|
93
|
+
// object on the window.
|
|
94
|
+
let source = '';
|
|
95
|
+
Object.defineProperty(made, 'src', {
|
|
96
|
+
get: function () { return source; },
|
|
97
|
+
set: function (value) {
|
|
98
|
+
source = value;
|
|
99
|
+
record.scripts.push(value);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return made;
|
|
104
|
+
},
|
|
105
|
+
head: {
|
|
106
|
+
appendChild: function (node) {
|
|
107
|
+
// The client script has "loaded". Give the page the object it
|
|
108
|
+
// looks for, then tell it the load finished.
|
|
109
|
+
document.cookie = 'unrelated=ignored; ' +
|
|
110
|
+
'51D_ScreenPixelsHeight=1080; 51D_ProfileIds=1-2-3';
|
|
111
|
+
sandbox.window.fod = {
|
|
112
|
+
complete: function (report) {
|
|
113
|
+
report({ fodid: Object.assign({}, IDS) });
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
if (typeof node.onload === 'function') { node.onload(); }
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
body: { appendChild: function () {} }
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
*
|
|
124
|
+
* @param body
|
|
125
|
+
*/
|
|
126
|
+
function response (body) {
|
|
127
|
+
return Promise.resolve({
|
|
128
|
+
ok: true,
|
|
129
|
+
status: 200,
|
|
130
|
+
url: 'stub',
|
|
131
|
+
text: function () { return Promise.resolve(JSON.stringify(body)); }
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
*
|
|
137
|
+
* @param url
|
|
138
|
+
*/
|
|
139
|
+
function fetchStub (url) {
|
|
140
|
+
record.fetches.push(url);
|
|
141
|
+
if (url.indexOf('id/verify-full') !== -1 ||
|
|
142
|
+
url.indexOf('id/verify-context') !== -1) {
|
|
143
|
+
return response({ result: 'sealed-result' });
|
|
144
|
+
}
|
|
145
|
+
if (url.indexOf('/redeem') !== -1) {
|
|
146
|
+
return response({
|
|
147
|
+
signature: 'verified',
|
|
148
|
+
context: 'verified',
|
|
149
|
+
serverSignature: 'verified'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return response({});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sandbox = {
|
|
156
|
+
console: {
|
|
157
|
+
log: function () {},
|
|
158
|
+
warn: function () {},
|
|
159
|
+
error: function (...a) { record.errors.push(a.join(' ')); }
|
|
160
|
+
},
|
|
161
|
+
document,
|
|
162
|
+
fetch: fetchStub,
|
|
163
|
+
URLSearchParams,
|
|
164
|
+
Promise,
|
|
165
|
+
setTimeout,
|
|
166
|
+
clearTimeout,
|
|
167
|
+
navigator: { clipboard: { writeText: function () {} } },
|
|
168
|
+
location: {
|
|
169
|
+
search: given ? '?51did=' + encodeURIComponent(given) : '',
|
|
170
|
+
origin: 'http://localhost:5100',
|
|
171
|
+
pathname: '/'
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
sandbox.window = sandbox;
|
|
175
|
+
sandbox.globalThis = sandbox;
|
|
176
|
+
|
|
177
|
+
vm.createContext(sandbox);
|
|
178
|
+
try {
|
|
179
|
+
vm.runInContext(source, sandbox, { filename: 'page.html' });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
record.errors.push(String(error));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The page's work is a chain of promises, so let them settle before
|
|
185
|
+
// reporting. Several turns, because each step waits for the one before.
|
|
186
|
+
let turns = 0;
|
|
187
|
+
/**
|
|
188
|
+
*
|
|
189
|
+
*/
|
|
190
|
+
function settle () {
|
|
191
|
+
turns += 1;
|
|
192
|
+
if (turns < 20) {
|
|
193
|
+
setTimeout(settle, 0);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
console.log(JSON.stringify(record));
|
|
197
|
+
}
|
|
198
|
+
settle();
|