pinstripe 0.30.0 → 0.31.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.
@@ -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)} (composed of ${JSON.stringify(App.create(appName, this.context).compose())} views)`);
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
- if(app){
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
- View.names.forEach(viewName => {
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, IS_SERVER } from './constants.js';
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, abortController.signal))
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,93 @@
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
+
9
+ export const ProofOfWork = Class.extend().include({
10
+ meta(){
11
+ this.include(Singleton)
12
+ },
13
+
14
+ async generateProofOfWork(input, options = {}){
15
+ const { difficulty = 1, steps = 1000, onProgress = () => {}, abortSignal = { aborted: false } } = options;
16
+ const inputHash = await this.createSha1Hash(JSON.stringify(input));
17
+ const salt = await this.createSha1Hash(Math.random());
18
+ const timestamp = this.getUTCTimestamp();
19
+ const target = this.calculateTarget(difficulty, steps);
20
+ const solution = [];
21
+
22
+ let counter = -1;
23
+ for(let i = 1; i <= steps && !abortSignal.aborted; i++){
24
+ while(!abortSignal.aborted){
25
+ counter++;
26
+ const hash = await this.createSha1Hash(`${difficulty}:${steps}:${inputHash}:${salt}:${timestamp}:${counter}`);
27
+ const integers = this.hexToIntegers(hash);
28
+ if(integers[0] <= target) break;
29
+ }
30
+ solution.push(counter);
31
+ const progressPercentage = Math.floor(i * (100 / steps) * 100) / 100;
32
+ onProgress(progressPercentage);
33
+ }
34
+
35
+ if(abortSignal.aborted) return;
36
+
37
+ return JSON.stringify({
38
+ salt,
39
+ timestamp,
40
+ solution,
41
+ });
42
+ },
43
+
44
+ async verifyProofOfWork(input, proofOfWork, options = {}){
45
+ const { difficulty = 1 } = options;
46
+ const { salt, timestamp, solution } = JSON.parse(proofOfWork);
47
+ const steps = solution.length;
48
+ const inputHash = await this.createSha1Hash(JSON.stringify(input));
49
+ const target = this.calculateTarget(difficulty, steps);
50
+ for(let i = 0; i < steps; i++){
51
+ const counter = solution[i];
52
+ const hash = await this.createSha1Hash(`${difficulty}:${steps}:${inputHash}:${salt}:${timestamp}:${counter}`.toString(), true);
53
+ const integers = this.hexToIntegers(hash);
54
+ if(integers[0] > target) return false;
55
+ }
56
+ return true;
57
+ },
58
+
59
+ async createSha1Hash(input) {
60
+ const buffer = new TextEncoder().encode(input);
61
+ const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-1', buffer)));
62
+ return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
63
+ },
64
+
65
+ hexToIntegers(hex){
66
+ const out = [];
67
+ for(let i = 0; i < hex.length; i += 8){
68
+ out.push(parseInt(hex.slice(i, i + 8), 16));
69
+ }
70
+ return out;
71
+ },
72
+
73
+ calculateTarget(difficulty, steps){
74
+ return (HASH_CASH_TARGET / difficulty) * steps;
75
+ },
76
+
77
+ getUTCTimestamp() {
78
+ const now = new Date();
79
+
80
+ const year = now.getUTCFullYear() % 100;
81
+ const month = String(now.getUTCMonth() + 1).padStart(2, '0');
82
+ const day = String(now.getUTCDate()).padStart(2, '0');
83
+ const hours = String(now.getUTCHours()).padStart(2, '0');
84
+ const minutes = String(now.getUTCMinutes()).padStart(2, '0');
85
+ const seconds = String(now.getUTCSeconds()).padStart(2, '0');
86
+
87
+ return `${year}${month}${day}${hours}${minutes}${seconds}`;
88
+ }
89
+ });
90
+
91
+ export const generateProofOfWork = (...args) => ProofOfWork.instance.generateProofOfWork(...args);
92
+
93
+ export const verifyProofOfWork = (...args) => ProofOfWork.instance.verifyProofOfWork(...args);
@@ -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 out = this.normalizeResponse(await this.app.renderView(prefixSegments.length ? [...prefixSegments, 'guard'].join('/') : 'guard', params));
58
- if(out){
59
- return out;
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 out = this.normalizeResponse(await this.app.renderView(prefixSegments.length ? [...prefixSegments, 'default'].join('/') : 'default', params));
72
- if(out){
73
- return out;
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
- out._method = 'get';
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
  }
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
2
2
 
3
3
  import { ValidationError } from '../validation_error.js';
4
4
  import { Inflector } from '../inflector.js';
5
+ import { verifyProofOfWork } from '../proof_of_work.js';
5
6
 
6
7
  export default {
7
8
  create(){
@@ -24,7 +25,7 @@ export default {
24
25
  try {
25
26
  if(requiresProofOfWork){
26
27
  if(!this.params._proofOfWork) throw new ValidationError({ _proofOfWork: 'Must not be blank' });
27
- if(!this.verifyProofOfWork(this.params._proofOfWork, values)) throw new ValidationError({ _proofOfWork: 'Must be a valid stamp' });
28
+ if(!verifyProofOfWork(values, this.params._proofOfWork)) throw new ValidationError({ _proofOfWork: 'Must be a valid stamp' });
28
29
  }
29
30
  return await formAdapter.submit(values, success) || this.renderHtml`
30
31
  <span data-component="pinstripe-anchor" data-target="_parent">
@@ -74,12 +75,6 @@ export default {
74
75
  submitTitle,
75
76
  cancelTitle
76
77
  });
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
78
  }
84
79
  }
85
80
 
@@ -1,12 +1,6 @@
1
1
 
2
2
  export default {
3
3
  create(){
4
- return async (name, params = {}) => {
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
- @keyframes form-spinner {
95
- to {transform: rotate(360deg);}
96
- }
97
-
98
- .spinner {
99
- position: relative;
94
+ .proof-of-work-progress {
100
95
  display: inline-block;
101
- width: 1em;
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
- <span class="${this.cssClasses.spinner}"></span>
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.30.0",
5
+ "version": "0.31.0",
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": "07d0e77a033fd16381b2641ad22158dc1857899e"
56
+ "gitHead": "d1a939e12913a99d7b2817987d9942a95c3e6ec5"
57
57
  }