@abtnode/blocklet-services 1.8.69-beta-b0bb2d67 → 1.8.69-beta-650a290b

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/README.md CHANGED
@@ -93,6 +93,7 @@ node.onReady(() => {
93
93
  - /api/did/issue-passport
94
94
  - /api/did/lost-passport-list
95
95
  - /api/did/lost-passport-issue
96
+ - /api/did/pre-setup
96
97
  - /api/did/setup
97
98
  - /api/did/switch-profile
98
99
  - /api/did/switch-passport
package/api/index.js CHANGED
@@ -112,18 +112,30 @@ module.exports = function createServer(node, serverOptions = {}) {
112
112
  });
113
113
 
114
114
  // Cross process events
115
- [BlockletEvents.updated, BlockletEvents.started, BlockletEvents.removed, BlockletEvents.statusChange].forEach(
116
- (name) => {
117
- eventHub.on(name, (data) => {
118
- const did = get(data, 'meta.did');
119
- if (did) {
120
- logger.info('delete blocklet cache on update', { did, pid: process.pid });
121
- cache.del(cache.keyFns.blocklet(did));
122
- cache.del(cache.keyFns.blockletInfo(did));
123
- }
124
- });
125
- }
126
- );
115
+ [
116
+ BlockletEvents.updated,
117
+ BlockletEvents.started,
118
+ BlockletEvents.removed,
119
+ BlockletEvents.statusChange,
120
+ BlockletEvents.installed,
121
+ ].forEach((name) => {
122
+ eventHub.on(name, (data) => {
123
+ const did = get(data, 'meta.did');
124
+ if (did) {
125
+ logger.info('delete blocklet cache on update', { did, pid: process.pid });
126
+ cache.del(cache.keyFns.blocklet(did));
127
+ cache.del(cache.keyFns.blockletInfo(did));
128
+ }
129
+
130
+ // structV1Did is just for migration purpose and should be removed in the future
131
+ const structV1Did = get(data, 'structV1Did');
132
+ if (structV1Did) {
133
+ logger.info('delete blocklet cache on update', { structV1Did, pid: process.pid });
134
+ cache.del(cache.keyFns.blocklet(structV1Did));
135
+ cache.del(cache.keyFns.blockletInfo(structV1Did));
136
+ }
137
+ });
138
+ });
127
139
  eventHub.on(EVENTS.NODE_UPDATED, () => {
128
140
  logger.info('node update', { pid: process.pid });
129
141
  cache.del(cache.keyFns.node());
@@ -281,7 +293,7 @@ module.exports = function createServer(node, serverOptions = {}) {
281
293
  // error handler
282
294
  // eslint-disable-next-line no-unused-vars
283
295
  server.use((err, req, res, next) => {
284
- logger.error('Something broke', { error: err });
296
+ logger.error('Something broke', { url: req.url, error: err });
285
297
  res.status(500).send(`Blocklet Service: Something broke! ${err.message}`);
286
298
  });
287
299
 
@@ -29,6 +29,7 @@ const {
29
29
  createPassport,
30
30
  upsertToPassports,
31
31
  } = require('@abtnode/auth/lib/passport');
32
+ const { getKeyPairClaim } = require('@abtnode/auth/lib/server');
32
33
 
33
34
  const { getRolesFromAuthConfig, getBlockletAppIdList } = require('@blocklet/meta/lib/util');
34
35
 
@@ -506,4 +507,55 @@ module.exports = {
506
507
  return sessionToken;
507
508
  },
508
509
  },
510
+
511
+ migrateToStructV2: {
512
+ getClaims: ({ node }) => ({
513
+ verifiableCredential: async ({ extraParams: { locale }, context: { request } }) => {
514
+ const blocklet = await request.getBlocklet();
515
+ const { wallet } = await request.getBlockletInfo();
516
+
517
+ const trustedPassports = (blocklet.trustedPassports || []).map((x) => x.issuerDid);
518
+ const trustedIssuers = [wallet.address, ...trustedPassports].filter(Boolean);
519
+
520
+ return {
521
+ type: 'verifiableCredential',
522
+ description: messages.requestPassport[locale],
523
+ item: vcTypes,
524
+ trustedIssuers,
525
+ optional: false,
526
+ };
527
+ },
528
+ keyPair: getKeyPairClaim({ node }),
529
+ }),
530
+
531
+ onAuth: async ({ claims, challenge, userDid, extraParams: { locale }, req, node }) => {
532
+ const blocklet = await req.getBlocklet();
533
+ const { wallet, did: teamDid } = await req.getBlockletInfo();
534
+ const appId = wallet.address;
535
+
536
+ // Get passport vc
537
+ const vc = await getPassportVc({ blocklet, appId, claims, challenge, locale });
538
+ if (!vc) {
539
+ throw new Error(messages.missingCredentialClaim[locale]);
540
+ }
541
+
542
+ const role = await getRoleFromVC({ vc, appId, node, locale, blocklet, teamDid });
543
+
544
+ if (role !== ROLES.OWNER) {
545
+ throw new Error(
546
+ {
547
+ zh: '只有应用所有者才能执行此操作',
548
+ en: 'Only the application owner can perform this action',
549
+ }[locale]
550
+ );
551
+ }
552
+
553
+ const keyPair = claims.find((x) => x.type === 'keyPair');
554
+ if (!keyPair) {
555
+ throw new Error(messages.missingKeyPair[locale]);
556
+ }
557
+
558
+ return { blocklet, keyPair, user: { role, did: userDid } };
559
+ },
560
+ },
509
561
  };
@@ -3,16 +3,20 @@ const qs = require('querystring');
3
3
  const LRU = require('lru-cache');
4
4
  const pick = require('lodash/pick');
5
5
 
6
+ const md5 = require('@abtnode/util/lib/md5');
6
7
  const { AUTH_CERT_TYPE } = require('@abtnode/constant');
7
8
 
9
+ const cacheKey = (userDid, appDid) => md5(`${userDid}:${appDid}`);
10
+
8
11
  const proxyToDaemon = ({ proxy, pathname, sessionSecret }) => {
9
12
  const cache = new LRU({
10
- max: 50, // cache at most 50 blocklet
13
+ max: 50,
11
14
  maxAge: 86400 * 1000, // cache for 1 day
12
15
  });
13
16
 
14
17
  const getToken = (did, user) => {
15
- const cacheToken = cache.get(user.did);
18
+ const key = cacheKey(user.did, did);
19
+ const cacheToken = cache.get(key);
16
20
  if (cacheToken) {
17
21
  return cacheToken;
18
22
  }
@@ -28,7 +32,7 @@ const proxyToDaemon = ({ proxy, pathname, sessionSecret }) => {
28
32
  { expiresIn: '1d' }
29
33
  );
30
34
 
31
- cache.set(user.did, token);
35
+ cache.set(key, token);
32
36
 
33
37
  return token;
34
38
  };
@@ -0,0 +1,55 @@
1
+ const jwt = require('jsonwebtoken');
2
+ const { toHex } = require('@ocap/util');
3
+ const logger = require('@abtnode/logger')('blocklet-service:connect:migrate');
4
+
5
+ const Client = require('@abtnode/client');
6
+ const { AUTH_CERT_TYPE } = require('@abtnode/constant');
7
+
8
+ const { migrateToStructV2 } = require('../../../libs/connect/session');
9
+
10
+ const { getClaims, onAuth } = migrateToStructV2;
11
+
12
+ module.exports = function createRoutes(node, options) {
13
+ return {
14
+ action: 'migrate-app-to-struct-v2',
15
+
16
+ claims: getClaims(node),
17
+
18
+ onAuth: async ({ claims, challenge, userDid, extraParams: { locale }, req }) => {
19
+ try {
20
+ const { blocklet, keyPair, user } = await onAuth({
21
+ claims,
22
+ challenge,
23
+ userDid,
24
+ extraParams: { locale },
25
+ req,
26
+ node,
27
+ });
28
+
29
+ const input = {
30
+ did: blocklet.meta.did,
31
+ appSk: toHex(keyPair.secret),
32
+ };
33
+
34
+ const token = jwt.sign(
35
+ {
36
+ type: AUTH_CERT_TYPE.BLOCKLET_USER,
37
+ did: userDid,
38
+ role: user.role,
39
+ blockletDid: blocklet.meta.did,
40
+ },
41
+ options.sessionSecret,
42
+ { expiresIn: '10s' }
43
+ );
44
+
45
+ const client = new Client(`http://127.0.0.1:${process.env.ABT_NODE_PORT}/api/gql`);
46
+ client.setAuthToken(token);
47
+
48
+ await client.migrateApplicationToStructV2({ input });
49
+ } catch (error) {
50
+ logger.error('migrate application failed', { error, userDid });
51
+ throw error;
52
+ }
53
+ },
54
+ };
55
+ };
@@ -0,0 +1,24 @@
1
+ const pick = require('lodash/pick');
2
+ const { getSetupBlockletClaims } = require('@abtnode/auth/lib/server');
3
+ const verifySignature = require('@abtnode/auth/lib/util/verify-signature');
4
+
5
+ const logger = require('@abtnode/logger')(require('../../../../package.json').name);
6
+
7
+ module.exports = function createRoutes() {
8
+ return {
9
+ action: 'pre-setup',
10
+ authPrincipal: false,
11
+ claims: getSetupBlockletClaims(),
12
+ onAuth: async ({ claims, userDid, userPk, extraParams: { locale } }) => {
13
+ const claim = claims.find((x) => x.type === 'signature');
14
+ verifySignature(claim, userDid, userPk, locale);
15
+ logger.info('pre-setup.connect.success', { userDid });
16
+ return {
17
+ nextWorkflowData: {
18
+ claim: pick(claim, ['origin', 'sig']),
19
+ pk: userPk,
20
+ },
21
+ };
22
+ },
23
+ };
24
+ };
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable arrow-parens */
2
2
  const get = require('lodash/get');
3
3
  const { messages } = require('@abtnode/auth/lib/auth');
4
- const formatContext = require('@abtnode/util/lib/format-context');
5
- const { getServerAuthMethod } = require('@abtnode/auth/lib/util/get-auth-method');
6
- const { getSetupBlockletClaims, ensureBlockletPermission } = require('@abtnode/auth/lib/server');
7
4
  const { extractUserAvatar } = require('@abtnode/util/lib/user-avatar');
5
+ const formatContext = require('@abtnode/util/lib/format-context');
6
+ const verifySignature = require('@abtnode/auth/lib/util/verify-signature');
8
7
 
9
8
  const logger = require('@abtnode/logger')(require('../../../../package.json').name);
10
9
 
@@ -31,52 +30,29 @@ const checkOwner = async ({ node, userDid, blocklet }) => {
31
30
  module.exports = function createRoutes(node, _authenticator, createSessionToken) {
32
31
  return {
33
32
  action: 'setup',
34
- onConnect: async ({ req, userDid, extraParams: { launchType } }) => {
35
- const [blocklet, info] = await Promise.all([req.getBlocklet(), req.getNodeInfo()]);
36
-
37
- const claims = {
38
- profile: async ({ extraParams }) => {
39
- const { locale } = extraParams;
33
+ onConnect: async ({ req, userDid, extraParams: { locale } }) => {
34
+ const blocklet = await req.getBlocklet();
35
+ await checkOwner({ node, userDid, blocklet });
40
36
 
41
- return {
42
- fields: ['fullName', 'avatar'],
43
- description: messages.description[locale],
44
- };
37
+ return {
38
+ profile: {
39
+ fields: ['fullName', 'avatar'],
40
+ description: messages.description[locale],
45
41
  },
46
42
  };
47
-
48
- const authMethod = getServerAuthMethod(info, launchType);
49
- const serverClaims = await getSetupBlockletClaims(node, authMethod, blocklet);
50
-
51
- await checkOwner({ node, userDid, blocklet });
52
-
53
- return Object.assign(serverClaims, claims);
54
43
  },
55
44
 
56
- // eslint-disable-next-line consistent-return
57
- onAuth: async ({ claims, userDid, userPk, updateSession, extraParams, req, baseUrl, challenge }) => {
58
- const { locale, launchType, chainHost } = extraParams;
59
- const [blocklet, info] = await Promise.all([req.getBlocklet(), req.getNodeInfo()]);
45
+ onAuth: async ({ claims, userDid, userPk, updateSession, extraParams, req, baseUrl }) => {
46
+ const { locale, previousWorkflowData: proof } = extraParams;
47
+ const blocklet = await req.getBlocklet();
60
48
  const teamDid = blocklet.meta.did;
49
+ const user = await checkOwner({ node, userDid, blocklet });
61
50
 
62
- const authMethod = getServerAuthMethod(info, launchType);
63
- if (authMethod === 'nft' && !chainHost) {
64
- throw new Error(messages.invalidParams[locale]);
51
+ // ensure owner proof form previous workflow
52
+ if (!proof || !proof.claim || !proof.pk) {
53
+ throw new Error('No owner proof found from previous workflow');
65
54
  }
66
-
67
- await ensureBlockletPermission({
68
- authMethod,
69
- node,
70
- userDid,
71
- claims,
72
- challenge,
73
- locale,
74
- blocklet,
75
- isAuth: true,
76
- chainHost,
77
- });
78
-
79
- const user = await checkOwner({ node, userDid, blocklet });
55
+ verifySignature(proof.claim, blocklet.appDid, proof.pk, locale);
80
56
 
81
57
  try {
82
58
  if (user) {
@@ -27,11 +27,13 @@ const createInviteRoutes = require('./connect/invite');
27
27
  const createIssuePassportAuth = require('./connect/issue-passport');
28
28
  const createLostPassportListAuth = require('./connect/lost-passport-list');
29
29
  const createLostPassportIssueAuth = require('./connect/lost-passport-issue');
30
+ const createPreSetupAuth = require('./connect/pre-setup');
30
31
  const createSetupAuth = require('./connect/setup');
31
32
  const createSwitchProfileAuth = require('./connect/switch-profile');
32
33
  const createSwitchPassportAuth = require('./connect/switch-passport');
33
34
  const createRotateKeyPairAuth = require('./connect/rotate-key-pair');
34
35
  const createFuelAuth = require('./connect/fuel');
36
+ const createMigrateToStructV2Routes = require('./connect/migrate-app-to-struct-v2');
35
37
  const createSessionRoutes = require('./session');
36
38
  const createPassportRoutes = require('./passport');
37
39
  const { getRedirectUrl } = require('../../util');
@@ -167,11 +169,13 @@ const init = ({ node, options }) => {
167
169
  handler.attach(Object.assign({ app }, createIssuePassportAuth(node, authenticator, createSessionToken)));
168
170
  handler.attach(Object.assign({ app }, createLostPassportListAuth(node, authenticator, createSessionToken)));
169
171
  handler.attach(Object.assign({ app }, createLostPassportIssueAuth(node, authenticator, createSessionToken)));
172
+ handler.attach(Object.assign({ app }, createPreSetupAuth(node, authenticator, createSessionToken)));
170
173
  handler.attach(Object.assign({ app }, createSetupAuth(node, authenticator, createSessionToken)));
171
174
  handler.attach(Object.assign({ app }, createSwitchProfileAuth(node, authenticator, createSessionToken)));
172
175
  handler.attach(Object.assign({ app }, createSwitchPassportAuth(node, authenticator, createSessionToken)));
173
176
  handler.attach(Object.assign({ app }, createRotateKeyPairAuth(node, authenticator, createSessionToken)));
174
177
  handler.attach(Object.assign({ app }, createFuelAuth(authenticator, createSessionToken)));
178
+ handler.attach(Object.assign({ app }, createMigrateToStructV2Routes(node, options)));
175
179
  });
176
180
  };
177
181
 
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "files": {
3
3
  "main.css": "/.blocklet/proxy/blocklet-service/static/css/main.632501d5.css",
4
- "main.js": "/.blocklet/proxy/blocklet-service/static/js/main.7700a901.js",
4
+ "main.js": "/.blocklet/proxy/blocklet-service/static/js/main.c9dd5af4.js",
5
5
  "static/js/560.4d01281e.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/560.4d01281e.chunk.js",
6
6
  "static/js/255.4b68f586.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/255.4b68f586.chunk.js",
7
7
  "static/js/371.60842581.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/371.60842581.chunk.js",
8
8
  "static/js/737.76692b09.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/737.76692b09.chunk.js",
9
- "static/js/906.8d0bbdde.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/906.8d0bbdde.chunk.js",
9
+ "static/js/906.ebb2512d.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/906.ebb2512d.chunk.js",
10
10
  "static/js/868.43103624.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/868.43103624.chunk.js",
11
- "static/js/409.3622d7f6.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/409.3622d7f6.chunk.js",
11
+ "static/js/409.ceb64579.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/409.ceb64579.chunk.js",
12
12
  "static/js/682.c64ae291.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/682.c64ae291.chunk.js",
13
13
  "static/js/711.6c22b7c7.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/711.6c22b7c7.chunk.js",
14
14
  "static/js/437.d815f0c0.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/437.d815f0c0.chunk.js",
15
15
  "static/js/690.f9a59613.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/690.f9a59613.chunk.js",
16
- "static/js/313.546141b2.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/313.546141b2.chunk.js",
16
+ "static/js/950.07fa9e3d.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/950.07fa9e3d.chunk.js",
17
17
  "static/js/712.9667cdcd.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/712.9667cdcd.chunk.js",
18
18
  "static/js/511.1dd226f9.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/511.1dd226f9.chunk.js",
19
19
  "static/js/248.ad6363a4.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/248.ad6363a4.chunk.js",
20
20
  "static/css/503.b2c1f856.chunk.css": "/.blocklet/proxy/blocklet-service/static/css/503.b2c1f856.chunk.css",
21
- "static/js/503.c96d3943.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/503.c96d3943.chunk.js",
21
+ "static/js/503.1b995e29.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/503.1b995e29.chunk.js",
22
22
  "static/js/203.0c856580.chunk.js": "/.blocklet/proxy/blocklet-service/static/js/203.0c856580.chunk.js",
23
23
  "static/media/ubuntu-mono-all-400-normal.woff": "/.blocklet/proxy/blocklet-service/static/media/ubuntu-mono-all-400-normal.c879328bc62e9c68268f.woff",
24
24
  "static/media/lato-all-400-normal.woff": "/.blocklet/proxy/blocklet-service/static/media/lato-all-400-normal.3dc1eff492ab1f598560.woff",
@@ -42,28 +42,28 @@
42
42
  "router-template-styles/styles.css": "/.blocklet/proxy/blocklet-service/router-template-styles/styles.css",
43
43
  "index.html": "/.blocklet/proxy/blocklet-service/index.html",
44
44
  "main.632501d5.css.map": "/.blocklet/proxy/blocklet-service/static/css/main.632501d5.css.map",
45
- "main.7700a901.js.map": "/.blocklet/proxy/blocklet-service/static/js/main.7700a901.js.map",
45
+ "main.c9dd5af4.js.map": "/.blocklet/proxy/blocklet-service/static/js/main.c9dd5af4.js.map",
46
46
  "560.4d01281e.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/560.4d01281e.chunk.js.map",
47
47
  "255.4b68f586.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/255.4b68f586.chunk.js.map",
48
48
  "371.60842581.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/371.60842581.chunk.js.map",
49
49
  "737.76692b09.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/737.76692b09.chunk.js.map",
50
- "906.8d0bbdde.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/906.8d0bbdde.chunk.js.map",
50
+ "906.ebb2512d.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/906.ebb2512d.chunk.js.map",
51
51
  "868.43103624.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/868.43103624.chunk.js.map",
52
- "409.3622d7f6.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/409.3622d7f6.chunk.js.map",
52
+ "409.ceb64579.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/409.ceb64579.chunk.js.map",
53
53
  "682.c64ae291.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/682.c64ae291.chunk.js.map",
54
54
  "711.6c22b7c7.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/711.6c22b7c7.chunk.js.map",
55
55
  "437.d815f0c0.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/437.d815f0c0.chunk.js.map",
56
56
  "690.f9a59613.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/690.f9a59613.chunk.js.map",
57
- "313.546141b2.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/313.546141b2.chunk.js.map",
57
+ "950.07fa9e3d.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/950.07fa9e3d.chunk.js.map",
58
58
  "712.9667cdcd.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/712.9667cdcd.chunk.js.map",
59
59
  "511.1dd226f9.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/511.1dd226f9.chunk.js.map",
60
60
  "248.ad6363a4.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/248.ad6363a4.chunk.js.map",
61
61
  "503.b2c1f856.chunk.css.map": "/.blocklet/proxy/blocklet-service/static/css/503.b2c1f856.chunk.css.map",
62
- "503.c96d3943.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/503.c96d3943.chunk.js.map",
62
+ "503.1b995e29.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/503.1b995e29.chunk.js.map",
63
63
  "203.0c856580.chunk.js.map": "/.blocklet/proxy/blocklet-service/static/js/203.0c856580.chunk.js.map"
64
64
  },
65
65
  "entrypoints": [
66
66
  "static/css/main.632501d5.css",
67
- "static/js/main.7700a901.js"
67
+ "static/js/main.c9dd5af4.js"
68
68
  ]
69
69
  }
package/build/index.html CHANGED
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><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><script src=".well-known/service/api/env"></script><script src="/__blocklet__.js"></script><script defer="defer" src="/.blocklet/proxy/blocklet-service/static/js/main.7700a901.js"></script><link href="/.blocklet/proxy/blocklet-service/static/css/main.632501d5.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"/><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><script src=".well-known/service/api/env"></script><script src="/__blocklet__.js"></script><script defer="defer" src="/.blocklet/proxy/blocklet-service/static/js/main.c9dd5af4.js"></script><link href="/.blocklet/proxy/blocklet-service/static/css/main.632501d5.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>