@polkadot/extension-base 0.59.2 → 0.61.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/background/handlers/Extension.js +34 -34
- package/background/handlers/State.d.ts +1 -0
- package/background/handlers/State.js +109 -69
- package/background/handlers/Tabs.d.ts +1 -0
- package/background/handlers/Tabs.js +37 -22
- package/cjs/background/handlers/Extension.js +34 -34
- package/cjs/background/handlers/State.d.ts +1 -0
- package/cjs/background/handlers/State.js +109 -69
- package/cjs/background/handlers/Tabs.d.ts +1 -0
- package/cjs/background/handlers/Tabs.js +37 -22
- package/cjs/packageInfo.js +1 -1
- package/cjs/page/PostMessageProvider.js +15 -15
- package/cjs/stores/Base.js +7 -7
- package/package.json +15 -14
- package/packageInfo.js +1 -1
- package/page/PostMessageProvider.js +15 -15
- package/stores/Base.js +7 -7
|
@@ -19,16 +19,16 @@ function isJsonPayload(value) {
|
|
|
19
19
|
return value.genesisHash !== undefined;
|
|
20
20
|
}
|
|
21
21
|
export default class Extension {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
#cachedUnlocks;
|
|
23
|
+
#state;
|
|
24
24
|
constructor(state) {
|
|
25
|
-
this
|
|
26
|
-
this
|
|
25
|
+
this.#cachedUnlocks = {};
|
|
26
|
+
this.#state = state;
|
|
27
27
|
}
|
|
28
28
|
transformAccounts(accounts) {
|
|
29
29
|
return Object.values(accounts).map(({ json: { address, meta }, type }) => ({
|
|
30
30
|
address,
|
|
31
|
-
isDefaultAuthSelected: this.
|
|
31
|
+
isDefaultAuthSelected: this.#state.defaultAuthAccountSelection.includes(address),
|
|
32
32
|
...meta,
|
|
33
33
|
type
|
|
34
34
|
}));
|
|
@@ -77,26 +77,26 @@ export default class Extension {
|
|
|
77
77
|
async accountsForget({ address }) {
|
|
78
78
|
const authorizedAccountsDiff = [];
|
|
79
79
|
// cycle through authUrls and prepare the array of diff
|
|
80
|
-
Object.entries(this.
|
|
80
|
+
Object.entries(this.#state.authUrls).forEach(([url, urlInfo]) => {
|
|
81
81
|
// Note that urlInfo.authorizedAccounts may be undefined if this website entry
|
|
82
82
|
// was created before the "account authorization per website" functionality was introduced
|
|
83
83
|
if (urlInfo.authorizedAccounts?.includes(address)) {
|
|
84
84
|
authorizedAccountsDiff.push([url, urlInfo.authorizedAccounts.filter((previousAddress) => previousAddress !== address)]);
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
|
-
await this.
|
|
87
|
+
await this.#state.updateAuthorizedAccounts(authorizedAccountsDiff);
|
|
88
88
|
// cycle through default account selection for auth and remove any occurrence of the account
|
|
89
|
-
const newDefaultAuthAccounts = this.
|
|
90
|
-
await this.
|
|
89
|
+
const newDefaultAuthAccounts = this.#state.defaultAuthAccountSelection.filter((defaultSelectionAddress) => defaultSelectionAddress !== address);
|
|
90
|
+
await this.#state.updateDefaultAuthAccounts(newDefaultAuthAccounts);
|
|
91
91
|
keyring.forgetAccount(address);
|
|
92
92
|
return true;
|
|
93
93
|
}
|
|
94
94
|
refreshAccountPasswordCache(pair) {
|
|
95
95
|
const { address } = pair;
|
|
96
|
-
const savedExpiry = this
|
|
96
|
+
const savedExpiry = this.#cachedUnlocks[address] || 0;
|
|
97
97
|
const remainingTime = savedExpiry - Date.now();
|
|
98
98
|
if (remainingTime < 0) {
|
|
99
|
-
this
|
|
99
|
+
this.#cachedUnlocks[address] = 0;
|
|
100
100
|
pair.lock();
|
|
101
101
|
return 0;
|
|
102
102
|
}
|
|
@@ -133,22 +133,22 @@ export default class Extension {
|
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
135
135
|
authorizeApprove({ authorizedAccounts, id }) {
|
|
136
|
-
const queued = this.
|
|
136
|
+
const queued = this.#state.getAuthRequest(id);
|
|
137
137
|
assert(queued, 'Unable to find request');
|
|
138
138
|
const { resolve } = queued;
|
|
139
139
|
resolve({ authorizedAccounts, result: true });
|
|
140
140
|
return true;
|
|
141
141
|
}
|
|
142
142
|
async authorizeUpdate({ authorizedAccounts, url }) {
|
|
143
|
-
return await this.
|
|
143
|
+
return await this.#state.updateAuthorizedAccounts([[url, authorizedAccounts]]);
|
|
144
144
|
}
|
|
145
145
|
getAuthList() {
|
|
146
|
-
return { list: this.
|
|
146
|
+
return { list: this.#state.authUrls };
|
|
147
147
|
}
|
|
148
148
|
// FIXME This looks very much like what we have in accounts
|
|
149
149
|
authorizeSubscribe(id, port) {
|
|
150
150
|
const cb = createSubscription(id, port);
|
|
151
|
-
const subscription = this.
|
|
151
|
+
const subscription = this.#state.authSubject.subscribe((requests) => cb(requests));
|
|
152
152
|
port.onDisconnect.addListener(() => {
|
|
153
153
|
unsubscribe(id);
|
|
154
154
|
subscription.unsubscribe();
|
|
@@ -156,21 +156,21 @@ export default class Extension {
|
|
|
156
156
|
return true;
|
|
157
157
|
}
|
|
158
158
|
async metadataApprove({ id }) {
|
|
159
|
-
const queued = this.
|
|
159
|
+
const queued = this.#state.getMetaRequest(id);
|
|
160
160
|
assert(queued, 'Unable to find request');
|
|
161
161
|
const { request, resolve } = queued;
|
|
162
|
-
await this.
|
|
162
|
+
await this.#state.saveMetadata(request);
|
|
163
163
|
resolve(true);
|
|
164
164
|
return true;
|
|
165
165
|
}
|
|
166
166
|
metadataGet(genesisHash) {
|
|
167
|
-
return this.
|
|
167
|
+
return this.#state.knownMetadata.find((result) => result.genesisHash === genesisHash) || null;
|
|
168
168
|
}
|
|
169
169
|
metadataList() {
|
|
170
|
-
return this.
|
|
170
|
+
return this.#state.knownMetadata;
|
|
171
171
|
}
|
|
172
172
|
metadataReject({ id }) {
|
|
173
|
-
const queued = this.
|
|
173
|
+
const queued = this.#state.getMetaRequest(id);
|
|
174
174
|
assert(queued, 'Unable to find request');
|
|
175
175
|
const { reject } = queued;
|
|
176
176
|
reject(new Error('Rejected'));
|
|
@@ -178,7 +178,7 @@ export default class Extension {
|
|
|
178
178
|
}
|
|
179
179
|
metadataSubscribe(id, port) {
|
|
180
180
|
const cb = createSubscription(id, port);
|
|
181
|
-
const subscription = this.
|
|
181
|
+
const subscription = this.#state.metaSubject.subscribe((requests) => cb(requests));
|
|
182
182
|
port.onDisconnect.addListener(() => {
|
|
183
183
|
unsubscribe(id);
|
|
184
184
|
subscription.unsubscribe();
|
|
@@ -239,7 +239,7 @@ export default class Extension {
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
signingApprovePassword({ id, password, savePass }) {
|
|
242
|
-
const queued = this.
|
|
242
|
+
const queued = this.#state.getSignRequest(id);
|
|
243
243
|
assert(queued, 'Unable to find request');
|
|
244
244
|
const { reject, request, resolve } = queued;
|
|
245
245
|
const pair = keyring.getPair(queued.account.address);
|
|
@@ -260,7 +260,7 @@ export default class Extension {
|
|
|
260
260
|
const { payload } = request;
|
|
261
261
|
if (isJsonPayload(payload)) {
|
|
262
262
|
// Get the metadata for the genesisHash
|
|
263
|
-
const metadata = this.
|
|
263
|
+
const metadata = this.#state.knownMetadata.find(({ genesisHash }) => genesisHash === payload.genesisHash);
|
|
264
264
|
if (metadata) {
|
|
265
265
|
// we have metadata, expand it and extract the info/registry
|
|
266
266
|
const expanded = metadataExpand(metadata, false);
|
|
@@ -282,7 +282,7 @@ export default class Extension {
|
|
|
282
282
|
// unlike queued.account.address the following
|
|
283
283
|
// address is encoded with the default prefix
|
|
284
284
|
// which what is used for password caching mapping
|
|
285
|
-
this
|
|
285
|
+
this.#cachedUnlocks[pair.address] = Date.now() + PASSWORD_EXPIRY_MS;
|
|
286
286
|
}
|
|
287
287
|
else {
|
|
288
288
|
pair.lock();
|
|
@@ -294,21 +294,21 @@ export default class Extension {
|
|
|
294
294
|
return true;
|
|
295
295
|
}
|
|
296
296
|
signingApproveSignature({ id, signature, signedTransaction }) {
|
|
297
|
-
const queued = this.
|
|
297
|
+
const queued = this.#state.getSignRequest(id);
|
|
298
298
|
assert(queued, 'Unable to find request');
|
|
299
299
|
const { resolve } = queued;
|
|
300
300
|
resolve({ id, signature, signedTransaction });
|
|
301
301
|
return true;
|
|
302
302
|
}
|
|
303
303
|
signingCancel({ id }) {
|
|
304
|
-
const queued = this.
|
|
304
|
+
const queued = this.#state.getSignRequest(id);
|
|
305
305
|
assert(queued, 'Unable to find request');
|
|
306
306
|
const { reject } = queued;
|
|
307
307
|
reject(new Error('Cancelled'));
|
|
308
308
|
return true;
|
|
309
309
|
}
|
|
310
310
|
signingIsLocked({ id }) {
|
|
311
|
-
const queued = this.
|
|
311
|
+
const queued = this.#state.getSignRequest(id);
|
|
312
312
|
assert(queued, 'Unable to find request');
|
|
313
313
|
const address = queued.request.payload.address;
|
|
314
314
|
const pair = keyring.getPair(address);
|
|
@@ -322,7 +322,7 @@ export default class Extension {
|
|
|
322
322
|
// FIXME This looks very much like what we have in authorization
|
|
323
323
|
signingSubscribe(id, port) {
|
|
324
324
|
const cb = createSubscription(id, port);
|
|
325
|
-
const subscription = this.
|
|
325
|
+
const subscription = this.#state.signSubject.subscribe((requests) => cb(requests));
|
|
326
326
|
port.onDisconnect.addListener(() => {
|
|
327
327
|
unsubscribe(id);
|
|
328
328
|
subscription.unsubscribe();
|
|
@@ -371,13 +371,13 @@ export default class Extension {
|
|
|
371
371
|
return true;
|
|
372
372
|
}
|
|
373
373
|
async removeAuthorization(url) {
|
|
374
|
-
const remAuth = await this.
|
|
374
|
+
const remAuth = await this.#state.removeAuthorization(url);
|
|
375
375
|
return { list: remAuth };
|
|
376
376
|
}
|
|
377
377
|
// Reject the authorization request and add the URL to the authorized list with no keys.
|
|
378
378
|
// The site will not prompt for re-authorization on future visits.
|
|
379
379
|
rejectAuthRequest(id) {
|
|
380
|
-
const queued = this.
|
|
380
|
+
const queued = this.#state.getAuthRequest(id);
|
|
381
381
|
assert(queued, 'Unable to find request');
|
|
382
382
|
const { reject } = queued;
|
|
383
383
|
reject(new Error('Rejected'));
|
|
@@ -385,16 +385,16 @@ export default class Extension {
|
|
|
385
385
|
// Cancel the authorization request and do not add the URL to the authorized list.
|
|
386
386
|
// The site will prompt for authorization on future visits.
|
|
387
387
|
cancelAuthRequest(id) {
|
|
388
|
-
const queued = this.
|
|
388
|
+
const queued = this.#state.getAuthRequest(id);
|
|
389
389
|
assert(queued, 'Unable to find request');
|
|
390
390
|
const { reject } = queued;
|
|
391
391
|
reject(new Error('Cancelled'));
|
|
392
392
|
}
|
|
393
393
|
updateCurrentTabs({ urls }) {
|
|
394
|
-
this.
|
|
394
|
+
this.#state.updateCurrentTabsUrl(urls);
|
|
395
395
|
}
|
|
396
396
|
getConnectedTabsUrl() {
|
|
397
|
-
return this.
|
|
397
|
+
return this.#state.getConnectedTabsUrl();
|
|
398
398
|
}
|
|
399
399
|
// Weird thought, the eslint override is not needed in Tabs
|
|
400
400
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
@@ -469,7 +469,7 @@ export default class Extension {
|
|
|
469
469
|
case 'pri(seed.validate)':
|
|
470
470
|
return this.seedValidate(request);
|
|
471
471
|
case 'pri(settings.notification)':
|
|
472
|
-
return this.
|
|
472
|
+
return this.#state.setNotification(request);
|
|
473
473
|
case 'pri(signing.approve.password)':
|
|
474
474
|
return this.signingApprovePassword(request);
|
|
475
475
|
case 'pri(signing.approve.signature)':
|
|
@@ -90,6 +90,7 @@ export default class State {
|
|
|
90
90
|
rpcUnsubscribe(request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise<boolean>;
|
|
91
91
|
saveMetadata(meta: MetadataDef): Promise<void>;
|
|
92
92
|
setNotification(notification: string): boolean;
|
|
93
|
+
private handleSignRequest;
|
|
93
94
|
sign(url: string, request: RequestSign, account: AccountJson): Promise<ResponseSigning>;
|
|
94
95
|
}
|
|
95
96
|
export {};
|
|
@@ -63,35 +63,38 @@ async function extractMetadata(store) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
export default class State {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
#authUrls = new Map();
|
|
67
|
+
#lastRequestTimestamps = new Map();
|
|
68
|
+
#maxEntries = 10;
|
|
69
|
+
#rateLimitInterval = 3000; // 3 seconds
|
|
70
|
+
#authRequests = {};
|
|
71
|
+
#metaStore = new MetadataStore();
|
|
69
72
|
// Map of providers currently injected in tabs
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
#injectedProviders = new Map();
|
|
74
|
+
#metaRequests = {};
|
|
75
|
+
#notification = settings.notification;
|
|
73
76
|
// Map of all providers exposed by the extension, they are retrievable by key
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
#providers;
|
|
78
|
+
#signRequests = {};
|
|
79
|
+
#windows = [];
|
|
80
|
+
#connectedTabsUrl = [];
|
|
78
81
|
authSubject = new BehaviorSubject([]);
|
|
79
82
|
metaSubject = new BehaviorSubject([]);
|
|
80
83
|
signSubject = new BehaviorSubject([]);
|
|
81
84
|
authUrlSubjects = {};
|
|
82
85
|
defaultAuthAccountSelection = [];
|
|
83
86
|
constructor(providers = {}) {
|
|
84
|
-
this
|
|
87
|
+
this.#providers = providers;
|
|
85
88
|
}
|
|
86
89
|
async init() {
|
|
87
|
-
await extractMetadata(this
|
|
90
|
+
await extractMetadata(this.#metaStore);
|
|
88
91
|
// retrieve previously set authorizations
|
|
89
92
|
const storageAuthUrls = await chrome.storage.local.get(AUTH_URLS_KEY);
|
|
90
93
|
const authString = storageAuthUrls?.[AUTH_URLS_KEY] || '{}';
|
|
91
94
|
const previousAuth = JSON.parse(authString);
|
|
92
|
-
this
|
|
95
|
+
this.#authUrls = new Map(Object.entries(previousAuth));
|
|
93
96
|
// Initialize authUrlSubjects for each URL
|
|
94
|
-
|
|
97
|
+
this.#authUrls.forEach((authInfo, url) => {
|
|
95
98
|
this.authUrlSubjects[url] = new BehaviorSubject(authInfo);
|
|
96
99
|
});
|
|
97
100
|
// retrieve previously set default auth accounts
|
|
@@ -104,49 +107,49 @@ export default class State {
|
|
|
104
107
|
return knownMetadata();
|
|
105
108
|
}
|
|
106
109
|
get numAuthRequests() {
|
|
107
|
-
return Object.keys(this
|
|
110
|
+
return Object.keys(this.#authRequests).length;
|
|
108
111
|
}
|
|
109
112
|
get numMetaRequests() {
|
|
110
|
-
return Object.keys(this
|
|
113
|
+
return Object.keys(this.#metaRequests).length;
|
|
111
114
|
}
|
|
112
115
|
get numSignRequests() {
|
|
113
|
-
return Object.keys(this
|
|
116
|
+
return Object.keys(this.#signRequests).length;
|
|
114
117
|
}
|
|
115
118
|
get allAuthRequests() {
|
|
116
119
|
return Object
|
|
117
|
-
.values(this
|
|
120
|
+
.values(this.#authRequests)
|
|
118
121
|
.map(({ id, request, url }) => ({ id, request, url }));
|
|
119
122
|
}
|
|
120
123
|
get allMetaRequests() {
|
|
121
124
|
return Object
|
|
122
|
-
.values(this
|
|
125
|
+
.values(this.#metaRequests)
|
|
123
126
|
.map(({ id, request, url }) => ({ id, request, url }));
|
|
124
127
|
}
|
|
125
128
|
get allSignRequests() {
|
|
126
129
|
return Object
|
|
127
|
-
.values(this
|
|
130
|
+
.values(this.#signRequests)
|
|
128
131
|
.map(({ account, id, request, url }) => ({ account, id, request, url }));
|
|
129
132
|
}
|
|
130
133
|
get authUrls() {
|
|
131
|
-
return this
|
|
134
|
+
return Object.fromEntries(this.#authUrls);
|
|
132
135
|
}
|
|
133
136
|
popupClose() {
|
|
134
|
-
this.
|
|
135
|
-
this
|
|
137
|
+
this.#windows.forEach((id) => withErrorLog(() => chrome.windows.remove(id)));
|
|
138
|
+
this.#windows = [];
|
|
136
139
|
}
|
|
137
140
|
popupOpen() {
|
|
138
|
-
this
|
|
139
|
-
chrome.windows.create(this
|
|
141
|
+
this.#notification !== 'extension' &&
|
|
142
|
+
chrome.windows.create(this.#notification === 'window'
|
|
140
143
|
? NORMAL_WINDOW_OPTS
|
|
141
144
|
: POPUP_WINDOW_OPTS, (window) => {
|
|
142
145
|
if (window) {
|
|
143
|
-
this.
|
|
146
|
+
this.#windows.push(window.id || 0);
|
|
144
147
|
}
|
|
145
148
|
});
|
|
146
149
|
}
|
|
147
150
|
authComplete = (id, resolve, reject) => {
|
|
148
151
|
const complete = async (authorizedAccounts = []) => {
|
|
149
|
-
const { idStr, request: { origin }, url } = this
|
|
152
|
+
const { idStr, request: { origin }, url } = this.#authRequests[id];
|
|
150
153
|
const strippedUrl = this.stripUrl(url);
|
|
151
154
|
const authInfo = {
|
|
152
155
|
authorizedAccounts,
|
|
@@ -155,7 +158,7 @@ export default class State {
|
|
|
155
158
|
origin,
|
|
156
159
|
url
|
|
157
160
|
};
|
|
158
|
-
this.
|
|
161
|
+
this.#authUrls.set(strippedUrl, authInfo);
|
|
159
162
|
if (!this.authUrlSubjects[strippedUrl]) {
|
|
160
163
|
this.authUrlSubjects[strippedUrl] = new BehaviorSubject(authInfo);
|
|
161
164
|
}
|
|
@@ -164,14 +167,14 @@ export default class State {
|
|
|
164
167
|
}
|
|
165
168
|
await this.saveCurrentAuthList();
|
|
166
169
|
await this.updateDefaultAuthAccounts(authorizedAccounts);
|
|
167
|
-
delete this
|
|
170
|
+
delete this.#authRequests[id];
|
|
168
171
|
this.updateIconAuth(true);
|
|
169
172
|
};
|
|
170
173
|
return {
|
|
171
174
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
172
175
|
reject: async (error) => {
|
|
173
176
|
if (error.message === 'Cancelled') {
|
|
174
|
-
delete this
|
|
177
|
+
delete this.#authRequests[id];
|
|
175
178
|
this.updateIconAuth(true);
|
|
176
179
|
reject(new Error('Connection request was cancelled by the user.'));
|
|
177
180
|
}
|
|
@@ -209,17 +212,17 @@ export default class State {
|
|
|
209
212
|
: undefined;
|
|
210
213
|
})
|
|
211
214
|
.filter((value) => !!value);
|
|
212
|
-
this
|
|
215
|
+
this.#connectedTabsUrl = connectedTabs;
|
|
213
216
|
}
|
|
214
217
|
getConnectedTabsUrl() {
|
|
215
|
-
return this
|
|
218
|
+
return this.#connectedTabsUrl;
|
|
216
219
|
}
|
|
217
220
|
deleteAuthRequest(requestId) {
|
|
218
|
-
delete this
|
|
221
|
+
delete this.#authRequests[requestId];
|
|
219
222
|
this.updateIconAuth(true);
|
|
220
223
|
}
|
|
221
224
|
async saveCurrentAuthList() {
|
|
222
|
-
await chrome.storage.local.set({ [AUTH_URLS_KEY]: JSON.stringify(this
|
|
225
|
+
await chrome.storage.local.set({ [AUTH_URLS_KEY]: JSON.stringify(Object.fromEntries(this.#authUrls)) });
|
|
223
226
|
}
|
|
224
227
|
async saveDefaultAuthAccounts() {
|
|
225
228
|
await chrome.storage.local.set({ [DEFAULT_AUTH_ACCOUNTS]: JSON.stringify(this.defaultAuthAccountSelection) });
|
|
@@ -230,7 +233,7 @@ export default class State {
|
|
|
230
233
|
}
|
|
231
234
|
metaComplete = (id, resolve, reject) => {
|
|
232
235
|
const complete = () => {
|
|
233
|
-
delete this
|
|
236
|
+
delete this.#metaRequests[id];
|
|
234
237
|
this.updateIconMeta(true);
|
|
235
238
|
};
|
|
236
239
|
return {
|
|
@@ -246,7 +249,7 @@ export default class State {
|
|
|
246
249
|
};
|
|
247
250
|
signComplete = (id, resolve, reject) => {
|
|
248
251
|
const complete = () => {
|
|
249
|
-
delete this
|
|
252
|
+
delete this.#signRequests[id];
|
|
250
253
|
this.updateIconSign(true);
|
|
251
254
|
};
|
|
252
255
|
return {
|
|
@@ -261,9 +264,22 @@ export default class State {
|
|
|
261
264
|
};
|
|
262
265
|
};
|
|
263
266
|
stripUrl(url) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
try {
|
|
268
|
+
const parsedUrl = new URL(url);
|
|
269
|
+
if (!['http:', 'https:', 'ipfs:', 'ipns:'].includes(parsedUrl.protocol)) {
|
|
270
|
+
throw new Error(`Invalid protocol ${parsedUrl.protocol}`);
|
|
271
|
+
}
|
|
272
|
+
// For ipfs/ipns which don't have a standard origin, we handle it differently.
|
|
273
|
+
if (parsedUrl.protocol === 'ipfs:' || parsedUrl.protocol === 'ipns:') {
|
|
274
|
+
// ipfs://<hash> | ipns://<hash>
|
|
275
|
+
return `${parsedUrl.protocol}//${parsedUrl.hostname}`;
|
|
276
|
+
}
|
|
277
|
+
return parsedUrl.origin;
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
console.error(e);
|
|
281
|
+
throw new Error('Invalid URL');
|
|
282
|
+
}
|
|
267
283
|
}
|
|
268
284
|
updateIcon(shouldClose) {
|
|
269
285
|
const authCount = this.numAuthRequests;
|
|
@@ -280,15 +296,15 @@ export default class State {
|
|
|
280
296
|
}
|
|
281
297
|
}
|
|
282
298
|
async removeAuthorization(url) {
|
|
283
|
-
const entry = this.
|
|
299
|
+
const entry = this.#authUrls.get(url);
|
|
284
300
|
assert(entry, `The source ${url} is not known`);
|
|
285
|
-
|
|
301
|
+
this.#authUrls.delete(url);
|
|
286
302
|
await this.saveCurrentAuthList();
|
|
287
303
|
if (this.authUrlSubjects[url]) {
|
|
288
304
|
entry.authorizedAccounts = [];
|
|
289
305
|
this.authUrlSubjects[url].next(entry);
|
|
290
306
|
}
|
|
291
|
-
return this.
|
|
307
|
+
return this.authUrls;
|
|
292
308
|
}
|
|
293
309
|
updateIconAuth(shouldClose) {
|
|
294
310
|
this.authSubject.next(this.allAuthRequests);
|
|
@@ -304,8 +320,12 @@ export default class State {
|
|
|
304
320
|
}
|
|
305
321
|
async updateAuthorizedAccounts(authorizedAccountsDiff) {
|
|
306
322
|
authorizedAccountsDiff.forEach(([url, authorizedAccountDiff]) => {
|
|
307
|
-
this.
|
|
308
|
-
|
|
323
|
+
const authInfo = this.#authUrls.get(url);
|
|
324
|
+
if (authInfo) {
|
|
325
|
+
authInfo.authorizedAccounts = authorizedAccountDiff;
|
|
326
|
+
this.#authUrls.set(url, authInfo);
|
|
327
|
+
this.authUrlSubjects[url].next(authInfo);
|
|
328
|
+
}
|
|
309
329
|
});
|
|
310
330
|
await this.saveCurrentAuthList();
|
|
311
331
|
}
|
|
@@ -313,12 +333,13 @@ export default class State {
|
|
|
313
333
|
const idStr = this.stripUrl(url);
|
|
314
334
|
// Do not enqueue duplicate authorization requests.
|
|
315
335
|
const isDuplicate = Object
|
|
316
|
-
.values(this
|
|
336
|
+
.values(this.#authRequests)
|
|
317
337
|
.some((request) => request.idStr === idStr);
|
|
318
338
|
assert(!isDuplicate, `The source ${url} has a pending authorization request`);
|
|
319
|
-
if (this.
|
|
339
|
+
if (this.#authUrls.has(idStr)) {
|
|
320
340
|
// this url was seen in the past
|
|
321
|
-
|
|
341
|
+
const authInfo = this.#authUrls.get(idStr);
|
|
342
|
+
assert(authInfo?.authorizedAccounts || authInfo?.isAllowed, `The source ${url} is not allowed to interact with this extension`);
|
|
322
343
|
return {
|
|
323
344
|
authorizedAccounts: [],
|
|
324
345
|
result: false
|
|
@@ -326,7 +347,7 @@ export default class State {
|
|
|
326
347
|
}
|
|
327
348
|
return new Promise((resolve, reject) => {
|
|
328
349
|
const id = getId();
|
|
329
|
-
this
|
|
350
|
+
this.#authRequests[id] = {
|
|
330
351
|
...this.authComplete(id, resolve, reject),
|
|
331
352
|
id,
|
|
332
353
|
idStr,
|
|
@@ -338,14 +359,14 @@ export default class State {
|
|
|
338
359
|
});
|
|
339
360
|
}
|
|
340
361
|
ensureUrlAuthorized(url) {
|
|
341
|
-
const entry = this.
|
|
362
|
+
const entry = this.#authUrls.get(this.stripUrl(url));
|
|
342
363
|
assert(entry, `The source ${url} has not been enabled yet`);
|
|
343
364
|
return true;
|
|
344
365
|
}
|
|
345
366
|
injectMetadata(url, request) {
|
|
346
367
|
return new Promise((resolve, reject) => {
|
|
347
368
|
const id = getId();
|
|
348
|
-
this
|
|
369
|
+
this.#metaRequests[id] = {
|
|
349
370
|
...this.metaComplete(id, resolve, reject),
|
|
350
371
|
id,
|
|
351
372
|
request,
|
|
@@ -356,73 +377,92 @@ export default class State {
|
|
|
356
377
|
});
|
|
357
378
|
}
|
|
358
379
|
getAuthRequest(id) {
|
|
359
|
-
return this
|
|
380
|
+
return this.#authRequests[id];
|
|
360
381
|
}
|
|
361
382
|
getMetaRequest(id) {
|
|
362
|
-
return this
|
|
383
|
+
return this.#metaRequests[id];
|
|
363
384
|
}
|
|
364
385
|
getSignRequest(id) {
|
|
365
|
-
return this
|
|
386
|
+
return this.#signRequests[id];
|
|
366
387
|
}
|
|
367
388
|
// List all providers the extension is exposing
|
|
368
389
|
rpcListProviders() {
|
|
369
|
-
return Promise.resolve(Object.keys(this
|
|
370
|
-
acc[key] = this
|
|
390
|
+
return Promise.resolve(Object.keys(this.#providers).reduce((acc, key) => {
|
|
391
|
+
acc[key] = this.#providers[key].meta;
|
|
371
392
|
return acc;
|
|
372
393
|
}, {}));
|
|
373
394
|
}
|
|
374
395
|
rpcSend(request, port) {
|
|
375
|
-
const provider = this.
|
|
396
|
+
const provider = this.#injectedProviders.get(port);
|
|
376
397
|
assert(provider, 'Cannot call pub(rpc.subscribe) before provider is set');
|
|
377
398
|
return provider.send(request.method, request.params);
|
|
378
399
|
}
|
|
379
400
|
// Start a provider, return its meta
|
|
380
401
|
rpcStartProvider(key, port) {
|
|
381
|
-
assert(Object.keys(this
|
|
382
|
-
if (this.
|
|
383
|
-
return Promise.resolve(this
|
|
402
|
+
assert(Object.keys(this.#providers).includes(key), `Provider ${key} is not exposed by extension`);
|
|
403
|
+
if (this.#injectedProviders.get(port)) {
|
|
404
|
+
return Promise.resolve(this.#providers[key].meta);
|
|
384
405
|
}
|
|
385
406
|
// Instantiate the provider
|
|
386
|
-
this.
|
|
407
|
+
this.#injectedProviders.set(port, this.#providers[key].start());
|
|
387
408
|
// Close provider connection when page is closed
|
|
388
409
|
port.onDisconnect.addListener(() => {
|
|
389
|
-
const provider = this.
|
|
410
|
+
const provider = this.#injectedProviders.get(port);
|
|
390
411
|
if (provider) {
|
|
391
412
|
withErrorLog(() => provider.disconnect());
|
|
392
413
|
}
|
|
393
|
-
this.
|
|
414
|
+
this.#injectedProviders.delete(port);
|
|
394
415
|
});
|
|
395
|
-
return Promise.resolve(this
|
|
416
|
+
return Promise.resolve(this.#providers[key].meta);
|
|
396
417
|
}
|
|
397
418
|
rpcSubscribe({ method, params, type }, cb, port) {
|
|
398
|
-
const provider = this.
|
|
419
|
+
const provider = this.#injectedProviders.get(port);
|
|
399
420
|
assert(provider, 'Cannot call pub(rpc.subscribe) before provider is set');
|
|
400
421
|
return provider.subscribe(type, method, params, cb);
|
|
401
422
|
}
|
|
402
423
|
rpcSubscribeConnected(_request, cb, port) {
|
|
403
|
-
const provider = this.
|
|
424
|
+
const provider = this.#injectedProviders.get(port);
|
|
404
425
|
assert(provider, 'Cannot call pub(rpc.subscribeConnected) before provider is set');
|
|
405
426
|
cb(null, provider.isConnected); // Immediately send back current isConnected
|
|
406
427
|
provider.on('connected', () => cb(null, true));
|
|
407
428
|
provider.on('disconnected', () => cb(null, false));
|
|
408
429
|
}
|
|
409
430
|
rpcUnsubscribe(request, port) {
|
|
410
|
-
const provider = this.
|
|
431
|
+
const provider = this.#injectedProviders.get(port);
|
|
411
432
|
assert(provider, 'Cannot call pub(rpc.unsubscribe) before provider is set');
|
|
412
433
|
return provider.unsubscribe(request.type, request.method, request.subscriptionId);
|
|
413
434
|
}
|
|
414
435
|
async saveMetadata(meta) {
|
|
415
|
-
await this.
|
|
436
|
+
await this.#metaStore.set(meta.genesisHash, meta);
|
|
416
437
|
addMetadata(meta);
|
|
417
438
|
}
|
|
418
439
|
setNotification(notification) {
|
|
419
|
-
this
|
|
440
|
+
this.#notification = notification;
|
|
420
441
|
return true;
|
|
421
442
|
}
|
|
443
|
+
handleSignRequest(origin) {
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
const lastTime = this.#lastRequestTimestamps.get(origin) || 0;
|
|
446
|
+
if (now - lastTime < this.#rateLimitInterval) {
|
|
447
|
+
throw new Error('Rate limit exceeded. Try again later.');
|
|
448
|
+
}
|
|
449
|
+
// If we're about to exceed max entries, evict the oldest
|
|
450
|
+
if (!this.#lastRequestTimestamps.has(origin) && this.#lastRequestTimestamps.size >= this.#maxEntries) {
|
|
451
|
+
const oldestKey = this.#lastRequestTimestamps.keys().next().value;
|
|
452
|
+
oldestKey && this.#lastRequestTimestamps.delete(oldestKey);
|
|
453
|
+
}
|
|
454
|
+
this.#lastRequestTimestamps.set(origin, now);
|
|
455
|
+
}
|
|
422
456
|
sign(url, request, account) {
|
|
423
457
|
const id = getId();
|
|
458
|
+
try {
|
|
459
|
+
this.handleSignRequest(url);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
return Promise.reject(error);
|
|
463
|
+
}
|
|
424
464
|
return new Promise((resolve, reject) => {
|
|
425
|
-
this
|
|
465
|
+
this.#signRequests[id] = {
|
|
426
466
|
...this.signComplete(id, resolve, reject),
|
|
427
467
|
account,
|
|
428
468
|
id,
|
|
@@ -20,6 +20,7 @@ export default class Tabs {
|
|
|
20
20
|
private rpcSubscribeConnected;
|
|
21
21
|
private rpcUnsubscribe;
|
|
22
22
|
private redirectPhishingLanding;
|
|
23
|
+
private parseUrl;
|
|
23
24
|
private redirectIfPhishing;
|
|
24
25
|
handle<TMessageType extends MessageTypes>(id: string, type: TMessageType, request: RequestTypes[TMessageType], url: string, port?: chrome.runtime.Port): Promise<ResponseTypes[keyof ResponseTypes]>;
|
|
25
26
|
}
|