@hot-updater/aws 1.2.0-rc.7 → 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 +96 -163
- package/dist/index.d.cts +23 -5
- package/dist/index.d.mts +23 -5
- package/dist/index.mjs +97 -164
- package/dist/init/index.cjs +40 -28
- package/dist/init/index.mjs +42 -30
- package/dist/lambda/index.cjs +133 -62
- package/package.json +8 -7
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;
|
|
@@ -314,10 +313,30 @@ async apply() {
|
|
|
314
313
|
};
|
|
315
314
|
};
|
|
316
315
|
//#endregion
|
|
316
|
+
//#region src/lazyCredentials.ts
|
|
317
|
+
/**
|
|
318
|
+
* `credentials` as the AWS SDK takes them: access keys with a lazy field
|
|
319
|
+
* become a provider, which the SDK calls once, on its first request.
|
|
320
|
+
*/
|
|
321
|
+
const toSdkCredentials = (credentials) => {
|
|
322
|
+
if (credentials === void 0 || typeof credentials === "function") return credentials;
|
|
323
|
+
const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
|
|
324
|
+
if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
|
|
325
|
+
return async () => ({
|
|
326
|
+
...rest,
|
|
327
|
+
accessKeyId: await resolveLazy(accessKeyId),
|
|
328
|
+
secretAccessKey: await resolveLazy(secretAccessKey),
|
|
329
|
+
...sessionToken === void 0 ? {} : { sessionToken: await resolveLazy(sessionToken) }
|
|
330
|
+
});
|
|
331
|
+
};
|
|
332
|
+
//#endregion
|
|
317
333
|
//#region src/dynamoDB.ts
|
|
318
334
|
/** The DynamoDB client ignores the CloudFront and batching settings. */
|
|
319
|
-
const adapterOf = ({ tableName, ...clientConfig }) => {
|
|
320
|
-
const client = new DynamoDBClient(
|
|
335
|
+
const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
|
|
336
|
+
const client = new DynamoDBClient({
|
|
337
|
+
...clientConfig,
|
|
338
|
+
credentials: toSdkCredentials(credentials)
|
|
339
|
+
});
|
|
321
340
|
const store = createDynamoDBStore({
|
|
322
341
|
client,
|
|
323
342
|
tableName
|
|
@@ -352,7 +371,10 @@ const marked = (target) => ({ [DATABASE_TARGET]: target });
|
|
|
352
371
|
*/
|
|
353
372
|
const dynamoDB = (config) => {
|
|
354
373
|
const { client, adapter } = adapterOf(config);
|
|
355
|
-
const cloudFront = createUpdateRouteInvalidation(
|
|
374
|
+
const cloudFront = createUpdateRouteInvalidation({
|
|
375
|
+
...config,
|
|
376
|
+
credentials: toSdkCredentials(config.credentials)
|
|
377
|
+
});
|
|
356
378
|
return {
|
|
357
379
|
...createEngineDatabase({
|
|
358
380
|
name: "dynamoDB",
|
|
@@ -495,153 +517,17 @@ const cloudFrontDownloadUrl = (config) => {
|
|
|
495
517
|
};
|
|
496
518
|
//#endregion
|
|
497
519
|
//#region src/s3Storage.ts
|
|
498
|
-
/** Devices download right after the update check that returns the URL. */
|
|
499
|
-
const PRESIGNED_URL_EXPIRES_IN_SECONDS = 3600;
|
|
500
|
-
const isObjectNotFoundError = (error) => error instanceof Error && (error.name === "NotFound" || error.name === "NoSuchKey");
|
|
501
520
|
function s3Storage(config) {
|
|
502
|
-
const { bucketName, basePath, getDownloadUrl, ...s3Config } = config;
|
|
503
|
-
|
|
504
|
-
const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
505
|
-
const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
|
|
506
|
-
const getListPrefix = (prefix = "") => {
|
|
507
|
-
const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, "");
|
|
508
|
-
const value = [normalizedBasePath, normalizedPrefix].filter(Boolean).join("/");
|
|
509
|
-
return value ? `${value}/` : "";
|
|
510
|
-
};
|
|
511
|
-
/**
|
|
512
|
-
* The URI `put` returns for the key, or null for a key no `put` writes,
|
|
513
|
-
* such as a folder marker that ends in "/".
|
|
514
|
-
*/
|
|
515
|
-
const getListedStorageUri = (key) => {
|
|
516
|
-
try {
|
|
517
|
-
return createStorageUri({
|
|
518
|
-
bucket: bucketName,
|
|
519
|
-
key,
|
|
520
|
-
protocol: "s3"
|
|
521
|
-
});
|
|
522
|
-
} catch {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
const getRelativeKey = (key) => {
|
|
527
|
-
if (!normalizedBasePath) return key;
|
|
528
|
-
const basePrefix = `${normalizedBasePath}/`;
|
|
529
|
-
return key.startsWith(basePrefix) ? key.slice(basePrefix.length) : key;
|
|
530
|
-
};
|
|
531
|
-
const parseAndValidate = (storageUri) => {
|
|
532
|
-
const parsed = parseStorageUri(storageUri, "s3");
|
|
533
|
-
if (parsed.bucket !== bucketName) throw new Error(`Bucket name mismatch: expected "${bucketName}", but found "${parsed.bucket}".`);
|
|
534
|
-
return parsed;
|
|
535
|
-
};
|
|
536
|
-
return createStorageAdapter({
|
|
521
|
+
const { bucketName, basePath, getDownloadUrl, credentials, ...s3Config } = config;
|
|
522
|
+
return createS3Storage({
|
|
537
523
|
name: "s3Storage",
|
|
538
|
-
protocol: "s3"
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
ContinuationToken: continuationToken,
|
|
546
|
-
Prefix: getListPrefix(prefix)
|
|
547
|
-
}));
|
|
548
|
-
for (const object of response.Contents ?? []) {
|
|
549
|
-
if (!object.Key) continue;
|
|
550
|
-
const storageUri = getListedStorageUri(object.Key);
|
|
551
|
-
if (storageUri === null) continue;
|
|
552
|
-
objects.push({
|
|
553
|
-
key: getRelativeKey(object.Key),
|
|
554
|
-
lastModifiedAt: object.LastModified,
|
|
555
|
-
size: object.Size ?? 0,
|
|
556
|
-
storageUri
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
continuationToken = response.NextContinuationToken;
|
|
560
|
-
} while (continuationToken);
|
|
561
|
-
return objects;
|
|
562
|
-
},
|
|
563
|
-
async deleteObjects(relativeKeys) {
|
|
564
|
-
const keys = relativeKeys.map((key) => getStorageKey(key));
|
|
565
|
-
for (let offset = 0; offset < keys.length; offset += 1e3) {
|
|
566
|
-
const batch = keys.slice(offset, offset + 1e3);
|
|
567
|
-
if (batch.length === 0) continue;
|
|
568
|
-
const response = await client.send(new DeleteObjectsCommand({
|
|
569
|
-
Bucket: bucketName,
|
|
570
|
-
Delete: {
|
|
571
|
-
Objects: batch.map((Key) => ({ Key })),
|
|
572
|
-
Quiet: true
|
|
573
|
-
}
|
|
574
|
-
}));
|
|
575
|
-
if (response.Errors && response.Errors.length > 0) throw new Error(`Failed to delete ${response.Errors.length} S3 object(s).`);
|
|
576
|
-
}
|
|
577
|
-
},
|
|
578
|
-
async put({ key, body, contentLength, contentType }) {
|
|
579
|
-
const storageKey = getStorageKey(key);
|
|
580
|
-
const response = await new Upload({
|
|
581
|
-
client,
|
|
582
|
-
params: {
|
|
583
|
-
Body: body,
|
|
584
|
-
Bucket: bucketName,
|
|
585
|
-
CacheControl: "max-age=31536000",
|
|
586
|
-
...contentLength === void 0 ? {} : { ContentLength: contentLength },
|
|
587
|
-
ContentType: contentType,
|
|
588
|
-
Key: storageKey
|
|
589
|
-
}
|
|
590
|
-
}).done();
|
|
591
|
-
if (!response.Bucket || !response.Key) throw new Error("Upload failed");
|
|
592
|
-
return { storageUri: createStorageUri({
|
|
593
|
-
bucket: bucketName,
|
|
594
|
-
key: storageKey,
|
|
595
|
-
protocol: "s3"
|
|
596
|
-
}) };
|
|
597
|
-
},
|
|
598
|
-
async get({ storageUri }) {
|
|
599
|
-
const { key } = parseAndValidate(storageUri);
|
|
600
|
-
try {
|
|
601
|
-
const response = await client.send(new GetObjectCommand({
|
|
602
|
-
Bucket: bucketName,
|
|
603
|
-
Key: key
|
|
604
|
-
}));
|
|
605
|
-
if (!response.Body) return { response: null };
|
|
606
|
-
const headers = new Headers();
|
|
607
|
-
if (response.ContentType) headers.set("content-type", response.ContentType);
|
|
608
|
-
if (response.ContentLength !== void 0) headers.set("content-length", String(response.ContentLength));
|
|
609
|
-
return { response: new Response(response.Body.transformToWebStream(), { headers }) };
|
|
610
|
-
} catch (error) {
|
|
611
|
-
if (isObjectNotFoundError(error)) return { response: null };
|
|
612
|
-
throw error;
|
|
613
|
-
}
|
|
614
|
-
},
|
|
615
|
-
async getDownloadUrl({ storageUri }) {
|
|
616
|
-
const { key } = parseAndValidate(storageUri);
|
|
617
|
-
if (getDownloadUrl) return getDownloadUrl({ storageUri });
|
|
618
|
-
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
619
|
-
return { url: await getSignedUrl(client, new GetObjectCommand({
|
|
620
|
-
Bucket: bucketName,
|
|
621
|
-
Key: key
|
|
622
|
-
}), { expiresIn: PRESIGNED_URL_EXPIRES_IN_SECONDS }) };
|
|
623
|
-
},
|
|
624
|
-
async exists({ storageUri }) {
|
|
625
|
-
const { key } = parseAndValidate(storageUri);
|
|
626
|
-
try {
|
|
627
|
-
await client.send(new HeadObjectCommand({
|
|
628
|
-
Bucket: bucketName,
|
|
629
|
-
Key: key
|
|
630
|
-
}));
|
|
631
|
-
return { exists: true };
|
|
632
|
-
} catch (error) {
|
|
633
|
-
if (isObjectNotFoundError(error)) return { exists: false };
|
|
634
|
-
throw error;
|
|
635
|
-
}
|
|
636
|
-
},
|
|
637
|
-
async delete({ storageUri }) {
|
|
638
|
-
const { key } = parseAndValidate(storageUri);
|
|
639
|
-
await client.send(new DeleteObjectCommand({
|
|
640
|
-
Bucket: bucketName,
|
|
641
|
-
Key: key
|
|
642
|
-
}));
|
|
643
|
-
return { deleted: true };
|
|
644
|
-
}
|
|
524
|
+
protocol: "s3"
|
|
525
|
+
}, {
|
|
526
|
+
...applyS3RuntimeAwsConfig(s3Config),
|
|
527
|
+
credentials,
|
|
528
|
+
bucketName,
|
|
529
|
+
basePath,
|
|
530
|
+
getDownloadUrl
|
|
645
531
|
});
|
|
646
532
|
}
|
|
647
533
|
//#endregion
|
|
@@ -666,7 +552,22 @@ const EXPIRED_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set([
|
|
|
666
552
|
]);
|
|
667
553
|
/** Errors of the SDK finding no credentials, or an expired SSO session. */
|
|
668
554
|
const NO_CREDENTIAL_ERRORS = /* @__PURE__ */ new Set(["CredentialsProviderError", "TokenProviderError"]);
|
|
669
|
-
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 });
|
|
670
571
|
const errorName = (error) => error instanceof Error ? error.name : "";
|
|
671
572
|
const errorMessage = (error) => (error instanceof Error ? error.message : String(error)).replace(/\.+$/u, "");
|
|
672
573
|
const resolveEndpoint = (endpoint) => {
|
|
@@ -681,7 +582,11 @@ const resolveEndpoint = (endpoint) => {
|
|
|
681
582
|
"127.0.0.1",
|
|
682
583
|
"[::1]"
|
|
683
584
|
])).has(url.hostname);
|
|
684
|
-
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
|
+
]);
|
|
685
590
|
return url.toString();
|
|
686
591
|
};
|
|
687
592
|
/** Masks a value in the CLI's output, which loads `@hot-updater/cli-tools`. */
|
|
@@ -708,20 +613,20 @@ const checkCredentials = async (credentials) => {
|
|
|
708
613
|
*/
|
|
709
614
|
const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region }) => {
|
|
710
615
|
const checkSettings = () => {
|
|
711
|
-
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\"."]);
|
|
712
617
|
if (prefix !== "" && !SECRET_ID_PATTERN.test(prefix)) throw new Error(`${NAME}'s prefix takes letters, digits, and /_+=.@- only.`);
|
|
713
618
|
};
|
|
714
619
|
const secretId = (name) => {
|
|
715
620
|
checkSettings();
|
|
716
621
|
const id = `${prefix}${name}`;
|
|
717
|
-
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.`]);
|
|
718
623
|
return id;
|
|
719
624
|
};
|
|
720
625
|
const resolveCredentials = () => {
|
|
721
626
|
if (credentials !== void 0) return async () => checkCredentials(typeof credentials === "function" ? await credentials() : credentials);
|
|
722
627
|
const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
|
|
723
628
|
const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
|
|
724
|
-
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."]);
|
|
725
630
|
};
|
|
726
631
|
let opened;
|
|
727
632
|
const open = () => {
|
|
@@ -744,12 +649,40 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
|
|
|
744
649
|
const describeFailure = (verb, id, error, permissions) => {
|
|
745
650
|
const name = errorName(error);
|
|
746
651
|
const status = error?.$metadata?.httpStatusCode;
|
|
747
|
-
const detail = `${name || "Error"}${status ? ` (${status})` : ""}: ${errorMessage(error)}
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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);
|
|
753
686
|
};
|
|
754
687
|
const read = async ({ sdk, client }, id) => {
|
|
755
688
|
let output;
|
|
@@ -779,8 +712,8 @@ const secretsManager = ({ credentials, endpoint, prefix = "hot-updater/", region
|
|
|
779
712
|
},
|
|
780
713
|
set: async (name, value) => {
|
|
781
714
|
const id = secretId(name);
|
|
782
|
-
if (typeof value !== "string" || value === "") throw new Error(`${NAME}
|
|
783
|
-
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.`]);
|
|
784
717
|
await mask([value]);
|
|
785
718
|
const { sdk, client } = await open();
|
|
786
719
|
const token = randomUUID();
|
package/dist/init/index.cjs
CHANGED
|
@@ -6579,10 +6579,30 @@ async apply() {
|
|
|
6579
6579
|
};
|
|
6580
6580
|
};
|
|
6581
6581
|
//#endregion
|
|
6582
|
+
//#region src/lazyCredentials.ts
|
|
6583
|
+
/**
|
|
6584
|
+
* `credentials` as the AWS SDK takes them: access keys with a lazy field
|
|
6585
|
+
* become a provider, which the SDK calls once, on its first request.
|
|
6586
|
+
*/
|
|
6587
|
+
const toSdkCredentials = (credentials) => {
|
|
6588
|
+
if (credentials === void 0 || typeof credentials === "function") return credentials;
|
|
6589
|
+
const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
|
|
6590
|
+
if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
|
|
6591
|
+
return async () => ({
|
|
6592
|
+
...rest,
|
|
6593
|
+
accessKeyId: await (0, _hot_updater_plugin_core.resolveLazy)(accessKeyId),
|
|
6594
|
+
secretAccessKey: await (0, _hot_updater_plugin_core.resolveLazy)(secretAccessKey),
|
|
6595
|
+
...sessionToken === void 0 ? {} : { sessionToken: await (0, _hot_updater_plugin_core.resolveLazy)(sessionToken) }
|
|
6596
|
+
});
|
|
6597
|
+
};
|
|
6598
|
+
//#endregion
|
|
6582
6599
|
//#region src/dynamoDB.ts
|
|
6583
6600
|
/** The DynamoDB client ignores the CloudFront and batching settings. */
|
|
6584
|
-
const adapterOf = ({ tableName, ...clientConfig }) => {
|
|
6585
|
-
const client = new _aws_sdk_client_dynamodb.DynamoDBClient(
|
|
6601
|
+
const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
|
|
6602
|
+
const client = new _aws_sdk_client_dynamodb.DynamoDBClient({
|
|
6603
|
+
...clientConfig,
|
|
6604
|
+
credentials: toSdkCredentials(credentials)
|
|
6605
|
+
});
|
|
6586
6606
|
const store = createDynamoDBStore({
|
|
6587
6607
|
client,
|
|
6588
6608
|
tableName
|
|
@@ -6617,7 +6637,10 @@ const marked = (target) => ({ [DATABASE_TARGET]: target });
|
|
|
6617
6637
|
*/
|
|
6618
6638
|
const dynamoDB = (config) => {
|
|
6619
6639
|
const { client, adapter } = adapterOf(config);
|
|
6620
|
-
const cloudFront = createUpdateRouteInvalidation(
|
|
6640
|
+
const cloudFront = createUpdateRouteInvalidation({
|
|
6641
|
+
...config,
|
|
6642
|
+
credentials: toSdkCredentials(config.credentials)
|
|
6643
|
+
});
|
|
6621
6644
|
return {
|
|
6622
6645
|
...(0, _hot_updater_plugin_core.createEngineDatabase)({
|
|
6623
6646
|
name: "dynamoDB",
|
|
@@ -8419,10 +8442,11 @@ var SSMKeyPairManager = class {
|
|
|
8419
8442
|
//#region iac/templates.ts
|
|
8420
8443
|
/**
|
|
8421
8444
|
* The config init writes. With the `resources` init set up, it names them,
|
|
8422
|
-
* and an account's access keys are read with `
|
|
8445
|
+
* and an account's access keys are read with `secret()`, from the
|
|
8423
8446
|
* environment and `.env.hotupdater`, then the config's `secrets` adapter.
|
|
8424
|
-
* Without them, as the agent scaffold writes it
|
|
8425
|
-
*
|
|
8447
|
+
* Without them, as the agent scaffold writes it before the agent creates the
|
|
8448
|
+
* resources, it reads their names from the environment, where the agent
|
|
8449
|
+
* records them. A profile or SSO sign-in is each person's own.
|
|
8426
8450
|
*/
|
|
8427
8451
|
const getConfigScaffold = (build, authMode, resources) => {
|
|
8428
8452
|
const value = (envKey, resolved) => resolved === void 0 ? `process.env.${envKey}!` : JSON.stringify(resolved);
|
|
@@ -8478,31 +8502,19 @@ const awsOptions = {
|
|
|
8478
8502
|
};`.trim()
|
|
8479
8503
|
}];
|
|
8480
8504
|
break;
|
|
8481
|
-
case "account":
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
code: resources === void 0 ? `
|
|
8505
|
+
case "account": helperStatements = [{
|
|
8506
|
+
name: "awsOptions",
|
|
8507
|
+
strategy: "merge-object",
|
|
8508
|
+
replaceIncompatibleProperties: ["credentials"],
|
|
8509
|
+
code: `
|
|
8487
8510
|
const awsOptions = {
|
|
8488
8511
|
region: ${region},
|
|
8489
8512
|
credentials: {
|
|
8490
|
-
accessKeyId:
|
|
8491
|
-
secretAccessKey:
|
|
8492
|
-
},
|
|
8493
|
-
};`.trim() : `
|
|
8494
|
-
const awsOptions = {
|
|
8495
|
-
region: ${region},
|
|
8496
|
-
credentials: async () => {
|
|
8497
|
-
const env = await deploySecrets();
|
|
8498
|
-
return {
|
|
8499
|
-
accessKeyId: env.HOT_UPDATER_S3_ACCESS_KEY_ID,
|
|
8500
|
-
secretAccessKey: env.HOT_UPDATER_S3_SECRET_ACCESS_KEY,
|
|
8501
|
-
};
|
|
8513
|
+
accessKeyId: secret("HOT_UPDATER_S3_ACCESS_KEY_ID"),
|
|
8514
|
+
secretAccessKey: secret("HOT_UPDATER_S3_SECRET_ACCESS_KEY"),
|
|
8502
8515
|
},
|
|
8503
8516
|
};`.trim()
|
|
8504
|
-
|
|
8505
|
-
if (resources !== void 0) helperStatements.unshift((0, _hot_updater_cli_tools.secretEnvHelper)("deploySecrets", ["HOT_UPDATER_S3_ACCESS_KEY_ID", "HOT_UPDATER_S3_SECRET_ACCESS_KEY"]));
|
|
8517
|
+
}];
|
|
8506
8518
|
}
|
|
8507
8519
|
const builder = new _hot_updater_cli_tools.ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
|
|
8508
8520
|
imports: [{
|
|
@@ -8528,9 +8540,9 @@ const awsOptions = {
|
|
|
8528
8540
|
named: [authMode.profile ? "fromIni" : "fromNodeProviderChain"]
|
|
8529
8541
|
});
|
|
8530
8542
|
break;
|
|
8531
|
-
case "account":
|
|
8543
|
+
case "account": builder.addImport({
|
|
8532
8544
|
pkg: "hot-updater",
|
|
8533
|
-
named: ["
|
|
8545
|
+
named: ["secret"]
|
|
8534
8546
|
});
|
|
8535
8547
|
}
|
|
8536
8548
|
return (0, _hot_updater_cli_tools.createHotUpdaterConfigScaffoldFromBuilder)(builder, { helperStatements });
|
package/dist/init/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { ConfigBuilder, InitError, MissingInitInputsError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitProviderInputs, colors, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput,
|
|
2
|
+
import { ConfigBuilder, InitError, MissingInitInputsError, assembleServer, assertInfrastructureGenerationAtUrl, assertInfrastructureGenerationPayload, assertInitProviderInputs, colors, confirmInitInputPersistence, copyDirToTmp, createHotUpdaterConfigScaffoldFromBuilder, createZip, ensureInstallPackages, getCwd, getHotUpdaterInitInputEnv, getInitProviderEnvVars, getInitProviderTextPromptValues, link, makeEnv, p, printAppSetup, provisionClientCredential, readHotUpdaterInitEnv, resolveInitProviderInput, transformEnv, writeHotUpdaterFiles } from "@hot-updater/cli-tools";
|
|
3
3
|
import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { ChildProcess, execFile, spawn, spawnSync } from "node:child_process";
|
|
@@ -18,7 +18,7 @@ import { finished } from "node:stream/promises";
|
|
|
18
18
|
import { Duplex, PassThrough, Readable, Transform, Writable, getDefaultHighWaterMark } from "node:stream";
|
|
19
19
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
20
20
|
import { CreateTableCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDB, DynamoDBClient, UpdateTimeToLiveCommand, waitUntilTableExists } from "@aws-sdk/client-dynamodb";
|
|
21
|
-
import { SETTINGS_TABLE, aggregateBatchingTables, createEngineDatabase, createKvAdapter, encodeKvKey, migrateCoreSchema, toolingTargetOf } from "@hot-updater/plugin-core";
|
|
21
|
+
import { SETTINGS_TABLE, aggregateBatchingTables, createEngineDatabase, createKvAdapter, encodeKvKey, migrateCoreSchema, resolveLazy, toolingTargetOf } from "@hot-updater/plugin-core";
|
|
22
22
|
import { CloudFront, CloudFrontClient, CreateInvalidationCommand, GetInvalidationCommand } from "@aws-sdk/client-cloudfront";
|
|
23
23
|
import { BatchGetCommand, BatchWriteCommand, DynamoDBDocumentClient, QueryCommand, TransactWriteCommand } from "@aws-sdk/lib-dynamodb";
|
|
24
24
|
import { fromIni, fromNodeProviderChain, fromSSO } from "@aws-sdk/credential-providers";
|
|
@@ -6574,10 +6574,30 @@ async apply() {
|
|
|
6574
6574
|
};
|
|
6575
6575
|
};
|
|
6576
6576
|
//#endregion
|
|
6577
|
+
//#region src/lazyCredentials.ts
|
|
6578
|
+
/**
|
|
6579
|
+
* `credentials` as the AWS SDK takes them: access keys with a lazy field
|
|
6580
|
+
* become a provider, which the SDK calls once, on its first request.
|
|
6581
|
+
*/
|
|
6582
|
+
const toSdkCredentials = (credentials) => {
|
|
6583
|
+
if (credentials === void 0 || typeof credentials === "function") return credentials;
|
|
6584
|
+
const { accessKeyId, secretAccessKey, sessionToken, ...rest } = credentials;
|
|
6585
|
+
if (typeof accessKeyId !== "function" && typeof secretAccessKey !== "function" && typeof sessionToken !== "function") return credentials;
|
|
6586
|
+
return async () => ({
|
|
6587
|
+
...rest,
|
|
6588
|
+
accessKeyId: await resolveLazy(accessKeyId),
|
|
6589
|
+
secretAccessKey: await resolveLazy(secretAccessKey),
|
|
6590
|
+
...sessionToken === void 0 ? {} : { sessionToken: await resolveLazy(sessionToken) }
|
|
6591
|
+
});
|
|
6592
|
+
};
|
|
6593
|
+
//#endregion
|
|
6577
6594
|
//#region src/dynamoDB.ts
|
|
6578
6595
|
/** The DynamoDB client ignores the CloudFront and batching settings. */
|
|
6579
|
-
const adapterOf = ({ tableName, ...clientConfig }) => {
|
|
6580
|
-
const client = new DynamoDBClient(
|
|
6596
|
+
const adapterOf = ({ tableName, credentials, ...clientConfig }) => {
|
|
6597
|
+
const client = new DynamoDBClient({
|
|
6598
|
+
...clientConfig,
|
|
6599
|
+
credentials: toSdkCredentials(credentials)
|
|
6600
|
+
});
|
|
6581
6601
|
const store = createDynamoDBStore({
|
|
6582
6602
|
client,
|
|
6583
6603
|
tableName
|
|
@@ -6612,7 +6632,10 @@ const marked = (target) => ({ [DATABASE_TARGET]: target });
|
|
|
6612
6632
|
*/
|
|
6613
6633
|
const dynamoDB = (config) => {
|
|
6614
6634
|
const { client, adapter } = adapterOf(config);
|
|
6615
|
-
const cloudFront = createUpdateRouteInvalidation(
|
|
6635
|
+
const cloudFront = createUpdateRouteInvalidation({
|
|
6636
|
+
...config,
|
|
6637
|
+
credentials: toSdkCredentials(config.credentials)
|
|
6638
|
+
});
|
|
6616
6639
|
return {
|
|
6617
6640
|
...createEngineDatabase({
|
|
6618
6641
|
name: "dynamoDB",
|
|
@@ -8414,10 +8437,11 @@ var SSMKeyPairManager = class {
|
|
|
8414
8437
|
//#region iac/templates.ts
|
|
8415
8438
|
/**
|
|
8416
8439
|
* The config init writes. With the `resources` init set up, it names them,
|
|
8417
|
-
* and an account's access keys are read with `
|
|
8440
|
+
* and an account's access keys are read with `secret()`, from the
|
|
8418
8441
|
* environment and `.env.hotupdater`, then the config's `secrets` adapter.
|
|
8419
|
-
* Without them, as the agent scaffold writes it
|
|
8420
|
-
*
|
|
8442
|
+
* Without them, as the agent scaffold writes it before the agent creates the
|
|
8443
|
+
* resources, it reads their names from the environment, where the agent
|
|
8444
|
+
* records them. A profile or SSO sign-in is each person's own.
|
|
8421
8445
|
*/
|
|
8422
8446
|
const getConfigScaffold = (build, authMode, resources) => {
|
|
8423
8447
|
const value = (envKey, resolved) => resolved === void 0 ? `process.env.${envKey}!` : JSON.stringify(resolved);
|
|
@@ -8473,31 +8497,19 @@ const awsOptions = {
|
|
|
8473
8497
|
};`.trim()
|
|
8474
8498
|
}];
|
|
8475
8499
|
break;
|
|
8476
|
-
case "account":
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
code: resources === void 0 ? `
|
|
8500
|
+
case "account": helperStatements = [{
|
|
8501
|
+
name: "awsOptions",
|
|
8502
|
+
strategy: "merge-object",
|
|
8503
|
+
replaceIncompatibleProperties: ["credentials"],
|
|
8504
|
+
code: `
|
|
8482
8505
|
const awsOptions = {
|
|
8483
8506
|
region: ${region},
|
|
8484
8507
|
credentials: {
|
|
8485
|
-
accessKeyId:
|
|
8486
|
-
secretAccessKey:
|
|
8487
|
-
},
|
|
8488
|
-
};`.trim() : `
|
|
8489
|
-
const awsOptions = {
|
|
8490
|
-
region: ${region},
|
|
8491
|
-
credentials: async () => {
|
|
8492
|
-
const env = await deploySecrets();
|
|
8493
|
-
return {
|
|
8494
|
-
accessKeyId: env.HOT_UPDATER_S3_ACCESS_KEY_ID,
|
|
8495
|
-
secretAccessKey: env.HOT_UPDATER_S3_SECRET_ACCESS_KEY,
|
|
8496
|
-
};
|
|
8508
|
+
accessKeyId: secret("HOT_UPDATER_S3_ACCESS_KEY_ID"),
|
|
8509
|
+
secretAccessKey: secret("HOT_UPDATER_S3_SECRET_ACCESS_KEY"),
|
|
8497
8510
|
},
|
|
8498
8511
|
};`.trim()
|
|
8499
|
-
|
|
8500
|
-
if (resources !== void 0) helperStatements.unshift(secretEnvHelper("deploySecrets", ["HOT_UPDATER_S3_ACCESS_KEY_ID", "HOT_UPDATER_S3_SECRET_ACCESS_KEY"]));
|
|
8512
|
+
}];
|
|
8501
8513
|
}
|
|
8502
8514
|
const builder = new ConfigBuilder().setBuildType(build).setStorage(storageConfig).setDatabase(databaseConfig).setPlugins({
|
|
8503
8515
|
imports: [{
|
|
@@ -8523,9 +8535,9 @@ const awsOptions = {
|
|
|
8523
8535
|
named: [authMode.profile ? "fromIni" : "fromNodeProviderChain"]
|
|
8524
8536
|
});
|
|
8525
8537
|
break;
|
|
8526
|
-
case "account":
|
|
8538
|
+
case "account": builder.addImport({
|
|
8527
8539
|
pkg: "hot-updater",
|
|
8528
|
-
named: ["
|
|
8540
|
+
named: ["secret"]
|
|
8529
8541
|
});
|
|
8530
8542
|
}
|
|
8531
8543
|
return createHotUpdaterConfigScaffoldFromBuilder(builder, { helperStatements });
|