@abtnode/blocklet-services 1.16.17-beta-6f0c7674 → 1.16.17-beta-703fb879

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.
@@ -0,0 +1,80 @@
1
+ /* eslint-disable consistent-return */
2
+ /* eslint-disable no-bitwise */
3
+ const fetch = require('node-fetch').default;
4
+
5
+ const U200D = String.fromCharCode(8205); // zero-width joiner
6
+ const UFE0Fg = /\uFE0F/g; // variation selector regex
7
+
8
+ const getIconCode = (char) => toCodePoint(char.indexOf(U200D) < 0 ? char.replace(UFE0Fg, '') : char);
9
+
10
+ const toCodePoint = (unicodeSurrogates) => {
11
+ const r = [];
12
+ let c = 0;
13
+ let p = 0;
14
+ let i = 0;
15
+ while (i < unicodeSurrogates.length) {
16
+ c = unicodeSurrogates.charCodeAt(i++);
17
+ if (p) {
18
+ r.push((65536 + ((p - 55296) << 10) + (c - 56320)).toString(16));
19
+ p = 0;
20
+ } else if (c >= 55296 && c <= 56319) {
21
+ p = c;
22
+ } else {
23
+ r.push(c.toString(16));
24
+ }
25
+ }
26
+ return r.join('-');
27
+ };
28
+
29
+ const apis = {
30
+ twemoji: (code) => `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${code.toLowerCase()}.svg`,
31
+ openmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/openmoji@2.0.0/svg/',
32
+ blobmoji: 'https://cdn.jsdelivr.net/npm/@svgmoji/blob@2.0.0/svg/',
33
+ noto: 'https://cdn.jsdelivr.net/gh/svgmoji/svgmoji/packages/svgmoji__noto/svg/',
34
+ fluent: (code) => `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_color.svg`,
35
+ fluentFlat: (code) =>
36
+ `https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/${code.toLowerCase()}_flat.svg`,
37
+ };
38
+
39
+ // https://github.com/svgmoji/svgmoji
40
+ const loadEmoji = (code, type) => {
41
+ const api = apis[type] ?? apis.twemoji;
42
+ if (typeof api === 'function') {
43
+ return fetch(api(code));
44
+ }
45
+ return fetch(`${api}${code.toUpperCase()}.svg`);
46
+ };
47
+
48
+ const cache = new Map();
49
+ const loadDynamicAsset = (emojiType = 'twemoji') => {
50
+ const fn = async (languageCode, text) => {
51
+ if (languageCode === 'emoji') {
52
+ const code = getIconCode(text);
53
+ try {
54
+ const emoji = await loadEmoji(code, emojiType);
55
+ return `data:image/svg+xml;base64,${btoa(await emoji.text())}`;
56
+ } catch (err) {
57
+ console.error(`Failed to fetch emoji: ${text}:${code}`, err);
58
+ }
59
+ }
60
+ };
61
+
62
+ return async (...args) => {
63
+ const key = JSON.stringify({ ...args, emojiType });
64
+ const cached = cache.get(key);
65
+ if (cached) {
66
+ return cached;
67
+ }
68
+
69
+ const font = await fn(...args);
70
+ cache.set(key, font);
71
+ return font;
72
+ };
73
+ };
74
+
75
+ module.exports = {
76
+ apis,
77
+ getIconCode,
78
+ loadEmoji,
79
+ loadDynamicAsset,
80
+ };
@@ -15,6 +15,7 @@ const logger = require('@abtnode/logger')('@abtnode/blocklet-services/og');
15
15
 
16
16
  const { getTemplate, getLogoSvg } = require('./template');
17
17
  const { getCacheFilePath } = require('../image');
18
+ const emoji = require('./emoji');
18
19
 
19
20
  const TEMPLATES = ['default', 'section', 'cover'];
20
21
 
@@ -64,6 +65,12 @@ const schema = Joi.object({
64
65
  logo: Joi.string()
65
66
  .uri({ scheme: ['https'] })
66
67
  .optional(),
68
+
69
+ // custom emoji
70
+ emoji: Joi.string()
71
+ .valid(...Object.keys(emoji.apis))
72
+ .optional()
73
+ .default('twemoji'),
67
74
  }).options({ stripUnknown: true, allowUnknown: true, noDefaults: false });
68
75
 
69
76
  const generateTasks = {};
@@ -133,9 +140,14 @@ const convertExternalImage = (url, dest) => {
133
140
  headers: { Accept: 'image/*' },
134
141
  },
135
142
  (res) => {
143
+ if (res.statusCode && res.statusCode >= 400) {
144
+ reject(new Error(`unexpected external image status: ${res.statusCode}`));
145
+ return;
146
+ }
147
+
136
148
  const [type] = (res.headers['content-type'] || '').split('/');
137
149
  if (type !== 'image') {
138
- reject(new Error('external image is not an image'));
150
+ reject(new Error(`unexpected external image format: ${type}`));
139
151
  return;
140
152
  }
141
153
 
@@ -226,6 +238,7 @@ const generateOgImage = async (params, tmpDir) => {
226
238
  style: 'normal',
227
239
  },
228
240
  ],
241
+ loadAdditionalAsset: emoji.loadDynamicAsset(params.emoji),
229
242
  });
230
243
 
231
244
  if (params.format === 'svg') {
@@ -88,6 +88,8 @@ const init = ({ node, options }) => {
88
88
  * {boolean} res.blocked
89
89
  * {boolean} res.authenticated
90
90
  * {boolean} res.authorized
91
+ * {boolean} res.ignored
92
+ * {string} res.payable
91
93
  */
92
94
  const checkAuth = async ({ req } = {}) => {
93
95
  const config = (await req.getServiceConfig(NODE_SERVICES.AUTH)) || {};
@@ -100,32 +102,39 @@ const init = ({ node, options }) => {
100
102
 
101
103
  const shouldIgnore = shouldIgnoreUrl(req.url, ignoreUrls);
102
104
  if (shouldIgnore) {
103
- return {};
105
+ return { ignored: true };
104
106
  }
105
107
 
108
+ const teamDid = req.getBlockletDid();
109
+
106
110
  if (!config.whoCanAccess || config.whoCanAccess === WHO_CAN_ACCESS.ALL) {
107
111
  if (config.blockUnauthenticated && !req.user) {
108
112
  return { blocked: true, authenticated: false };
109
113
  }
110
114
  } else if (!req.user) {
111
- return { blocked: true, authenticated: false };
115
+ const rbac = await node.getRBAC(teamDid);
116
+ const allRoles = await rbac.getRoles(teamDid);
117
+ const payableRole = allRoles.find((x) => x.extra?.acquire?.pay);
118
+ return { blocked: true, authenticated: false, payable: payableRole?.extra?.acquire?.pay };
112
119
  } else if (config.whoCanAccess === WHO_CAN_ACCESS.OWNER && req.user.role !== ROLES.OWNER) {
113
120
  return { blocked: true, authenticated: true, authorized: false };
114
121
  } else if (config.whoCanAccess.startsWith(WHO_CAN_ACCESS_PREFIX_ROLES)) {
115
122
  const roles = getRolesFromAuthConfig(config);
116
123
  if (!roles.includes(req.user.role)) {
117
- return { blocked: true, authenticated: true, authorized: false };
124
+ const rbac = await node.getRBAC(teamDid);
125
+ const allRoles = await rbac.getRoles(teamDid);
126
+ const payableRole = allRoles.find((x) => roles.includes(x.name) && x.extra?.acquire?.pay);
127
+ return { blocked: true, authenticated: true, authorized: false, payable: payableRole?.extra?.acquire?.pay };
118
128
  }
119
129
  }
120
130
 
121
131
  if (!config.blockUnauthorized) {
122
- return {};
132
+ return { ignored: true };
123
133
  }
124
134
 
125
135
  const rule = await req.getRoutingRule();
126
136
  if (rule.to && rule.to.interfaceName) {
127
137
  const permissionName = genPermissionName(rule.to.interfaceName);
128
- const teamDid = req.getBlockletDid();
129
138
  const rbac = await node.getRBAC(teamDid);
130
139
 
131
140
  if (await rbac.can(req.user.role, ...permissionName.split('_'))) {
@@ -245,12 +254,12 @@ const init = ({ node, options }) => {
245
254
  };
246
255
 
247
256
  middlewares.checkAuth = async (req, res, next) => {
248
- const { blocked, authenticated, authorized } = await checkAuth({ req });
257
+ const { blocked, authenticated, authorized, payable } = await checkAuth({ req });
249
258
 
250
259
  if (blocked) {
251
260
  if (!authenticated) {
252
261
  if (req.accepts(['html', 'json']) === 'html') {
253
- res.redirect(getRedirectUrl({ req, pagePath: '/login' }));
262
+ res.redirect(getRedirectUrl({ req, pagePath: '/login', params: { payable } }));
254
263
  } else {
255
264
  // Security principles: user should not known the reason
256
265
  res.status(404).json({ code: 404, error: REASON_404 });
@@ -260,7 +269,7 @@ const init = ({ node, options }) => {
260
269
 
261
270
  if (!authorized) {
262
271
  if (req.accepts(['html', 'json']) === 'html') {
263
- res.redirect(getRedirectUrl({ req, pagePath: '/login' }));
272
+ res.redirect(getRedirectUrl({ req, pagePath: '/login', params: { authenticated: 1, payable } }));
264
273
  } else {
265
274
  // Security principles: user should not known the reason
266
275
  res.status(404).json({ code: 404, error: REASON_404 });
@@ -28,6 +28,11 @@ const createImageService = ({ node }) => {
28
28
  status: res.statusCode,
29
29
  });
30
30
 
31
+ if (res.statusCode && res.statusCode >= 400) {
32
+ reject(new Error(`unexpected upstream response status: ${res.statusCode}`));
33
+ return;
34
+ }
35
+
31
36
  const [type, extension] = (res.headers['content-type'] || '').split('/');
32
37
  if (type !== 'image') {
33
38
  reject(new Error(`unexpected response type from upstream: ${type}, expected image`));
package/api/util/index.js CHANGED
@@ -81,7 +81,7 @@ const ensureProxyUrl = async (req) => {
81
81
  return { target };
82
82
  };
83
83
 
84
- const getRedirectUrl = ({ req, pagePath }) => {
84
+ const getRedirectUrl = ({ req, pagePath, params = {} }) => {
85
85
  const groupPrefix = getGroupPrefix(req);
86
86
  const componentPrefix = getComponentPrefix(req);
87
87
  const redirectPrefix = groupPrefix === componentPrefix ? groupPrefix : componentPrefix;
@@ -136,6 +136,13 @@ const getRedirectUrl = ({ req, pagePath }) => {
136
136
  url.searchParams.set('chainHost', chainHost);
137
137
  }
138
138
 
139
+ Object.keys(params).forEach((key) => {
140
+ if (params[key] === undefined) {
141
+ return;
142
+ }
143
+ url.searchParams.set(key, params[key]);
144
+ });
145
+
139
146
  return `${url.pathname}${url.search}`;
140
147
  };
141
148
 
@@ -1,8 +1,8 @@
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.9c5b4fb1.js",
5
- "static/js/4716.4a231f60.chunk.js": "/.well-known/service/static/static/js/4716.4a231f60.chunk.js",
4
+ "main.js": "/.well-known/service/static/static/js/main.3cf7fedb.js",
5
+ "static/js/4716.1ecc2bb6.chunk.js": "/.well-known/service/static/static/js/4716.1ecc2bb6.chunk.js",
6
6
  "static/js/4076.e73cb63a.chunk.js": "/.well-known/service/static/static/js/4076.e73cb63a.chunk.js",
7
7
  "static/js/6658.0a27e04e.chunk.js": "/.well-known/service/static/static/js/6658.0a27e04e.chunk.js",
8
8
  "static/js/3025.e2b1dac4.chunk.js": "/.well-known/service/static/static/js/3025.e2b1dac4.chunk.js",
@@ -107,8 +107,8 @@
107
107
  "index.html": "/.well-known/service/static/index.html",
108
108
  "static/media/space-connected.svg": "/.well-known/service/static/static/media/space-connected.9a4e18fd2bc7d065191b0d241a131c28.svg",
109
109
  "main.7ea79dc8.css.map": "/.well-known/service/static/static/css/main.7ea79dc8.css.map",
110
- "main.9c5b4fb1.js.map": "/.well-known/service/static/static/js/main.9c5b4fb1.js.map",
111
- "4716.4a231f60.chunk.js.map": "/.well-known/service/static/static/js/4716.4a231f60.chunk.js.map",
110
+ "main.3cf7fedb.js.map": "/.well-known/service/static/static/js/main.3cf7fedb.js.map",
111
+ "4716.1ecc2bb6.chunk.js.map": "/.well-known/service/static/static/js/4716.1ecc2bb6.chunk.js.map",
112
112
  "4076.e73cb63a.chunk.js.map": "/.well-known/service/static/static/js/4076.e73cb63a.chunk.js.map",
113
113
  "6658.0a27e04e.chunk.js.map": "/.well-known/service/static/static/js/6658.0a27e04e.chunk.js.map",
114
114
  "3025.e2b1dac4.chunk.js.map": "/.well-known/service/static/static/js/3025.e2b1dac4.chunk.js.map",
@@ -192,6 +192,6 @@
192
192
  },
193
193
  "entrypoints": [
194
194
  "static/css/main.7ea79dc8.css",
195
- "static/js/main.9c5b4fb1.js"
195
+ "static/js/main.3cf7fedb.js"
196
196
  ]
197
197
  }
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.9c5b4fb1.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.3cf7fedb.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>