pinstripe 0.30.0 → 0.31.1
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/development.db +0 -0
- package/lib/commands/list_apps.js +1 -1
- package/lib/commands/list_views.js +3 -21
- package/lib/commands/purge_used_hashes.js +14 -0
- package/lib/component.js +6 -36
- package/lib/migrations/1708772281_create_used_hash.js +9 -0
- package/lib/migrations/_file_importer.js +2 -0
- package/lib/models/_file_importer.js +2 -0
- package/lib/models/used_hash.js +7 -0
- package/lib/proof_of_work.js +84 -0
- package/lib/services/fetch.js +16 -46
- package/lib/services/only_once.js +24 -0
- package/lib/services/render_form.js +6 -9
- package/lib/services/render_view.js +1 -7
- package/lib/view.js +6 -0
- package/lib/views/shared/_form.js +16 -27
- package/package.json +2 -2
package/development.db
ADDED
|
File without changes
|
|
@@ -8,7 +8,7 @@ export default {
|
|
|
8
8
|
console.log('The following apps are available:');
|
|
9
9
|
console.log('');
|
|
10
10
|
App.names.forEach(appName => {
|
|
11
|
-
console.log(` * ${chalk.green(appName)}
|
|
11
|
+
console.log(` * ${chalk.green(appName)}`);
|
|
12
12
|
});
|
|
13
13
|
console.log('');
|
|
14
14
|
}
|
|
@@ -6,31 +6,13 @@ export default {
|
|
|
6
6
|
run(){
|
|
7
7
|
const { extractOptions } = this.cliUtils;
|
|
8
8
|
|
|
9
|
-
const { app } = extractOptions();
|
|
9
|
+
const { app = 'main' } = extractOptions();
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
this.listComposedViews(typeof app == 'string' ? app : 'main');
|
|
13
|
-
} else {
|
|
14
|
-
this.listAllViews();
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
listComposedViews(appName){
|
|
19
|
-
const { viewNames, resolveView } = View.mapperFor(App.create(appName, this.context).compose());
|
|
20
|
-
console.log('');
|
|
21
|
-
console.log(`The following views have been composed for app "${appName}":`);
|
|
22
|
-
console.log('');
|
|
23
|
-
viewNames.forEach(viewName => {
|
|
24
|
-
console.log(` * ${chalk.green(viewName)} -> ${chalk.green(resolveView(viewName))}`);
|
|
25
|
-
});
|
|
26
|
-
console.log('');
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
listAllViews(){
|
|
11
|
+
const { viewNames } = View.mapperFor(App.create(app, this.context).compose());
|
|
30
12
|
console.log('');
|
|
31
13
|
console.log(`The following views are available:`);
|
|
32
14
|
console.log('');
|
|
33
|
-
|
|
15
|
+
viewNames.forEach(viewName => {
|
|
34
16
|
console.log(` * ${chalk.green(viewName)}`);
|
|
35
17
|
});
|
|
36
18
|
console.log('');
|
package/lib/component.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { fileURLToPath } from 'url'; // pinstripe-if-client: const fileURLToPath = undefined;
|
|
3
3
|
|
|
4
4
|
import { Class } from './class.js';
|
|
5
|
-
import { TEXT_ONLY_TAGS
|
|
5
|
+
import { TEXT_ONLY_TAGS } from './constants.js';
|
|
6
6
|
import { Inflector } from './inflector.js';
|
|
7
7
|
import { VirtualNode } from './virtual_node.js';
|
|
8
8
|
import { Registry } from './registry.js';
|
|
9
9
|
import { ComponentEvent } from './component_event.js';
|
|
10
10
|
import { Client } from './client.js'; // pinstripe-if-client: const Client = undefined;
|
|
11
|
+
import { generateProofOfWork } from './proof_of_work.js';
|
|
11
12
|
|
|
12
13
|
export const Component = Class.extend().include({
|
|
13
14
|
meta(){
|
|
@@ -462,7 +463,10 @@ export const Component = Class.extend().include({
|
|
|
462
463
|
if(!(otherOptions.body instanceof FormData)) throw new Error(`Proof of work requires form data to be present`);
|
|
463
464
|
const values = {};
|
|
464
465
|
otherOptions.body.forEach((value, key) => values[key] = value);
|
|
465
|
-
otherOptions.body.append('_proofOfWork', await generateProofOfWork(values,
|
|
466
|
+
otherOptions.body.append('_proofOfWork', await generateProofOfWork(values, {
|
|
467
|
+
abortSignal: abortController.signal,
|
|
468
|
+
onProgress: progress => this.trigger('proofOfWorkProgress', { data: progress, bubbles: false })
|
|
469
|
+
}))
|
|
466
470
|
}
|
|
467
471
|
const promises = [
|
|
468
472
|
fetch(normalizedUrl, { signal: abortController.signal, ...otherOptions }),
|
|
@@ -712,38 +716,4 @@ function normalizeVirtualNode(){
|
|
|
712
716
|
}
|
|
713
717
|
}
|
|
714
718
|
|
|
715
|
-
async function generateProofOfWork(values, abortSignal){
|
|
716
|
-
const stringifiedValues = JSON.stringify(values);
|
|
717
|
-
const timestamp = getUTCTimestamp();
|
|
718
|
-
const random = btoa(`${Math.random()}`);
|
|
719
|
-
let counter = 0;
|
|
720
|
-
while(!abortSignal.aborted){
|
|
721
|
-
const candidateSolution = `1:20:${timestamp}:?::${random}:${btoa(`${counter}`)}`;
|
|
722
|
-
const hash = await createSha1Hash(candidateSolution.replace(/\?/, stringifiedValues));
|
|
723
|
-
if(hash.match(/^00000/)) return candidateSolution;
|
|
724
|
-
counter++;
|
|
725
|
-
}
|
|
726
|
-
return '';
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function getUTCTimestamp() {
|
|
730
|
-
const now = new Date();
|
|
731
|
-
|
|
732
|
-
const year = now.getUTCFullYear() % 100;
|
|
733
|
-
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
|
|
734
|
-
const day = String(now.getUTCDate()).padStart(2, '0');
|
|
735
|
-
const hours = String(now.getUTCHours()).padStart(2, '0');
|
|
736
|
-
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
|
|
737
|
-
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
|
|
738
|
-
|
|
739
|
-
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
async function createSha1Hash(input) {
|
|
744
|
-
const buffer = new TextEncoder().encode(input);
|
|
745
|
-
const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-1', buffer)));
|
|
746
|
-
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
747
|
-
}
|
|
748
|
-
|
|
749
719
|
ComponentEvent.Component = Component;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
import crypto from 'crypto' // pinstripe-if-client: const crypto = window.crypto;
|
|
3
|
+
|
|
4
|
+
import { Class } from './class.js';
|
|
5
|
+
import { Singleton } from './singleton.js';
|
|
6
|
+
|
|
7
|
+
const HASH_CASH_TARGET = Math.pow(2, 32 - 20);
|
|
8
|
+
const DEFAULT_DIFFICULTY = 1 / 20;
|
|
9
|
+
const DEFAULT_EXPIRY_IN_SECONDS = 10 * 60;
|
|
10
|
+
|
|
11
|
+
export const ProofOfWork = Class.extend().include({
|
|
12
|
+
meta(){
|
|
13
|
+
this.include(Singleton)
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
async generateProofOfWork(input, options = {}){
|
|
17
|
+
const { difficulty = DEFAULT_DIFFICULTY, steps = 1000, onProgress = () => {}, abortSignal = { aborted: false } } = options;
|
|
18
|
+
const inputHash = await this.createSha1Hash(JSON.stringify(input));
|
|
19
|
+
const salt = await this.createSha1Hash(Math.random());
|
|
20
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
21
|
+
const target = this.calculateTarget(difficulty, steps);
|
|
22
|
+
const solution = [];
|
|
23
|
+
|
|
24
|
+
let counter = -1;
|
|
25
|
+
for(let i = 1; i <= steps && !abortSignal.aborted; i++){
|
|
26
|
+
while(!abortSignal.aborted){
|
|
27
|
+
counter++;
|
|
28
|
+
const hash = await this.createSha1Hash(`${difficulty}:${steps}:${inputHash}:${salt}:${timestamp}:${counter}`);
|
|
29
|
+
const integers = this.hexToIntegers(hash);
|
|
30
|
+
if(integers[0] <= target) break;
|
|
31
|
+
}
|
|
32
|
+
solution.push(counter);
|
|
33
|
+
const progressPercentage = Math.floor(i * (100 / steps) * 100) / 100;
|
|
34
|
+
onProgress(progressPercentage);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if(abortSignal.aborted) return;
|
|
38
|
+
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
salt,
|
|
41
|
+
timestamp,
|
|
42
|
+
solution,
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async verifyProofOfWork(input, proofOfWork, options = {}){
|
|
47
|
+
const { difficulty = DEFAULT_DIFFICULTY, expiryInSeconds = DEFAULT_EXPIRY_IN_SECONDS } = options;
|
|
48
|
+
const { salt, timestamp, solution } = JSON.parse(proofOfWork);
|
|
49
|
+
if(Math.floor(Date.now() / 1000) - timestamp > expiryInSeconds) return false;
|
|
50
|
+
const steps = solution.length;
|
|
51
|
+
if(typeof steps !== 'number' || steps <= 0) return false;
|
|
52
|
+
const inputHash = await this.createSha1Hash(JSON.stringify(input));
|
|
53
|
+
const target = this.calculateTarget(difficulty, steps);
|
|
54
|
+
for(let i = 0; i < steps; i++){
|
|
55
|
+
const counter = solution[i];
|
|
56
|
+
const hash = await this.createSha1Hash(`${difficulty}:${steps}:${inputHash}:${salt}:${timestamp}:${counter}`.toString(), true);
|
|
57
|
+
const integers = this.hexToIntegers(hash);
|
|
58
|
+
if(integers[0] > target) return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async createSha1Hash(input) {
|
|
64
|
+
const buffer = new TextEncoder().encode(input);
|
|
65
|
+
const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-1', buffer)));
|
|
66
|
+
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
hexToIntegers(hex){
|
|
70
|
+
const out = [];
|
|
71
|
+
for(let i = 0; i < hex.length; i += 8){
|
|
72
|
+
out.push(parseInt(hex.slice(i, i + 8), 16));
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
calculateTarget(difficulty, steps){
|
|
78
|
+
return (HASH_CASH_TARGET / difficulty) * steps;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const generateProofOfWork = (...args) => ProofOfWork.instance.generateProofOfWork(...args);
|
|
83
|
+
|
|
84
|
+
export const verifyProofOfWork = (...args) => ProofOfWork.instance.verifyProofOfWork(...args);
|
package/lib/services/fetch.js
CHANGED
|
@@ -15,29 +15,18 @@ export default {
|
|
|
15
15
|
const normalizedParams = this.normalizeParams(params);
|
|
16
16
|
this.context.params = normalizedParams;
|
|
17
17
|
|
|
18
|
-
const viewName = normalizedParams._url.pathname.replace(/^\/|\/$/g, '');
|
|
18
|
+
const viewName = normalizedParams._url.pathname.replace(/^\/|\/$/g, '') || 'index';
|
|
19
19
|
|
|
20
20
|
let out = await this.renderGuardViews(viewName, normalizedParams);
|
|
21
|
-
if(out)
|
|
22
|
-
return out;
|
|
23
|
-
}
|
|
21
|
+
if(out) return out;
|
|
24
22
|
|
|
25
23
|
if(!viewName.match(/(^|\/)_[^\/]+(|\/index)$/)){
|
|
26
|
-
out = this.normalizeResponse(await this.app.renderView(viewName != '' ? `${viewName}/index`: 'index', normalizedParams));
|
|
27
|
-
if(out){
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
24
|
out = this.normalizeResponse(await this.app.renderView(viewName, normalizedParams));
|
|
32
|
-
if(out)
|
|
33
|
-
return out;
|
|
34
|
-
}
|
|
25
|
+
if(out) return out;
|
|
35
26
|
}
|
|
36
27
|
|
|
37
28
|
out = await this.renderDefaultViews(viewName, normalizedParams);
|
|
38
|
-
if(out)
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
29
|
+
if(out) return out;
|
|
41
30
|
|
|
42
31
|
return [404, {'content-type': 'text/plain'}, ['Not found']];
|
|
43
32
|
},
|
|
@@ -45,22 +34,12 @@ export default {
|
|
|
45
34
|
async renderGuardViews(viewName, params){
|
|
46
35
|
const viewNameSegments = viewName != '' ? viewName.split(/\//) : [];
|
|
47
36
|
|
|
48
|
-
const candidateIndexView = [...viewNameSegments, 'index'].join('/');
|
|
49
|
-
const candidateDefaultView = [...viewNameSegments, 'default'].join('/');
|
|
50
|
-
|
|
51
|
-
if(!this.app.isView(candidateIndexView) && !this.app.isView(candidateDefaultView)){
|
|
52
|
-
viewNameSegments.pop();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
37
|
const prefixSegments = [];
|
|
56
38
|
while(true){
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if(viewNameSegments.length == 0){
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
39
|
+
const candidateGuardViewName = prefixSegments.length ? [...prefixSegments, 'guard'].join('/') : 'guard';
|
|
40
|
+
const out = this.normalizeResponse(await this.app.renderView(candidateGuardViewName, params));
|
|
41
|
+
if(out) return out;
|
|
42
|
+
if(viewNameSegments.length == 0) break;
|
|
64
43
|
prefixSegments.push(viewNameSegments.shift());
|
|
65
44
|
}
|
|
66
45
|
},
|
|
@@ -68,28 +47,19 @@ export default {
|
|
|
68
47
|
async renderDefaultViews(viewName, params){
|
|
69
48
|
const prefixSegments = viewName != '' ? viewName.split(/\//) : [];
|
|
70
49
|
while(true){
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if(prefixSegments.length == 0){
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
50
|
+
const candidateDefaultViewName = prefixSegments.length ? [...prefixSegments, 'default'].join('/') : 'default';
|
|
51
|
+
const out = this.normalizeResponse(await this.app.renderView(candidateDefaultViewName, params));
|
|
52
|
+
if(out) return out;
|
|
53
|
+
if(prefixSegments.length == 0) break;
|
|
78
54
|
prefixSegments.pop();
|
|
79
55
|
}
|
|
80
56
|
},
|
|
81
57
|
|
|
82
58
|
normalizeParams(params){
|
|
83
59
|
const out = { ...params }
|
|
84
|
-
if(!out._method)
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
if(!params._url){
|
|
88
|
-
out._url = new URL('http://localhost')
|
|
89
|
-
}
|
|
90
|
-
if(!params._headers){
|
|
91
|
-
out._headers = {};
|
|
92
|
-
}
|
|
60
|
+
if(!out._method) out._method = 'get';
|
|
61
|
+
if(!params._url) out._url = new URL('http://localhost');
|
|
62
|
+
if(!params._headers) out._headers = {};
|
|
93
63
|
return out;
|
|
94
64
|
},
|
|
95
65
|
|
|
@@ -109,7 +79,7 @@ export default {
|
|
|
109
79
|
const normalizedHeaders = {};
|
|
110
80
|
Object.keys(headers).forEach(name => {
|
|
111
81
|
normalizedHeaders[name.toLowerCase()] = headers[name];
|
|
112
|
-
})
|
|
82
|
+
});
|
|
113
83
|
|
|
114
84
|
return [ status, normalizedHeaders, body ];
|
|
115
85
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
create(){
|
|
6
|
+
return this;
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async hasBeenUsed(key){
|
|
10
|
+
return await this.database.withoutTenantScope.usedHashes.where({value: this.createHash(key)}).count() > 0;
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async markAsUsed(key, options = {}){
|
|
14
|
+
const { expiresAt = new Date(Date.now() + (1000 * 60 * 60 * 24)) } = options;
|
|
15
|
+
await this.database.usedHashes.insert({
|
|
16
|
+
value: this.createHash(key),
|
|
17
|
+
expiresAt
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
createHash(key){
|
|
22
|
+
return crypto.createHash('sha1').update(JSON.stringify(key)).digest('base64');
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import * as crypto from 'crypto';
|
|
2
1
|
|
|
3
2
|
import { ValidationError } from '../validation_error.js';
|
|
4
3
|
import { Inflector } from '../inflector.js';
|
|
4
|
+
import { verifyProofOfWork } from '../proof_of_work.js';
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
create(){
|
|
@@ -24,13 +24,16 @@ export default {
|
|
|
24
24
|
try {
|
|
25
25
|
if(requiresProofOfWork){
|
|
26
26
|
if(!this.params._proofOfWork) throw new ValidationError({ _proofOfWork: 'Must not be blank' });
|
|
27
|
-
if(!
|
|
27
|
+
if(!await verifyProofOfWork(values, this.params._proofOfWork)) throw new ValidationError({ _proofOfWork: 'Must be a valid' });
|
|
28
|
+
if(await this.onlyOnce.hasBeenUsed({ proofOfWork: this.params._proofOfWork })) throw new ValidationError({ _proofOfWork: 'Must be unused' });
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
const out = await formAdapter.submit(values, success) || this.renderHtml`
|
|
30
31
|
<span data-component="pinstripe-anchor" data-target="_parent">
|
|
31
32
|
<script type="pinstripe">this.parent.trigger('click');</script>
|
|
32
33
|
</span>
|
|
33
34
|
`;
|
|
35
|
+
if(requiresProofOfWork) await this.onlyOnce.markAsUsed({ proofOfWork: this.params._proofOfWork });
|
|
36
|
+
return out;
|
|
34
37
|
} catch(e){
|
|
35
38
|
if(!(e instanceof ValidationError)){
|
|
36
39
|
throw e;
|
|
@@ -74,12 +77,6 @@ export default {
|
|
|
74
77
|
submitTitle,
|
|
75
78
|
cancelTitle
|
|
76
79
|
});
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
verifyProofOfWork(proofOfWork, values){
|
|
80
|
-
const hash = crypto.createHash('sha1').update(proofOfWork.replace(/\?/, JSON.stringify(values)), 'binary').digest('hex');
|
|
81
|
-
if(hash.match(/^00000/)) return true;
|
|
82
|
-
return false;
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
export default {
|
|
3
3
|
create(){
|
|
4
|
-
return
|
|
5
|
-
const out = await this.app.renderView(name != '' ? `${name}/index`: 'index', params);
|
|
6
|
-
if(out){
|
|
7
|
-
return out;
|
|
8
|
-
}
|
|
9
|
-
return this.app.renderView(name, params);
|
|
10
|
-
}
|
|
4
|
+
return (name, params = {}) => this.app.renderView(name, params);
|
|
11
5
|
}
|
|
12
6
|
};
|
package/lib/view.js
CHANGED
|
@@ -35,6 +35,7 @@ export const View = Class.extend().include({
|
|
|
35
35
|
if(!this.cache.mappers) this.cache.mappers = {};
|
|
36
36
|
if(!this.cache.mappers[cacheKey]){
|
|
37
37
|
const map = {};
|
|
38
|
+
|
|
38
39
|
namespaces.forEach(namespace => {
|
|
39
40
|
this.names.forEach(name => {
|
|
40
41
|
const pattern = new RegExp(`^${namespace}/(.*)$`);
|
|
@@ -43,6 +44,11 @@ export const View = Class.extend().include({
|
|
|
43
44
|
map[matches[1]] = name;
|
|
44
45
|
})
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
Object.keys(map).forEach(name => {
|
|
49
|
+
const matches = name.match(/^(.*)\/index$/);
|
|
50
|
+
if(matches && !map[matches[1]]) map[matches[1]] = map[name];
|
|
51
|
+
});
|
|
46
52
|
|
|
47
53
|
this.cache.mappers[cacheKey] = Class.extend().include({
|
|
48
54
|
isView(name){
|
|
@@ -91,35 +91,25 @@ export const styles = `
|
|
|
91
91
|
font-size: 12px;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
to {transform: rotate(360deg);}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.spinner {
|
|
99
|
-
position: relative;
|
|
94
|
+
.proof-of-work-progress {
|
|
100
95
|
display: inline-block;
|
|
101
|
-
|
|
102
|
-
height: 1em;
|
|
103
|
-
margin-right: 1em;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.spinner:before {
|
|
107
|
-
content: '';
|
|
108
|
-
box-sizing: border-box;
|
|
109
|
-
position: absolute;
|
|
110
|
-
top: 50%;
|
|
111
|
-
left: 50%;
|
|
112
|
-
width: 1em;
|
|
113
|
-
height: 1em;
|
|
114
|
-
margin-top: -0.5em;
|
|
115
|
-
margin-left: -0.5em;
|
|
116
|
-
border-radius: 50%;
|
|
117
|
-
border: 2px solid #ccc;
|
|
118
|
-
border-top-color: #000;
|
|
119
|
-
animation: form-spinner .6s linear infinite;
|
|
96
|
+
margin-left: 0.5em;
|
|
120
97
|
}
|
|
121
98
|
`;
|
|
122
99
|
|
|
100
|
+
export const decorators = {
|
|
101
|
+
proofOfWorkProgress(){
|
|
102
|
+
this.frame.on('proofOfWorkProgress', e => {
|
|
103
|
+
const progress = e.detail;
|
|
104
|
+
this.patch({
|
|
105
|
+
...this.attributes,
|
|
106
|
+
value: progress
|
|
107
|
+
});
|
|
108
|
+
this.patch(`${progress} %`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
123
113
|
export default {
|
|
124
114
|
render(){
|
|
125
115
|
const {
|
|
@@ -224,8 +214,7 @@ export default {
|
|
|
224
214
|
${() => {
|
|
225
215
|
if(isPlaceholder && requiresProofOfWork) return this.renderHtml`
|
|
226
216
|
<span class="${this.cssClasses.loadingIndicator}">
|
|
227
|
-
<
|
|
228
|
-
Generating anti-spam code - please be patient...
|
|
217
|
+
Generating anti-spam code <progress class="${this.cssClasses.proofOfWorkProgress}" max="100">0.0 %</progress>
|
|
229
218
|
</span>
|
|
230
219
|
`;
|
|
231
220
|
}}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "pinstripe",
|
|
4
4
|
"description": "A slick web framework for Node.js.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.31.1",
|
|
6
6
|
"author": "Jody Salt",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"exports": {
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"url": "git://github.com/blognami/pinstripe.git",
|
|
54
54
|
"directory": "packages/pinstripe"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "73c659004059facb56e4f96e32d2d443e989d348"
|
|
57
57
|
}
|