@abtnode/blocklet-services 1.16.15-beta-d464647a → 1.16.15-beta-a635f48d

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/api/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const http = require('http');
2
+ const path = require('path');
2
3
  const get = require('lodash/get');
3
4
  const morgan = require('morgan');
4
5
  const express = require('express');
@@ -243,7 +244,16 @@ module.exports = function createServer(node, serverOptions = {}) {
243
244
  }
244
245
 
245
246
  // Static assets
246
- StaticService.attachStaticResources({ app: server, proxy });
247
+ StaticService.attachStaticResources({
248
+ app: server,
249
+ proxy,
250
+ extraResources: [
251
+ {
252
+ prefix: '/open-graph',
253
+ dir: path.join(node.dataDirs.tmp, 'services', 'open-graph'),
254
+ },
255
+ ],
256
+ });
247
257
 
248
258
  // Studio service
249
259
  StudioService.init({ app: server });
package/api/libs/image.js CHANGED
@@ -98,8 +98,7 @@ const schema = Joi.object({
98
98
  .options({ stripUnknown: true, allowUnknown: true, noDefaults: false });
99
99
 
100
100
  const isImageAccepted = (req) => {
101
- const extension = toLower(path.extname(req.path).slice(1));
102
- return FORMATS.includes(EXTENSIONS[extension]) && FORMATS.some((x) => req.accepts(`image/${x}`));
101
+ return FORMATS.some((x) => req.accepts(`image/${x}`));
103
102
  };
104
103
 
105
104
  const isImageRequest = (req) => {
@@ -134,7 +133,7 @@ const processAndRespond = (req, res, cacheDir, getSrc, ext, sendOptions = { maxA
134
133
  if (params.f === 'webp') {
135
134
  params.f = undefined;
136
135
  }
137
- if (extension === 'webp' && !params.f) {
136
+ if ((!extension || extension === 'webp') && !params.f) {
138
137
  params.f = 'png';
139
138
  }
140
139
  }
@@ -245,21 +244,21 @@ const processImage = (src, extension, dest, params) => {
245
244
  resolve(dest);
246
245
  });
247
246
 
248
- out.on('error', (error) => {
249
- reject(error);
247
+ out.on('error', (err) => {
248
+ reject(err);
250
249
  });
251
250
 
252
251
  pipeline[format || EXTENSIONS[extension]]({ quality, progressive: !!progressive, force: true });
253
252
 
254
- pipeline.on('error', (error) => {
255
- reject(error);
253
+ pipeline.on('error', (err) => {
254
+ reject(err);
256
255
  });
257
256
 
258
257
  // run the pipeline
259
258
  src.pipe(pipeline).pipe(out);
260
- }).catch((error) => {
259
+ }).catch((err) => {
261
260
  rmSync(dest, { force: true });
262
- throw error;
261
+ throw err;
263
262
  });
264
263
  };
265
264
 
@@ -1,6 +1,7 @@
1
1
  // A simple open graph service based on satori and sharp
2
2
  const fs = require('fs-extra');
3
3
  const path = require('path');
4
+ const https = require('https');
4
5
  const sharp = require('sharp');
5
6
  const joinUrl = require('url-join');
6
7
  const { Joi } = require('@arcblock/validator');
@@ -25,6 +26,7 @@ const schema = Joi.object({
25
26
 
26
27
  title: Joi.string()
27
28
  .max(128)
29
+ .trim()
28
30
  .when('template', {
29
31
  is: 'default',
30
32
  then: Joi.optional().default(''),
@@ -33,6 +35,7 @@ const schema = Joi.object({
33
35
 
34
36
  description: Joi.string()
35
37
  .max(512)
38
+ .trim()
36
39
  .when('template', {
37
40
  is: 'section',
38
41
  then: Joi.required(),
@@ -47,6 +50,7 @@ const schema = Joi.object({
47
50
  otherwise: Joi.optional().default(''),
48
51
  }),
49
52
 
53
+ // customize cover
50
54
  cover: Joi.string()
51
55
  .uri({ scheme: ['https'] })
52
56
  .when('template', {
@@ -54,23 +58,28 @@ const schema = Joi.object({
54
58
  then: Joi.required(),
55
59
  otherwise: Joi.optional().default(''),
56
60
  }),
61
+
62
+ // customize logo
63
+ logo: Joi.string()
64
+ .uri({ scheme: ['https'] })
65
+ .optional(),
57
66
  }).options({ stripUnknown: true, allowUnknown: true, noDefaults: false });
58
67
 
59
- const tasks = {};
60
- const getOgImage = (input, info, dataDir, format) => {
61
- if (fs.existsSync(dataDir) === false) {
62
- fs.mkdirSync(dataDir, { recursive: true });
63
- }
68
+ const generateTasks = {};
69
+ const getOgImage = ({ input, info, dataDir, format, tmpDir }) => {
70
+ fs.ensureDirSync(dataDir);
71
+ fs.ensureDirSync(tmpDir);
64
72
 
65
73
  const color = getPassportColor(info.passportColor, info.did);
66
- const logoUrl = joinUrl(info.appUrl, WELLKNOWN_SERVICE_PATH_PREFIX, '/blocklet/logo');
67
74
  const { value, error } = schema.validate(input);
68
75
  if (error) {
69
76
  throw new Error(`open graph service params invalid: ${formatError(error)}`);
70
77
  }
71
78
  if (value.template === 'cover') {
72
- value.description = value.description || value.title;
73
- value.title = info.name;
79
+ if (value.title && !value.description) {
80
+ value.description = value.title;
81
+ value.title = info.name;
82
+ }
74
83
  }
75
84
  if (!value.title) {
76
85
  value.title = info.name;
@@ -78,8 +87,11 @@ const getOgImage = (input, info, dataDir, format) => {
78
87
  if (!value.description) {
79
88
  value.description = info.description;
80
89
  }
90
+ if (!value.logo) {
91
+ value.logo = joinUrl(info.appUrl, WELLKNOWN_SERVICE_PATH_PREFIX, '/blocklet/logo?imageFilter=resize&w=120');
92
+ }
81
93
 
82
- const params = { ...value, width: 1200, height: 630, color, logoUrl, format };
94
+ const params = { ...value, width: 1200, height: 630, color, format };
83
95
  const cacheKey = md5(stringify(params));
84
96
  const nocache = input.nocache === '1';
85
97
  const destPath = path.join(dataDir, `${cacheKey}.${format}`);
@@ -87,14 +99,14 @@ const getOgImage = (input, info, dataDir, format) => {
87
99
  return destPath;
88
100
  }
89
101
 
90
- tasks[cacheKey] ??= generateOgImage(params).finally(() => {
102
+ generateTasks[cacheKey] ??= generateOgImage(params, tmpDir).finally(() => {
91
103
  setTimeout(() => {
92
- delete tasks[cacheKey];
104
+ delete generateTasks[cacheKey];
93
105
  }, 1000);
94
106
  });
95
107
 
96
108
  return new Promise((resolve, reject) => {
97
- tasks[cacheKey]
109
+ generateTasks[cacheKey]
98
110
  .then((buffer) => {
99
111
  logger.info('open graph generate succeed', { params, destPath });
100
112
  fs.writeFileSync(destPath, buffer);
@@ -107,9 +119,95 @@ const getOgImage = (input, info, dataDir, format) => {
107
119
  });
108
120
  };
109
121
 
122
+ const convertExternalImage = (url, dest) => {
123
+ return new Promise((resolve, reject) => {
124
+ const tmp = new URL(url);
125
+ https
126
+ .request(
127
+ {
128
+ hostname: tmp.hostname,
129
+ port: tmp.port,
130
+ path: tmp.pathname + tmp.search,
131
+ method: 'GET',
132
+ headers: { Accept: 'image/*' },
133
+ },
134
+ (res) => {
135
+ const [type] = (res.headers['content-type'] || '').split('/');
136
+ if (type !== 'image') {
137
+ reject(new Error('external image is not an image'));
138
+ return;
139
+ }
140
+
141
+ const pipeline = sharp({ animated: false }).timeout({ seconds: 30 });
142
+ pipeline.png({ force: true });
143
+
144
+ const out = fs.createWriteStream(dest);
145
+ out.on('close', () => {
146
+ resolve(dest);
147
+ });
148
+
149
+ out.on('error', (error) => {
150
+ reject(error);
151
+ });
152
+
153
+ res.pipe(pipeline).pipe(out);
154
+ }
155
+ )
156
+ .on('error', reject)
157
+ .end();
158
+ });
159
+ };
160
+
161
+ const getCachedExternalImage = (cacheKey, port = process.env.ABT_NODE_SERVICE_PORT) => {
162
+ return `http://127.0.0.1:${port}/open-graph/${cacheKey}.png`;
163
+ };
164
+
165
+ const cacheTasks = {};
166
+ const ensureExternalImage = (params, key, tmpDir) => {
167
+ if (params[key]) {
168
+ const cacheKey = md5(params[key]);
169
+ const destPath = path.join(tmpDir, `${cacheKey}.png`);
170
+ if (fs.existsSync(destPath)) {
171
+ return getCachedExternalImage(cacheKey);
172
+ }
173
+
174
+ cacheTasks[cacheKey] ??= convertExternalImage(params[key], destPath).finally(() => {
175
+ setTimeout(() => {
176
+ delete cacheTasks[cacheKey];
177
+ }, 1000);
178
+ });
179
+
180
+ return new Promise((resolve, reject) => {
181
+ cacheTasks[cacheKey]
182
+ .then(() => {
183
+ logger.info('external image convert succeed', { params, destPath });
184
+ resolve(getCachedExternalImage(cacheKey));
185
+ })
186
+ .catch((err) => {
187
+ logger.error('external image convert failed', { error: err, params });
188
+ reject(err);
189
+ });
190
+ });
191
+ }
192
+
193
+ return params[key];
194
+ };
195
+
196
+ // ensure external images are processed
197
+ const ensureExternalImages = async (params, tmpDir) => {
198
+ await Promise.all(
199
+ ['cover', 'logo'].map(async (x) => {
200
+ params[x] = await ensureExternalImage(params, x, tmpDir);
201
+ })
202
+ );
203
+ };
204
+
110
205
  const fallbackFont = fs.readFile(path.resolve(__dirname, '../../fonts/noto-sans-sc-regular.otf'));
111
- const generateOgImage = async (params) => {
206
+ const generateOgImage = async (params, tmpDir) => {
112
207
  const { default: satori } = await import('satori');
208
+
209
+ await ensureExternalImages(params, tmpDir);
210
+
113
211
  const raw = await getTemplate(params);
114
212
  if (params.format === 'html') {
115
213
  return raw;
@@ -32,7 +32,7 @@ const getLogoSvg = (color) => {
32
32
  </svg>`;
33
33
  };
34
34
 
35
- const getDefaultTemplate = ({ width, height, logoUrl, format, color, title, description }, fn) => {
35
+ const getDefaultTemplate = ({ width, height, logo, format, color, title, description }, fn) => {
36
36
  return fn`<div
37
37
  style="
38
38
  width: ${width}px;
@@ -46,7 +46,7 @@ const getDefaultTemplate = ({ width, height, logoUrl, format, color, title, desc
46
46
  "
47
47
  >
48
48
  <img
49
- src="${logoUrl}"
49
+ src="${logo}"
50
50
  height="120"
51
51
  width="120"
52
52
  style="margin-left: 96px; height: 120px; width: 120px; margin-bottom: 64px"
@@ -82,7 +82,7 @@ const getDefaultTemplate = ({ width, height, logoUrl, format, color, title, desc
82
82
  </div>`;
83
83
  };
84
84
 
85
- const getSectionTemplate = ({ width, height, logoUrl, color, format, title, description, section }, fn) => {
85
+ const getSectionTemplate = ({ width, height, logo, color, format, title, description, section }, fn) => {
86
86
  return fn` <div
87
87
  style="
88
88
  width: ${width};
@@ -96,7 +96,7 @@ const getSectionTemplate = ({ width, height, logoUrl, color, format, title, desc
96
96
  "
97
97
  >
98
98
  <img
99
- src="${logoUrl}"
99
+ src="${logo}"
100
100
  height="90"
101
101
  width="90"
102
102
  style="margin-left: 96px; height: 90px; width: 90px; margin-bottom: 64px"
@@ -148,7 +148,7 @@ const getSectionTemplate = ({ width, height, logoUrl, color, format, title, desc
148
148
  </div>`;
149
149
  };
150
150
 
151
- const getCoverTemplate = ({ width, height, logoUrl, color, title, description, cover }, fn) => {
151
+ const getCoverTemplate = ({ width, height, logo, color, title, description, cover }, fn) => {
152
152
  const textColor = getTextColor(color.start);
153
153
  return fn`<div style="width: ${width}px; height: ${height}px; display: flex; background: ${color.start};">
154
154
  <div style="display: flex; height: ${height}px; background: ${color.start}; width: 45%">
@@ -166,7 +166,7 @@ const getCoverTemplate = ({ width, height, logoUrl, color, title, description, c
166
166
  ${description}
167
167
  </h2>
168
168
  <div style="display: flex; justify-content: flex-start; align-items: center">
169
- <img src="${logoUrl}" height="60" width="60" style="height: 60px; width: 60px" />
169
+ <img src="${logo}" height="60" width="60" style="height: 60px; width: 60px" />
170
170
  <h3
171
171
  style="
172
172
  font-size: 2rem;
@@ -433,6 +433,18 @@ module.exports = {
433
433
  );
434
434
 
435
435
  stream.pipe(res);
436
+
437
+ node
438
+ .createAuditLog(
439
+ {
440
+ action: 'downloadLog',
441
+ args: { teamDid: did, days },
442
+ context: formatContext(req),
443
+ result: {},
444
+ },
445
+ node
446
+ )
447
+ .catch(console.error);
436
448
  } catch (error) {
437
449
  res.status(500).send(error.message);
438
450
  }
@@ -491,6 +503,7 @@ module.exports = {
491
503
  });
492
504
  });
493
505
 
506
+ const tmpDir = path.join(node.dataDirs.tmp, 'services', 'open-graph');
494
507
  server.get(`${prefix}/blocklet/og.(png|html)`, async (req, res) => {
495
508
  try {
496
509
  const format = req.path.split('.').pop();
@@ -499,7 +512,7 @@ module.exports = {
499
512
  const cache = req.query.nocache !== '1';
500
513
 
501
514
  const dataDir = path.join(blocklet.env.dataDir, OPEN_GRAPH_DIR);
502
- const sourceFile = await getOgImage(req.query, info, dataDir, format);
515
+ const sourceFile = await getOgImage({ input: req.query, info, dataDir, format, tmpDir });
503
516
  if (format === 'png' && isImageAccepted(req) && isImageRequest(req)) {
504
517
  const appDir = path.join(cacheDir, blocklet.appPid);
505
518
  processAndRespond(req, res, appDir, () =>
@@ -1,3 +1,4 @@
1
+ const fs = require('fs-extra');
1
2
  const path = require('path');
2
3
  const serveStatic = require('serve-static');
3
4
 
@@ -31,7 +32,12 @@ const attachUtils = ({ req, res, proxy }) => {
31
32
  };
32
33
  };
33
34
 
34
- const attachStaticResources = ({ app, proxy }) => {
35
+ const attachStaticResources = ({ app, proxy, extraResources = [] }) => {
36
+ extraResources.forEach((x) => {
37
+ fs.ensureDirSync(x.dir);
38
+ app.use(x.prefix, serveStatic(x.dir, { index: false, maxAge: 0 }));
39
+ });
40
+
35
41
  if (isFeDevelopment) {
36
42
  app.use('/static', (req, res) => {
37
43
  proxy.safeWeb(req, res, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "files": {
3
3
  "main.css": "/.well-known/service/static/static/css/main.7ea79dc8.css",
4
- "main.js": "/.well-known/service/static/static/js/main.ad0241d5.js",
4
+ "main.js": "/.well-known/service/static/static/js/main.d5146fdd.js",
5
5
  "static/js/4716.60c7a9b1.chunk.js": "/.well-known/service/static/static/js/4716.60c7a9b1.chunk.js",
6
6
  "static/js/6856.01a0c9a2.chunk.js": "/.well-known/service/static/static/js/6856.01a0c9a2.chunk.js",
7
7
  "static/js/1660.c490fcee.chunk.js": "/.well-known/service/static/static/js/1660.c490fcee.chunk.js",
@@ -28,12 +28,12 @@
28
28
  "static/js/3593.98d9341a.chunk.js": "/.well-known/service/static/static/js/3593.98d9341a.chunk.js",
29
29
  "static/css/5355.c4fb5c4a.chunk.css": "/.well-known/service/static/static/css/5355.c4fb5c4a.chunk.css",
30
30
  "static/js/5355.cc6d4f2b.chunk.js": "/.well-known/service/static/static/js/5355.cc6d4f2b.chunk.js",
31
- "static/js/6771.6efce01a.chunk.js": "/.well-known/service/static/static/js/6771.6efce01a.chunk.js",
31
+ "static/js/6771.dfad334c.chunk.js": "/.well-known/service/static/static/js/6771.dfad334c.chunk.js",
32
32
  "static/css/5982.ac464505.chunk.css": "/.well-known/service/static/static/css/5982.ac464505.chunk.css",
33
- "static/js/5982.19c5663f.chunk.js": "/.well-known/service/static/static/js/5982.19c5663f.chunk.js",
33
+ "static/js/5982.e80abbc4.chunk.js": "/.well-known/service/static/static/js/5982.e80abbc4.chunk.js",
34
34
  "static/js/4023.cb71646d.chunk.js": "/.well-known/service/static/static/js/4023.cb71646d.chunk.js",
35
35
  "static/js/6452.853cf474.chunk.js": "/.well-known/service/static/static/js/6452.853cf474.chunk.js",
36
- "static/js/1565.3e18f09a.chunk.js": "/.well-known/service/static/static/js/1565.3e18f09a.chunk.js",
36
+ "static/js/1565.77a767e9.chunk.js": "/.well-known/service/static/static/js/1565.77a767e9.chunk.js",
37
37
  "static/js/2393.9c045444.chunk.js": "/.well-known/service/static/static/js/2393.9c045444.chunk.js",
38
38
  "static/js/5683.7e2e9b58.chunk.js": "/.well-known/service/static/static/js/5683.7e2e9b58.chunk.js",
39
39
  "static/js/2100.1e7f0c60.chunk.js": "/.well-known/service/static/static/js/2100.1e7f0c60.chunk.js",
@@ -97,7 +97,7 @@
97
97
  "index.html": "/.well-known/service/static/index.html",
98
98
  "static/media/space-connected.svg": "/.well-known/service/static/static/media/space-connected.9a4e18fd2bc7d065191b0d241a131c28.svg",
99
99
  "main.7ea79dc8.css.map": "/.well-known/service/static/static/css/main.7ea79dc8.css.map",
100
- "main.ad0241d5.js.map": "/.well-known/service/static/static/js/main.ad0241d5.js.map",
100
+ "main.d5146fdd.js.map": "/.well-known/service/static/static/js/main.d5146fdd.js.map",
101
101
  "4716.60c7a9b1.chunk.js.map": "/.well-known/service/static/static/js/4716.60c7a9b1.chunk.js.map",
102
102
  "6856.01a0c9a2.chunk.js.map": "/.well-known/service/static/static/js/6856.01a0c9a2.chunk.js.map",
103
103
  "1660.c490fcee.chunk.js.map": "/.well-known/service/static/static/js/1660.c490fcee.chunk.js.map",
@@ -124,12 +124,12 @@
124
124
  "3593.98d9341a.chunk.js.map": "/.well-known/service/static/static/js/3593.98d9341a.chunk.js.map",
125
125
  "5355.c4fb5c4a.chunk.css.map": "/.well-known/service/static/static/css/5355.c4fb5c4a.chunk.css.map",
126
126
  "5355.cc6d4f2b.chunk.js.map": "/.well-known/service/static/static/js/5355.cc6d4f2b.chunk.js.map",
127
- "6771.6efce01a.chunk.js.map": "/.well-known/service/static/static/js/6771.6efce01a.chunk.js.map",
127
+ "6771.dfad334c.chunk.js.map": "/.well-known/service/static/static/js/6771.dfad334c.chunk.js.map",
128
128
  "5982.ac464505.chunk.css.map": "/.well-known/service/static/static/css/5982.ac464505.chunk.css.map",
129
- "5982.19c5663f.chunk.js.map": "/.well-known/service/static/static/js/5982.19c5663f.chunk.js.map",
129
+ "5982.e80abbc4.chunk.js.map": "/.well-known/service/static/static/js/5982.e80abbc4.chunk.js.map",
130
130
  "4023.cb71646d.chunk.js.map": "/.well-known/service/static/static/js/4023.cb71646d.chunk.js.map",
131
131
  "6452.853cf474.chunk.js.map": "/.well-known/service/static/static/js/6452.853cf474.chunk.js.map",
132
- "1565.3e18f09a.chunk.js.map": "/.well-known/service/static/static/js/1565.3e18f09a.chunk.js.map",
132
+ "1565.77a767e9.chunk.js.map": "/.well-known/service/static/static/js/1565.77a767e9.chunk.js.map",
133
133
  "2393.9c045444.chunk.js.map": "/.well-known/service/static/static/js/2393.9c045444.chunk.js.map",
134
134
  "5683.7e2e9b58.chunk.js.map": "/.well-known/service/static/static/js/5683.7e2e9b58.chunk.js.map",
135
135
  "2100.1e7f0c60.chunk.js.map": "/.well-known/service/static/static/js/2100.1e7f0c60.chunk.js.map",
@@ -172,6 +172,6 @@
172
172
  },
173
173
  "entrypoints": [
174
174
  "static/css/main.7ea79dc8.css",
175
- "static/js/main.ad0241d5.js"
175
+ "static/js/main.d5146fdd.js"
176
176
  ]
177
177
  }
package/build/index.html CHANGED
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico?imageFilter=resize&w=32"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/><meta name="theme-color" content="#000000"/><title>Blocklet Service</title><link rel="manifest" href="/.well-known/service/manifest.json"/><script src="/.well-known/service/api/env"></script><script src="/__blocklet__.js"></script><script defer="defer" src="/.well-known/service/static/static/js/main.ad0241d5.js"></script><link href="/.well-known/service/static/static/css/main.7ea79dc8.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico?imageFilter=resize&w=32"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/><meta name="theme-color" content="#000000"/><title>Blocklet Service</title><link rel="manifest" href="/.well-known/service/manifest.json"/><script src="/.well-known/service/api/env"></script><script src="/__blocklet__.js"></script><script defer="defer" src="/.well-known/service/static/static/js/main.d5146fdd.js"></script><link href="/.well-known/service/static/static/css/main.7ea79dc8.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>