@hot-updater/aws 1.2.0-rc.8 → 1.2.0-rc.9
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/index.cjs +69 -162
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +70 -163
- package/dist/lambda/index.cjs +105 -64
- package/package.json +8 -7
package/dist/index.cjs
CHANGED
|
@@ -5,8 +5,7 @@ let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
|
|
|
5
5
|
let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
|
|
6
6
|
let _aws_sdk_client_ssm = require("@aws-sdk/client-ssm");
|
|
7
7
|
let _aws_sdk_cloudfront_signer = require("@aws-sdk/cloudfront-signer");
|
|
8
|
-
let
|
|
9
|
-
let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
|
|
8
|
+
let _hot_updater_storage_s3 = require("@hot-updater/storage-s3");
|
|
10
9
|
let node_crypto = require("node:crypto");
|
|
11
10
|
//#region src/cloudFrontInvalidation.ts
|
|
12
11
|
const DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
@@ -519,156 +518,17 @@ const cloudFrontDownloadUrl = (config) => {
|
|
|
519
518
|
};
|
|
520
519
|
//#endregion
|
|
521
520
|
//#region src/s3Storage.ts
|
|
522
|
-
/** Devices download right after the update check that returns the URL. */
|
|
523
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
524
|
-
const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
|
|
525
521
|
function s3Storage(config) {
|
|
526
522
|
const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
|
|
527
|
-
|
|
528
|
-
...s3Config,
|
|
529
|
-
credentials: toSdkCredentials(credentials)
|
|
530
|
-
}));
|
|
531
|
-
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
532
|
-
const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
|
|
533
|
-
const getListPrefix = (prefix = "") => {
|
|
534
|
-
const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
|
|
535
|
-
const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
|
|
536
|
-
return value ? `${value}/` : "";
|
|
537
|
-
};
|
|
538
|
-
/**
|
|
539
|
-
* The URI `put` returns for the key, or null for a key no `put` writes,
|
|
540
|
-
* such as a folder marker that ends in "/".
|
|
541
|
-
*/
|
|
542
|
-
const getListedStorageUri = (key) => {
|
|
543
|
-
try {
|
|
544
|
-
return (0, _hot_updater_plugin_core.createStorageUri)({
|
|
545
|
-
bucket: bucketName,
|
|
546
|
-
key,
|
|
547
|
-
protocol: "s3"
|
|
548
|
-
});
|
|
549
|
-
} catch {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
};
|
|
553
|
-
const getRelativeKey = (key) => {
|
|
554
|
-
if (!normalizedBasePath) return key;
|
|
555
|
-
const basePrefix = `${normalizedBasePath}/`;
|
|
556
|
-
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
557
|
-
};
|
|
558
|
-
const parseAndValidate = (storageUri) => {
|
|
559
|
-
const parsed = (0, _hot_updater_plugin_core.parseStorageUri)(storageUri, "s3");
|
|
560
|
-
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
561
|
-
return parsed;
|
|
562
|
-
};
|
|
563
|
-
return (0, _hot_updater_plugin_core.createStorageAdapter)({
|
|
523
|
+
return (0, _hot_updater_storage_s3.createS3Storage)({
|
|
564
524
|
name: "s3Storage",
|
|
565
|
-
protocol: "s3"
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
ContinuationToken: continuationToken,
|
|
573
|
-
Prefix: getListPrefix(prefix)
|
|
574
|
-
}));
|
|
575
|
-
for (const object of response.Contents ?? []) {
|
|
576
|
-
if (!object.Key) continue;
|
|
577
|
-
const storageUri = getListedStorageUri(object.Key);
|
|
578
|
-
if (storageUri === null) continue;
|
|
579
|
-
objects.push({
|
|
580
|
-
key: getRelativeKey(object.Key),
|
|
581
|
-
lastModifiedAt: object.LastModified,
|
|
582
|
-
size: object.Size ?? 0,
|
|
583
|
-
storageUri
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
continuationToken = response.NextContinuationToken;
|
|
587
|
-
} while (continuationToken);
|
|
588
|
-
return objects;
|
|
589
|
-
},
|
|
590
|
-
async deleteObjects(relativeKeys) {
|
|
591
|
-
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
592
|
-
for (let offset = 0; offset < keys.length; offset += 1e3) {
|
|
593
|
-
const batch = keys.slice(offset, offset + 1e3);
|
|
594
|
-
if (batch.length === 0) continue;
|
|
595
|
-
const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
|
|
596
|
-
Bucket: bucketName,
|
|
597
|
-
Delete: {
|
|
598
|
-
Objects: batch.map((Key) => ({ Key })),
|
|
599
|
-
Quiet: true
|
|
600
|
-
}
|
|
601
|
-
}));
|
|
602
|
-
if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
|
|
603
|
-
}
|
|
604
|
-
},
|
|
605
|
-
async put({ key, body, contentLength, contentType }) {
|
|
606
|
-
const storageKey = getStorageKey(key);
|
|
607
|
-
const response = await new _aws_sdk_lib_storage.Upload({
|
|
608
|
-
client,
|
|
609
|
-
params: {
|
|
610
|
-
Body: body,
|
|
611
|
-
Bucket: bucketName,
|
|
612
|
-
CacheControl: "max-age=31536000",
|
|
613
|
-
...contentLength === void 0 ? {} : { ContentLength: contentLength },
|
|
614
|
-
ContentType: contentType,
|
|
615
|
-
Key: storageKey
|
|
616
|
-
}
|
|
617
|
-
}).done();
|
|
618
|
-
if (!response.Bucket || !response.Key) throw new Error("Upload failed");
|
|
619
|
-
return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
|
|
620
|
-
bucket: bucketName,
|
|
621
|
-
key: storageKey,
|
|
622
|
-
protocol: "s3"
|
|
623
|
-
}) };
|
|
624
|
-
},
|
|
625
|
-
async get({ storageUri }) {
|
|
626
|
-
const { key } = parseAndValidate(storageUri);
|
|
627
|
-
try {
|
|
628
|
-
const response = await client.send(new _aws_sdk_client_s3.GetObjectCommand({
|
|
629
|
-
Bucket: bucketName,
|
|
630
|
-
Key: key
|
|
631
|
-
}));
|
|
632
|
-
if (!response.Body) return { response: null };
|
|
633
|
-
const headers = new Headers();
|
|
634
|
-
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
635
|
-
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
636
|
-
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
637
|
-
} catch (error) {
|
|
638
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
639
|
-
throw error;
|
|
640
|
-
}
|
|
641
|
-
},
|
|
642
|
-
async getDownloadUrl({ storageUri }) {
|
|
643
|
-
const { key } = parseAndValidate(storageUri);
|
|
644
|
-
if (getDownloadUrl) return getDownloadUrl({ storageUri });
|
|
645
|
-
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
646
|
-
return { url: await getSignedUrl(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
647
|
-
Bucket: bucketName,
|
|
648
|
-
Key: key
|
|
649
|
-
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
650
|
-
},
|
|
651
|
-
async exists({ storageUri }) {
|
|
652
|
-
const { key } = parseAndValidate(storageUri);
|
|
653
|
-
try {
|
|
654
|
-
await client.send(new _aws_sdk_client_s3.HeadObjectCommand({
|
|
655
|
-
Bucket: bucketName,
|
|
656
|
-
Key: key
|
|
657
|
-
}));
|
|
658
|
-
return { exists: true };
|
|
659
|
-
} catch (error) {
|
|
660
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
661
|
-
throw error;
|
|
662
|
-
}
|
|
663
|
-
},
|
|
664
|
-
async delete({ storageUri }) {
|
|
665
|
-
const { key } = parseAndValidate(storageUri);
|
|
666
|
-
await client.send(new _aws_sdk_client_s3.DeleteObjectCommand({
|
|
667
|
-
Bucket: bucketName,
|
|
668
|
-
Key: key
|
|
669
|
-
}));
|
|
670
|
-
return { deleted: true };
|
|
671
|
-
}
|
|
525
|
+
protocol: "s3"
|
|
526
|
+
}, {
|
|
527
|
+
...applyS3RuntimeAwsConfig(s3Config),
|
|
528
|
+
credentials,
|
|
529
|
+
bucketName,
|
|
530
|
+
basePath,
|
|
531
|
+
getDownloadUrl
|
|
672
532
|
});
|
|
673
533
|
}
|
|
674
534
|
//#endregion
|
|
@@ -693,7 +553,22 @@ const EXPIRED_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
|
|
|
693
553
|
]);
|
|
694
554
|
/** Errors of the SDK finding no credentials, or an expired SSO session. */
|
|
695
555
|
const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set(["CredentialsProviderError", "TokenProviderError"]);
|
|
696
|
-
const SIGN_IN_HINT = "In CI, set them with aws-actions/configure-aws-credentials
|
|
556
|
+
const SIGN_IN_HINT = ["In CI, set them with aws-actions/configure-aws-credentials.", "On your machine: aws sso login --profile <profile>, then set AWS_PROFILE."];
|
|
557
|
+
/** The widest line a message has: the CLI prints no wider. */
|
|
558
|
+
const LINE_WIDTH = 76;
|
|
559
|
+
/** `text`, such as AWS's own message, in lines that fit, broken at spaces. */
|
|
560
|
+
const wrap = (text) => {
|
|
561
|
+
const lines = [];
|
|
562
|
+
let line = "";
|
|
563
|
+
for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
|
|
564
|
+
lines.push(line);
|
|
565
|
+
line = word;
|
|
566
|
+
} else line = line ? `${line} ${word}` : word;
|
|
567
|
+
if (line) lines.push(line);
|
|
568
|
+
return lines;
|
|
569
|
+
};
|
|
570
|
+
/** An error of one line each, for a terminal. */
|
|
571
|
+
const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
|
|
697
572
|
const errorName = (error) => error instanceof Error ? error.name : "";
|
|
698
573
|
const errorMessage = (error) => (error instanceof Error ? error.message : String(error)).replace(/\.+$/u, "");
|
|
699
574
|
const resolveEndpoint = (endpoint) => {
|
|
@@ -708,7 +583,11 @@ const resolveEndpoint = (endpoint) => {
|
|
|
708
583
|
"127.0.0.1",
|
|
709
584
|
"[::1]"
|
|
710
585
|
])).has(url.hostname);
|
|
711
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw
|
|
586
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw failure([
|
|
587
|
+
`${NAME} can't use its endpoint.`,
|
|
588
|
+
"Use HTTPS, or HTTP on a loopback address.",
|
|
589
|
+
"Leave out credentials, a query, and a fragment."
|
|
590
|
+
]);
|
|
712
591
|
return url.toString();
|
|
713
592
|
};
|
|
714
593
|
/** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
|
|
@@ -735,20 +614,20 @@ const checkCredentials = async (credentials) => {
|
|
|
735
614
|
*/
|
|
736
615
|
const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region }) => {
|
|
737
616
|
const checkSettings = () => {
|
|
738
|
-
if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw
|
|
617
|
+
if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw failure([`${NAME} can't use the region ${JSON.stringify(region)}.`, "Set an AWS region, such as \"us-east-1\"."]);
|
|
739
618
|
if (prefix !== "" && !SECRET_ID_PATTERN.test(prefix)) throw new Error(`${NAME}'s prefix takes letters, digits, and /_+=.@- only.`);
|
|
740
619
|
};
|
|
741
620
|
const secretId = (name) => {
|
|
742
621
|
checkSettings();
|
|
743
622
|
const id = `${prefix}${name}`;
|
|
744
|
-
if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw
|
|
623
|
+
if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw failure([`${NAME} can't name a secret ${JSON.stringify(id)}.`, `AWS takes up to ${MAX_SECRET_ID_LENGTH} letters, digits, and /_+=.@- characters.`]);
|
|
745
624
|
return id;
|
|
746
625
|
};
|
|
747
626
|
const resolveCredentials = () => {
|
|
748
627
|
if (credentials !== void 0) return async () => checkCredentials(typeof credentials === "function" ? await credentials() : credentials);
|
|
749
628
|
const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
|
|
750
629
|
const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
|
|
751
|
-
if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw
|
|
630
|
+
if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw failure([`${NAME} found ${accessKeyId ? "AWS_ACCESS_KEY_ID" : "AWS_SECRET_ACCESS_KEY"} without ${accessKeyId ? "AWS_SECRET_ACCESS_KEY" : "AWS_ACCESS_KEY_ID"}.`, "Set both, or neither to use a profile."]);
|
|
752
631
|
};
|
|
753
632
|
let opened;
|
|
754
633
|
const open = () => {
|
|
@@ -771,12 +650,40 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
|
|
|
771
650
|
const describeFailure = (verb, id, error, permissions) => {
|
|
772
651
|
const name = errorName(error);
|
|
773
652
|
const status = error?.$metadata?.httpStatusCode;
|
|
774
|
-
const detail = `${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}
|
|
775
|
-
|
|
776
|
-
if (
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
653
|
+
const detail = wrap(`${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`);
|
|
654
|
+
const secret = `Secret: ${id}`;
|
|
655
|
+
if (NO_CREDENTIAL_ERRORS.has(name)) return failure([
|
|
656
|
+
`${NAME} found no AWS credentials to ${verb} a secret.`,
|
|
657
|
+
secret,
|
|
658
|
+
...wrap(`${errorMessage(error)}.`),
|
|
659
|
+
...SIGN_IN_HINT
|
|
660
|
+
], error);
|
|
661
|
+
if (EXPIRED_CREDENTIAL_ERRORS.has(name)) return failure([
|
|
662
|
+
`AWS Secrets Manager refused the sign-in to ${verb} a secret.`,
|
|
663
|
+
secret,
|
|
664
|
+
...detail,
|
|
665
|
+
"They are expired or invalid: sign in again.",
|
|
666
|
+
...SIGN_IN_HINT
|
|
667
|
+
], error);
|
|
668
|
+
if (name === "AccessDeniedException") return failure([
|
|
669
|
+
`AWS Secrets Manager refused to ${verb} a secret.`,
|
|
670
|
+
secret,
|
|
671
|
+
...detail,
|
|
672
|
+
`The AWS identity needs ${permissions} on it.`,
|
|
673
|
+
`With a customer managed KMS key, it needs ${verb === "read" ? "kms:Decrypt" : "kms:GenerateDataKey"} too.`
|
|
674
|
+
], error);
|
|
675
|
+
if (/marked for deletion|scheduled for deletion/iu.test(errorMessage(error))) return failure([
|
|
676
|
+
`AWS Secrets Manager can't ${verb} a secret.`,
|
|
677
|
+
secret,
|
|
678
|
+
"It is scheduled for deletion. Restore it, or wait until it is gone:",
|
|
679
|
+
"aws secretsmanager restore-secret \\",
|
|
680
|
+
` --secret-id ${id}`
|
|
681
|
+
], error);
|
|
682
|
+
return failure([
|
|
683
|
+
`AWS Secrets Manager failed to ${verb} a secret.`,
|
|
684
|
+
secret,
|
|
685
|
+
...detail
|
|
686
|
+
], error);
|
|
780
687
|
};
|
|
781
688
|
const read = async ({ sdk, client }, id) => {
|
|
782
689
|
let output;
|
|
@@ -806,8 +713,8 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
|
|
|
806
713
|
},
|
|
807
714
|
set: async (name, value) => {
|
|
808
715
|
const id = secretId(name);
|
|
809
|
-
if (typeof value !== "string" || value === "") throw new Error(`${NAME}
|
|
810
|
-
if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw
|
|
716
|
+
if (typeof value !== "string" || value === "") throw new Error(`${NAME} can't store an empty ${id}.`);
|
|
717
|
+
if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw failure([`${NAME} can't store ${id}.`, `AWS takes up to ${MAX_SECRET_BYTES} bytes per secret.`]);
|
|
811
718
|
await mask([value]);
|
|
812
719
|
const { sdk, client } = await open();
|
|
813
720
|
const token = (0, node_crypto.randomUUID)();
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
|
|
2
2
|
import { EngineDatabase, Lazy, PluginTables, SecretsAdapter, StorageAdapter, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
3
3
|
import { S3ClientConfig } from "@aws-sdk/client-s3";
|
|
4
|
+
import { S3StorageOperations } from "@hot-updater/storage-s3";
|
|
4
5
|
//#region src/lazyCredentials.d.ts
|
|
5
6
|
type SdkCredentials = S3ClientConfig["credentials"];
|
|
6
7
|
/**
|
|
@@ -88,7 +89,6 @@ interface S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
|
|
|
88
89
|
*/
|
|
89
90
|
getDownloadUrl?: StorageAdapter["getDownloadUrl"];
|
|
90
91
|
}
|
|
91
|
-
type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
92
92
|
declare function s3Storage(config: S3StorageConfig): StorageAdapterWith<S3StorageOperations>;
|
|
93
93
|
//#endregion
|
|
94
94
|
//#region src/secretsManager.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
|
|
2
2
|
import { EngineDatabase, Lazy, PluginTables, SecretsAdapter, StorageAdapter, StorageAdapterWith } from "@hot-updater/plugin-core";
|
|
3
|
+
import { S3StorageOperations } from "@hot-updater/storage-s3";
|
|
3
4
|
import { S3ClientConfig } from "@aws-sdk/client-s3";
|
|
4
5
|
//#region src/lazyCredentials.d.ts
|
|
5
6
|
type SdkCredentials = S3ClientConfig["credentials"];
|
|
@@ -88,7 +89,6 @@ interface S3StorageConfig extends Omit<S3ClientConfig, "credentials"> {
|
|
|
88
89
|
*/
|
|
89
90
|
getDownloadUrl?: StorageAdapter["getDownloadUrl"];
|
|
90
91
|
}
|
|
91
|
-
type S3StorageOperations = "put" | "get" | "getDownloadUrl" | "exists" | "delete" | "listObjects" | "deleteObjects";
|
|
92
92
|
declare function s3Storage(config: S3StorageConfig): StorageAdapterWith<S3StorageOperations>;
|
|
93
93
|
//#endregion
|
|
94
94
|
//#region src/secretsManager.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { CreateTableCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, UpdateTimeToLiveCommand, waitUntilTableExists } from "@aws-sdk/client-dynamodb";
|
|
2
|
-
import { BUNDLE_STORAGE_PREFIX, CONTENT_ADDRESSED_ASSET_PREFIX, createEngineDatabase, createKvAdapter,
|
|
2
|
+
import { BUNDLE_STORAGE_PREFIX, CONTENT_ADDRESSED_ASSET_PREFIX, createEngineDatabase, createKvAdapter, migrateCoreSchema, resolveLazy } from "@hot-updater/plugin-core";
|
|
3
3
|
import { CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
|
|
4
4
|
import { BatchGetCommand, BatchWriteCommand, DynamoDBDocumentClient, QueryCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
|
|
5
5
|
import { SSM } from "@aws-sdk/client-ssm";
|
|
6
6
|
import { getSignedUrl } from "@aws-sdk/cloudfront-signer";
|
|
7
|
-
import {
|
|
8
|
-
import { Upload } from "@aws-sdk/lib-storage";
|
|
7
|
+
import { createS3Storage } from "@hot-updater/storage-s3";
|
|
9
8
|
import { randomUUID } from "node:crypto";
|
|
10
9
|
//#region src/cloudFrontInvalidation.ts
|
|
11
10
|
const DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
@@ -518,156 +517,17 @@ const cloudFrontDownloadUrl = (config) => {
|
|
|
518
517
|
};
|
|
519
518
|
//#endregion
|
|
520
519
|
//#region src/s3Storage.ts
|
|
521
|
-
/** Devices download right after the update check that returns the URL. */
|
|
522
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
523
|
-
const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
|
|
524
520
|
function s3Storage(config) {
|
|
525
521
|
const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
|
|
526
|
-
|
|
527
|
-
...s3Config,
|
|
528
|
-
credentials: toSdkCredentials(credentials)
|
|
529
|
-
}));
|
|
530
|
-
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
531
|
-
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
532
|
-
const getListPrefix = (prefix = "") => {
|
|
533
|
-
const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
|
|
534
|
-
const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
|
|
535
|
-
return value ? `${value}/` : "";
|
|
536
|
-
};
|
|
537
|
-
/**
|
|
538
|
-
* The URI `put` returns for the key, or null for a key no `put` writes,
|
|
539
|
-
* such as a folder marker that ends in "/".
|
|
540
|
-
*/
|
|
541
|
-
const getListedStorageUri = (key) => {
|
|
542
|
-
try {
|
|
543
|
-
return createStorageUri({
|
|
544
|
-
bucket: bucketName,
|
|
545
|
-
key,
|
|
546
|
-
protocol: "s3"
|
|
547
|
-
});
|
|
548
|
-
} catch {
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
const getRelativeKey = (key) => {
|
|
553
|
-
if (!normalizedBasePath) return key;
|
|
554
|
-
const basePrefix = `${normalizedBasePath}/`;
|
|
555
|
-
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
556
|
-
};
|
|
557
|
-
const parseAndValidate = (storageUri) => {
|
|
558
|
-
const parsed = parseStorageUri(storageUri, "s3");
|
|
559
|
-
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
560
|
-
return parsed;
|
|
561
|
-
};
|
|
562
|
-
return createStorageAdapter({
|
|
522
|
+
return createS3Storage({
|
|
563
523
|
name: "s3Storage",
|
|
564
|
-
protocol: "s3"
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
ContinuationToken: continuationToken,
|
|
572
|
-
Prefix: getListPrefix(prefix)
|
|
573
|
-
}));
|
|
574
|
-
for (const object of response.Contents ?? []) {
|
|
575
|
-
if (!object.Key) continue;
|
|
576
|
-
const storageUri = getListedStorageUri(object.Key);
|
|
577
|
-
if (storageUri === null) continue;
|
|
578
|
-
objects.push({
|
|
579
|
-
key: getRelativeKey(object.Key),
|
|
580
|
-
lastModifiedAt: object.LastModified,
|
|
581
|
-
size: object.Size ?? 0,
|
|
582
|
-
storageUri
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
continuationToken = response.NextContinuationToken;
|
|
586
|
-
} while (continuationToken);
|
|
587
|
-
return objects;
|
|
588
|
-
},
|
|
589
|
-
async deleteObjects(relativeKeys) {
|
|
590
|
-
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
591
|
-
for (let offset = 0; offset < keys.length; offset += 1e3) {
|
|
592
|
-
const batch = keys.slice(offset, offset + 1e3);
|
|
593
|
-
if (batch.length === 0) continue;
|
|
594
|
-
const response = await client.send(new DeleteObjectsCommand({
|
|
595
|
-
Bucket: bucketName,
|
|
596
|
-
Delete: {
|
|
597
|
-
Objects: batch.map((Key) => ({ Key })),
|
|
598
|
-
Quiet: true
|
|
599
|
-
}
|
|
600
|
-
}));
|
|
601
|
-
if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
|
|
602
|
-
}
|
|
603
|
-
},
|
|
604
|
-
async put({ key, body, contentLength, contentType }) {
|
|
605
|
-
const storageKey = getStorageKey(key);
|
|
606
|
-
const response = await new Upload({
|
|
607
|
-
client,
|
|
608
|
-
params: {
|
|
609
|
-
Body: body,
|
|
610
|
-
Bucket: bucketName,
|
|
611
|
-
CacheControl: "max-age=31536000",
|
|
612
|
-
...contentLength === void 0 ? {} : { ContentLength: contentLength },
|
|
613
|
-
ContentType: contentType,
|
|
614
|
-
Key: storageKey
|
|
615
|
-
}
|
|
616
|
-
}).done();
|
|
617
|
-
if (!response.Bucket || !response.Key) throw new Error("Upload failed");
|
|
618
|
-
return { storageUri: createStorageUri({
|
|
619
|
-
bucket: bucketName,
|
|
620
|
-
key: storageKey,
|
|
621
|
-
protocol: "s3"
|
|
622
|
-
}) };
|
|
623
|
-
},
|
|
624
|
-
async get({ storageUri }) {
|
|
625
|
-
const { key } = parseAndValidate(storageUri);
|
|
626
|
-
try {
|
|
627
|
-
const response = await client.send(new GetObjectCommand({
|
|
628
|
-
Bucket: bucketName,
|
|
629
|
-
Key: key
|
|
630
|
-
}));
|
|
631
|
-
if (!response.Body) return { response: null };
|
|
632
|
-
const headers = new Headers();
|
|
633
|
-
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
634
|
-
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
635
|
-
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
636
|
-
} catch (error) {
|
|
637
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
638
|
-
throw error;
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
async getDownloadUrl({ storageUri }) {
|
|
642
|
-
const { key } = parseAndValidate(storageUri);
|
|
643
|
-
if (getDownloadUrl) return getDownloadUrl({ storageUri });
|
|
644
|
-
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
645
|
-
return { url: await getSignedUrl(client, new GetObjectCommand({
|
|
646
|
-
Bucket: bucketName,
|
|
647
|
-
Key: key
|
|
648
|
-
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
649
|
-
},
|
|
650
|
-
async exists({ storageUri }) {
|
|
651
|
-
const { key } = parseAndValidate(storageUri);
|
|
652
|
-
try {
|
|
653
|
-
await client.send(new HeadObjectCommand({
|
|
654
|
-
Bucket: bucketName,
|
|
655
|
-
Key: key
|
|
656
|
-
}));
|
|
657
|
-
return { exists: true };
|
|
658
|
-
} catch (error) {
|
|
659
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
660
|
-
throw error;
|
|
661
|
-
}
|
|
662
|
-
},
|
|
663
|
-
async delete({ storageUri }) {
|
|
664
|
-
const { key } = parseAndValidate(storageUri);
|
|
665
|
-
await client.send(new DeleteObjectCommand({
|
|
666
|
-
Bucket: bucketName,
|
|
667
|
-
Key: key
|
|
668
|
-
}));
|
|
669
|
-
return { deleted: true };
|
|
670
|
-
}
|
|
524
|
+
protocol: "s3"
|
|
525
|
+
}, {
|
|
526
|
+
...applyS3RuntimeAwsConfig(s3Config),
|
|
527
|
+
credentials,
|
|
528
|
+
bucketName,
|
|
529
|
+
basePath,
|
|
530
|
+
getDownloadUrl
|
|
671
531
|
});
|
|
672
532
|
}
|
|
673
533
|
//#endregion
|
|
@@ -692,7 +552,22 @@ const EXPIRED_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
|
|
|
692
552
|
]);
|
|
693
553
|
/** Errors of the SDK finding no credentials, or an expired SSO session. */
|
|
694
554
|
const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set(["CredentialsProviderError", "TokenProviderError"]);
|
|
695
|
-
const SIGN_IN_HINT = "In CI, set them with aws-actions/configure-aws-credentials
|
|
555
|
+
const SIGN_IN_HINT = ["In CI, set them with aws-actions/configure-aws-credentials.", "On your machine: aws sso login --profile <profile>, then set AWS_PROFILE."];
|
|
556
|
+
/** The widest line a message has: the CLI prints no wider. */
|
|
557
|
+
const LINE_WIDTH = 76;
|
|
558
|
+
/** `text`, such as AWS's own message, in lines that fit, broken at spaces. */
|
|
559
|
+
const wrap = (text) => {
|
|
560
|
+
const lines = [];
|
|
561
|
+
let line = "";
|
|
562
|
+
for (const word of text.split(/\s+/u).filter(Boolean)) if (line && line.length + 1 + word.length > LINE_WIDTH) {
|
|
563
|
+
lines.push(line);
|
|
564
|
+
line = word;
|
|
565
|
+
} else line = line ? `${line} ${word}` : word;
|
|
566
|
+
if (line) lines.push(line);
|
|
567
|
+
return lines;
|
|
568
|
+
};
|
|
569
|
+
/** An error of one line each, for a terminal. */
|
|
570
|
+
const failure = (lines, cause) => new Error(lines.join("\n"), cause === void 0 ? void 0 : { cause });
|
|
696
571
|
const errorName = (error) => error instanceof Error ? error.name : "";
|
|
697
572
|
const errorMessage = (error) => (error instanceof Error ? error.message : String(error)).replace(/\.+$/u, "");
|
|
698
573
|
const resolveEndpoint = (endpoint) => {
|
|
@@ -707,7 +582,11 @@ const resolveEndpoint = (endpoint) => {
|
|
|
707
582
|
"127.0.0.1",
|
|
708
583
|
"[::1]"
|
|
709
584
|
])).has(url.hostname);
|
|
710
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw
|
|
585
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback) || url.username || url.password || url.search || url.hash) throw failure([
|
|
586
|
+
`${NAME} can't use its endpoint.`,
|
|
587
|
+
"Use HTTPS, or HTTP on a loopback address.",
|
|
588
|
+
"Leave out credentials, a query, and a fragment."
|
|
589
|
+
]);
|
|
711
590
|
return url.toString();
|
|
712
591
|
};
|
|
713
592
|
/** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
|
|
@@ -734,20 +613,20 @@ const checkCredentials = async (credentials) => {
|
|
|
734
613
|
*/
|
|
735
614
|
const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region }) => {
|
|
736
615
|
const checkSettings = () => {
|
|
737
|
-
if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw
|
|
616
|
+
if (typeof region !== "string" || !REGION_PATTERN.test(region)) throw failure([`${NAME} can't use the region ${JSON.stringify(region)}.`, "Set an AWS region, such as \"us-east-1\"."]);
|
|
738
617
|
if (prefix !== "" && !SECRET_ID_PATTERN.test(prefix)) throw new Error(`${NAME}'s prefix takes letters, digits, and /_+=.@- only.`);
|
|
739
618
|
};
|
|
740
619
|
const secretId = (name) => {
|
|
741
620
|
checkSettings();
|
|
742
621
|
const id = `${prefix}${name}`;
|
|
743
|
-
if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw
|
|
622
|
+
if (!SECRET_ID_PATTERN.test(name) || id.length > MAX_SECRET_ID_LENGTH) throw failure([`${NAME} can't name a secret ${JSON.stringify(id)}.`, `AWS takes up to ${MAX_SECRET_ID_LENGTH} letters, digits, and /_+=.@- characters.`]);
|
|
744
623
|
return id;
|
|
745
624
|
};
|
|
746
625
|
const resolveCredentials = () => {
|
|
747
626
|
if (credentials !== void 0) return async () => checkCredentials(typeof credentials === "function" ? await credentials() : credentials);
|
|
748
627
|
const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
|
|
749
628
|
const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
|
|
750
|
-
if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw
|
|
629
|
+
if (Boolean(accessKeyId) !== Boolean(secretAccessKey)) throw failure([`${NAME} found ${accessKeyId ? "AWS_ACCESS_KEY_ID" : "AWS_SECRET_ACCESS_KEY"} without ${accessKeyId ? "AWS_SECRET_ACCESS_KEY" : "AWS_ACCESS_KEY_ID"}.`, "Set both, or neither to use a profile."]);
|
|
751
630
|
};
|
|
752
631
|
let opened;
|
|
753
632
|
const open = () => {
|
|
@@ -770,12 +649,40 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
|
|
|
770
649
|
const describeFailure = (verb, id, error, permissions) => {
|
|
771
650
|
const name = errorName(error);
|
|
772
651
|
const status = error?.$metadata?.httpStatusCode;
|
|
773
|
-
const detail = `${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}
|
|
774
|
-
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
652
|
+
const detail = wrap(`${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}.`);
|
|
653
|
+
const secret = `Secret: ${id}`;
|
|
654
|
+
if (NO_CREDENTIAL_ERRORS.has(name)) return failure([
|
|
655
|
+
`${NAME} found no AWS credentials to ${verb} a secret.`,
|
|
656
|
+
secret,
|
|
657
|
+
...wrap(`${errorMessage(error)}.`),
|
|
658
|
+
...SIGN_IN_HINT
|
|
659
|
+
], error);
|
|
660
|
+
if (EXPIRED_CREDENTIAL_ERRORS.has(name)) return failure([
|
|
661
|
+
`AWS Secrets Manager refused the sign-in to ${verb} a secret.`,
|
|
662
|
+
secret,
|
|
663
|
+
...detail,
|
|
664
|
+
"They are expired or invalid: sign in again.",
|
|
665
|
+
...SIGN_IN_HINT
|
|
666
|
+
], error);
|
|
667
|
+
if (name === "AccessDeniedException") return failure([
|
|
668
|
+
`AWS Secrets Manager refused to ${verb} a secret.`,
|
|
669
|
+
secret,
|
|
670
|
+
...detail,
|
|
671
|
+
`The AWS identity needs ${permissions} on it.`,
|
|
672
|
+
`With a customer managed KMS key, it needs ${verb === "read" ? "kms:Decrypt" : "kms:GenerateDataKey"} too.`
|
|
673
|
+
], error);
|
|
674
|
+
if (/marked for deletion|scheduled for deletion/iu.test(errorMessage(error))) return failure([
|
|
675
|
+
`AWS Secrets Manager can't ${verb} a secret.`,
|
|
676
|
+
secret,
|
|
677
|
+
"It is scheduled for deletion. Restore it, or wait until it is gone:",
|
|
678
|
+
"aws secretsmanager restore-secret \\",
|
|
679
|
+
` --secret-id ${id}`
|
|
680
|
+
], error);
|
|
681
|
+
return failure([
|
|
682
|
+
`AWS Secrets Manager failed to ${verb} a secret.`,
|
|
683
|
+
secret,
|
|
684
|
+
...detail
|
|
685
|
+
], error);
|
|
779
686
|
};
|
|
780
687
|
const read = async ({ sdk, client }, id) => {
|
|
781
688
|
let output;
|
|
@@ -805,8 +712,8 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
|
|
|
805
712
|
},
|
|
806
713
|
set: async (name, value) => {
|
|
807
714
|
const id = secretId(name);
|
|
808
|
-
if (typeof value !== "string" || value === "") throw new Error(`${NAME}
|
|
809
|
-
if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw
|
|
715
|
+
if (typeof value !== "string" || value === "") throw new Error(`${NAME} can't store an empty ${id}.`);
|
|
716
|
+
if (Buffer.byteLength(value, "utf8") > MAX_SECRET_BYTES) throw failure([`${NAME} can't store ${id}.`, `AWS takes up to ${MAX_SECRET_BYTES} bytes per secret.`]);
|
|
810
717
|
await mask([value]);
|
|
811
718
|
const { sdk, client } = await open();
|
|
812
719
|
const token = randomUUID();
|
package/dist/lambda/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ let _aws_sdk_client_cloudfront = require("@aws-sdk/client-cloudfront");
|
|
|
30
30
|
let _aws_sdk_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
|
|
31
31
|
let _aws_sdk_client_s3 = require("@aws-sdk/client-s3");
|
|
32
32
|
let _aws_sdk_lib_storage = require("@aws-sdk/lib-storage");
|
|
33
|
+
let _aws_sdk_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
33
34
|
const SHA256_FILE_HASH_RE = /^[0-9a-f]{64}$/;
|
|
34
35
|
const isContentAddressedAssetFileHash = (value) => {
|
|
35
36
|
return typeof value === "string" && SHA256_FILE_HASH_RE.test(value);
|
|
@@ -4711,7 +4712,7 @@ const createKvAdapter = ({ store, tablePrefix = "" }) => {
|
|
|
4711
4712
|
};
|
|
4712
4713
|
//#endregion
|
|
4713
4714
|
//#region ../../packages/server/dist/version.mjs
|
|
4714
|
-
const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.
|
|
4715
|
+
const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.9";
|
|
4715
4716
|
//#endregion
|
|
4716
4717
|
//#region ../../packages/server/dist/handlerVersionRoutes.mjs
|
|
4717
4718
|
/**
|
|
@@ -14304,7 +14305,7 @@ async apply() {
|
|
|
14304
14305
|
* `credentials` as the AWS SDK takes them: access keys with a lazy field
|
|
14305
14306
|
* become a provider, which the SDK calls once, on its first request.
|
|
14306
14307
|
*/
|
|
14307
|
-
const toSdkCredentials = (credentials) => {
|
|
14308
|
+
const toSdkCredentials$1 = (credentials) => {
|
|
14308
14309
|
if (credentials === void 0 || typeof credentials === "function") return credentials;
|
|
14309
14310
|
const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
|
|
14310
14311
|
if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
|
|
@@ -14321,7 +14322,7 @@ const toSdkCredentials = (credentials) => {
|
|
|
14321
14322
|
const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
|
|
14322
14323
|
const client = new _aws_sdk_client_dynamodb.DynamoDBClient({
|
|
14323
14324
|
...clientConfig,
|
|
14324
|
-
credentials: toSdkCredentials(credentials)
|
|
14325
|
+
credentials: toSdkCredentials$1(credentials)
|
|
14325
14326
|
});
|
|
14326
14327
|
const store = createDynamoDBStore({
|
|
14327
14328
|
client,
|
|
@@ -14349,7 +14350,7 @@ const dynamoDB = (config) => {
|
|
|
14349
14350
|
const { client, adapter } = adapterOf(config);
|
|
14350
14351
|
const cloudFront = createUpdateRouteInvalidation({
|
|
14351
14352
|
...config,
|
|
14352
|
-
credentials: toSdkCredentials(config.credentials)
|
|
14353
|
+
credentials: toSdkCredentials$1(config.credentials)
|
|
14353
14354
|
});
|
|
14354
14355
|
return {
|
|
14355
14356
|
...createEngineDatabase({
|
|
@@ -14367,21 +14368,49 @@ const dynamoDB = (config) => {
|
|
|
14367
14368
|
};
|
|
14368
14369
|
};
|
|
14369
14370
|
//#endregion
|
|
14370
|
-
//#region
|
|
14371
|
+
//#region ../storage-s3/dist/index.mjs
|
|
14372
|
+
/**
|
|
14373
|
+
* `credentials` as the AWS SDK takes them: access keys with a lazy field
|
|
14374
|
+
* become a provider, which the SDK calls once, on its first request.
|
|
14375
|
+
*/
|
|
14376
|
+
const toSdkCredentials = (credentials) => {
|
|
14377
|
+
if (credentials === void 0 || typeof credentials === "function") return credentials;
|
|
14378
|
+
const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
|
|
14379
|
+
if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
|
|
14380
|
+
return async () => ({
|
|
14381
|
+
...rest,
|
|
14382
|
+
accessKeyId: await resolveLazy(accessKeyId),
|
|
14383
|
+
secretAccessKey: await resolveLazy(secretAccessKey),
|
|
14384
|
+
...sessionToken === void 0 ? {} : { sessionToken: await resolveLazy(sessionToken) }
|
|
14385
|
+
});
|
|
14386
|
+
};
|
|
14371
14387
|
/** Devices download right after the update check that returns the URL. */
|
|
14372
|
-
const
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14388
|
+
const DEFAULT_SIGNED_LIFETIME_SECONDS = 3600;
|
|
14389
|
+
/** SigV4's limit on a presigned URL. */
|
|
14390
|
+
const MAX_SIGNED_LIFETIME_SECONDS = 604800;
|
|
14391
|
+
/** DeleteObjects takes at most 1000 keys a request. */
|
|
14392
|
+
const DELETE_BATCH_SIZE = 1e3;
|
|
14393
|
+
const isObjectNotFoundError = (error) => {
|
|
14394
|
+
if (error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey")) return true;
|
|
14395
|
+
return typeof error === "object" && error !== null && "$metadata" in error && error.$metadata?.httpStatusCode === 404;
|
|
14396
|
+
};
|
|
14397
|
+
const trimSlashes = (value) => value.replace(/^\/+|\/+$/g, "");
|
|
14398
|
+
/**
|
|
14399
|
+
* A storage adapter over a bucket's S3 API, which AWS S3, Cloudflare R2, and
|
|
14400
|
+
* Neon Object Storage all serve. Each provider package passes its own name,
|
|
14401
|
+
* URI protocol, and client defaults.
|
|
14402
|
+
*/
|
|
14403
|
+
const createS3Storage = ({ name, protocol }, config) => {
|
|
14404
|
+
const { bucketName, basePath, getDownloadUrl, signedUrlExpiresIn = DEFAULT_SIGNED_LIFETIME_SECONDS, credentials, ...clientConfig } = config;
|
|
14405
|
+
if (!Number.isFinite(signedUrlExpiresIn) || signedUrlExpiresIn <= 0 || signedUrlExpiresIn > MAX_SIGNED_LIFETIME_SECONDS) throw new Error(`${name} signedUrlExpiresIn must be between 1 and ${MAX_SIGNED_LIFETIME_SECONDS} seconds (seven days).`);
|
|
14406
|
+
const client = new _aws_sdk_client_s3.S3Client({
|
|
14407
|
+
...clientConfig,
|
|
14378
14408
|
credentials: toSdkCredentials(credentials)
|
|
14379
|
-
})
|
|
14380
|
-
const normalizedBasePath = basePath
|
|
14409
|
+
});
|
|
14410
|
+
const normalizedBasePath = trimSlashes(basePath ?? "");
|
|
14381
14411
|
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
14382
14412
|
const getListPrefix = (prefix = "") => {
|
|
14383
|
-
const
|
|
14384
|
-
const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
|
|
14413
|
+
const value = [normalizedBasePath, trimSlashes(prefix)].filter(Boolean).join("/");
|
|
14385
14414
|
return value ? `${value}/` : "";
|
|
14386
14415
|
};
|
|
14387
14416
|
/**
|
|
@@ -14393,7 +14422,7 @@ function s3Storage(config) {
|
|
|
14393
14422
|
return createStorageUri({
|
|
14394
14423
|
bucket: bucketName,
|
|
14395
14424
|
key,
|
|
14396
|
-
protocol
|
|
14425
|
+
protocol
|
|
14397
14426
|
});
|
|
14398
14427
|
} catch {
|
|
14399
14428
|
return null;
|
|
@@ -14405,55 +14434,16 @@ function s3Storage(config) {
|
|
|
14405
14434
|
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
14406
14435
|
};
|
|
14407
14436
|
const parseAndValidate = (storageUri) => {
|
|
14408
|
-
const parsed = parseStorageUri(storageUri,
|
|
14437
|
+
const parsed = parseStorageUri(storageUri, protocol);
|
|
14409
14438
|
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
14410
14439
|
return parsed;
|
|
14411
14440
|
};
|
|
14412
14441
|
return createStorageAdapter({
|
|
14413
|
-
name
|
|
14414
|
-
protocol
|
|
14415
|
-
async listObjects(prefix) {
|
|
14416
|
-
const objects = [];
|
|
14417
|
-
let continuationToken;
|
|
14418
|
-
do {
|
|
14419
|
-
const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
14420
|
-
Bucket: bucketName,
|
|
14421
|
-
ContinuationToken: continuationToken,
|
|
14422
|
-
Prefix: getListPrefix(prefix)
|
|
14423
|
-
}));
|
|
14424
|
-
for (const object of response.Contents ?? []) {
|
|
14425
|
-
if (!object.Key) continue;
|
|
14426
|
-
const storageUri = getListedStorageUri(object.Key);
|
|
14427
|
-
if (storageUri === null) continue;
|
|
14428
|
-
objects.push({
|
|
14429
|
-
key: getRelativeKey(object.Key),
|
|
14430
|
-
lastModifiedAt: object.LastModified,
|
|
14431
|
-
size: object.Size ?? 0,
|
|
14432
|
-
storageUri
|
|
14433
|
-
});
|
|
14434
|
-
}
|
|
14435
|
-
continuationToken = response.NextContinuationToken;
|
|
14436
|
-
} while (continuationToken);
|
|
14437
|
-
return objects;
|
|
14438
|
-
},
|
|
14439
|
-
async deleteObjects(relativeKeys) {
|
|
14440
|
-
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
14441
|
-
for (let offset = 0; offset < keys.length; offset += 1e3) {
|
|
14442
|
-
const batch = keys.slice(offset, offset + 1e3);
|
|
14443
|
-
if (batch.length === 0) continue;
|
|
14444
|
-
const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
|
|
14445
|
-
Bucket: bucketName,
|
|
14446
|
-
Delete: {
|
|
14447
|
-
Objects: batch.map((Key) => ({ Key })),
|
|
14448
|
-
Quiet: true
|
|
14449
|
-
}
|
|
14450
|
-
}));
|
|
14451
|
-
if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
|
|
14452
|
-
}
|
|
14453
|
-
},
|
|
14442
|
+
name,
|
|
14443
|
+
protocol,
|
|
14454
14444
|
async put({ key, body, contentLength, contentType }) {
|
|
14455
14445
|
const storageKey = getStorageKey(key);
|
|
14456
|
-
|
|
14446
|
+
await new _aws_sdk_lib_storage.Upload({
|
|
14457
14447
|
client,
|
|
14458
14448
|
params: {
|
|
14459
14449
|
Body: body,
|
|
@@ -14464,11 +14454,10 @@ function s3Storage(config) {
|
|
|
14464
14454
|
Key: storageKey
|
|
14465
14455
|
}
|
|
14466
14456
|
}).done();
|
|
14467
|
-
if (!response.Bucket || !response.Key) throw new Error("Upload failed");
|
|
14468
14457
|
return { storageUri: createStorageUri({
|
|
14469
14458
|
bucket: bucketName,
|
|
14470
14459
|
key: storageKey,
|
|
14471
|
-
protocol
|
|
14460
|
+
protocol
|
|
14472
14461
|
}) };
|
|
14473
14462
|
},
|
|
14474
14463
|
async get({ storageUri }) {
|
|
@@ -14491,11 +14480,10 @@ function s3Storage(config) {
|
|
|
14491
14480
|
async getDownloadUrl({ storageUri }) {
|
|
14492
14481
|
const { key } = parseAndValidate(storageUri);
|
|
14493
14482
|
if (getDownloadUrl) return getDownloadUrl({ storageUri });
|
|
14494
|
-
|
|
14495
|
-
return { url: await getSignedUrl(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
14483
|
+
return { url: await (0, _aws_sdk_s3_request_presigner.getSignedUrl)(client, new _aws_sdk_client_s3.GetObjectCommand({
|
|
14496
14484
|
Bucket: bucketName,
|
|
14497
14485
|
Key: key
|
|
14498
|
-
}), { expiresIn:
|
|
14486
|
+
}), { expiresIn: signedUrlExpiresIn }) };
|
|
14499
14487
|
},
|
|
14500
14488
|
async exists({ storageUri }) {
|
|
14501
14489
|
const { key } = parseAndValidate(storageUri);
|
|
@@ -14517,8 +14505,61 @@ function s3Storage(config) {
|
|
|
14517
14505
|
Key: key
|
|
14518
14506
|
}));
|
|
14519
14507
|
return { deleted: true };
|
|
14508
|
+
},
|
|
14509
|
+
async listObjects(prefix) {
|
|
14510
|
+
const objects = [];
|
|
14511
|
+
let continuationToken;
|
|
14512
|
+
do {
|
|
14513
|
+
const response = await client.send(new _aws_sdk_client_s3.ListObjectsV2Command({
|
|
14514
|
+
Bucket: bucketName,
|
|
14515
|
+
ContinuationToken: continuationToken,
|
|
14516
|
+
Prefix: getListPrefix(prefix)
|
|
14517
|
+
}));
|
|
14518
|
+
for (const object of response.Contents ?? []) {
|
|
14519
|
+
if (!object.Key) continue;
|
|
14520
|
+
const storageUri = getListedStorageUri(object.Key);
|
|
14521
|
+
if (storageUri === null) continue;
|
|
14522
|
+
objects.push({
|
|
14523
|
+
key: getRelativeKey(object.Key),
|
|
14524
|
+
lastModifiedAt: object.LastModified,
|
|
14525
|
+
size: object.Size ?? 0,
|
|
14526
|
+
storageUri
|
|
14527
|
+
});
|
|
14528
|
+
}
|
|
14529
|
+
continuationToken = response.NextContinuationToken;
|
|
14530
|
+
} while (continuationToken);
|
|
14531
|
+
return objects;
|
|
14532
|
+
},
|
|
14533
|
+
async deleteObjects(relativeKeys) {
|
|
14534
|
+
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
14535
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_SIZE) {
|
|
14536
|
+
const batch = keys.slice(offset, offset + DELETE_BATCH_SIZE);
|
|
14537
|
+
const response = await client.send(new _aws_sdk_client_s3.DeleteObjectsCommand({
|
|
14538
|
+
Bucket: bucketName,
|
|
14539
|
+
Delete: {
|
|
14540
|
+
Objects: batch.map((Key) => ({ Key })),
|
|
14541
|
+
Quiet: true
|
|
14542
|
+
}
|
|
14543
|
+
}));
|
|
14544
|
+
if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s) from bucket "${bucketName}".`);
|
|
14545
|
+
}
|
|
14520
14546
|
}
|
|
14521
14547
|
});
|
|
14548
|
+
};
|
|
14549
|
+
//#endregion
|
|
14550
|
+
//#region src/s3Storage.ts
|
|
14551
|
+
function s3Storage(config) {
|
|
14552
|
+
const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
|
|
14553
|
+
return createS3Storage({
|
|
14554
|
+
name: "s3Storage",
|
|
14555
|
+
protocol: "s3"
|
|
14556
|
+
}, {
|
|
14557
|
+
...applyS3RuntimeAwsConfig(s3Config),
|
|
14558
|
+
credentials,
|
|
14559
|
+
bucketName,
|
|
14560
|
+
basePath,
|
|
14561
|
+
getDownloadUrl
|
|
14562
|
+
});
|
|
14522
14563
|
}
|
|
14523
14564
|
//#endregion
|
|
14524
14565
|
//#region lambda/index.ts
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/aws",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.2.0-rc.
|
|
4
|
+
"version": "1.2.0-rc.9",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"es-toolkit": "^1.32.0",
|
|
46
46
|
"execa": "9.5.2",
|
|
47
47
|
"mime": "^4.0.4",
|
|
48
|
-
"@hot-updater/protocol": "1.2.0-rc.
|
|
49
|
-
"@hot-updater/
|
|
50
|
-
"@hot-updater/
|
|
48
|
+
"@hot-updater/protocol": "1.2.0-rc.9",
|
|
49
|
+
"@hot-updater/mock": "1.2.0-rc.9",
|
|
50
|
+
"@hot-updater/test-utils": "1.2.0-rc.9"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@aws-sdk/client-cloudfront": "3.1066.0",
|
|
@@ -64,9 +64,10 @@
|
|
|
64
64
|
"@aws-sdk/lib-dynamodb": "3.1066.0",
|
|
65
65
|
"@aws-sdk/s3-request-presigner": "3.1066.0",
|
|
66
66
|
"hono": "4.12.34",
|
|
67
|
-
"@hot-updater/cli-tools": "1.2.0-rc.
|
|
68
|
-
"@hot-updater/plugin-core": "1.2.0-rc.
|
|
69
|
-
"@hot-updater/server": "1.2.0-rc.
|
|
67
|
+
"@hot-updater/cli-tools": "1.2.0-rc.9",
|
|
68
|
+
"@hot-updater/plugin-core": "1.2.0-rc.9",
|
|
69
|
+
"@hot-updater/server": "1.2.0-rc.9",
|
|
70
|
+
"@hot-updater/storage-s3": "1.2.0-rc.9"
|
|
70
71
|
},
|
|
71
72
|
"scripts": {
|
|
72
73
|
"build": "tsdown",
|