@awsless/cli 0.0.45 → 0.0.46-next.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.
@@ -0,0 +1,465 @@
1
+ // src/feature/rpc/server/handle.ts
2
+ import { ExpectedError, ViewableError } from "@awsless/lambda";
3
+ import { invokeRoute as invokeRoute2 } from "awsless";
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ // src/feature/rpc/server/auth.ts
7
+ import { toSeconds } from "@awsless/duration";
8
+ import { WeakCache } from "@awsless/weak-cache";
9
+ import { addSeconds, isFuture } from "date-fns";
10
+ import { getRouteEnv, invokeRoute } from "awsless";
11
+
12
+ // src/feature/rpc/server/validate.ts
13
+ import {
14
+ array,
15
+ duration,
16
+ json,
17
+ literal,
18
+ maxLength,
19
+ minLength,
20
+ object,
21
+ optional,
22
+ pipe,
23
+ record,
24
+ safeParse,
25
+ string,
26
+ union,
27
+ unknown
28
+ } from "@awsless/validate";
29
+ var requestSchema = object({
30
+ requestContext: object({
31
+ http: object({
32
+ method: literal("POST"),
33
+ userAgent: string(),
34
+ sourceIp: string()
35
+ })
36
+ }),
37
+ headers: record(string(), string()),
38
+ body: json(pipe(array(object({
39
+ name: pipe(string(), minLength(1), maxLength(64)),
40
+ payload: optional(record(string(), unknown()))
41
+ })), minLength(1), maxLength(10)))
42
+ });
43
+ var parseRequest = (body) => {
44
+ return safeParse(requestSchema, body);
45
+ };
46
+ var authResponseSchema = union([
47
+ object({
48
+ authorized: literal(true),
49
+ context: optional(record(string(), unknown())),
50
+ allowedFunctions: optional(array(string())),
51
+ lockKey: optional(string()),
52
+ ttl: duration()
53
+ }),
54
+ object({
55
+ authorized: literal(false)
56
+ })
57
+ ]);
58
+ var parseAuthResponse = (body) => {
59
+ return safeParse(authResponseSchema, body);
60
+ };
61
+
62
+ // src/feature/rpc/server/auth.ts
63
+ var cache = new WeakCache;
64
+ var authenticate = async (token) => {
65
+ const authRoute = getRouteEnv("AUTH");
66
+ if (!authRoute) {
67
+ return {
68
+ authorized: true,
69
+ context: {}
70
+ };
71
+ }
72
+ if (!token) {
73
+ return {
74
+ authorized: false,
75
+ reason: "No authentication token provided"
76
+ };
77
+ }
78
+ const cacheKey = `${authRoute}:${token}`;
79
+ const entry = cache.get(cacheKey);
80
+ if (entry) {
81
+ if (isFuture(entry.ttl)) {
82
+ return {
83
+ authorized: true,
84
+ context: entry.context,
85
+ lockKey: entry.lockKey,
86
+ allowedFunctions: entry.allowedFunctions
87
+ };
88
+ } else {
89
+ cache.delete(cacheKey);
90
+ }
91
+ }
92
+ let response;
93
+ try {
94
+ response = await invokeRoute(authRoute, { token });
95
+ } catch (error) {
96
+ console.error(error);
97
+ return {
98
+ authorized: false,
99
+ reason: "Invoke auth handle error"
100
+ };
101
+ }
102
+ const result = parseAuthResponse(response);
103
+ if (!result.success) {
104
+ return {
105
+ authorized: false,
106
+ reason: "Invalid auth handle response"
107
+ };
108
+ }
109
+ if (!result.output.authorized) {
110
+ return {
111
+ authorized: false,
112
+ reason: "Invalid auth token"
113
+ };
114
+ }
115
+ const now = new Date;
116
+ const ttl = addSeconds(now, Number(toSeconds(result.output.ttl)));
117
+ const context = result.output.context;
118
+ const allowedFunctions = result.output.allowedFunctions;
119
+ const lockKey = result.output.lockKey;
120
+ cache.set(cacheKey, {
121
+ ttl,
122
+ context,
123
+ lockKey,
124
+ allowedFunctions
125
+ });
126
+ return {
127
+ authorized: true,
128
+ context,
129
+ lockKey,
130
+ allowedFunctions
131
+ };
132
+ };
133
+
134
+ // src/feature/rpc/server/error.ts
135
+ var INVALID_REQUEST = (issues) => {
136
+ return {
137
+ type: "invalid-request",
138
+ message: `Invalid request payload: ${issues.at(0)?.message}`
139
+ };
140
+ };
141
+ var UNAUTHORIZED = (reason) => ({
142
+ type: "unauthorized",
143
+ message: reason ?? "Unauthorized"
144
+ });
145
+ var INTERNAL_SERVER_ERROR = {
146
+ type: "internal-server-error",
147
+ message: "Internal Server Error"
148
+ };
149
+ var TOO_MANY_REQUESTS = {
150
+ type: "too-many-requests",
151
+ message: "Only one request can be processed at a time. Please try again shortly."
152
+ };
153
+ var ONE_FUNCTION_AT_A_TIME = {
154
+ type: "one-function-at-a-time",
155
+ message: "Only one locked function is allowed to be called at a time."
156
+ };
157
+ var NO_LOCK_KEY_PROVIDED = {
158
+ type: "no-lock-key-provided",
159
+ message: "Your trying to call a locked function without providing a `lockKey`."
160
+ };
161
+ var PERMISSION_ACCESS_DENIED = {
162
+ ok: false,
163
+ error: {
164
+ type: "permission-access-denied",
165
+ message: `You do not have permissions to call this function.`
166
+ }
167
+ };
168
+ var UNKNOWN_FUNCTION_NAME = {
169
+ ok: false,
170
+ error: {
171
+ type: "unknown-function-name",
172
+ message: "Unknown function name"
173
+ }
174
+ };
175
+ var INTERNAL_FUNCTION_ERROR = {
176
+ ok: false,
177
+ error: {
178
+ type: "internal-function-error",
179
+ message: "Oops, something went wrong!"
180
+ }
181
+ };
182
+ var UNKNOWN_ERROR = {
183
+ ok: false,
184
+ error: {
185
+ type: "unknown-error",
186
+ message: "Unknown error"
187
+ }
188
+ };
189
+ var EXPECTED_ERROR = (error) => ({
190
+ ok: false,
191
+ error: {
192
+ type: error.type,
193
+ message: error.message
194
+ }
195
+ });
196
+
197
+ // src/feature/rpc/server/internal/allowed-functions.ts
198
+ import { getRouteEnv as getRouteEnv2 } from "awsless";
199
+ var allowedFunctions = (_, auth) => {
200
+ return {
201
+ ok: true,
202
+ data: getAllowedFunctions(auth)
203
+ };
204
+ };
205
+ var getAllowedFunctions = (auth) => {
206
+ if (!getRouteEnv2("AUTH")) {
207
+ return ["*"];
208
+ }
209
+ if (!auth.authorized) {
210
+ return [];
211
+ }
212
+ if (typeof auth.allowedFunctions === "undefined") {
213
+ return ["*"];
214
+ }
215
+ return auth.allowedFunctions;
216
+ };
217
+
218
+ // src/feature/rpc/server/internal/index.ts
219
+ var internalFunctions = {
220
+ allowedFunctions
221
+ };
222
+ var invokeInternalFunction = (name, payload, auth) => {
223
+ const fn = internalFunctions[name];
224
+ if (!fn) {
225
+ return UNKNOWN_FUNCTION_NAME;
226
+ }
227
+ return fn(payload, auth);
228
+ };
229
+
230
+ // src/feature/rpc/server/lock.ts
231
+ import { ConditionalCheckFailedException, deleteItem, updateItem } from "@awsless/dynamodb";
232
+ import { addSeconds as addSeconds2 } from "date-fns";
233
+ import { getRouteEnv as getRouteEnv4 } from "awsless";
234
+
235
+ // src/feature/rpc/server/table.ts
236
+ import { define, object as object2, string as string2, ttl } from "@awsless/dynamodb";
237
+ import { getRouteEnv as getRouteEnv3 } from "awsless";
238
+ var getLockTable = () => {
239
+ return define(getRouteEnv3("LOCK_TABLE") ?? "lock", {
240
+ hash: "key",
241
+ schema: object2({
242
+ key: string2(),
243
+ ttl: ttl(),
244
+ requestId: string2()
245
+ })
246
+ });
247
+ };
248
+
249
+ // src/feature/rpc/server/lock.ts
250
+ var lockRequest = async (requestId, key) => {
251
+ const timeout = parseInt(getRouteEnv4("TIMEOUT") ?? "60", 10);
252
+ const now = new Date;
253
+ const ttl2 = addSeconds2(now, timeout * 2);
254
+ try {
255
+ await updateItem(getLockTable(), { key }, {
256
+ update: (e) => [
257
+ e.requestId.set(requestId),
258
+ e.ttl.set(ttl2)
259
+ ],
260
+ when: (e) => e.or([
261
+ e.key.notExists(),
262
+ e.ttl.lt(now)
263
+ ])
264
+ });
265
+ } catch (error) {
266
+ if (error instanceof ConditionalCheckFailedException) {
267
+ return false;
268
+ }
269
+ throw error;
270
+ }
271
+ return true;
272
+ };
273
+ var unlockRequest = async (requestId, key) => {
274
+ try {
275
+ await deleteItem(getLockTable(), { key }, {
276
+ when: (e) => [
277
+ e.key.exists(),
278
+ e.requestId.eq(requestId)
279
+ ]
280
+ });
281
+ } catch (error) {
282
+ if (error instanceof ConditionalCheckFailedException) {
283
+ console.error("Failed to unlock request");
284
+ return;
285
+ }
286
+ throw error;
287
+ }
288
+ };
289
+ var lock = (requestId, key) => {
290
+ return lockRequest(requestId, key);
291
+ };
292
+ var unlock = async (requestId, key) => {
293
+ return unlockRequest(requestId, key);
294
+ };
295
+
296
+ // src/feature/rpc/server/response.ts
297
+ import { stringify } from "@awsless/json";
298
+ var response = (statusCode, results) => {
299
+ return {
300
+ headers: {
301
+ "content-type": "application/json"
302
+ },
303
+ statusCode,
304
+ body: stringify(results)
305
+ };
306
+ };
307
+
308
+ // src/feature/rpc/server/schema.ts
309
+ import { getRouteEnv as getRouteEnv5 } from "awsless";
310
+ var getFunctionDetails = (name) => {
311
+ const entry = getRouteEnv5(`QUERY:${name}`);
312
+ if (!entry) {
313
+ return;
314
+ }
315
+ const details = JSON.parse(entry);
316
+ return {
317
+ name: details.function,
318
+ lock: details.lock
319
+ };
320
+ };
321
+
322
+ // src/feature/rpc/server/viewer.ts
323
+ var toNumber = (value) => {
324
+ if (!value) {
325
+ return;
326
+ }
327
+ return parseFloat(value);
328
+ };
329
+ var stripPortFromIp = (value) => {
330
+ if (!value) {
331
+ return;
332
+ }
333
+ const parts = value.split(":");
334
+ parts.pop();
335
+ return parts.join(":");
336
+ };
337
+ var buildViewerPayload = (request) => {
338
+ const http = request.requestContext.http;
339
+ const getViewer = (name) => {
340
+ return request.headers[`cloudfront-viewer-${name}`];
341
+ };
342
+ const isViewer = (enums) => {
343
+ for (const [name, value] of Object.entries(enums)) {
344
+ if (request.headers[`cloudfront-is-${name}-viewer`] === "true") {
345
+ return value;
346
+ }
347
+ }
348
+ return;
349
+ };
350
+ return {
351
+ userAgent: http.userAgent,
352
+ ip: stripPortFromIp(getViewer("address")),
353
+ city: getViewer("city"),
354
+ region: {
355
+ code: getViewer("country-region"),
356
+ name: getViewer("country-region-name")
357
+ },
358
+ country: {
359
+ code: getViewer("country"),
360
+ name: getViewer("country-name")
361
+ },
362
+ metroCode: getViewer("metro-code"),
363
+ postalCode: getViewer("postal-code"),
364
+ timeZone: getViewer("time-zone"),
365
+ latitude: toNumber(getViewer("latitude")),
366
+ longitude: toNumber(getViewer("longitude")),
367
+ device: isViewer({
368
+ mobile: "mobile",
369
+ tablet: "tablet",
370
+ desktop: "desktop",
371
+ smarttv: "tv"
372
+ }),
373
+ os: isViewer({
374
+ ios: "ios",
375
+ android: "android"
376
+ })
377
+ };
378
+ };
379
+
380
+ // src/feature/rpc/server/handle.ts
381
+ var handle_default = async (event) => {
382
+ try {
383
+ const requestId = randomUUID();
384
+ const request = parseRequest(event);
385
+ if (!request.success) {
386
+ return response(400, INVALID_REQUEST(request.issues));
387
+ }
388
+ const auth = await authenticate(request.output.headers.authentication);
389
+ if (!auth.authorized) {
390
+ return response(405, UNAUTHORIZED(auth.reason));
391
+ }
392
+ console.log({
393
+ requestId,
394
+ lockKey: auth.lockKey,
395
+ authContext: auth.context,
396
+ request: request.output.body,
397
+ ip: request.output.requestContext.http.sourceIp
398
+ });
399
+ const calls = request.output.body.map((fn) => {
400
+ return {
401
+ ...fn,
402
+ details: fn.name.startsWith("$") ? undefined : getFunctionDetails(fn.name)
403
+ };
404
+ });
405
+ const lockedCalls = calls.filter((fn) => fn.details?.lock === true);
406
+ if (lockedCalls.length > 0 && !auth.lockKey) {
407
+ return response(400, NO_LOCK_KEY_PROVIDED);
408
+ }
409
+ if (lockedCalls.length > 1) {
410
+ return response(400, ONE_FUNCTION_AT_A_TIME);
411
+ }
412
+ if (lockedCalls.length > 0 && auth.lockKey) {
413
+ const locked = await lock(requestId, auth.lockKey);
414
+ if (!locked) {
415
+ return response(429, TOO_MANY_REQUESTS);
416
+ }
417
+ }
418
+ const result = await Promise.allSettled(calls.map(async (fn) => {
419
+ if (fn.name.startsWith("$")) {
420
+ return invokeInternalFunction(fn.name.slice(1), fn.payload, auth);
421
+ }
422
+ if (!(("details" in fn) && fn.details)) {
423
+ return UNKNOWN_FUNCTION_NAME;
424
+ }
425
+ if (Array.isArray(auth.allowedFunctions) && !auth.allowedFunctions.includes(fn.name) && !auth.allowedFunctions.includes("*")) {
426
+ return PERMISSION_ACCESS_DENIED;
427
+ }
428
+ let data;
429
+ try {
430
+ data = await invokeRoute2(fn.details.name, {
431
+ ...fn.payload ?? {},
432
+ ...auth.context ?? {},
433
+ viewer: buildViewerPayload(request.output)
434
+ });
435
+ } catch (error) {
436
+ if (error instanceof ViewableError || error instanceof ExpectedError) {
437
+ return EXPECTED_ERROR(error);
438
+ } else {
439
+ console.error(error);
440
+ return INTERNAL_FUNCTION_ERROR;
441
+ }
442
+ }
443
+ return {
444
+ ok: true,
445
+ data
446
+ };
447
+ }));
448
+ if (lockedCalls.length > 0 && auth.lockKey) {
449
+ await unlock(requestId, auth.lockKey);
450
+ }
451
+ return response(200, result.map((item) => {
452
+ if (item.status === "fulfilled") {
453
+ return item.value;
454
+ }
455
+ return UNKNOWN_ERROR;
456
+ }));
457
+ } catch (error) {
458
+ console.log("internal error");
459
+ console.error(error);
460
+ return response(500, INTERNAL_SERVER_ERROR);
461
+ }
462
+ };
463
+ export {
464
+ handle_default as default
465
+ };