firebase-functions 6.1.1 → 6.2.0
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.
|
@@ -84,14 +84,32 @@ export interface CallableRequest<T = any> {
|
|
|
84
84
|
* The raw request handled by the callable.
|
|
85
85
|
*/
|
|
86
86
|
rawRequest: Request;
|
|
87
|
+
/**
|
|
88
|
+
* Whether this is a streaming request.
|
|
89
|
+
* Code can be optimized by not trying to generate a stream of chunks to
|
|
90
|
+
* call response.sendChunk on if request.acceptsStreaming is false.
|
|
91
|
+
* It is always safe, however, to call response.sendChunk as this will
|
|
92
|
+
* noop if acceptsStreaming is false.
|
|
93
|
+
*/
|
|
94
|
+
acceptsStreaming: boolean;
|
|
87
95
|
}
|
|
88
96
|
/**
|
|
89
|
-
* CallableProxyResponse
|
|
90
|
-
*
|
|
97
|
+
* CallableProxyResponse allows streaming response chunks and listening to signals
|
|
98
|
+
* triggered in events such as a disconnect.
|
|
91
99
|
*/
|
|
92
|
-
export interface
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
export interface CallableResponse<T = unknown> {
|
|
101
|
+
/**
|
|
102
|
+
* Writes a chunk of the response body to the client. This method can be called
|
|
103
|
+
* multiple times to stream data progressively.
|
|
104
|
+
* Returns a promise of whether the data was written. This can be false, for example,
|
|
105
|
+
* if the request was not a streaming request. Rejects if there is a network error.
|
|
106
|
+
*/
|
|
107
|
+
sendChunk: (chunk: T) => Promise<boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* An AbortSignal that is triggered when the client disconnects or the
|
|
110
|
+
* request is terminated prematurely.
|
|
111
|
+
*/
|
|
112
|
+
signal: AbortSignal;
|
|
95
113
|
}
|
|
96
114
|
/**
|
|
97
115
|
* The set of Firebase Functions status codes. The codes are the same at the
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
// SOFTWARE.
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.onCallHandler = exports.checkAuthToken = exports.unsafeDecodeAppCheckToken = exports.unsafeDecodeIdToken = exports.unsafeDecodeToken = exports.decode = exports.encode = exports.isValidRequest = exports.HttpsError = exports.ORIGINAL_AUTH_HEADER = exports.CALLABLE_AUTH_HEADER = void 0;
|
|
24
|
+
exports.onCallHandler = exports.checkAuthToken = exports.unsafeDecodeAppCheckToken = exports.unsafeDecodeIdToken = exports.unsafeDecodeToken = exports.decode = exports.encode = exports.isValidRequest = exports.HttpsError = exports.DEFAULT_HEARTBEAT_SECONDS = exports.ORIGINAL_AUTH_HEADER = exports.CALLABLE_AUTH_HEADER = void 0;
|
|
25
25
|
const cors = require("cors");
|
|
26
26
|
const logger = require("../../logger");
|
|
27
27
|
// TODO(inlined): Decide whether we want to un-version apps or whether we want a
|
|
@@ -35,6 +35,8 @@ const JWT_REGEX = /^[a-zA-Z0-9\-_=]+?\.[a-zA-Z0-9\-_=]+?\.([a-zA-Z0-9\-_=]+)?$/;
|
|
|
35
35
|
exports.CALLABLE_AUTH_HEADER = "x-callable-context-auth";
|
|
36
36
|
/** @internal */
|
|
37
37
|
exports.ORIGINAL_AUTH_HEADER = "x-original-auth";
|
|
38
|
+
/** @internal */
|
|
39
|
+
exports.DEFAULT_HEARTBEAT_SECONDS = 30;
|
|
38
40
|
/**
|
|
39
41
|
* Standard error codes and HTTP statuses for different ways a request can fail,
|
|
40
42
|
* as defined by:
|
|
@@ -281,13 +283,9 @@ async function checkTokens(req, ctx, options) {
|
|
|
281
283
|
app: "INVALID",
|
|
282
284
|
auth: "INVALID",
|
|
283
285
|
};
|
|
284
|
-
await Promise.all([
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}),
|
|
288
|
-
Promise.resolve().then(async () => {
|
|
289
|
-
verifications.app = await checkAppCheckToken(req, ctx, options);
|
|
290
|
-
}),
|
|
286
|
+
[verifications.auth, verifications.app] = await Promise.all([
|
|
287
|
+
checkAuthToken(req, ctx),
|
|
288
|
+
checkAppCheckToken(req, ctx, options),
|
|
291
289
|
]);
|
|
292
290
|
const logPayload = {
|
|
293
291
|
verifications,
|
|
@@ -403,6 +401,31 @@ function encodeSSE(data) {
|
|
|
403
401
|
/** @internal */
|
|
404
402
|
function wrapOnCallHandler(options, handler, version) {
|
|
405
403
|
return async (req, res) => {
|
|
404
|
+
var _a;
|
|
405
|
+
const abortController = new AbortController();
|
|
406
|
+
let heartbeatInterval = null;
|
|
407
|
+
const heartbeatSeconds = options.heartbeatSeconds === undefined ? exports.DEFAULT_HEARTBEAT_SECONDS : options.heartbeatSeconds;
|
|
408
|
+
const clearScheduledHeartbeat = () => {
|
|
409
|
+
if (heartbeatInterval) {
|
|
410
|
+
clearTimeout(heartbeatInterval);
|
|
411
|
+
heartbeatInterval = null;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const scheduleHeartbeat = () => {
|
|
415
|
+
clearScheduledHeartbeat();
|
|
416
|
+
if (!abortController.signal.aborted) {
|
|
417
|
+
heartbeatInterval = setTimeout(() => {
|
|
418
|
+
if (!abortController.signal.aborted) {
|
|
419
|
+
res.write(": ping\n");
|
|
420
|
+
scheduleHeartbeat();
|
|
421
|
+
}
|
|
422
|
+
}, heartbeatSeconds * 1000);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
res.on("close", () => {
|
|
426
|
+
clearScheduledHeartbeat();
|
|
427
|
+
abortController.abort();
|
|
428
|
+
});
|
|
406
429
|
try {
|
|
407
430
|
if (!isValidRequest(req)) {
|
|
408
431
|
logger.error("Invalid request, unable to process.");
|
|
@@ -461,6 +484,12 @@ function wrapOnCallHandler(options, handler, version) {
|
|
|
461
484
|
throw new HttpsError("invalid-argument", "Unsupported Accept header 'text/event-stream'");
|
|
462
485
|
}
|
|
463
486
|
const data = decode(req.body.data);
|
|
487
|
+
if (options.authPolicy) {
|
|
488
|
+
const authorized = await options.authPolicy((_a = context.auth) !== null && _a !== void 0 ? _a : null, data);
|
|
489
|
+
if (!authorized) {
|
|
490
|
+
throw new HttpsError("permission-denied", "Permission Denied");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
464
493
|
let result;
|
|
465
494
|
if (version === "gcfv1") {
|
|
466
495
|
result = await handler(data, context);
|
|
@@ -469,53 +498,92 @@ function wrapOnCallHandler(options, handler, version) {
|
|
|
469
498
|
const arg = {
|
|
470
499
|
...context,
|
|
471
500
|
data,
|
|
501
|
+
acceptsStreaming,
|
|
472
502
|
};
|
|
473
|
-
// TODO: set up optional heartbeat
|
|
474
503
|
const responseProxy = {
|
|
475
|
-
|
|
476
|
-
if (acceptsStreaming) {
|
|
477
|
-
const formattedData = encodeSSE({ message: chunk });
|
|
478
|
-
return res.write(formattedData);
|
|
479
|
-
}
|
|
504
|
+
sendChunk(chunk) {
|
|
480
505
|
// if client doesn't accept sse-protocol, response.write() is no-op.
|
|
506
|
+
if (!acceptsStreaming) {
|
|
507
|
+
return Promise.resolve(false);
|
|
508
|
+
}
|
|
509
|
+
// if connection is already closed, response.write() is no-op.
|
|
510
|
+
if (abortController.signal.aborted) {
|
|
511
|
+
return Promise.resolve(false);
|
|
512
|
+
}
|
|
513
|
+
const formattedData = encodeSSE({ message: chunk });
|
|
514
|
+
let resolve;
|
|
515
|
+
let reject;
|
|
516
|
+
const p = new Promise((res, rej) => {
|
|
517
|
+
resolve = res;
|
|
518
|
+
reject = rej;
|
|
519
|
+
});
|
|
520
|
+
const wrote = res.write(formattedData, (error) => {
|
|
521
|
+
if (error) {
|
|
522
|
+
reject(error);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
resolve(wrote);
|
|
526
|
+
});
|
|
527
|
+
// Reset heartbeat timer after successful write
|
|
528
|
+
if (wrote && heartbeatInterval !== null && heartbeatSeconds > 0) {
|
|
529
|
+
scheduleHeartbeat();
|
|
530
|
+
}
|
|
531
|
+
return p;
|
|
481
532
|
},
|
|
482
|
-
|
|
533
|
+
signal: abortController.signal,
|
|
483
534
|
};
|
|
484
535
|
if (acceptsStreaming) {
|
|
485
536
|
// SSE always responds with 200
|
|
486
537
|
res.status(200);
|
|
538
|
+
if (heartbeatSeconds !== null && heartbeatSeconds > 0) {
|
|
539
|
+
scheduleHeartbeat();
|
|
540
|
+
}
|
|
487
541
|
}
|
|
488
542
|
// For some reason the type system isn't picking up that the handler
|
|
489
543
|
// is a one argument function.
|
|
490
544
|
result = await handler(arg, responseProxy);
|
|
545
|
+
clearScheduledHeartbeat();
|
|
491
546
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
547
|
+
if (!abortController.signal.aborted) {
|
|
548
|
+
// Encode the result as JSON to preserve types like Dates.
|
|
549
|
+
result = encode(result);
|
|
550
|
+
// If there was some result, encode it in the body.
|
|
551
|
+
const responseBody = { result };
|
|
552
|
+
if (acceptsStreaming) {
|
|
553
|
+
res.write(encodeSSE(responseBody));
|
|
554
|
+
res.end();
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
res.status(200).send(responseBody);
|
|
558
|
+
}
|
|
499
559
|
}
|
|
500
560
|
else {
|
|
501
|
-
res.
|
|
561
|
+
res.end();
|
|
502
562
|
}
|
|
503
563
|
}
|
|
504
564
|
catch (err) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
565
|
+
if (!abortController.signal.aborted) {
|
|
566
|
+
let httpErr = err;
|
|
567
|
+
if (!(err instanceof HttpsError)) {
|
|
568
|
+
// This doesn't count as an 'explicit' error.
|
|
569
|
+
logger.error("Unhandled error", err);
|
|
570
|
+
httpErr = new HttpsError("internal", "INTERNAL");
|
|
571
|
+
}
|
|
572
|
+
const { status } = httpErr.httpErrorCode;
|
|
573
|
+
const body = { error: httpErr.toJSON() };
|
|
574
|
+
if (version === "gcfv2" && req.header("accept") === "text/event-stream") {
|
|
575
|
+
res.send(encodeSSE(body));
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
res.status(status).send(body);
|
|
579
|
+
}
|
|
515
580
|
}
|
|
516
581
|
else {
|
|
517
|
-
res.
|
|
582
|
+
res.end();
|
|
518
583
|
}
|
|
519
584
|
}
|
|
585
|
+
finally {
|
|
586
|
+
clearScheduledHeartbeat();
|
|
587
|
+
}
|
|
520
588
|
};
|
|
521
589
|
}
|
package/lib/runtime/loader.js
CHANGED
|
@@ -40,8 +40,8 @@ async function loadModule(functionsDir) {
|
|
|
40
40
|
return require(path.resolve(absolutePath));
|
|
41
41
|
}
|
|
42
42
|
catch (e) {
|
|
43
|
-
if (e.code === "ERR_REQUIRE_ESM") {
|
|
44
|
-
// This is an ESM package!
|
|
43
|
+
if (e.code === "ERR_REQUIRE_ESM" || e.code === "ERR_REQUIRE_ASYNC_MODULE") {
|
|
44
|
+
// This is an ESM package, or one containing top-level awaits!
|
|
45
45
|
const modulePath = require.resolve(absolutePath);
|
|
46
46
|
// Resolve module path to file:// URL. Required for windows support.
|
|
47
47
|
const moduleURL = url.pathToFileURL(modulePath).href;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as express from "express";
|
|
2
2
|
import { ResetValue } from "../../common/options";
|
|
3
|
-
import { CallableRequest,
|
|
3
|
+
import { CallableRequest, CallableResponse, FunctionsErrorCode, HttpsError, Request, AuthData } from "../../common/providers/https";
|
|
4
4
|
import { ManifestEndpoint } from "../../runtime/manifest";
|
|
5
5
|
import { GlobalOptions, SupportedRegion } from "../options";
|
|
6
6
|
import { Expression } from "../../params";
|
|
@@ -101,7 +101,7 @@ export interface HttpsOptions extends Omit<GlobalOptions, "region" | "enforceApp
|
|
|
101
101
|
/**
|
|
102
102
|
* Options that can be set on a callable HTTPS function.
|
|
103
103
|
*/
|
|
104
|
-
export interface CallableOptions extends HttpsOptions {
|
|
104
|
+
export interface CallableOptions<T = any> extends HttpsOptions {
|
|
105
105
|
/**
|
|
106
106
|
* Determines whether Firebase AppCheck is enforced.
|
|
107
107
|
* When true, requests with invalid tokens autorespond with a 401
|
|
@@ -132,7 +132,29 @@ export interface CallableOptions extends HttpsOptions {
|
|
|
132
132
|
* further decisions, such as requiring additional security checks or rejecting the request.
|
|
133
133
|
*/
|
|
134
134
|
consumeAppCheckToken?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Time in seconds between sending heartbeat messages to keep the connection
|
|
137
|
+
* alive. Set to `null` to disable heartbeats.
|
|
138
|
+
*
|
|
139
|
+
* Defaults to 30 seconds.
|
|
140
|
+
*/
|
|
141
|
+
heartbeatSeconds?: number | null;
|
|
142
|
+
/**
|
|
143
|
+
* Callback for whether a request is authorized.
|
|
144
|
+
*
|
|
145
|
+
* Designed to allow reusable auth policies to be passed as an options object. Two built-in reusable policies exist:
|
|
146
|
+
* isSignedIn and hasClaim.
|
|
147
|
+
*/
|
|
148
|
+
authPolicy?: (auth: AuthData | null, data: T) => boolean | Promise<boolean>;
|
|
135
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* An auth policy that requires a user to be signed in.
|
|
152
|
+
*/
|
|
153
|
+
export declare const isSignedIn: () => (auth: AuthData | null) => boolean;
|
|
154
|
+
/**
|
|
155
|
+
* An auth policy that requires a user to be both signed in and have a specific claim (optionally with a specific value)
|
|
156
|
+
*/
|
|
157
|
+
export declare const hasClaim: (claim: string, value?: string) => (auth: AuthData | null) => boolean;
|
|
136
158
|
/**
|
|
137
159
|
* Handles HTTPS requests.
|
|
138
160
|
*/
|
|
@@ -149,12 +171,16 @@ res: express.Response) => void | Promise<void>) & {
|
|
|
149
171
|
/**
|
|
150
172
|
* Creates a callable method for clients to call using a Firebase SDK.
|
|
151
173
|
*/
|
|
152
|
-
export interface CallableFunction<T, Return> extends HttpsFunction {
|
|
174
|
+
export interface CallableFunction<T, Return, Stream = unknown> extends HttpsFunction {
|
|
153
175
|
/** Executes the handler function with the provided data as input. Used for unit testing.
|
|
154
176
|
* @param data - An input for the handler function.
|
|
155
177
|
* @returns The output of the handler function.
|
|
156
178
|
*/
|
|
157
|
-
run(
|
|
179
|
+
run(request: CallableRequest<T>): Return;
|
|
180
|
+
stream(request: CallableRequest<T>, response: CallableResponse<Stream>): {
|
|
181
|
+
stream: AsyncIterator<Stream>;
|
|
182
|
+
output: Return;
|
|
183
|
+
};
|
|
158
184
|
}
|
|
159
185
|
/**
|
|
160
186
|
* Handles HTTPS requests.
|
|
@@ -175,10 +201,10 @@ export declare function onRequest(handler: (request: Request, response: express.
|
|
|
175
201
|
* @param handler - A function that takes a {@link https.CallableRequest}.
|
|
176
202
|
* @returns A function that you can export and deploy.
|
|
177
203
|
*/
|
|
178
|
-
export declare function onCall<T = any, Return = any | Promise<any
|
|
204
|
+
export declare function onCall<T = any, Return = any | Promise<any>, Stream = unknown>(opts: CallableOptions<T>, handler: (request: CallableRequest<T>, response?: CallableResponse<Stream>) => Return): CallableFunction<T, Return extends Promise<unknown> ? Return : Promise<Return>, Stream>;
|
|
179
205
|
/**
|
|
180
206
|
* Declares a callable method for clients to call using a Firebase SDK.
|
|
181
207
|
* @param handler - A function that takes a {@link https.CallableRequest}.
|
|
182
208
|
* @returns A function that you can export and deploy.
|
|
183
209
|
*/
|
|
184
|
-
export declare function onCall<T = any, Return = any | Promise<any
|
|
210
|
+
export declare function onCall<T = any, Return = any | Promise<any>, Stream = unknown>(handler: (request: CallableRequest<T>, response?: CallableResponse<Stream>) => Return): CallableFunction<T, Return extends Promise<unknown> ? Return : Promise<Return>>;
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
// SOFTWARE.
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.onCall = exports.onRequest = exports.HttpsError = void 0;
|
|
24
|
+
exports.onCall = exports.onRequest = exports.hasClaim = exports.isSignedIn = exports.HttpsError = void 0;
|
|
25
25
|
/**
|
|
26
26
|
* Cloud functions to handle HTTPS request or callable RPCs.
|
|
27
27
|
* @packageDocumentation
|
|
@@ -35,6 +35,24 @@ Object.defineProperty(exports, "HttpsError", { enumerable: true, get: function (
|
|
|
35
35
|
const manifest_1 = require("../../runtime/manifest");
|
|
36
36
|
const options = require("../options");
|
|
37
37
|
const onInit_1 = require("../../common/onInit");
|
|
38
|
+
/**
|
|
39
|
+
* An auth policy that requires a user to be signed in.
|
|
40
|
+
*/
|
|
41
|
+
const isSignedIn = () => (auth) => !!auth;
|
|
42
|
+
exports.isSignedIn = isSignedIn;
|
|
43
|
+
/**
|
|
44
|
+
* An auth policy that requires a user to be both signed in and have a specific claim (optionally with a specific value)
|
|
45
|
+
*/
|
|
46
|
+
const hasClaim = (claim, value) => (auth) => {
|
|
47
|
+
if (!auth) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (!(claim in auth.token)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return !value || auth.token[claim] === value;
|
|
54
|
+
};
|
|
55
|
+
exports.hasClaim = hasClaim;
|
|
38
56
|
function onRequest(optsOrHandler, handler) {
|
|
39
57
|
let opts;
|
|
40
58
|
if (arguments.length === 1) {
|
|
@@ -131,11 +149,13 @@ function onCall(optsOrHandler, handler) {
|
|
|
131
149
|
origin = origin[0];
|
|
132
150
|
}
|
|
133
151
|
// fix the length of handler to make the call to handler consistent
|
|
134
|
-
const fixedLen = (req, resp) =>
|
|
152
|
+
const fixedLen = (req, resp) => handler(req, resp);
|
|
135
153
|
let func = (0, https_1.onCallHandler)({
|
|
136
154
|
cors: { origin, methods: "POST" },
|
|
137
155
|
enforceAppCheck: (_a = opts.enforceAppCheck) !== null && _a !== void 0 ? _a : options.getGlobalOptions().enforceAppCheck,
|
|
138
156
|
consumeAppCheckToken: opts.consumeAppCheckToken,
|
|
157
|
+
heartbeatSeconds: opts.heartbeatSeconds,
|
|
158
|
+
authPolicy: opts.authPolicy,
|
|
139
159
|
}, fixedLen, "gcfv2");
|
|
140
160
|
func = (0, trace_1.wrapTraceContext)((0, onInit_1.withInit)(func));
|
|
141
161
|
Object.defineProperty(func, "__trigger", {
|
|
@@ -174,7 +194,18 @@ function onCall(optsOrHandler, handler) {
|
|
|
174
194
|
},
|
|
175
195
|
callableTrigger: {},
|
|
176
196
|
};
|
|
197
|
+
// TODO: in the next major version, do auth/appcheck in these helper methods too.
|
|
177
198
|
func.run = (0, onInit_1.withInit)(handler);
|
|
199
|
+
func.stream = () => {
|
|
200
|
+
return {
|
|
201
|
+
stream: {
|
|
202
|
+
next() {
|
|
203
|
+
return Promise.reject("Coming soon");
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
output: Promise.reject("Coming soon"),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
178
209
|
return func;
|
|
179
210
|
}
|
|
180
211
|
exports.onCall = onCall;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firebase-functions",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Firebase SDK for Cloud Functions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"firebase",
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
"@types/nock": "^10.0.3",
|
|
288
288
|
"@types/node": "^14.18.24",
|
|
289
289
|
"@types/node-fetch": "^3.0.3",
|
|
290
|
-
"@types/sinon": "^
|
|
290
|
+
"@types/sinon": "^9.0.11",
|
|
291
291
|
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
|
292
292
|
"@typescript-eslint/parser": "^5.33.1",
|
|
293
293
|
"api-extractor-model-me": "^0.1.1",
|
|
@@ -313,7 +313,7 @@
|
|
|
313
313
|
"prettier": "^2.7.1",
|
|
314
314
|
"protobufjs-cli": "^1.1.1",
|
|
315
315
|
"semver": "^7.3.5",
|
|
316
|
-
"sinon": "^
|
|
316
|
+
"sinon": "^9.2.4",
|
|
317
317
|
"ts-node": "^10.4.0",
|
|
318
318
|
"typescript": "^4.3.5",
|
|
319
319
|
"yargs": "^15.3.1"
|