@taskcluster/client-web 88.0.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/README.md +373 -0
- package/package.json +43 -0
- package/src/Client.js +385 -0
- package/src/clients/Auth.js +466 -0
- package/src/clients/AuthEvents.js +62 -0
- package/src/clients/Github.js +150 -0
- package/src/clients/GithubEvents.js +68 -0
- package/src/clients/Hooks.js +181 -0
- package/src/clients/HooksEvents.js +38 -0
- package/src/clients/Index.js +152 -0
- package/src/clients/Notify.js +144 -0
- package/src/clients/NotifyEvents.js +27 -0
- package/src/clients/Object.js +135 -0
- package/src/clients/PurgeCache.js +92 -0
- package/src/clients/Queue.js +734 -0
- package/src/clients/QueueEvents.js +129 -0
- package/src/clients/Secrets.js +103 -0
- package/src/clients/WorkerManager.js +303 -0
- package/src/clients/WorkerManagerEvents.js +112 -0
- package/src/credentials.js +210 -0
- package/src/emitter.js +58 -0
- package/src/fetch.js +93 -0
- package/src/index.js +26 -0
- package/src/utils.js +123 -0
package/src/Client.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { withRootUrl } from 'taskcluster-lib-urls';
|
|
2
|
+
import { stringify } from 'query-string';
|
|
3
|
+
import hawk from 'hawk';
|
|
4
|
+
import fetch from './fetch';
|
|
5
|
+
|
|
6
|
+
export default class Client {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
const defaults = {
|
|
9
|
+
credentials: null,
|
|
10
|
+
authorizedScopes: null,
|
|
11
|
+
timeout: 30 * 1000,
|
|
12
|
+
retries: 5,
|
|
13
|
+
delayFactor: 100,
|
|
14
|
+
randomizationFactor: 0.25,
|
|
15
|
+
maxDelay: 30 * 1000,
|
|
16
|
+
serviceName: '',
|
|
17
|
+
serviceVersion: 'v1',
|
|
18
|
+
exchangePrefix: '',
|
|
19
|
+
credentialAgent: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
this.options = {
|
|
23
|
+
...defaults,
|
|
24
|
+
...options,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (!this.options.rootUrl) {
|
|
28
|
+
throw new Error('Missing required option "rootUrl"');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
this.options.randomizationFactor < 0 ||
|
|
33
|
+
this.options.randomizationFactor >= 1
|
|
34
|
+
) {
|
|
35
|
+
throw new Error('options.randomizationFactor must be between 0 and 1');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (this.options.accessToken) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'options.accessToken is no longer supported; use options.credentials',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { reference } = options;
|
|
45
|
+
|
|
46
|
+
if (reference) {
|
|
47
|
+
if (reference.serviceName) {
|
|
48
|
+
this.options.serviceName = reference.serviceName;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (reference.serviceVersion) {
|
|
52
|
+
this.options.serviceVersion = reference.serviceVersion;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (reference.exchangePrefix) {
|
|
56
|
+
this.options.exchangePrefix = reference.exchangePrefix;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (reference.entries) {
|
|
60
|
+
reference.entries.forEach(entry => {
|
|
61
|
+
if (entry.type === 'function') {
|
|
62
|
+
// eslint-disable-next-line func-names
|
|
63
|
+
this[entry.name] = function(...args) {
|
|
64
|
+
this.validate(entry, args);
|
|
65
|
+
|
|
66
|
+
return this.request(entry, args);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
this[entry.name].entry = entry;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (entry.type === 'topic-exchange') {
|
|
73
|
+
// eslint-disable-next-line func-names
|
|
74
|
+
this[entry.name] = function(pattern) {
|
|
75
|
+
return this.normalizePattern(entry, pattern);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
use(optionsUpdates) {
|
|
84
|
+
const options = { ...this.options, ...optionsUpdates };
|
|
85
|
+
|
|
86
|
+
return new this.constructor(options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getMethodExpectedArity({ input, args }) {
|
|
90
|
+
return input ? args.length + 1 : args.length;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* eslint-disable consistent-return */
|
|
94
|
+
buildExtraData(credentials) {
|
|
95
|
+
if (!credentials) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { authorizedScopes } = this.options;
|
|
100
|
+
const { clientId, accessToken, certificate } = credentials;
|
|
101
|
+
|
|
102
|
+
if (!clientId || !accessToken) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const extra = {};
|
|
107
|
+
|
|
108
|
+
// If there is a certificate we have temporary credentials, and we
|
|
109
|
+
// must provide the certificate
|
|
110
|
+
if (certificate) {
|
|
111
|
+
extra.certificate =
|
|
112
|
+
typeof certificate === 'string' ? JSON.parse(certificate) : certificate;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// If set of authorized scopes is provided, we'll restrict the request
|
|
116
|
+
// to only use these scopes
|
|
117
|
+
if (Array.isArray(authorizedScopes)) {
|
|
118
|
+
extra.authorizedScopes = authorizedScopes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If extra has any keys, base64 encode it
|
|
122
|
+
if (Object.keys(extra).length) {
|
|
123
|
+
return window.btoa(JSON.stringify(extra));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/* eslint-enable consistent-return */
|
|
127
|
+
|
|
128
|
+
buildEndpoint(entry, args) {
|
|
129
|
+
return entry.route.replace(/<([^<>]+)>/g, (text, arg) => {
|
|
130
|
+
const index = entry.args.indexOf(arg);
|
|
131
|
+
|
|
132
|
+
// Preserve original
|
|
133
|
+
if (index === -1) {
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const param = args[index];
|
|
138
|
+
const type = typeof param;
|
|
139
|
+
|
|
140
|
+
if (type !== 'string' && type !== 'number') {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`URL parameter \`${arg}\` expected a string but was provided type "${type}"`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return encodeURIComponent(param);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
buildUrl(method, ...args) {
|
|
151
|
+
if (!method) {
|
|
152
|
+
throw new Error('buildUrl is missing required `method` argument');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Find the method
|
|
156
|
+
const { entry } = method;
|
|
157
|
+
|
|
158
|
+
if (!entry || entry.type !== 'function') {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'Method in buildUrl must be an API method from the same object',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Get the query string options taken
|
|
165
|
+
const optionKeys = entry.query || [];
|
|
166
|
+
const supportsOptions = optionKeys.length !== 0;
|
|
167
|
+
const arity = entry.args.length;
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
args.length !== arity &&
|
|
171
|
+
(!supportsOptions || args.length !== arity + 1)
|
|
172
|
+
) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Method \`${entry.name}.buildUrl\` expected ${arity +
|
|
175
|
+
1} argument(s) but received ${args.length + 1}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const endpoint = this.buildEndpoint(entry, args);
|
|
180
|
+
|
|
181
|
+
if (args[arity]) {
|
|
182
|
+
Object.keys(args[arity]).forEach(key => {
|
|
183
|
+
if (!optionKeys.includes(key)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Method \`${entry.name}\` expected options ${optionKeys.join(
|
|
186
|
+
', ',
|
|
187
|
+
)} but received ${key}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const queryArgs = args[arity] && stringify(args[arity]);
|
|
194
|
+
const query = queryArgs ? `?${queryArgs}` : '';
|
|
195
|
+
|
|
196
|
+
return withRootUrl(this.options.rootUrl).api(
|
|
197
|
+
this.options.serviceName,
|
|
198
|
+
this.options.serviceVersion,
|
|
199
|
+
`${endpoint}${query}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async buildSignedUrl(method, ...args) {
|
|
204
|
+
const credentials = this.options.credentialAgent
|
|
205
|
+
? await this.options.credentialAgent.getCredentials()
|
|
206
|
+
: this.options.credentials;
|
|
207
|
+
return this._buildSignedUrlSync(method, credentials, args);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
buildSignedUrlSync(method, ...args) {
|
|
211
|
+
if (this.options.credentialAgent) {
|
|
212
|
+
throw new Error('buildSignedUrlSync cannot be used with a credentialAgent');
|
|
213
|
+
}
|
|
214
|
+
return this._buildSignedUrlSync(method, this.options.credentials, args);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_buildSignedUrlSync(method, credentials, args) {
|
|
218
|
+
if (!method) {
|
|
219
|
+
throw new Error('buildSignedUrl is missing required `method` argument');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Find reference entry
|
|
223
|
+
const { entry } = method;
|
|
224
|
+
|
|
225
|
+
if (entry.method.toLowerCase() !== 'get') {
|
|
226
|
+
throw new Error('buildSignedUrl only works for GET requests');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Default to 15 minutes before expiration
|
|
230
|
+
let expiration = 15 * 60;
|
|
231
|
+
// Check if method supports query-string options
|
|
232
|
+
const supportsOptions = (entry.query || []).length !== 0;
|
|
233
|
+
// if longer than method + args, then we have options too
|
|
234
|
+
const arity = entry.args.length + (supportsOptions ? 1 : 0);
|
|
235
|
+
|
|
236
|
+
if (args.length > arity) {
|
|
237
|
+
// Get request options
|
|
238
|
+
const options = args.pop();
|
|
239
|
+
|
|
240
|
+
if (options.expiration) {
|
|
241
|
+
// eslint-disable-next-line prefer-destructuring
|
|
242
|
+
expiration = options.expiration;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (typeof expiration !== 'number') {
|
|
246
|
+
throw new Error('options.expiration must be a number');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const url = this.buildUrl(method, ...args);
|
|
251
|
+
|
|
252
|
+
if (!credentials) {
|
|
253
|
+
throw new Error('buildSignedUrl missing required credentials');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const { clientId, accessToken } = credentials;
|
|
257
|
+
|
|
258
|
+
if (!clientId) {
|
|
259
|
+
throw new Error('buildSignedUrl missing required credentials clientId');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!accessToken) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
'buildSignedUrl missing required credentials accessToken',
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const bewit = hawk.uri.getBewit(url, {
|
|
269
|
+
credentials: {
|
|
270
|
+
id: clientId,
|
|
271
|
+
key: accessToken,
|
|
272
|
+
algorithm: 'sha256',
|
|
273
|
+
},
|
|
274
|
+
ttlSec: expiration,
|
|
275
|
+
ext: this.buildExtraData(credentials),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
return url.includes('?')
|
|
279
|
+
? `${url}&bewit=${bewit}`
|
|
280
|
+
: `${url}?bewit=${bewit}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
validate(entry, args = []) {
|
|
284
|
+
const expectedArity = this.getMethodExpectedArity(entry);
|
|
285
|
+
const queryOptions = entry.query || [];
|
|
286
|
+
const arity = args.length;
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
arity !== expectedArity &&
|
|
290
|
+
(queryOptions.length === 0 || arity !== expectedArity + 1)
|
|
291
|
+
) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`${
|
|
294
|
+
entry.name
|
|
295
|
+
} expected ${expectedArity} arguments but only received ${arity}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Object.keys(args[expectedArity] || {}).forEach(key => {
|
|
300
|
+
if (!queryOptions.includes(key)) {
|
|
301
|
+
throw new Error(`${key} is not a valid option for ${entry.name}.
|
|
302
|
+
Valid options include: ${queryOptions.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
normalizePattern(entry, pattern) {
|
|
308
|
+
const initialPattern = pattern || {};
|
|
309
|
+
|
|
310
|
+
if (!(initialPattern instanceof Object)) {
|
|
311
|
+
throw new Error('routingKeyPattern must be an object');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const routingKeyPattern = entry.routingKey
|
|
315
|
+
.map(key => {
|
|
316
|
+
const value = key.constant || initialPattern[key.name];
|
|
317
|
+
|
|
318
|
+
if (typeof value === 'number') {
|
|
319
|
+
return `${value}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (typeof value === 'string') {
|
|
323
|
+
if (value.includes('.') && !key.multipleWords) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`routingKeyPattern "${value}" for ${
|
|
326
|
+
key.name
|
|
327
|
+
} cannot contain dots since it does not hold multiple words`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (value != null) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`routingKey value "${value}" is not a valid pattern for ${key.name}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return key.multipleWords ? '#' : '*';
|
|
341
|
+
})
|
|
342
|
+
.join('.');
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
routingKeyPattern,
|
|
346
|
+
routingKeyReference: entry.routingKey.map(item => ({ ...item })),
|
|
347
|
+
exchange: `${this.options.exchangePrefix}${entry.exchange}`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async request(entry, args) {
|
|
352
|
+
const expectedArity = this.getMethodExpectedArity(entry);
|
|
353
|
+
const endpoint = this.buildEndpoint(entry, args);
|
|
354
|
+
const query = args[expectedArity]
|
|
355
|
+
? `?${stringify(args[expectedArity])}`
|
|
356
|
+
: '';
|
|
357
|
+
const url = withRootUrl(this.options.rootUrl).api(
|
|
358
|
+
this.options.serviceName,
|
|
359
|
+
this.options.serviceVersion,
|
|
360
|
+
`${endpoint}${query}`,
|
|
361
|
+
);
|
|
362
|
+
const options = { method: entry.method };
|
|
363
|
+
const credentials = this.options.credentialAgent
|
|
364
|
+
? await this.options.credentialAgent.getCredentials()
|
|
365
|
+
: this.options.credentials;
|
|
366
|
+
|
|
367
|
+
if (entry.input) {
|
|
368
|
+
options.body = JSON.stringify(args[expectedArity - 1]);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (credentials) {
|
|
372
|
+
options.credentials = credentials;
|
|
373
|
+
options.extra = this.buildExtraData(credentials);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return fetch(url, options);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
Client.create = (reference) =>
|
|
381
|
+
class extends Client {
|
|
382
|
+
constructor(options) {
|
|
383
|
+
super({ ...options, reference });
|
|
384
|
+
}
|
|
385
|
+
};
|