@thirdweb-dev/service-utils 0.0.0-dev-f2d9bcd-20230713055621 → 0.0.0-dev-2666fdc-20230713214856

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.
@@ -1,75 +1,115 @@
1
1
  import fetch from 'isomorphic-unfetch';
2
+ import { createHash } from 'crypto';
2
3
 
3
- async function authorizeWorkerService(options) {
4
+ const SERVICE_NAMES = ["bundler", "rpc", "storage"];
5
+ const SERVICES = [{
6
+ name: "storage",
7
+ title: "Storage",
8
+ description: "IPFS Upload and Download",
9
+ actions: [{
10
+ name: "read",
11
+ title: "Download",
12
+ description: "Download a file from Storage"
13
+ }, {
14
+ name: "write",
15
+ title: "Upload",
16
+ description: "Upload a file to Storage"
17
+ }]
18
+ }, {
19
+ name: "rpc",
20
+ title: "RPC",
21
+ description: "Accelerated RPC Edge",
22
+ // all actions allowed
23
+ actions: []
24
+ }, {
25
+ name: "bundler",
26
+ title: "Smart Wallets",
27
+ description: "Bundler & Paymaster services",
28
+ // all actions allowed
29
+ actions: []
30
+ }];
31
+
32
+ async function authorizeCFWorkerService(options) {
33
+ const {
34
+ kvStore,
35
+ ctx,
36
+ authOptions,
37
+ serviceConfig,
38
+ validations
39
+ } = options;
40
+ const {
41
+ clientId
42
+ } = authOptions;
4
43
  let cachedKey;
5
- if (!options.clientId) {
6
- return {
7
- authorized: false,
8
- errorMessage: "The ClientId is missing. Make sure it is included with your Authorization Bearer request header.",
9
- errorCode: "MISSING_CLIENT_ID",
10
- statusCode: 422
11
- };
12
- }
13
44
 
14
45
  // first, check if the key is in KV
15
46
  try {
16
- const kvKey = await options.kvStore.get(options.clientId);
47
+ const kvKey = await kvStore.get(clientId);
17
48
  if (kvKey) {
18
49
  cachedKey = JSON.parse(kvKey);
19
50
  }
20
51
  } catch (err) {
21
52
  // ignore JSON parse, assuming not valid
22
53
  }
23
- const origin = options.headers.get("Origin") || "";
24
- let originHost;
25
- if (origin) {
26
- try {
27
- const originUrl = new URL(origin);
28
- originHost = originUrl.host;
29
- } catch (error) {
30
- // ignore, will be verified by domains
31
- }
32
- }
33
54
  const updateKv = async keyData => {
34
- options.kvStore.put(options.clientId, JSON.stringify(keyData), {
35
- expirationTtl: options.authOpts.cacheTtl || 60
55
+ kvStore.put(clientId, JSON.stringify(keyData), {
56
+ expirationTtl: serviceConfig.cacheTtl || 60
36
57
  });
37
58
  };
38
- return authorize(options.clientId, {
39
- ...options.authOpts,
40
- origin: originHost,
41
- cachedKey,
42
- onRefetchComplete: keyData => {
43
- options?.ctx?.waitUntil(updateKv(keyData));
44
- }
45
- }, options.validations);
59
+ return authorize({
60
+ authOptions,
61
+ serviceConfig: {
62
+ ...serviceConfig,
63
+ cachedKey,
64
+ onRefetchComplete: keyData => {
65
+ ctx.waitUntil(updateKv(keyData));
66
+ }
67
+ },
68
+ validations
69
+ });
70
+ }
71
+ async function authorizeNodeService(options) {
72
+ const {
73
+ authOptions,
74
+ serviceConfig,
75
+ validations
76
+ } = options;
77
+ return authorize({
78
+ authOptions,
79
+ serviceConfig,
80
+ validations
81
+ });
82
+ }
83
+ function hashSecret(secret) {
84
+ return createHash("sha256").update(secret).digest("hex");
85
+ }
86
+ function hashClientId(secret) {
87
+ const hashed = createHash("sha256").update(secret).digest("hex");
88
+ return hashed.slice(0, 32);
46
89
  }
47
90
 
48
91
  /**
49
- * Authorizes a request for a given clientId
50
- *
51
- * @param clientId The String client id
52
- * @params authOpts The Object auth options
53
- * origin - The String origin
54
- * apiUrl - The String API URL
55
- * scope - The ServiceName scope identifier
56
- * cachedKey - The ApiKey (optional) cached key
57
- * onRefetchComplete - The Func to trigger after key refetch from API
58
- * @params validations The Object of validations to run on a key
59
- * serviceTargetAddresses - The Array (optional) of service target addresses to validate
60
- * serviceActions - The Array (optional) of service actions to validate
92
+ * Authorizes a request for a given client ID
61
93
  *
62
94
  * @returns The Promise AuthorizationResponse
63
95
  */
64
- async function authorize(clientId, authOpts, validations) {
96
+ async function authorize(options) {
65
97
  try {
98
+ const {
99
+ authOptions,
100
+ serviceConfig,
101
+ validations
102
+ } = options;
103
+ const {
104
+ clientId
105
+ } = authOptions;
66
106
  const {
67
107
  apiUrl,
68
- origin,
69
108
  scope,
109
+ serviceKey,
70
110
  cachedKey,
71
111
  onRefetchComplete
72
- } = authOpts;
112
+ } = serviceConfig;
73
113
  let keyData = cachedKey;
74
114
 
75
115
  // no cached key, re-fetch from API
@@ -77,8 +117,8 @@ async function authorize(clientId, authOpts, validations) {
77
117
  const response = await fetch(`${apiUrl}/v1/keys/use/?scope=${scope}&clientId=${clientId}`, {
78
118
  method: "GET",
79
119
  headers: {
80
- "content-type": "application/json",
81
- "x-service-api-key": authOpts.serviceAPIKey
120
+ "x-service-api-key": serviceKey,
121
+ "content-type": "application/json"
82
122
  }
83
123
  });
84
124
  const apiResponse = await response.json();
@@ -102,107 +142,22 @@ async function authorize(clientId, authOpts, validations) {
102
142
  //
103
143
  // Run validations
104
144
  //
105
- const {
106
- serviceActions,
107
- serviceTargetAddresses
108
- } = validations || {};
109
-
110
- // validate domains
111
- if (keyData.domains && keyData.domains?.length > 0) {
112
- let originHost = "";
113
- if (origin) {
114
- try {
115
- const originUrl = new URL(origin);
116
- originHost = originUrl.host;
117
- } catch (error) {
118
- // ignore, will be verified by domains
119
- }
120
- }
121
- if (
122
- // find matching domain, or if all domains allowed
123
- !keyData.domains.find(d => {
124
- if (d === "*") {
125
- return true;
126
- }
127
-
128
- // If the allowedDomain has a wildcard,
129
- // we'll check that the ending of our domain matches the wildcard
130
- if (d.startsWith("*.")) {
131
- const wildcard = d.slice(2);
132
- return originHost.endsWith(wildcard);
133
- }
134
-
135
- // If there's no wildcard, we'll check for an exact match
136
- return d === originHost;
137
- })) {
138
- return {
139
- authorized: false,
140
- errorMessage: "The domain is not authorized for this key.",
141
- errorCode: "DOMAIN_UNAUTHORIZED",
142
- statusCode: 403
143
- };
144
- }
145
+ const authResponse = authAccess(authOptions, keyData);
146
+ if (!authResponse?.authorized) {
147
+ return authResponse;
145
148
  }
146
-
147
- // validate services
148
- if (keyData.services && keyData.services?.length > 0) {
149
- const service = (keyData.services || []).find(srv => srv.name === scope);
150
- if (!service) {
151
- return {
152
- authorized: false,
153
- errorMessage: `The service "${scope}" is not authorized for this key.`,
154
- errorCode: "SERVICE_UNAUTHORIZED",
155
- statusCode: 403
156
- };
157
- }
158
-
159
- // validate service actions
160
- if (serviceActions) {
161
- let unknownAction;
162
- serviceActions.forEach(action => {
163
- if (!service.actions.includes(action)) {
164
- unknownAction = action;
165
- }
166
- });
167
- if (unknownAction) {
168
- return {
169
- authorized: false,
170
- errorMessage: `The service "${scope}" action "${unknownAction}" is not authorized for this key.`,
171
- errorCode: "SERVICE_ACTION_UNAUTHORIZED",
172
- statusCode: 403
173
- };
174
- }
175
- }
176
-
177
- // validate service target addresses
178
- if (serviceTargetAddresses && !service.targetAddresses.find(addr => addr === "*" || serviceTargetAddresses.includes(addr))) {
179
- return {
180
- authorized: false,
181
- errorMessage: `The service "${scope}" target address is not authorized for this key.`,
182
- errorCode: "SERVICE_TARGET_ADDRESS_UNAUTHORIZED",
183
- statusCode: 403
184
- };
185
- }
149
+ const authzResponse = authzServices(validations, keyData, scope);
150
+ if (!authzResponse?.authorized) {
151
+ return authzResponse;
186
152
  }
153
+ // FIXME: validate bundleId
187
154
 
188
- // validate bundleIds
189
- // if (
190
- // bundleIds &&
191
- // !keyData.bundleIds.find((addr) => addr === "*" || bundleIds.includes(addr))
192
- // ) {
193
- // return {
194
- // authorized: false,
195
- // errorMessage: `The service "${scope}" for BundlerIds ${bundleIds} is not authorized for this key.`,
196
- // errorCode: "SERVICE_TARGET_ADDRESS_UNAUTHORIZED",
197
- // statusCode: 403,
198
- // };
199
- // }
200
155
  return {
201
156
  authorized: true,
202
157
  data: keyData
203
158
  };
204
159
  } catch (err) {
205
- console.error("Failed to authorize this key", err);
160
+ console.error("Failed to authorize this key.", err);
206
161
  return {
207
162
  authorized: false,
208
163
  errorMessage: "Internal error",
@@ -211,60 +166,116 @@ async function authorize(clientId, authOpts, validations) {
211
166
  };
212
167
  }
213
168
  }
214
- async function authorizeNodeService(options) {
215
- if (!options.clientId) {
169
+ function authAccess(authOptions, apiKey) {
170
+ const {
171
+ origin,
172
+ secretHash: providedSecretHash
173
+ } = authOptions;
174
+ const {
175
+ domains,
176
+ secretHash
177
+ } = apiKey;
178
+ if (providedSecretHash) {
179
+ if (secretHash !== providedSecretHash) {
180
+ return {
181
+ authorized: false,
182
+ errorMessage: "The secret is invalid.",
183
+ errorCode: "SECRET_INVALID",
184
+ statusCode: 401
185
+ };
186
+ }
216
187
  return {
217
- authorized: false,
218
- errorMessage: "The API key is missing. Make sure it is included with your Authorization Bearer request header.",
219
- errorCode: "MISSING_API_KEY",
220
- statusCode: 422
188
+ authorized: true
221
189
  };
222
190
  }
223
- const origin = typeof options.headers["Origin"] === "string" ? options.headers["Origin"] : options.headers["Origin"]?.join("");
224
- let originHost;
191
+
192
+ // validate domains
225
193
  if (origin) {
226
- try {
227
- const originUrl = new URL(origin);
228
- originHost = originUrl.hostname;
229
- } catch (error) {
230
- // ignore, will be verified by domains
194
+ if (
195
+ // find matching domain, or if all domains allowed
196
+ domains.find(d => {
197
+ if (d === "*") {
198
+ return true;
199
+ }
200
+
201
+ // If the allowedDomain has a wildcard,
202
+ // we'll check that the ending of our domain matches the wildcard
203
+ if (d.startsWith("*.")) {
204
+ const domainRoot = d.slice(2);
205
+ return origin.endsWith(domainRoot);
206
+ }
207
+
208
+ // If there's no wildcard, we'll check for an exact match
209
+ return d === origin;
210
+ })) {
211
+ return {
212
+ authorized: true
213
+ };
231
214
  }
215
+ return {
216
+ authorized: false,
217
+ errorMessage: "The origin is not authorized for this key.",
218
+ errorCode: "ORIGIN_UNAUTHORIZED",
219
+ statusCode: 401
220
+ };
232
221
  }
233
- return authorize(options.clientId, {
234
- ...options.authOpts,
235
- origin: originHost,
236
- cachedKey: undefined
237
- }, options.validations);
222
+
223
+ // FIXME: validate bundle id
224
+ return {
225
+ authorized: false,
226
+ errorMessage: "The keys are invalid.",
227
+ errorCode: "UNAUTHORIZED",
228
+ statusCode: 401
229
+ };
230
+ }
231
+ function authzServices(validations, apiKey, scope) {
232
+ const {
233
+ services
234
+ } = apiKey;
235
+ const {
236
+ serviceTargetAddresses,
237
+ serviceAction
238
+ } = validations;
239
+
240
+ // validate services
241
+ const service = services.find(srv => srv.name === scope);
242
+ if (!service) {
243
+ return {
244
+ authorized: false,
245
+ errorMessage: `The service "${scope}" is not authorized for this key.`,
246
+ errorCode: "SERVICE_UNAUTHORIZED",
247
+ statusCode: 403
248
+ };
249
+ }
250
+
251
+ // validate service actions
252
+ if (serviceAction) {
253
+ if (!service.actions.includes(serviceAction)) {
254
+ return {
255
+ authorized: false,
256
+ errorMessage: `The service "${scope}" action "${serviceAction}" is not authorized for this key.`,
257
+ errorCode: "SERVICE_ACTION_UNAUTHORIZED",
258
+ statusCode: 403
259
+ };
260
+ }
261
+ }
262
+
263
+ // validate service target addresses
264
+ if (serviceTargetAddresses && !service.targetAddresses.find(addr => addr === "*" || serviceTargetAddresses.includes(addr))) {
265
+ return {
266
+ authorized: false,
267
+ errorMessage: `The service "${scope}" target address is not authorized for this key.`,
268
+ errorCode: "SERVICE_TARGET_ADDRESS_UNAUTHORIZED",
269
+ statusCode: 403
270
+ };
271
+ }
272
+ return {
273
+ authorized: true
274
+ };
238
275
  }
239
276
 
240
- const SERVICES = [{
241
- name: "storage",
242
- title: "Storage",
243
- description: "IPFS Upload and Download",
244
- actions: [{
245
- name: "read",
246
- title: "Download",
247
- description: "Download a file from Storage"
248
- }, {
249
- name: "write",
250
- title: "Upload",
251
- description: "Upload a file to Storage"
252
- }]
253
- }, {
254
- name: "rpc",
255
- title: "RPC",
256
- description: "Accelerated RPC Edge",
257
- // all actions allowed
258
- actions: []
259
- }, {
260
- name: "bundler",
261
- title: "Smart Wallets",
262
- description: "Bundler & Paymaster services",
263
- // all actions allowed
264
- actions: []
265
- }];
266
277
  function getServiceByName(name) {
267
278
  return SERVICES.find(srv => srv.name === name);
268
279
  }
269
280
 
270
- export { SERVICES, authorizeNodeService, authorizeWorkerService, getServiceByName };
281
+ export { SERVICES, SERVICE_NAMES, authorizeCFWorkerService, authorizeNodeService, getServiceByName, hashClientId, hashSecret };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdweb-dev/service-utils",
3
- "version": "0.0.0-dev-f2d9bcd-20230713055621",
3
+ "version": "0.0.0-dev-2666fdc-20230713214856",
4
4
  "main": "dist/thirdweb-dev-service-utils.cjs.js",
5
5
  "module": "dist/thirdweb-dev-service-utils.esm.js",
6
6
  "exports": {