@vibecodemax/cli 0.1.6 → 0.1.8
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/cli.js +511 -76
- package/dist/storageS3.js +1 -73
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import {
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { checkS3Context, setupS3Storage } from "./storageS3.js";
|
|
6
7
|
const SETUP_STATE_PATH = path.join(".vibecodemax", "setup-state.json");
|
|
8
|
+
const SETUP_CONFIG_PATH = path.join(".vibecodemax", "setup-config.json");
|
|
7
9
|
const MANAGEMENT_API_BASE = "https://api.supabase.com";
|
|
8
10
|
const DEFAULT_LOCALHOST_URL = "http://localhost:3000";
|
|
9
11
|
const STORAGE_PUBLIC_BUCKET = "public-assets";
|
|
10
12
|
const STORAGE_PRIVATE_BUCKET = "private-uploads";
|
|
11
13
|
const STORAGE_REQUIRED_FILE_SIZE_LIMIT = 10 * 1024 * 1024;
|
|
12
|
-
const STORAGE_HEALTHCHECK_PREFIX = "_vibecodemax/healthcheck/default";
|
|
13
14
|
const STORAGE_MIME_TYPES_BY_CATEGORY = {
|
|
14
15
|
images: ["image/jpeg", "image/png", "image/webp", "image/gif"],
|
|
15
16
|
documents: [
|
|
@@ -23,15 +24,25 @@ const STORAGE_MIME_TYPES_BY_CATEGORY = {
|
|
|
23
24
|
video: ["video/mp4", "video/webm", "video/quicktime"],
|
|
24
25
|
};
|
|
25
26
|
const STORAGE_DEFAULT_MIME_CATEGORIES = ["images"];
|
|
26
|
-
const
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
const STRIPE_EVENTS = [
|
|
28
|
+
"checkout.session.completed",
|
|
29
|
+
"customer.subscription.created",
|
|
30
|
+
"customer.subscription.updated",
|
|
31
|
+
"customer.subscription.deleted",
|
|
32
|
+
"invoice.payment_succeeded",
|
|
33
|
+
"invoice.payment_failed",
|
|
34
|
+
];
|
|
35
|
+
const LEMON_EVENTS = [
|
|
36
|
+
"order_created",
|
|
37
|
+
"subscription_created",
|
|
38
|
+
"subscription_updated",
|
|
39
|
+
"subscription_cancelled",
|
|
40
|
+
"subscription_resumed",
|
|
41
|
+
"subscription_expired",
|
|
42
|
+
"subscription_paused",
|
|
43
|
+
"subscription_unpaused",
|
|
44
|
+
"subscription_payment_failed",
|
|
45
|
+
"subscription_payment_success",
|
|
35
46
|
];
|
|
36
47
|
function printJson(value) {
|
|
37
48
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
@@ -58,7 +69,7 @@ function parseArgs(argv) {
|
|
|
58
69
|
let subcommand;
|
|
59
70
|
const flags = {};
|
|
60
71
|
let index = 0;
|
|
61
|
-
if ((command === "admin" || command === "storage") && rest[0] && !rest[0].startsWith("--")) {
|
|
72
|
+
if ((command === "admin" || command === "storage" || command === "payments") && rest[0] && !rest[0].startsWith("--")) {
|
|
62
73
|
subcommand = rest[0];
|
|
63
74
|
index = 1;
|
|
64
75
|
}
|
|
@@ -114,6 +125,18 @@ function loadLocalEnv(cwd = process.cwd()) {
|
|
|
114
125
|
},
|
|
115
126
|
};
|
|
116
127
|
}
|
|
128
|
+
function readSetupConfig(cwd = process.cwd()) {
|
|
129
|
+
const raw = readFileIfExists(path.join(cwd, SETUP_CONFIG_PATH)).trim();
|
|
130
|
+
if (!raw)
|
|
131
|
+
return {};
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(raw);
|
|
134
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
117
140
|
function isNonEmptyString(value) {
|
|
118
141
|
return typeof value === "string" && value.trim().length > 0;
|
|
119
142
|
}
|
|
@@ -656,6 +679,44 @@ function runLinkedSupabaseCommand(command, cwd, failureCode, fallbackMessage) {
|
|
|
656
679
|
}
|
|
657
680
|
return typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
658
681
|
}
|
|
682
|
+
function isMigrationOrderingConflict(message) {
|
|
683
|
+
return /Found local migration files to be inserted before the last migration on remote database\./i.test(message);
|
|
684
|
+
}
|
|
685
|
+
function runStorageMigrationPush(commandBase, cwd) {
|
|
686
|
+
const firstResult = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", commandBase], {
|
|
687
|
+
cwd,
|
|
688
|
+
env: process.env,
|
|
689
|
+
encoding: "utf8",
|
|
690
|
+
});
|
|
691
|
+
if (firstResult.status === 0) {
|
|
692
|
+
return {
|
|
693
|
+
stdout: typeof firstResult.stdout === "string" ? firstResult.stdout.trim() : "",
|
|
694
|
+
includeAll: false,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const firstMessage = [firstResult.stderr, firstResult.stdout]
|
|
698
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
699
|
+
.find(Boolean) || "Failed to apply storage migrations to the linked Supabase project.";
|
|
700
|
+
if (!isMigrationOrderingConflict(firstMessage)) {
|
|
701
|
+
fail("STORAGE_POLICY_MIGRATION_APPLY_FAILED", firstMessage);
|
|
702
|
+
}
|
|
703
|
+
const retryCommand = `${commandBase} --include-all`;
|
|
704
|
+
const retryResult = spawnSync(process.env.SHELL || "/bin/zsh", ["-lc", retryCommand], {
|
|
705
|
+
cwd,
|
|
706
|
+
env: process.env,
|
|
707
|
+
encoding: "utf8",
|
|
708
|
+
});
|
|
709
|
+
if (retryResult.status !== 0) {
|
|
710
|
+
const retryMessage = [retryResult.stderr, retryResult.stdout]
|
|
711
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
712
|
+
.find(Boolean) || "Failed to apply storage migrations to the linked Supabase project.";
|
|
713
|
+
fail("STORAGE_POLICY_MIGRATION_APPLY_FAILED", retryMessage);
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
stdout: typeof retryResult.stdout === "string" ? retryResult.stdout.trim() : "",
|
|
717
|
+
includeAll: true,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
659
720
|
function normalizeStorageMimeCategories(rawValue) {
|
|
660
721
|
const requested = rawValue
|
|
661
722
|
.split(",")
|
|
@@ -664,11 +725,18 @@ function normalizeStorageMimeCategories(rawValue) {
|
|
|
664
725
|
const selected = requested.filter((value) => Object.prototype.hasOwnProperty.call(STORAGE_MIME_TYPES_BY_CATEGORY, value));
|
|
665
726
|
return selected.length > 0 ? [...new Set(selected)] : [...STORAGE_DEFAULT_MIME_CATEGORIES];
|
|
666
727
|
}
|
|
667
|
-
function resolveStorageMimeCategories(
|
|
668
|
-
const
|
|
669
|
-
|
|
728
|
+
function resolveStorageMimeCategories(cwd) {
|
|
729
|
+
const setupConfig = readSetupConfig(cwd);
|
|
730
|
+
const storageConfig = setupConfig.storage && typeof setupConfig.storage === "object"
|
|
731
|
+
? setupConfig.storage
|
|
732
|
+
: {};
|
|
733
|
+
const rawCategories = Array.isArray(storageConfig.mimeCategories)
|
|
734
|
+
? storageConfig.mimeCategories.filter((value) => isNonEmptyString(value))
|
|
735
|
+
: [];
|
|
736
|
+
if (rawCategories.length === 0) {
|
|
670
737
|
return [...STORAGE_DEFAULT_MIME_CATEGORIES];
|
|
671
|
-
|
|
738
|
+
}
|
|
739
|
+
return normalizeStorageMimeCategories(rawCategories.join(","));
|
|
672
740
|
}
|
|
673
741
|
function expandStorageMimeCategories(categories) {
|
|
674
742
|
const mimeTypes = [];
|
|
@@ -694,28 +762,47 @@ function parseStorageMigrationFilename(filename) {
|
|
|
694
762
|
return null;
|
|
695
763
|
}
|
|
696
764
|
function discoverStorageMigrationFiles(cwd) {
|
|
697
|
-
const
|
|
698
|
-
|
|
765
|
+
const storageMigrationsRoot = path.join(cwd, "supabase", "storage-migrations");
|
|
766
|
+
const activeMigrationsRoot = path.join(cwd, "supabase", "migrations");
|
|
767
|
+
if (!fs.existsSync(activeMigrationsRoot)) {
|
|
699
768
|
fail("MISSING_STORAGE_MIGRATIONS", "supabase/migrations is missing. Run bootstrap.base first so the local Supabase project is initialized.");
|
|
700
769
|
}
|
|
701
|
-
|
|
770
|
+
if (!fs.existsSync(storageMigrationsRoot)) {
|
|
771
|
+
fail("MISSING_STORAGE_MIGRATIONS", "supabase/storage-migrations is missing. Regenerate the project so storage-owned migrations are available locally.");
|
|
772
|
+
}
|
|
773
|
+
const policyFiles = fs.readdirSync(storageMigrationsRoot)
|
|
702
774
|
.map((filename) => parseStorageMigrationFilename(filename))
|
|
703
|
-
.filter((entry) => entry !== null && entry.scope
|
|
775
|
+
.filter((entry) => entry !== null && /(^|_)storage_policies$/.test(entry.scope))
|
|
704
776
|
.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
705
|
-
if (selected.length === 0) {
|
|
706
|
-
fail("MISSING_STORAGE_MIGRATIONS", "No storage migration files were found. Add a migration matching YYYYMMDDHHMMSS_storage_*.sql in supabase/migrations.");
|
|
707
|
-
}
|
|
708
|
-
const policyFiles = selected
|
|
709
|
-
.filter((entry) => /(^|_)storage_policies\.sql$/.test(entry.filename))
|
|
710
|
-
.map((entry) => entry.filename);
|
|
711
777
|
if (policyFiles.length === 0) {
|
|
712
|
-
fail("MISSING_STORAGE_POLICY_MIGRATION", "No storage policy migration file was found.
|
|
778
|
+
fail("MISSING_STORAGE_POLICY_MIGRATION", "No storage policy migration file was found in supabase/storage-migrations. This directory is for storage policy SQL only. Buckets are created by storage bootstrap through the Supabase Storage API. Regenerate the project or add a file ending in _storage_policies.sql.");
|
|
713
779
|
}
|
|
714
780
|
return {
|
|
715
|
-
files:
|
|
716
|
-
policyFiles,
|
|
781
|
+
files: policyFiles.map((entry) => entry.filename),
|
|
782
|
+
policyFiles: policyFiles.map((entry) => entry.filename),
|
|
717
783
|
};
|
|
718
784
|
}
|
|
785
|
+
function materializeStorageMigrationFiles(cwd, files) {
|
|
786
|
+
const storageMigrationsRoot = path.join(cwd, "supabase", "storage-migrations");
|
|
787
|
+
const activeMigrationsRoot = path.join(cwd, "supabase", "migrations");
|
|
788
|
+
const preparedFiles = [];
|
|
789
|
+
for (const filename of files) {
|
|
790
|
+
const sourcePath = path.join(storageMigrationsRoot, filename);
|
|
791
|
+
const targetPath = path.join(activeMigrationsRoot, filename);
|
|
792
|
+
const sourceSql = fs.readFileSync(sourcePath, "utf8");
|
|
793
|
+
if (fs.existsSync(targetPath)) {
|
|
794
|
+
const existingSql = fs.readFileSync(targetPath, "utf8");
|
|
795
|
+
if (existingSql !== sourceSql) {
|
|
796
|
+
fail("STORAGE_MIGRATION_CONFLICT", `Active migration ${filename} does not match supabase/storage-migrations/${filename}.`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
801
|
+
}
|
|
802
|
+
preparedFiles.push(filename);
|
|
803
|
+
}
|
|
804
|
+
return preparedFiles;
|
|
805
|
+
}
|
|
719
806
|
async function storageRequest(params) {
|
|
720
807
|
const response = await fetch(`${params.supabaseUrl}${params.endpoint}`, {
|
|
721
808
|
method: params.method,
|
|
@@ -842,9 +929,6 @@ async function ensureStorageBucket(supabaseUrl, serviceRoleKey, bucketId, isPubl
|
|
|
842
929
|
verified: true,
|
|
843
930
|
};
|
|
844
931
|
}
|
|
845
|
-
function buildStorageHealthcheckObjectPath(runId) {
|
|
846
|
-
return `${STORAGE_HEALTHCHECK_PREFIX}/${runId}/upload.txt`;
|
|
847
|
-
}
|
|
848
932
|
async function checkSupabaseContext() {
|
|
849
933
|
const { values, envLocalPath } = loadLocalEnv();
|
|
850
934
|
const missingKeys = [];
|
|
@@ -880,10 +964,11 @@ async function setupSupabaseStorage(flags) {
|
|
|
880
964
|
const serviceRoleKey = requireServiceRoleKey(values);
|
|
881
965
|
const dependencyManager = detectDependencyManager(cwd, flags);
|
|
882
966
|
const supabaseRunner = getSupabaseRunner(dependencyManager);
|
|
883
|
-
const mimeCategories = resolveStorageMimeCategories(
|
|
967
|
+
const mimeCategories = resolveStorageMimeCategories(cwd);
|
|
884
968
|
const allowedMimeTypes = expandStorageMimeCategories(mimeCategories);
|
|
885
969
|
const migrations = discoverStorageMigrationFiles(cwd);
|
|
886
|
-
|
|
970
|
+
const preparedMigrationFiles = materializeStorageMigrationFiles(cwd, migrations.files);
|
|
971
|
+
const migrationPush = runStorageMigrationPush(`${supabaseRunner} db push --linked`, cwd);
|
|
887
972
|
const publicBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PUBLIC_BUCKET, true, allowedMimeTypes);
|
|
888
973
|
const privateBucket = await ensureStorageBucket(supabaseUrl, serviceRoleKey, STORAGE_PRIVATE_BUCKET, false, allowedMimeTypes);
|
|
889
974
|
mergeEnvFile(envLocalPath, {
|
|
@@ -902,55 +987,393 @@ async function setupSupabaseStorage(flags) {
|
|
|
902
987
|
fileSizeLimit: STORAGE_REQUIRED_FILE_SIZE_LIMIT,
|
|
903
988
|
migrationFiles: migrations.files,
|
|
904
989
|
policyFiles: migrations.policyFiles,
|
|
990
|
+
preparedMigrationFiles,
|
|
905
991
|
policyMigrationsDiscovered: true,
|
|
906
992
|
policyMigrationsApplied: true,
|
|
907
|
-
|
|
908
|
-
policyVerificationMethod: "linked_db_push",
|
|
993
|
+
migrationPushIncludeAll: migrationPush.includeAll,
|
|
909
994
|
envWritten: ["SUPABASE_PUBLIC_BUCKET", "SUPABASE_PRIVATE_BUCKET"],
|
|
910
995
|
});
|
|
911
996
|
}
|
|
912
|
-
|
|
997
|
+
function normalizePaymentsMode(value) {
|
|
998
|
+
return value === "production" ? "production" : "test";
|
|
999
|
+
}
|
|
1000
|
+
function isStripePublishableKey(value) {
|
|
1001
|
+
return isNonEmptyString(value) && /^pk_(test|live)_[A-Za-z0-9_]+$/.test(value.trim());
|
|
1002
|
+
}
|
|
1003
|
+
function isStripeSecretKey(value) {
|
|
1004
|
+
return isNonEmptyString(value) && /^sk_(test|live)_[A-Za-z0-9_]+$/.test(value.trim());
|
|
1005
|
+
}
|
|
1006
|
+
function isStripeWebhookSecret(value) {
|
|
1007
|
+
return isNonEmptyString(value) && /^whsec_[A-Za-z0-9]+$/.test(value.trim());
|
|
1008
|
+
}
|
|
1009
|
+
function isNumericStoreId(value) {
|
|
1010
|
+
return isNonEmptyString(value) && /^\d+$/.test(value.trim());
|
|
1011
|
+
}
|
|
1012
|
+
function requireWebhookUrl(value, expectedPath, provider) {
|
|
1013
|
+
const trimmed = value.trim();
|
|
1014
|
+
if (!trimmed) {
|
|
1015
|
+
fail("INVALID_WEBHOOK_URL", `${provider} webhook URL is required.`);
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
const parsed = new URL(trimmed);
|
|
1019
|
+
if (parsed.protocol !== "https:" || parsed.pathname !== expectedPath) {
|
|
1020
|
+
fail("INVALID_WEBHOOK_URL", `${provider} webhook URL must be an https URL ending in ${expectedPath}.`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
catch {
|
|
1024
|
+
fail("INVALID_WEBHOOK_URL", `${provider} webhook URL must be a valid https URL ending in ${expectedPath}.`);
|
|
1025
|
+
}
|
|
1026
|
+
return trimmed;
|
|
1027
|
+
}
|
|
1028
|
+
function formEncode(value, prefix = "") {
|
|
1029
|
+
const entries = [];
|
|
1030
|
+
if (Array.isArray(value)) {
|
|
1031
|
+
value.forEach((item, index) => {
|
|
1032
|
+
entries.push(...formEncode(item, `${prefix}[${index}]`));
|
|
1033
|
+
});
|
|
1034
|
+
return entries;
|
|
1035
|
+
}
|
|
1036
|
+
if (value && typeof value === "object") {
|
|
1037
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1038
|
+
entries.push(...formEncode(nested, prefix ? `${prefix}[${key}]` : key));
|
|
1039
|
+
}
|
|
1040
|
+
return entries;
|
|
1041
|
+
}
|
|
1042
|
+
if (value === null || value === undefined)
|
|
1043
|
+
return entries;
|
|
1044
|
+
entries.push([prefix, String(value)]);
|
|
1045
|
+
return entries;
|
|
1046
|
+
}
|
|
1047
|
+
async function stripeRequest(params) {
|
|
1048
|
+
const url = new URL(`https://api.stripe.com${params.path}`);
|
|
1049
|
+
if (params.query) {
|
|
1050
|
+
for (const [key, value] of formEncode(params.query)) {
|
|
1051
|
+
url.searchParams.append(key, value);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const headers = {
|
|
1055
|
+
Authorization: `Bearer ${params.secretKey}`,
|
|
1056
|
+
};
|
|
1057
|
+
let body;
|
|
1058
|
+
if (params.body) {
|
|
1059
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1060
|
+
body = new URLSearchParams(formEncode(params.body));
|
|
1061
|
+
}
|
|
1062
|
+
const response = await fetch(url.toString(), {
|
|
1063
|
+
method: params.method,
|
|
1064
|
+
headers,
|
|
1065
|
+
body,
|
|
1066
|
+
});
|
|
1067
|
+
const json = await response.json().catch(() => null);
|
|
1068
|
+
if (!response.ok) {
|
|
1069
|
+
const message = extractErrorMessage(json?.error) || extractErrorMessage(json) || `Stripe returned ${response.status}`;
|
|
1070
|
+
fail("STRIPE_API_ERROR", message, 1, { status: response.status });
|
|
1071
|
+
}
|
|
1072
|
+
return json;
|
|
1073
|
+
}
|
|
1074
|
+
async function lemonRequest(params) {
|
|
1075
|
+
const url = new URL(`https://api.lemonsqueezy.com/v1${params.path}`);
|
|
1076
|
+
if (params.query) {
|
|
1077
|
+
for (const [key, value] of Object.entries(params.query)) {
|
|
1078
|
+
url.searchParams.set(key, value);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const response = await fetch(url.toString(), {
|
|
1082
|
+
method: params.method,
|
|
1083
|
+
headers: {
|
|
1084
|
+
Accept: "application/vnd.api+json",
|
|
1085
|
+
Authorization: `Bearer ${params.apiKey}`,
|
|
1086
|
+
...(params.body ? { "Content-Type": "application/vnd.api+json" } : {}),
|
|
1087
|
+
},
|
|
1088
|
+
body: params.body ? JSON.stringify(params.body) : undefined,
|
|
1089
|
+
});
|
|
1090
|
+
const json = await response.json().catch(() => null);
|
|
1091
|
+
if (!response.ok) {
|
|
1092
|
+
const detail = Array.isArray(json?.errors)
|
|
1093
|
+
? (json.errors[0]?.detail)
|
|
1094
|
+
: null;
|
|
1095
|
+
const message = isNonEmptyString(detail) ? detail : (extractErrorMessage(json) || `Lemon Squeezy returned ${response.status}`);
|
|
1096
|
+
fail("LEMONSQUEEZY_API_ERROR", message, 1, { status: response.status });
|
|
1097
|
+
}
|
|
1098
|
+
return json;
|
|
1099
|
+
}
|
|
1100
|
+
function checkStripeKeys(flags) {
|
|
913
1101
|
const { values } = loadLocalEnv();
|
|
914
|
-
const
|
|
915
|
-
const
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1102
|
+
const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
|
|
1103
|
+
const publishableKey = values.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
|
|
1104
|
+
const secretKey = values.STRIPE_SECRET_KEY;
|
|
1105
|
+
const expectedPublishablePrefix = mode === "test" ? "pk_test_" : "pk_live_";
|
|
1106
|
+
const expectedSecretPrefix = mode === "test" ? "sk_test_" : "sk_live_";
|
|
1107
|
+
const missingKeys = [];
|
|
1108
|
+
const invalidKeys = [];
|
|
1109
|
+
const mistakes = [];
|
|
1110
|
+
if (!isNonEmptyString(publishableKey)) {
|
|
1111
|
+
missingKeys.push("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY");
|
|
1112
|
+
}
|
|
1113
|
+
else if (!isStripePublishableKey(publishableKey)) {
|
|
1114
|
+
invalidKeys.push("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY");
|
|
1115
|
+
}
|
|
1116
|
+
else if (!publishableKey.startsWith(expectedPublishablePrefix)) {
|
|
1117
|
+
mistakes.push(`NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY does not match ${mode} mode.`);
|
|
1118
|
+
}
|
|
1119
|
+
if (!isNonEmptyString(secretKey)) {
|
|
1120
|
+
missingKeys.push("STRIPE_SECRET_KEY");
|
|
1121
|
+
}
|
|
1122
|
+
else if (!isStripeSecretKey(secretKey)) {
|
|
1123
|
+
invalidKeys.push("STRIPE_SECRET_KEY");
|
|
1124
|
+
}
|
|
1125
|
+
else if (!secretKey.startsWith(expectedSecretPrefix)) {
|
|
1126
|
+
mistakes.push(`STRIPE_SECRET_KEY does not match ${mode} mode.`);
|
|
1127
|
+
}
|
|
1128
|
+
if (isNonEmptyString(publishableKey) && isNonEmptyString(secretKey)) {
|
|
1129
|
+
if (publishableKey.startsWith("pk_live_") && secretKey.startsWith("sk_test_")) {
|
|
1130
|
+
mistakes.push("Publishable and secret keys are from different Stripe modes (pk_live with sk_test).");
|
|
1131
|
+
}
|
|
1132
|
+
if (publishableKey.startsWith("pk_test_") && secretKey.startsWith("sk_live_")) {
|
|
1133
|
+
mistakes.push("Publishable and secret keys are from different Stripe modes (pk_test with sk_live).");
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (missingKeys.length > 0 || invalidKeys.length > 0 || mistakes.length > 0) {
|
|
1137
|
+
fail("INVALID_PAYMENTS_ENV", "Stripe keys are missing, malformed, or inconsistent with the selected mode.", 1, { missingKeys, invalidKeys, mistakes });
|
|
1138
|
+
}
|
|
1139
|
+
printJson({
|
|
1140
|
+
ok: true,
|
|
1141
|
+
command: "payments check-stripe-keys",
|
|
1142
|
+
mode,
|
|
1143
|
+
verified: true,
|
|
1144
|
+
presentKeys: ["NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", "STRIPE_SECRET_KEY"],
|
|
1145
|
+
checks: ["presence", "format", "mode_consistency"],
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
function checkLemonKeys(flags) {
|
|
1149
|
+
const { values } = loadLocalEnv();
|
|
1150
|
+
const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
|
|
1151
|
+
const apiKey = values.LEMONSQUEEZY_API_KEY;
|
|
1152
|
+
const storeId = values.LEMONSQUEEZY_STORE_ID;
|
|
1153
|
+
const missingKeys = [];
|
|
1154
|
+
const invalidKeys = [];
|
|
1155
|
+
if (!isNonEmptyString(apiKey)) {
|
|
1156
|
+
missingKeys.push("LEMONSQUEEZY_API_KEY");
|
|
1157
|
+
}
|
|
1158
|
+
if (!isNonEmptyString(storeId)) {
|
|
1159
|
+
missingKeys.push("LEMONSQUEEZY_STORE_ID");
|
|
1160
|
+
}
|
|
1161
|
+
else if (!isNumericStoreId(storeId)) {
|
|
1162
|
+
invalidKeys.push("LEMONSQUEEZY_STORE_ID");
|
|
1163
|
+
}
|
|
1164
|
+
if (missingKeys.length > 0 || invalidKeys.length > 0) {
|
|
1165
|
+
fail("INVALID_PAYMENTS_ENV", "Lemon Squeezy API key or store ID is missing or malformed.", 1, { missingKeys, invalidKeys });
|
|
1166
|
+
}
|
|
1167
|
+
printJson({
|
|
1168
|
+
ok: true,
|
|
1169
|
+
command: "payments check-lemonsqueezy-keys",
|
|
1170
|
+
mode,
|
|
1171
|
+
verified: true,
|
|
1172
|
+
presentKeys: ["LEMONSQUEEZY_API_KEY", "LEMONSQUEEZY_STORE_ID"],
|
|
1173
|
+
checks: ["presence", "store_id_format"],
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
async function createStripeWebhook(flags) {
|
|
1177
|
+
const { envLocalPath, values } = loadLocalEnv();
|
|
1178
|
+
const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
|
|
1179
|
+
const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/stripe", "Stripe");
|
|
1180
|
+
const secretKey = values.STRIPE_SECRET_KEY;
|
|
1181
|
+
if (!isStripeSecretKey(secretKey)) {
|
|
1182
|
+
fail("INVALID_PAYMENTS_ENV", "STRIPE_SECRET_KEY is missing or malformed in .env.local.");
|
|
1183
|
+
}
|
|
1184
|
+
const listed = await stripeRequest({
|
|
1185
|
+
secretKey,
|
|
1186
|
+
method: "GET",
|
|
1187
|
+
path: "/v1/webhook_endpoints",
|
|
1188
|
+
query: { limit: 100 },
|
|
1189
|
+
});
|
|
1190
|
+
const existing = Array.isArray(listed.data)
|
|
1191
|
+
? listed.data.find((endpoint) => {
|
|
1192
|
+
const url = typeof endpoint.url === "string" ? endpoint.url : "";
|
|
1193
|
+
const events = Array.isArray(endpoint.enabled_events) ? endpoint.enabled_events : [];
|
|
1194
|
+
return url === webhookUrl && JSON.stringify([...events].sort()) === JSON.stringify([...STRIPE_EVENTS].sort());
|
|
1195
|
+
})
|
|
1196
|
+
: null;
|
|
1197
|
+
if (existing && isStripeWebhookSecret(values.STRIPE_WEBHOOK_SECRET)) {
|
|
1198
|
+
printJson({
|
|
1199
|
+
ok: true,
|
|
1200
|
+
command: "payments create-stripe-webhook",
|
|
1201
|
+
mode,
|
|
1202
|
+
created: false,
|
|
1203
|
+
reused: true,
|
|
1204
|
+
endpointId: typeof existing.id === "string" ? existing.id : null,
|
|
1205
|
+
webhookUrl,
|
|
1206
|
+
enabledEvents: STRIPE_EVENTS,
|
|
1207
|
+
envWritten: [],
|
|
1208
|
+
});
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const created = await stripeRequest({
|
|
1212
|
+
secretKey,
|
|
922
1213
|
method: "POST",
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1214
|
+
path: "/v1/webhook_endpoints",
|
|
1215
|
+
body: {
|
|
1216
|
+
url: webhookUrl,
|
|
1217
|
+
enabled_events: STRIPE_EVENTS,
|
|
1218
|
+
description: "VibeCodeMax payments webhook",
|
|
1219
|
+
},
|
|
926
1220
|
});
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1221
|
+
if (!isStripeWebhookSecret(created.secret)) {
|
|
1222
|
+
fail("STRIPE_WEBHOOK_CREATE_FAILED", "Stripe did not return a webhook signing secret.");
|
|
1223
|
+
}
|
|
1224
|
+
mergeEnvFile(envLocalPath, {
|
|
1225
|
+
STRIPE_WEBHOOK_SECRET: created.secret.trim(),
|
|
1226
|
+
});
|
|
1227
|
+
printJson({
|
|
1228
|
+
ok: true,
|
|
1229
|
+
command: "payments create-stripe-webhook",
|
|
1230
|
+
mode,
|
|
1231
|
+
created: true,
|
|
1232
|
+
reused: false,
|
|
1233
|
+
endpointId: typeof created.id === "string" ? created.id : null,
|
|
1234
|
+
webhookUrl: typeof created.url === "string" ? created.url : webhookUrl,
|
|
1235
|
+
enabledEvents: STRIPE_EVENTS,
|
|
1236
|
+
envWritten: ["STRIPE_WEBHOOK_SECRET"],
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
function checkStripeWebhookSecret(flags) {
|
|
1240
|
+
const { values } = loadLocalEnv();
|
|
1241
|
+
const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
|
|
1242
|
+
const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/stripe", "Stripe");
|
|
1243
|
+
const missingKeys = [];
|
|
1244
|
+
const invalidKeys = [];
|
|
1245
|
+
if (!isNonEmptyString(values.STRIPE_WEBHOOK_SECRET)) {
|
|
1246
|
+
missingKeys.push("STRIPE_WEBHOOK_SECRET");
|
|
1247
|
+
}
|
|
1248
|
+
else if (!isStripeWebhookSecret(values.STRIPE_WEBHOOK_SECRET)) {
|
|
1249
|
+
invalidKeys.push("STRIPE_WEBHOOK_SECRET");
|
|
1250
|
+
}
|
|
1251
|
+
if (missingKeys.length > 0 || invalidKeys.length > 0) {
|
|
1252
|
+
fail("INVALID_PAYMENTS_ENV", "Stripe webhook secret is missing or malformed in .env.local.", 1, { missingKeys, invalidKeys });
|
|
1253
|
+
}
|
|
1254
|
+
printJson({
|
|
1255
|
+
ok: true,
|
|
1256
|
+
command: "payments check-stripe-webhook-secret",
|
|
1257
|
+
mode,
|
|
1258
|
+
verified: true,
|
|
1259
|
+
webhookUrl,
|
|
1260
|
+
presentKeys: ["STRIPE_WEBHOOK_SECRET"],
|
|
1261
|
+
checks: ["presence", "format", "webhook_url_format"],
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
async function createLemonWebhook(flags) {
|
|
1265
|
+
const { envLocalPath, values } = loadLocalEnv();
|
|
1266
|
+
const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
|
|
1267
|
+
const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/lemonsqueezy", "Lemon Squeezy");
|
|
1268
|
+
const apiKey = values.LEMONSQUEEZY_API_KEY;
|
|
1269
|
+
const storeId = values.LEMONSQUEEZY_STORE_ID;
|
|
1270
|
+
if (!isNonEmptyString(apiKey)) {
|
|
1271
|
+
fail("INVALID_PAYMENTS_ENV", "LEMONSQUEEZY_API_KEY is missing in .env.local.");
|
|
1272
|
+
}
|
|
1273
|
+
if (!isNumericStoreId(storeId)) {
|
|
1274
|
+
fail("INVALID_PAYMENTS_ENV", "LEMONSQUEEZY_STORE_ID is missing or malformed in .env.local.");
|
|
1275
|
+
}
|
|
1276
|
+
const listed = await lemonRequest({
|
|
1277
|
+
apiKey,
|
|
1278
|
+
method: "GET",
|
|
1279
|
+
path: "/webhooks",
|
|
1280
|
+
query: {
|
|
1281
|
+
"filter[store_id]": storeId.trim(),
|
|
1282
|
+
"page[size]": "100",
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
const existing = Array.isArray(listed.data)
|
|
1286
|
+
? listed.data.find((endpoint) => {
|
|
1287
|
+
const attrs = endpoint.attributes && typeof endpoint.attributes === "object"
|
|
1288
|
+
? endpoint.attributes
|
|
1289
|
+
: {};
|
|
1290
|
+
const relationships = endpoint.relationships && typeof endpoint.relationships === "object"
|
|
1291
|
+
? endpoint.relationships
|
|
1292
|
+
: {};
|
|
1293
|
+
const endpointStoreId = typeof relationships.store?.data?.id === "string"
|
|
1294
|
+
? (relationships.store.data.id)
|
|
1295
|
+
: "";
|
|
1296
|
+
const events = Array.isArray(attrs.events) ? attrs.events : [];
|
|
1297
|
+
return String(attrs.url || "") === webhookUrl
|
|
1298
|
+
&& endpointStoreId === storeId.trim()
|
|
1299
|
+
&& JSON.stringify([...events].sort()) === JSON.stringify([...LEMON_EVENTS].sort());
|
|
1300
|
+
})
|
|
1301
|
+
: null;
|
|
1302
|
+
if (existing && isNonEmptyString(values.LEMONSQUEEZY_WEBHOOK_SECRET)) {
|
|
1303
|
+
printJson({
|
|
1304
|
+
ok: true,
|
|
1305
|
+
command: "payments create-lemonsqueezy-webhook",
|
|
1306
|
+
mode,
|
|
1307
|
+
created: false,
|
|
1308
|
+
reused: true,
|
|
1309
|
+
endpointId: typeof existing.id === "string" ? existing.id : null,
|
|
1310
|
+
webhookUrl,
|
|
1311
|
+
storeId: storeId.trim(),
|
|
1312
|
+
enabledEvents: LEMON_EVENTS,
|
|
1313
|
+
envWritten: [],
|
|
1314
|
+
});
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const generatedSecret = randomBytes(24).toString("hex");
|
|
1318
|
+
const created = await lemonRequest({
|
|
1319
|
+
apiKey,
|
|
930
1320
|
method: "POST",
|
|
931
|
-
|
|
932
|
-
body: {
|
|
933
|
-
|
|
1321
|
+
path: "/webhooks",
|
|
1322
|
+
body: {
|
|
1323
|
+
data: {
|
|
1324
|
+
type: "webhooks",
|
|
1325
|
+
attributes: {
|
|
1326
|
+
url: webhookUrl,
|
|
1327
|
+
events: LEMON_EVENTS,
|
|
1328
|
+
secret: generatedSecret,
|
|
1329
|
+
test_mode: mode === "test",
|
|
1330
|
+
},
|
|
1331
|
+
relationships: {
|
|
1332
|
+
store: {
|
|
1333
|
+
data: {
|
|
1334
|
+
type: "stores",
|
|
1335
|
+
id: storeId.trim(),
|
|
1336
|
+
},
|
|
1337
|
+
},
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
934
1341
|
});
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
serviceRoleKey,
|
|
938
|
-
method: "DELETE",
|
|
939
|
-
endpoint: `/storage/v1/object/${STORAGE_PUBLIC_BUCKET}`,
|
|
940
|
-
body: { prefixes: [objectPath] },
|
|
941
|
-
contentType: "application/json",
|
|
1342
|
+
mergeEnvFile(envLocalPath, {
|
|
1343
|
+
LEMONSQUEEZY_WEBHOOK_SECRET: generatedSecret,
|
|
942
1344
|
});
|
|
943
1345
|
printJson({
|
|
944
1346
|
ok: true,
|
|
945
|
-
command: "
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1347
|
+
command: "payments create-lemonsqueezy-webhook",
|
|
1348
|
+
mode,
|
|
1349
|
+
created: true,
|
|
1350
|
+
reused: false,
|
|
1351
|
+
endpointId: typeof created.data?.id === "string" ? created.data.id : null,
|
|
1352
|
+
webhookUrl: typeof created.data?.attributes?.url === "string" ? created.data.attributes.url : webhookUrl,
|
|
1353
|
+
storeId: storeId.trim(),
|
|
1354
|
+
enabledEvents: LEMON_EVENTS,
|
|
1355
|
+
envWritten: ["LEMONSQUEEZY_WEBHOOK_SECRET"],
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
function checkLemonWebhookSecret(flags) {
|
|
1359
|
+
const { values } = loadLocalEnv();
|
|
1360
|
+
const mode = normalizePaymentsMode(readStringFlag(flags, "mode"));
|
|
1361
|
+
const webhookUrl = requireWebhookUrl(readStringFlag(flags, "webhook-url"), "/api/webhooks/lemonsqueezy", "Lemon Squeezy");
|
|
1362
|
+
const missingKeys = [];
|
|
1363
|
+
if (!isNonEmptyString(values.LEMONSQUEEZY_WEBHOOK_SECRET)) {
|
|
1364
|
+
missingKeys.push("LEMONSQUEEZY_WEBHOOK_SECRET");
|
|
1365
|
+
}
|
|
1366
|
+
if (missingKeys.length > 0) {
|
|
1367
|
+
fail("INVALID_PAYMENTS_ENV", "Lemon Squeezy webhook secret is missing in .env.local.", 1, { missingKeys });
|
|
1368
|
+
}
|
|
1369
|
+
printJson({
|
|
1370
|
+
ok: true,
|
|
1371
|
+
command: "payments check-lemonsqueezy-webhook-secret",
|
|
1372
|
+
mode,
|
|
1373
|
+
verified: true,
|
|
1374
|
+
webhookUrl,
|
|
1375
|
+
presentKeys: ["LEMONSQUEEZY_WEBHOOK_SECRET"],
|
|
1376
|
+
checks: ["presence", "webhook_url_format"],
|
|
954
1377
|
});
|
|
955
1378
|
}
|
|
956
1379
|
async function main() {
|
|
@@ -963,10 +1386,14 @@ async function main() {
|
|
|
963
1386
|
"admin ensure-admin",
|
|
964
1387
|
"storage check-supabase-context",
|
|
965
1388
|
"storage setup-supabase",
|
|
966
|
-
"storage smoke-test-supabase",
|
|
967
1389
|
"storage check-s3-context",
|
|
968
1390
|
"storage setup-s3",
|
|
969
|
-
"
|
|
1391
|
+
"payments check-stripe-keys",
|
|
1392
|
+
"payments create-stripe-webhook",
|
|
1393
|
+
"payments check-stripe-webhook-secret",
|
|
1394
|
+
"payments check-lemonsqueezy-keys",
|
|
1395
|
+
"payments create-lemonsqueezy-webhook",
|
|
1396
|
+
"payments check-lemonsqueezy-webhook-secret",
|
|
970
1397
|
"configure-site-redirects",
|
|
971
1398
|
"configure-email-password",
|
|
972
1399
|
"enable-google-provider",
|
|
@@ -989,14 +1416,22 @@ async function main() {
|
|
|
989
1416
|
return checkSupabaseContext();
|
|
990
1417
|
if (command === "storage" && subcommand === "setup-supabase")
|
|
991
1418
|
return setupSupabaseStorage(flags);
|
|
992
|
-
if (command === "storage" && subcommand === "smoke-test-supabase")
|
|
993
|
-
return smokeTestSupabase();
|
|
994
1419
|
if (command === "storage" && subcommand === "check-s3-context")
|
|
995
1420
|
return checkS3Context(flags);
|
|
996
1421
|
if (command === "storage" && subcommand === "setup-s3")
|
|
997
1422
|
return setupS3Storage(flags);
|
|
998
|
-
if (command === "
|
|
999
|
-
return
|
|
1423
|
+
if (command === "payments" && subcommand === "check-stripe-keys")
|
|
1424
|
+
return checkStripeKeys(flags);
|
|
1425
|
+
if (command === "payments" && subcommand === "create-stripe-webhook")
|
|
1426
|
+
return createStripeWebhook(flags);
|
|
1427
|
+
if (command === "payments" && subcommand === "check-stripe-webhook-secret")
|
|
1428
|
+
return checkStripeWebhookSecret(flags);
|
|
1429
|
+
if (command === "payments" && subcommand === "check-lemonsqueezy-keys")
|
|
1430
|
+
return checkLemonKeys(flags);
|
|
1431
|
+
if (command === "payments" && subcommand === "create-lemonsqueezy-webhook")
|
|
1432
|
+
return createLemonWebhook(flags);
|
|
1433
|
+
if (command === "payments" && subcommand === "check-lemonsqueezy-webhook-secret")
|
|
1434
|
+
return checkLemonWebhookSecret(flags);
|
|
1000
1435
|
if (command === "configure-site-redirects")
|
|
1001
1436
|
return configureSiteRedirects(flags);
|
|
1002
1437
|
if (command === "configure-email-password")
|
package/dist/storageS3.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as crypto from "node:crypto";
|
|
4
|
-
import { S3Client, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketEncryptionCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketCorsCommand, GetBucketLocationCommand, GetPublicAccessBlockCommand, GetBucketEncryptionCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketCorsCommand,
|
|
4
|
+
import { S3Client, HeadBucketCommand, CreateBucketCommand, PutPublicAccessBlockCommand, PutBucketEncryptionCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketCorsCommand, GetBucketLocationCommand, GetPublicAccessBlockCommand, GetBucketEncryptionCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketCorsCommand, } from "@aws-sdk/client-s3";
|
|
5
5
|
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
|
|
6
6
|
const SETUP_CONFIG_PATH = path.join(".vibecodemax", "setup-config.json");
|
|
7
7
|
const DEFAULT_BUCKETS = {
|
|
@@ -10,7 +10,6 @@ const DEFAULT_BUCKETS = {
|
|
|
10
10
|
};
|
|
11
11
|
const DEFAULT_PROJECT_SLUG = "vibecodemax";
|
|
12
12
|
const DEFAULT_REGION = "us-east-1";
|
|
13
|
-
const HEALTHCHECK_PREFIX = "_vibecodemax/healthcheck/default";
|
|
14
13
|
const PUBLIC_ACCESS_BLOCK_PRIVATE = {
|
|
15
14
|
BlockPublicAcls: true,
|
|
16
15
|
IgnorePublicAcls: true,
|
|
@@ -482,55 +481,6 @@ async function verifyTwoBuckets(region, credentials, buckets) {
|
|
|
482
481
|
});
|
|
483
482
|
}
|
|
484
483
|
}
|
|
485
|
-
function buildObjectUrl(bucket, region, key) {
|
|
486
|
-
const encoded = key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
487
|
-
if (region === "us-east-1")
|
|
488
|
-
return `https://${bucket}.s3.amazonaws.com/${encoded}`;
|
|
489
|
-
return `https://${bucket}.s3.${region}.amazonaws.com/${encoded}`;
|
|
490
|
-
}
|
|
491
|
-
async function smokeTestBuckets(region, credentials, buckets) {
|
|
492
|
-
const { s3Client } = createAwsClients(region, credentials);
|
|
493
|
-
const runId = `run_${Date.now()}`;
|
|
494
|
-
const prefix = `${HEALTHCHECK_PREFIX}/${runId}`;
|
|
495
|
-
const publicKey = `${prefix}/public-probe.txt`;
|
|
496
|
-
const privateKey = `${prefix}/private-probe.txt`;
|
|
497
|
-
await s3Client.send(new PutObjectCommand({ Bucket: buckets.public, Key: publicKey, Body: "public healthcheck probe", ContentType: "text/plain" }));
|
|
498
|
-
await s3Client.send(new PutObjectCommand({ Bucket: buckets.private, Key: privateKey, Body: "private healthcheck probe", ContentType: "text/plain" }));
|
|
499
|
-
const publicList = await s3Client.send(new ListObjectsV2Command({ Bucket: buckets.public, Prefix: prefix }));
|
|
500
|
-
const privateList = await s3Client.send(new ListObjectsV2Command({ Bucket: buckets.private, Prefix: prefix }));
|
|
501
|
-
const listPassed = Number(publicList.KeyCount || 0) > 0 && Number(privateList.KeyCount || 0) > 0;
|
|
502
|
-
if (!listPassed) {
|
|
503
|
-
fail("AWS_SMOKE_TEST_FAILED", "AWS S3 smoke-test list operation did not return the uploaded healthcheck objects.", 1, { prefix });
|
|
504
|
-
}
|
|
505
|
-
const publicUrl = buildObjectUrl(buckets.public, region, publicKey);
|
|
506
|
-
const privateUrl = buildObjectUrl(buckets.private, region, privateKey);
|
|
507
|
-
const publicReadResponse = await fetch(publicUrl);
|
|
508
|
-
const privateReadResponse = await fetch(privateUrl);
|
|
509
|
-
const publicRead = publicReadResponse.ok;
|
|
510
|
-
const privateReadBlocked = !privateReadResponse.ok;
|
|
511
|
-
if (!publicRead || !privateReadBlocked) {
|
|
512
|
-
fail("AWS_SMOKE_TEST_FAILED", "AWS S3 smoke-test public/private read checks failed.", 1, {
|
|
513
|
-
publicReadStatus: publicReadResponse.status,
|
|
514
|
-
privateReadStatus: privateReadResponse.status,
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
await s3Client.send(new GetObjectCommand({ Bucket: buckets.private, Key: privateKey }));
|
|
518
|
-
await s3Client.send(new DeleteObjectCommand({ Bucket: buckets.public, Key: publicKey }));
|
|
519
|
-
await s3Client.send(new DeleteObjectCommand({ Bucket: buckets.private, Key: privateKey }));
|
|
520
|
-
return {
|
|
521
|
-
ok: true,
|
|
522
|
-
command: "storage smoke-test-s3",
|
|
523
|
-
runId,
|
|
524
|
-
prefix,
|
|
525
|
-
buckets,
|
|
526
|
-
upload: true,
|
|
527
|
-
list: true,
|
|
528
|
-
publicRead: true,
|
|
529
|
-
privateRead: true,
|
|
530
|
-
delete: true,
|
|
531
|
-
publicObjectUrl: publicUrl,
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
484
|
export async function checkS3Context(flags) {
|
|
535
485
|
const context = readAwsContext(flags);
|
|
536
486
|
if (context.missingKeys.length > 0) {
|
|
@@ -603,28 +553,6 @@ export async function setupS3Storage(flags) {
|
|
|
603
553
|
envWritten: ["AWS_REGION", "AWS_S3_PUBLIC_BUCKET", "AWS_S3_PRIVATE_BUCKET"],
|
|
604
554
|
});
|
|
605
555
|
}
|
|
606
|
-
export async function smokeTestS3(flags) {
|
|
607
|
-
const context = readAwsContext(flags);
|
|
608
|
-
const publicBucket = context.localEnv.values.AWS_S3_PUBLIC_BUCKET || "";
|
|
609
|
-
const privateBucket = context.localEnv.values.AWS_S3_PRIVATE_BUCKET || "";
|
|
610
|
-
if (context.missingKeys.length > 0) {
|
|
611
|
-
fail("MISSING_ENV", `Missing required AWS values: ${context.missingKeys.join(", ")}. Add them to .env.bootstrap.local.`);
|
|
612
|
-
}
|
|
613
|
-
if (!isNonEmptyString(publicBucket) || !isNonEmptyString(privateBucket)) {
|
|
614
|
-
fail("MISSING_ENV", "AWS_S3_PUBLIC_BUCKET or AWS_S3_PRIVATE_BUCKET is missing. Run storage setup first so .env.local is populated.");
|
|
615
|
-
}
|
|
616
|
-
const credentials = {
|
|
617
|
-
accessKeyId: context.accessKeyId,
|
|
618
|
-
secretAccessKey: context.secretAccessKey,
|
|
619
|
-
...(context.sessionToken ? { sessionToken: context.sessionToken } : {}),
|
|
620
|
-
};
|
|
621
|
-
await validateCredentials(context.region, credentials);
|
|
622
|
-
const result = await smokeTestBuckets(context.region, credentials, {
|
|
623
|
-
public: publicBucket.trim(),
|
|
624
|
-
private: privateBucket.trim(),
|
|
625
|
-
});
|
|
626
|
-
printJson(result);
|
|
627
|
-
}
|
|
628
556
|
export function __testOnlyDeterministicBuckets(projectSlug, accountId) {
|
|
629
557
|
return deterministicBuckets({ projectSlug, accountId });
|
|
630
558
|
}
|