@thirdweb-dev/service-utils 0.0.0-dev-f2d9bcd-20230713055621 → 0.0.0-dev-7953a1c-20230713221245

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