effortless-aws 0.33.0 → 0.33.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/dist/{chunk-HGSMOO4A.js → chunk-6HFS224S.js} +173 -34
- package/dist/index.d.ts +111 -27
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/wrap-api.js +9 -2
- package/dist/runtime/wrap-bucket.js +14 -3
- package/dist/runtime/wrap-cron.js +1 -1
- package/dist/runtime/wrap-fifo-queue.js +1 -1
- package/dist/runtime/wrap-table-stream.js +1 -1
- package/dist/runtime/wrap-worker.js +1 -1
- package/package.json +2 -2
|
@@ -233,9 +233,6 @@ var createTableClient = (tableName, options) => {
|
|
|
233
233
|
};
|
|
234
234
|
};
|
|
235
235
|
|
|
236
|
-
// src/handlers/auth.ts
|
|
237
|
-
import * as crypto from "crypto";
|
|
238
|
-
|
|
239
236
|
// src/handlers/handler-options.ts
|
|
240
237
|
var toSeconds = (d) => {
|
|
241
238
|
if (typeof d === "number") return d;
|
|
@@ -250,10 +247,35 @@ var toSeconds = (d) => {
|
|
|
250
247
|
};
|
|
251
248
|
|
|
252
249
|
// src/handlers/auth.ts
|
|
250
|
+
import * as crypto from "crypto";
|
|
251
|
+
var cfBase64Encode = (buffer) => buffer.toString("base64").replace(/\+/g, "-").replace(/=/g, "_").replace(/\//g, "~");
|
|
252
|
+
var signCfCookies = (policy, config) => {
|
|
253
|
+
const ttlSeconds = toSeconds(policy.ttl);
|
|
254
|
+
const expireTime = Math.floor(Date.now() / 1e3) + ttlSeconds;
|
|
255
|
+
const resource = config.domain === "*" ? `https://*${policy.path}` : `https://${config.domain}${policy.path}`;
|
|
256
|
+
const policyJson = JSON.stringify({
|
|
257
|
+
Statement: [{
|
|
258
|
+
Resource: resource,
|
|
259
|
+
Condition: {
|
|
260
|
+
DateLessThan: { "AWS:EpochTime": expireTime }
|
|
261
|
+
}
|
|
262
|
+
}]
|
|
263
|
+
});
|
|
264
|
+
const policyBase64 = cfBase64Encode(Buffer.from(policyJson, "utf-8"));
|
|
265
|
+
const signature = cfBase64Encode(
|
|
266
|
+
crypto.sign("sha1", Buffer.from(policyJson, "utf-8"), config.privateKey)
|
|
267
|
+
);
|
|
268
|
+
const cookieAttrs = `; Secure; SameSite=Lax; Path=/; Max-Age=${ttlSeconds}`;
|
|
269
|
+
return [
|
|
270
|
+
`CloudFront-Policy=${policyBase64}${cookieAttrs}`,
|
|
271
|
+
`CloudFront-Signature=${signature}${cookieAttrs}`,
|
|
272
|
+
`CloudFront-Key-Pair-Id=${config.keyPairId}${cookieAttrs}`
|
|
273
|
+
];
|
|
274
|
+
};
|
|
253
275
|
var AUTH_COOKIE_NAME = "__eff_session";
|
|
254
|
-
var createAuthRuntime = (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeader, apiTokenCacheTtlSeconds) => {
|
|
276
|
+
var createAuthRuntime = (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeader, apiTokenCacheTtlSeconds, cfSigningConfig) => {
|
|
255
277
|
const tokenCache = apiTokenCacheTtlSeconds ? /* @__PURE__ */ new Map() : void 0;
|
|
256
|
-
const
|
|
278
|
+
const sign2 = (payload) => crypto.createHmac("sha256", secret).update(payload).digest("base64url");
|
|
257
279
|
const cookieBase = `${AUTH_COOKIE_NAME}=`;
|
|
258
280
|
const cookieAttrs = "; HttpOnly; Secure; SameSite=Lax; Path=/";
|
|
259
281
|
const decodeSession = (cookieValue) => {
|
|
@@ -262,7 +284,7 @@ var createAuthRuntime = (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeade
|
|
|
262
284
|
if (dot === -1) return void 0;
|
|
263
285
|
const payload = cookieValue.slice(0, dot);
|
|
264
286
|
const sig = cookieValue.slice(dot + 1);
|
|
265
|
-
if (
|
|
287
|
+
if (sign2(payload) !== sig) return void 0;
|
|
266
288
|
try {
|
|
267
289
|
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
|
|
268
290
|
if (parsed.exp <= Math.floor(Date.now() / 1e3)) return void 0;
|
|
@@ -284,13 +306,16 @@ var createAuthRuntime = (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeade
|
|
|
284
306
|
const seconds = options?.expiresIn ? toSeconds(options.expiresIn) : defaultExpiresIn;
|
|
285
307
|
const exp = Math.floor(Date.now() / 1e3) + seconds;
|
|
286
308
|
const payload = Buffer.from(JSON.stringify({ exp, ...data }), "utf-8").toString("base64url");
|
|
287
|
-
const sig =
|
|
309
|
+
const sig = sign2(payload);
|
|
310
|
+
const sessionCookie = `${cookieBase}${payload}.${sig}${cookieAttrs}; Max-Age=${seconds}`;
|
|
311
|
+
const cfCookies = options?.cdnPolicy && cfSigningConfig ? signCfCookies(options.cdnPolicy, cfSigningConfig) : void 0;
|
|
288
312
|
return {
|
|
289
313
|
status: 200,
|
|
290
314
|
body: { ok: true },
|
|
291
315
|
headers: {
|
|
292
|
-
"set-cookie":
|
|
293
|
-
}
|
|
316
|
+
"set-cookie": sessionCookie
|
|
317
|
+
},
|
|
318
|
+
...cfCookies ? { cookies: [sessionCookie, ...cfCookies] } : {}
|
|
294
319
|
};
|
|
295
320
|
},
|
|
296
321
|
clearSession() {
|
|
@@ -327,34 +352,91 @@ var createAuthRuntime = (secret, defaultExpiresIn, apiTokenVerify, apiTokenHeade
|
|
|
327
352
|
|
|
328
353
|
// src/runtime/bucket-client.ts
|
|
329
354
|
import { S3 } from "@aws-sdk/client-s3";
|
|
330
|
-
var
|
|
331
|
-
|
|
332
|
-
|
|
355
|
+
var createBucketMethods = (getClient2, bucketName) => ({
|
|
356
|
+
bucketName,
|
|
357
|
+
async put(key, body, options) {
|
|
358
|
+
await getClient2().putObject({
|
|
359
|
+
Bucket: bucketName,
|
|
360
|
+
Key: key,
|
|
361
|
+
Body: typeof body === "string" ? Buffer.from(body) : body,
|
|
362
|
+
...options?.contentType ? { ContentType: options.contentType } : {}
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
async get(key) {
|
|
366
|
+
try {
|
|
367
|
+
const result = await getClient2().getObject({
|
|
368
|
+
Bucket: bucketName,
|
|
369
|
+
Key: key
|
|
370
|
+
});
|
|
371
|
+
const chunks = [];
|
|
372
|
+
const stream = result.Body;
|
|
373
|
+
for await (const chunk of stream) {
|
|
374
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
body: Buffer.concat(chunks),
|
|
378
|
+
contentType: result.ContentType
|
|
379
|
+
};
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof Error && (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404)) {
|
|
382
|
+
return void 0;
|
|
383
|
+
}
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
async delete(key) {
|
|
388
|
+
await getClient2().deleteObject({
|
|
389
|
+
Bucket: bucketName,
|
|
390
|
+
Key: key
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
async list(prefix) {
|
|
394
|
+
const items = [];
|
|
395
|
+
let continuationToken;
|
|
396
|
+
do {
|
|
397
|
+
const result = await getClient2().listObjectsV2({
|
|
398
|
+
Bucket: bucketName,
|
|
399
|
+
...prefix ? { Prefix: prefix } : {},
|
|
400
|
+
...continuationToken ? { ContinuationToken: continuationToken } : {}
|
|
401
|
+
});
|
|
402
|
+
for (const obj of result.Contents ?? []) {
|
|
403
|
+
if (obj.Key) {
|
|
404
|
+
items.push({
|
|
405
|
+
key: obj.Key,
|
|
406
|
+
size: obj.Size ?? 0,
|
|
407
|
+
lastModified: obj.LastModified
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
continuationToken = result.IsTruncated ? result.NextContinuationToken : void 0;
|
|
412
|
+
} while (continuationToken);
|
|
413
|
+
return items;
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
var createEntityClient = (getClient2, bucketName, entityName, cacheSeconds) => {
|
|
417
|
+
const entityKey = (id) => `${entityName}/${id}.json`;
|
|
333
418
|
return {
|
|
334
|
-
|
|
335
|
-
async put(key, body, options) {
|
|
419
|
+
async put(id, data) {
|
|
336
420
|
await getClient2().putObject({
|
|
337
421
|
Bucket: bucketName,
|
|
338
|
-
Key:
|
|
339
|
-
Body:
|
|
340
|
-
|
|
422
|
+
Key: entityKey(id),
|
|
423
|
+
Body: JSON.stringify(data),
|
|
424
|
+
ContentType: "application/json",
|
|
425
|
+
...cacheSeconds != null ? { CacheControl: `public, max-age=${cacheSeconds}` } : {}
|
|
341
426
|
});
|
|
342
427
|
},
|
|
343
|
-
async get(
|
|
428
|
+
async get(id) {
|
|
344
429
|
try {
|
|
345
430
|
const result = await getClient2().getObject({
|
|
346
431
|
Bucket: bucketName,
|
|
347
|
-
Key:
|
|
432
|
+
Key: entityKey(id)
|
|
348
433
|
});
|
|
349
434
|
const chunks = [];
|
|
350
435
|
const stream = result.Body;
|
|
351
436
|
for await (const chunk of stream) {
|
|
352
437
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
353
438
|
}
|
|
354
|
-
return
|
|
355
|
-
body: Buffer.concat(chunks),
|
|
356
|
-
contentType: result.ContentType
|
|
357
|
-
};
|
|
439
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
358
440
|
} catch (error) {
|
|
359
441
|
if (error instanceof Error && (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404)) {
|
|
360
442
|
return void 0;
|
|
@@ -362,28 +444,37 @@ var createBucketClient = (bucketName) => {
|
|
|
362
444
|
throw error;
|
|
363
445
|
}
|
|
364
446
|
},
|
|
365
|
-
async delete(
|
|
447
|
+
async delete(id) {
|
|
366
448
|
await getClient2().deleteObject({
|
|
367
449
|
Bucket: bucketName,
|
|
368
|
-
Key:
|
|
450
|
+
Key: entityKey(id)
|
|
369
451
|
});
|
|
370
452
|
},
|
|
371
|
-
async list(
|
|
453
|
+
async list() {
|
|
372
454
|
const items = [];
|
|
373
455
|
let continuationToken;
|
|
456
|
+
const prefix = `${entityName}/`;
|
|
374
457
|
do {
|
|
375
458
|
const result = await getClient2().listObjectsV2({
|
|
376
459
|
Bucket: bucketName,
|
|
377
|
-
|
|
460
|
+
Prefix: prefix,
|
|
378
461
|
...continuationToken ? { ContinuationToken: continuationToken } : {}
|
|
379
462
|
});
|
|
380
463
|
for (const obj of result.Contents ?? []) {
|
|
381
|
-
if (obj.Key)
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
464
|
+
if (!obj.Key || !obj.Key.endsWith(".json")) continue;
|
|
465
|
+
const id = obj.Key.slice(prefix.length, -".json".length);
|
|
466
|
+
try {
|
|
467
|
+
const getResult = await getClient2().getObject({
|
|
468
|
+
Bucket: bucketName,
|
|
469
|
+
Key: obj.Key
|
|
386
470
|
});
|
|
471
|
+
const chunks = [];
|
|
472
|
+
const stream = getResult.Body;
|
|
473
|
+
for await (const chunk of stream) {
|
|
474
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
475
|
+
}
|
|
476
|
+
items.push({ id, data: JSON.parse(Buffer.concat(chunks).toString("utf-8")) });
|
|
477
|
+
} catch {
|
|
387
478
|
}
|
|
388
479
|
}
|
|
389
480
|
continuationToken = result.IsTruncated ? result.NextContinuationToken : void 0;
|
|
@@ -392,6 +483,21 @@ var createBucketClient = (bucketName) => {
|
|
|
392
483
|
}
|
|
393
484
|
};
|
|
394
485
|
};
|
|
486
|
+
var createBucketClient = (bucketName) => {
|
|
487
|
+
let client2 = null;
|
|
488
|
+
const getClient2 = () => client2 ??= new S3({});
|
|
489
|
+
return createBucketMethods(getClient2, bucketName);
|
|
490
|
+
};
|
|
491
|
+
var createBucketClientWithEntities = (bucketName, entitiesConfig) => {
|
|
492
|
+
let client2 = null;
|
|
493
|
+
const getClient2 = () => client2 ??= new S3({});
|
|
494
|
+
const base = createBucketMethods(getClient2, bucketName);
|
|
495
|
+
const result = { ...base };
|
|
496
|
+
for (const [entityName, config] of Object.entries(entitiesConfig)) {
|
|
497
|
+
result[entityName] = createEntityClient(getClient2, bucketName, entityName, config.cacheSeconds);
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
500
|
+
};
|
|
395
501
|
|
|
396
502
|
// src/runtime/handler-utils.ts
|
|
397
503
|
import { readFileSync } from "fs";
|
|
@@ -566,7 +672,17 @@ var DEP_FACTORIES = {
|
|
|
566
672
|
const tagField = depHandler?.__spec?.tagField;
|
|
567
673
|
return createTableClient(name, tagField ? { tagField } : void 0);
|
|
568
674
|
},
|
|
569
|
-
bucket: (name) =>
|
|
675
|
+
bucket: (name, depHandler) => {
|
|
676
|
+
const entities = depHandler?.__spec?.entities;
|
|
677
|
+
if (entities && Object.keys(entities).length > 0) {
|
|
678
|
+
const config = {};
|
|
679
|
+
for (const [entityName, entityOpts] of Object.entries(entities)) {
|
|
680
|
+
config[entityName] = entityOpts.cache ? { cacheSeconds: toSeconds(entityOpts.cache) } : {};
|
|
681
|
+
}
|
|
682
|
+
return createBucketClientWithEntities(name, config);
|
|
683
|
+
}
|
|
684
|
+
return createBucketClient(name);
|
|
685
|
+
},
|
|
570
686
|
mailer: () => createEmailClient(),
|
|
571
687
|
queue: (name) => createQueueClient(name),
|
|
572
688
|
worker: (name) => createWorkerClient(name)
|
|
@@ -630,6 +746,25 @@ var createHandlerRuntime = (handler, handlerType, logLevel = "info", extraSetupA
|
|
|
630
746
|
resolvedParams = await buildParams(handler.config);
|
|
631
747
|
return resolvedParams;
|
|
632
748
|
};
|
|
749
|
+
let resolvedCfSigningConfig = null;
|
|
750
|
+
const getCfSigningConfig = async () => {
|
|
751
|
+
if (resolvedCfSigningConfig !== null) return resolvedCfSigningConfig;
|
|
752
|
+
const cfSigningKeySsmPath = process.env.EFF_CF_SIGNING_KEY;
|
|
753
|
+
const cfKeyPairId = process.env.EFF_CF_KEY_PAIR_ID;
|
|
754
|
+
const cfDomain = process.env.EFF_CF_DOMAIN;
|
|
755
|
+
if (!cfSigningKeySsmPath || !cfKeyPairId || !cfDomain) {
|
|
756
|
+
resolvedCfSigningConfig = void 0;
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
const values = await getParameters([cfSigningKeySsmPath]);
|
|
760
|
+
const privateKey = values.get(cfSigningKeySsmPath);
|
|
761
|
+
if (!privateKey) {
|
|
762
|
+
resolvedCfSigningConfig = void 0;
|
|
763
|
+
return void 0;
|
|
764
|
+
}
|
|
765
|
+
resolvedCfSigningConfig = { privateKey, keyPairId: cfKeyPairId, domain: cfDomain };
|
|
766
|
+
return resolvedCfSigningConfig;
|
|
767
|
+
};
|
|
633
768
|
const getAuthRuntime = async (setupCtx) => {
|
|
634
769
|
if (resolvedAuthRuntime !== null) return resolvedAuthRuntime;
|
|
635
770
|
const authOpts = setupCtx && typeof setupCtx === "object" && "auth" in setupCtx ? setupCtx.auth : void 0;
|
|
@@ -644,12 +779,14 @@ var createHandlerRuntime = (handler, handlerType, logLevel = "info", extraSetupA
|
|
|
644
779
|
const cacheTtlSeconds = apiToken?.cacheTtl ? toSeconds(apiToken.cacheTtl) : void 0;
|
|
645
780
|
const rawVerify = apiToken?.verify;
|
|
646
781
|
const wrappedVerify = rawVerify ? (args) => rawVerify(args.value) : void 0;
|
|
782
|
+
const cfSigningConfig = await getCfSigningConfig();
|
|
647
783
|
resolvedAuthRuntime = createAuthRuntime(
|
|
648
784
|
secret,
|
|
649
785
|
defaultExpires,
|
|
650
786
|
wrappedVerify,
|
|
651
787
|
apiToken?.header,
|
|
652
|
-
cacheTtlSeconds
|
|
788
|
+
cacheTtlSeconds,
|
|
789
|
+
cfSigningConfig
|
|
653
790
|
);
|
|
654
791
|
return resolvedAuthRuntime;
|
|
655
792
|
};
|
|
@@ -733,8 +870,10 @@ var createHandlerRuntime = (handler, handlerType, logLevel = "info", extraSetupA
|
|
|
733
870
|
|
|
734
871
|
export {
|
|
735
872
|
createTableClient,
|
|
873
|
+
toSeconds,
|
|
736
874
|
AUTH_COOKIE_NAME,
|
|
737
875
|
createBucketClient,
|
|
876
|
+
createBucketClientWithEntities,
|
|
738
877
|
buildDeps,
|
|
739
878
|
buildParams,
|
|
740
879
|
createHandlerRuntime
|
package/dist/index.d.ts
CHANGED
|
@@ -263,6 +263,12 @@ type HttpResponse = {
|
|
|
263
263
|
contentType?: ContentType;
|
|
264
264
|
/** Response headers (use for custom content-types not covered by contentType) */
|
|
265
265
|
headers?: Record<string, string>;
|
|
266
|
+
/**
|
|
267
|
+
* Multiple Set-Cookie values. Used by Lambda Function URLs to set multiple cookies
|
|
268
|
+
* in a single response (e.g., session cookie + CloudFront signed cookies).
|
|
269
|
+
* When present, takes precedence over `set-cookie` in `headers`.
|
|
270
|
+
*/
|
|
271
|
+
cookies?: string[];
|
|
266
272
|
/**
|
|
267
273
|
* Set to `true` to return binary data.
|
|
268
274
|
* When enabled, `body` must be a base64-encoded string and the response
|
|
@@ -317,7 +323,37 @@ type BucketClient = {
|
|
|
317
323
|
/** The underlying S3 bucket name */
|
|
318
324
|
bucketName: string;
|
|
319
325
|
};
|
|
326
|
+
/**
|
|
327
|
+
* Typed client for a single entity stored as JSON in a bucket.
|
|
328
|
+
* Objects are stored at `{entityName}/{id}.json`.
|
|
329
|
+
*/
|
|
330
|
+
type StoreEntityClient<T> = {
|
|
331
|
+
/** Store a JSON document by id */
|
|
332
|
+
put(id: string, data: T): Promise<void>;
|
|
333
|
+
/** Retrieve a JSON document by id. Returns undefined if not found. */
|
|
334
|
+
get(id: string): Promise<T | undefined>;
|
|
335
|
+
/** Delete a document by id */
|
|
336
|
+
delete(id: string): Promise<void>;
|
|
337
|
+
/** List all documents for this entity */
|
|
338
|
+
list(): Promise<{
|
|
339
|
+
id: string;
|
|
340
|
+
data: T;
|
|
341
|
+
}[]>;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* BucketClient extended with typed entity clients.
|
|
345
|
+
*/
|
|
346
|
+
type BucketClientWithEntities<Entities extends Record<string, any>> = BucketClient & {
|
|
347
|
+
[K in keyof Entities]: StoreEntityClient<Entities[K]>;
|
|
348
|
+
};
|
|
320
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Per-entity configuration for typed JSON key-value storage within a bucket.
|
|
352
|
+
*/
|
|
353
|
+
type BucketEntityConfig = {
|
|
354
|
+
/** Cache duration for CloudFront/browser caching (e.g., "10s", "5m", "1h"). No caching if omitted. */
|
|
355
|
+
cache?: Duration;
|
|
356
|
+
};
|
|
321
357
|
/**
|
|
322
358
|
* Configuration options for defineBucket.
|
|
323
359
|
*/
|
|
@@ -333,6 +369,8 @@ type BucketConfig = {
|
|
|
333
369
|
prefix?: string;
|
|
334
370
|
/** S3 key suffix filter for event notifications (e.g., ".jpg") */
|
|
335
371
|
suffix?: string;
|
|
372
|
+
/** Typed JSON entity definitions for key-value storage */
|
|
373
|
+
entities?: Record<string, BucketEntityConfig>;
|
|
336
374
|
};
|
|
337
375
|
/**
|
|
338
376
|
* S3 event record passed to onObjectCreated/onObjectRemoved callbacks.
|
|
@@ -354,8 +392,8 @@ type BucketEvent = {
|
|
|
354
392
|
/** Spread ctx into callback args (empty when no setup) */
|
|
355
393
|
type SpreadCtx$6<C> = [C] extends [undefined] ? {} : C & {};
|
|
356
394
|
/** Setup factory — receives bucket/deps/config/files based on what was declared */
|
|
357
|
-
type SetupArgs$6<D, P, HasFiles extends boolean> = {
|
|
358
|
-
bucket: BucketClient
|
|
395
|
+
type SetupArgs$6<D, P, HasFiles extends boolean, Entities extends Record<string, any> = {}> = {
|
|
396
|
+
bucket: {} extends Entities ? BucketClient : BucketClientWithEntities<Entities>;
|
|
359
397
|
} & ([D] extends [undefined] ? {} : {
|
|
360
398
|
deps: ResolveDeps<D>;
|
|
361
399
|
}) & ([P] extends [undefined] ? {} : {
|
|
@@ -379,7 +417,7 @@ type BucketObjectRemovedFn<C = undefined> = (args: {
|
|
|
379
417
|
* Internal handler object created by defineBucket
|
|
380
418
|
* @internal
|
|
381
419
|
*/
|
|
382
|
-
type BucketHandler$1<C = any> = {
|
|
420
|
+
type BucketHandler$1<C = any, Entities extends Record<string, any> = {}> = {
|
|
383
421
|
readonly __brand: "effortless-bucket";
|
|
384
422
|
readonly __spec: BucketConfig;
|
|
385
423
|
readonly onError?: (...args: any[]) => any;
|
|
@@ -398,31 +436,35 @@ type BucketOptions = {
|
|
|
398
436
|
/** S3 key suffix filter for event notifications (e.g., ".jpg") */
|
|
399
437
|
suffix?: string;
|
|
400
438
|
};
|
|
401
|
-
interface BucketBuilder<D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
|
|
439
|
+
interface BucketBuilder<D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false, Entities extends Record<string, any> = {}> {
|
|
402
440
|
/** Declare handler dependencies */
|
|
403
|
-
deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): BucketBuilder<D2, P, C, HasFiles>;
|
|
441
|
+
deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): BucketBuilder<D2, P, C, HasFiles, Entities>;
|
|
404
442
|
/** Declare SSM secrets */
|
|
405
|
-
config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): BucketBuilder<D, P2, C, HasFiles>;
|
|
443
|
+
config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): BucketBuilder<D, P2, C, HasFiles, Entities>;
|
|
406
444
|
/** Include static files in the Lambda ZIP */
|
|
407
|
-
include(glob: string): BucketBuilder<D, P, C, true>;
|
|
445
|
+
include(glob: string): BucketBuilder<D, P, C, true, Entities>;
|
|
446
|
+
/** Register a typed JSON entity stored as `{name}/{id}.json` in the bucket */
|
|
447
|
+
entity<N extends string, T>(name: N, options?: BucketEntityConfig): BucketBuilder<D, P, C, HasFiles, Entities & {
|
|
448
|
+
[K in N]: T;
|
|
449
|
+
}>;
|
|
408
450
|
/** Initialize shared state on cold start with lambda options */
|
|
409
|
-
setup(lambda: LambdaOptions): BucketBuilder<D, P, C, HasFiles>;
|
|
451
|
+
setup(lambda: LambdaOptions): BucketBuilder<D, P, C, HasFiles, Entities>;
|
|
410
452
|
/** Initialize shared state on cold start. Receives bucket (self-client), deps, config, files. */
|
|
411
|
-
setup<C2>(fn: (args: SetupArgs$6<D, P, HasFiles>) => C2 | Promise<C2>): BucketBuilder<D, P, C2, HasFiles>;
|
|
453
|
+
setup<C2>(fn: (args: SetupArgs$6<D, P, HasFiles, Entities>) => C2 | Promise<C2>): BucketBuilder<D, P, C2, HasFiles, Entities>;
|
|
412
454
|
/** Initialize shared state on cold start with lambda options. Receives bucket (self-client), deps, config, files. */
|
|
413
|
-
setup<C2>(fn: (args: SetupArgs$6<D, P, HasFiles>) => C2 | Promise<C2>, lambda: LambdaOptions): BucketBuilder<D, P, C2, HasFiles>;
|
|
455
|
+
setup<C2>(fn: (args: SetupArgs$6<D, P, HasFiles, Entities>) => C2 | Promise<C2>, lambda: LambdaOptions): BucketBuilder<D, P, C2, HasFiles, Entities>;
|
|
414
456
|
/** Handle errors thrown by callbacks */
|
|
415
457
|
onError(fn: (args: {
|
|
416
458
|
error: unknown;
|
|
417
|
-
} & SpreadCtx$6<C>) => void | Promise<void>): BucketBuilder<D, P, C, HasFiles>;
|
|
459
|
+
} & SpreadCtx$6<C>) => void | Promise<void>): BucketBuilder<D, P, C, HasFiles, Entities>;
|
|
418
460
|
/** Cleanup callback — runs after each invocation, before Lambda freezes */
|
|
419
|
-
onCleanup(fn: (args: SpreadCtx$6<C>) => void | Promise<void>): BucketBuilder<D, P, C, HasFiles>;
|
|
461
|
+
onCleanup(fn: (args: SpreadCtx$6<C>) => void | Promise<void>): BucketBuilder<D, P, C, HasFiles, Entities>;
|
|
420
462
|
/** Handle S3 ObjectCreated events (terminal — returns finalized handler) */
|
|
421
|
-
onObjectCreated(fn: BucketObjectCreatedFn<C>): BucketHandler$1<C>;
|
|
463
|
+
onObjectCreated(fn: BucketObjectCreatedFn<C>): BucketHandler$1<C, Entities>;
|
|
422
464
|
/** Handle S3 ObjectRemoved events (terminal — returns finalized handler) */
|
|
423
|
-
onObjectRemoved(fn: BucketObjectRemovedFn<C>): BucketHandler$1<C>;
|
|
465
|
+
onObjectRemoved(fn: BucketObjectRemovedFn<C>): BucketHandler$1<C, Entities>;
|
|
424
466
|
/** Finalize as resource-only bucket (no Lambda) */
|
|
425
|
-
build(): BucketHandler$1<C>;
|
|
467
|
+
build(): BucketHandler$1<C, Entities>;
|
|
426
468
|
}
|
|
427
469
|
/**
|
|
428
470
|
* Define an S3 bucket with optional event handlers.
|
|
@@ -926,10 +968,10 @@ type WorkerClient<T = unknown> = {
|
|
|
926
968
|
};
|
|
927
969
|
|
|
928
970
|
/** Dep value types supported by the deps declaration */
|
|
929
|
-
type AnyDepHandler = TableHandler$1<any, any> | BucketHandler$1<any> | MailerHandler | FifoQueueHandler$1<any, any> | WorkerHandler$1<any, any>;
|
|
971
|
+
type AnyDepHandler = TableHandler$1<any, any> | BucketHandler$1<any, any> | MailerHandler | FifoQueueHandler$1<any, any> | WorkerHandler$1<any, any>;
|
|
930
972
|
/** Maps a deps declaration to resolved runtime client types */
|
|
931
973
|
type ResolveDeps<D> = {
|
|
932
|
-
[K in keyof D]: D[K] extends TableHandler$1<infer T> ? TableClient<T> : D[K] extends BucketHandler$1 ? BucketClient : D[K] extends MailerHandler ? EmailClient : D[K] extends FifoQueueHandler$1<infer T> ? QueueClient<T> : D[K] extends WorkerHandler$1<infer T> ? WorkerClient<T> : never;
|
|
974
|
+
[K in keyof D]: D[K] extends TableHandler$1<infer T> ? TableClient<T> : D[K] extends BucketHandler$1<any, infer E> ? ({} extends E ? BucketClient : BucketClientWithEntities<E>) : D[K] extends MailerHandler ? EmailClient : D[K] extends FifoQueueHandler$1<infer T> ? QueueClient<T> : D[K] extends WorkerHandler$1<infer T> ? WorkerClient<T> : never;
|
|
933
975
|
};
|
|
934
976
|
|
|
935
977
|
/** DynamoDB Streams view type - determines what data is captured in stream records */
|
|
@@ -1160,6 +1202,18 @@ declare const defineApp: () => (options: AppConfig) => AppHandler;
|
|
|
1160
1202
|
type AnyRoutableHandler = {
|
|
1161
1203
|
readonly __brand: string;
|
|
1162
1204
|
};
|
|
1205
|
+
/** Route configuration for serving bucket content through CloudFront */
|
|
1206
|
+
type BucketRouteConfig = {
|
|
1207
|
+
bucket: {
|
|
1208
|
+
readonly __brand: "effortless-bucket";
|
|
1209
|
+
};
|
|
1210
|
+
/** Access control: "private" requires CloudFront signed cookies, "public" serves openly. Default: "public" */
|
|
1211
|
+
access?: "private" | "public";
|
|
1212
|
+
};
|
|
1213
|
+
/** A route value: either an API handler or a bucket route config */
|
|
1214
|
+
type RouteValue = AnyRoutableHandler | BucketRouteConfig;
|
|
1215
|
+
/** Type guard for bucket route entries */
|
|
1216
|
+
declare const isBucketRoute: (v: unknown) => v is BucketRouteConfig;
|
|
1163
1217
|
/** Simplified request object passed to middleware */
|
|
1164
1218
|
type MiddlewareRequest = {
|
|
1165
1219
|
uri: string;
|
|
@@ -1206,10 +1260,10 @@ type StaticSiteConfig = {
|
|
|
1206
1260
|
build?: string;
|
|
1207
1261
|
/** Custom domain name. Accepts a string (same domain for all stages) or a Record mapping stage names to domains (e.g., `{ prod: "example.com", dev: "dev.example.com" }`). Requires an ACM certificate in us-east-1. If the cert also covers www, a 301 redirect from www to non-www is set up automatically. */
|
|
1208
1262
|
domain?: string | Record<string, string>;
|
|
1209
|
-
/** CloudFront route overrides: path patterns forwarded to API Gateway
|
|
1210
|
-
* Keys are CloudFront path patterns (e.g., "/api/*"), values are HTTP handlers.
|
|
1211
|
-
* Example: `routes: { "/api/*": api }` */
|
|
1212
|
-
routes?: Record<string,
|
|
1263
|
+
/** CloudFront route overrides: path patterns forwarded to API Gateway or S3 bucket origins.
|
|
1264
|
+
* Keys are CloudFront path patterns (e.g., "/api/*"), values are HTTP handlers or bucket route configs.
|
|
1265
|
+
* Example: `routes: { "/api/*": api, "/files/*": { bucket: storage, access: "private" } }` */
|
|
1266
|
+
routes?: Record<string, RouteValue>;
|
|
1213
1267
|
/** Custom 404 error page path relative to `dir` (e.g. "404.html").
|
|
1214
1268
|
* For non-SPA sites only. If not set, a default page is generated automatically. */
|
|
1215
1269
|
errorPage?: string;
|
|
@@ -1236,17 +1290,27 @@ type StaticSiteHandler = {
|
|
|
1236
1290
|
*/
|
|
1237
1291
|
declare const defineStaticSite: () => (options: StaticSiteConfig) => StaticSiteHandler;
|
|
1238
1292
|
|
|
1293
|
+
/** Options for CloudFront signed cookie policy */
|
|
1294
|
+
type CdnPolicyOptions = {
|
|
1295
|
+
/** Path pattern to grant access to (e.g., "/files/users/123/*"). Supports `*` and `?` wildcards. */
|
|
1296
|
+
path: string;
|
|
1297
|
+
/** How long the CDN access is valid (e.g., "1h", "30m") */
|
|
1298
|
+
ttl: Duration;
|
|
1299
|
+
};
|
|
1239
1300
|
/** Options for creating a session */
|
|
1240
1301
|
type SessionOptions = {
|
|
1241
1302
|
expiresIn?: Duration;
|
|
1303
|
+
/** CloudFront signed cookie policy for CDN-level access control */
|
|
1304
|
+
cdnPolicy?: CdnPolicyOptions;
|
|
1242
1305
|
};
|
|
1243
|
-
/** Session response with Set-Cookie
|
|
1306
|
+
/** Session response with Set-Cookie headers */
|
|
1244
1307
|
type SessionResponse = {
|
|
1245
1308
|
status: 200;
|
|
1246
1309
|
body: {
|
|
1247
1310
|
ok: true;
|
|
1248
1311
|
};
|
|
1249
1312
|
headers: Record<string, string>;
|
|
1313
|
+
cookies?: string[];
|
|
1250
1314
|
};
|
|
1251
1315
|
/**
|
|
1252
1316
|
* Auth helpers injected into API handler callback args when `auth` is configured.
|
|
@@ -1300,12 +1364,28 @@ type OkHelper = (body?: unknown, status?: number) => HttpResponse;
|
|
|
1300
1364
|
/** Error response helper: `fail("message")` → `{ status: 400, body: { error: "message" } }` */
|
|
1301
1365
|
type FailHelper = (message: string, status?: number) => HttpResponse;
|
|
1302
1366
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
1367
|
+
/** Cache options for a GET route. Duration shorthand (e.g. "30s", "5m") or object for fine-grained control. */
|
|
1368
|
+
type CacheOptions = Duration | {
|
|
1369
|
+
ttl: Duration;
|
|
1370
|
+
swr?: Duration;
|
|
1371
|
+
scope?: "public" | "private";
|
|
1372
|
+
};
|
|
1373
|
+
/** Resolved cache config with numeric seconds */
|
|
1374
|
+
type ResolvedCache = {
|
|
1375
|
+
private?: false;
|
|
1376
|
+
ttl: number;
|
|
1377
|
+
swr: number;
|
|
1378
|
+
} | {
|
|
1379
|
+
private: true;
|
|
1380
|
+
ttl: number;
|
|
1381
|
+
};
|
|
1303
1382
|
/** Parsed route definition stored at runtime */
|
|
1304
1383
|
type RouteEntry = {
|
|
1305
1384
|
method: HttpMethod;
|
|
1306
1385
|
path: string;
|
|
1307
1386
|
onRequest: (...args: any[]) => any;
|
|
1308
1387
|
public?: boolean;
|
|
1388
|
+
cache?: ResolvedCache;
|
|
1309
1389
|
};
|
|
1310
1390
|
/** Spread ctx into route args: Omit auth config, add AuthHelpers if present */
|
|
1311
1391
|
type SpreadCtx$2<C> = ([C] extends [undefined] ? {} : Omit<C & {}, 'auth'>) & ([ExtractAuth<C>] extends [undefined] ? {} : {
|
|
@@ -1322,10 +1402,14 @@ type RouteArgs<C, ST> = SpreadCtx$2<C> & {
|
|
|
1322
1402
|
} : {});
|
|
1323
1403
|
/** Route handler function */
|
|
1324
1404
|
type RouteHandler<C, ST> = (args: RouteArgs<C, ST>) => Promise<HttpResponse | void> | HttpResponse | void;
|
|
1325
|
-
/** Route options
|
|
1405
|
+
/** Route options for all methods */
|
|
1326
1406
|
type RouteOptions = {
|
|
1327
1407
|
public?: boolean;
|
|
1328
1408
|
};
|
|
1409
|
+
/** Route options for GET — supports caching */
|
|
1410
|
+
type GetRouteOptions = RouteOptions & {
|
|
1411
|
+
cache?: CacheOptions;
|
|
1412
|
+
};
|
|
1329
1413
|
/** Setup factory — receives deps/config/files/enableAuth based on what was declared */
|
|
1330
1414
|
type SetupArgs$2<D, P, HasFiles extends boolean> = {
|
|
1331
1415
|
enableAuth: EnableAuth;
|
|
@@ -1375,7 +1459,7 @@ type ApiOptions = {
|
|
|
1375
1459
|
* Has `__brand` so CLI discovers it. Each `.get()/.post()` adds a route and returns self.
|
|
1376
1460
|
*/
|
|
1377
1461
|
interface ApiRoutes<C = undefined, ST extends boolean = false> extends ApiHandler<C> {
|
|
1378
|
-
get(path: `/${string}`, handler: RouteHandler<C, ST>, options?:
|
|
1462
|
+
get(path: `/${string}`, handler: RouteHandler<C, ST>, options?: GetRouteOptions): ApiRoutes<C, ST>;
|
|
1379
1463
|
post(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
|
|
1380
1464
|
put(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
|
|
1381
1465
|
patch(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
|
|
@@ -1410,7 +1494,7 @@ interface ApiBuilder<D = undefined, P = undefined, C = undefined, ST extends boo
|
|
|
1410
1494
|
/** Cleanup callback — runs after each invocation, before Lambda freezes */
|
|
1411
1495
|
onCleanup(fn: (args: SpreadCtx$2<C>) => void | Promise<void>): ApiBuilder<D, P, C, ST, HasFiles>;
|
|
1412
1496
|
/** Add a GET route (terminal — returns finalized handler with route methods) */
|
|
1413
|
-
get(path: `/${string}`, handler: RouteHandler<C, ST>, options?:
|
|
1497
|
+
get(path: `/${string}`, handler: RouteHandler<C, ST>, options?: GetRouteOptions): ApiRoutes<C, ST>;
|
|
1414
1498
|
/** Add a POST route (terminal) */
|
|
1415
1499
|
post(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
|
|
1416
1500
|
/** Add a PUT route (terminal) */
|
|
@@ -1685,9 +1769,9 @@ declare function defineMcp(options: McpOptions): McpBuilder;
|
|
|
1685
1769
|
|
|
1686
1770
|
type TableHandler<T = Record<string, unknown>> = TableHandler$1<T, any>;
|
|
1687
1771
|
type FifoQueueHandler<T = unknown> = FifoQueueHandler$1<T, any>;
|
|
1688
|
-
type BucketHandler = BucketHandler$1<any>;
|
|
1772
|
+
type BucketHandler<Entities extends Record<string, any> = {}> = BucketHandler$1<any, Entities>;
|
|
1689
1773
|
type CronHandler = CronHandler$1<any>;
|
|
1690
1774
|
type WorkerHandler<T = any> = WorkerHandler$1<T, any>;
|
|
1691
1775
|
type McpHandler = McpHandler$1<any>;
|
|
1692
1776
|
|
|
1693
|
-
export { type ApiAuthConfig, type ApiConfig, type ApiHandler, type ApiRoutes, type AppConfig, type AppHandler, type AuthHelpers, type BucketClient, type BucketConfig, type BucketEvent, type BucketHandler, type ConfigHelpers, type ContentType, type CronConfig, type CronHandler, type DefineSecretFn, type Duration, type EffortlessConfig, type EmailClient, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, type HttpMethod$1 as HttpMethod, type HttpRequest, type HttpResponse, type LogLevel, type MailerConfig, type MailerHandler, type McpConfig, type McpHandler, type McpInputSchema, type McpToolContent, type McpToolDef, type McpToolResult, type MiddlewareDeny, type MiddlewareHandler, type MiddlewareRedirect, type MiddlewareRequest, type MiddlewareResult, type ParamRef, type Permission, type PutInput, type PutOptions, type QueryByTagParams, type QueryParams, type QueueClient, type ResponseStream, type SecretRef, type SendEmailOptions, type SendMessageInput, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StaticSiteSeo, type StreamView, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type Timezone, type UpdateActions, type WorkerClient, type WorkerConfig, type WorkerHandler, type WorkerSendOptions, defineApi, defineApp, defineBucket, defineConfig, defineCron, defineFifoQueue, defineMailer, defineMcp, defineSecret, defineStaticSite, defineTable, defineWorker, generateBase64, generateHex, generateUuid, param, secret, toSeconds };
|
|
1777
|
+
export { type ApiAuthConfig, type ApiConfig, type ApiHandler, type ApiRoutes, type AppConfig, type AppHandler, type AuthHelpers, type BucketClient, type BucketClientWithEntities, type BucketConfig, type BucketEntityConfig, type BucketEvent, type BucketHandler, type BucketRouteConfig, type CacheOptions, type CdnPolicyOptions, type ConfigHelpers, type ContentType, type CronConfig, type CronHandler, type DefineSecretFn, type Duration, type EffortlessConfig, type EmailClient, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, type HttpMethod$1 as HttpMethod, type HttpRequest, type HttpResponse, type LogLevel, type MailerConfig, type MailerHandler, type McpConfig, type McpHandler, type McpInputSchema, type McpToolContent, type McpToolDef, type McpToolResult, type MiddlewareDeny, type MiddlewareHandler, type MiddlewareRedirect, type MiddlewareRequest, type MiddlewareResult, type ParamRef, type Permission, type PutInput, type PutOptions, type QueryByTagParams, type QueryParams, type QueueClient, type ResponseStream, type SecretRef, type SendEmailOptions, type SendMessageInput, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StaticSiteSeo, type StoreEntityClient, type StreamView, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type Timezone, type UpdateActions, type WorkerClient, type WorkerConfig, type WorkerHandler, type WorkerSendOptions, defineApi, defineApp, defineBucket, defineConfig, defineCron, defineFifoQueue, defineMailer, defineMcp, defineSecret, defineStaticSite, defineTable, defineWorker, generateBase64, generateHex, generateUuid, isBucketRoute, param, secret, toSeconds };
|
package/dist/index.js
CHANGED
|
@@ -116,6 +116,7 @@ var defineApp = () => (options) => ({
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
// src/handlers/define-static-site.ts
|
|
119
|
+
var isBucketRoute = (v) => v != null && typeof v === "object" && "bucket" in v && v.bucket != null && typeof v.bucket === "object" && v.bucket.__brand === "effortless-bucket";
|
|
119
120
|
var defineStaticSite = () => (options) => ({
|
|
120
121
|
__brand: "effortless-static-site",
|
|
121
122
|
__spec: options
|
|
@@ -231,6 +232,13 @@ function defineBucket(options) {
|
|
|
231
232
|
state.static = [...state.static ?? [], glob];
|
|
232
233
|
return builder;
|
|
233
234
|
},
|
|
235
|
+
entity(name, options2) {
|
|
236
|
+
state.spec = {
|
|
237
|
+
...state.spec,
|
|
238
|
+
entities: { ...state.spec.entities, [name]: options2 ?? {} }
|
|
239
|
+
};
|
|
240
|
+
return builder;
|
|
241
|
+
},
|
|
234
242
|
setup(fnOrLambda, maybeLambda) {
|
|
235
243
|
if (typeof fnOrLambda === "function") {
|
|
236
244
|
state.setup = fnOrLambda;
|
|
@@ -270,6 +278,20 @@ var defineMailer = () => (options) => ({
|
|
|
270
278
|
});
|
|
271
279
|
|
|
272
280
|
// src/handlers/define-api.ts
|
|
281
|
+
var resolveCache = (cache) => {
|
|
282
|
+
if (typeof cache === "number" || typeof cache === "string") {
|
|
283
|
+
const ttl2 = toSeconds(cache);
|
|
284
|
+
return { ttl: ttl2, swr: ttl2 * 2 };
|
|
285
|
+
}
|
|
286
|
+
const ttl = toSeconds(cache.ttl);
|
|
287
|
+
if (cache.scope === "private") {
|
|
288
|
+
return { private: true, ttl };
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
ttl,
|
|
292
|
+
swr: cache.swr != null ? toSeconds(cache.swr) : ttl * 2
|
|
293
|
+
};
|
|
294
|
+
};
|
|
273
295
|
function defineApi(options) {
|
|
274
296
|
const { basePath, stream } = options;
|
|
275
297
|
const state = {
|
|
@@ -280,11 +302,13 @@ function defineApi(options) {
|
|
|
280
302
|
routes: []
|
|
281
303
|
};
|
|
282
304
|
const addRoute = (method, path, handler, opts) => {
|
|
305
|
+
const routeCache = opts?.cache != null ? resolveCache(opts.cache) : void 0;
|
|
283
306
|
state.routes.push({
|
|
284
307
|
method,
|
|
285
308
|
path,
|
|
286
309
|
onRequest: handler,
|
|
287
|
-
...opts?.public ? { public: true } : {}
|
|
310
|
+
...opts?.public ? { public: true } : {},
|
|
311
|
+
...routeCache ? { cache: routeCache } : {}
|
|
288
312
|
});
|
|
289
313
|
};
|
|
290
314
|
const applyLambdaOptions = (lambda) => {
|
|
@@ -567,6 +591,7 @@ export {
|
|
|
567
591
|
generateBase64,
|
|
568
592
|
generateHex,
|
|
569
593
|
generateUuid,
|
|
594
|
+
isBucketRoute,
|
|
570
595
|
param,
|
|
571
596
|
secret,
|
|
572
597
|
toSeconds
|