@vibecodemax/cli 0.1.7 → 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.
Files changed (2) hide show
  1. package/dist/cli.js +422 -1
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
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 { randomBytes } from "node:crypto";
5
6
  import { checkS3Context, setupS3Storage } from "./storageS3.js";
6
7
  const SETUP_STATE_PATH = path.join(".vibecodemax", "setup-state.json");
7
8
  const SETUP_CONFIG_PATH = path.join(".vibecodemax", "setup-config.json");
@@ -23,6 +24,26 @@ 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"];
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",
46
+ ];
26
47
  function printJson(value) {
27
48
  process.stdout.write(`${JSON.stringify(value)}\n`);
28
49
  }
@@ -48,7 +69,7 @@ function parseArgs(argv) {
48
69
  let subcommand;
49
70
  const flags = {};
50
71
  let index = 0;
51
- if ((command === "admin" || command === "storage") && rest[0] && !rest[0].startsWith("--")) {
72
+ if ((command === "admin" || command === "storage" || command === "payments") && rest[0] && !rest[0].startsWith("--")) {
52
73
  subcommand = rest[0];
53
74
  index = 1;
54
75
  }
@@ -973,6 +994,388 @@ async function setupSupabaseStorage(flags) {
973
994
  envWritten: ["SUPABASE_PUBLIC_BUCKET", "SUPABASE_PRIVATE_BUCKET"],
974
995
  });
975
996
  }
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) {
1101
+ const { values } = loadLocalEnv();
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,
1213
+ method: "POST",
1214
+ path: "/v1/webhook_endpoints",
1215
+ body: {
1216
+ url: webhookUrl,
1217
+ enabled_events: STRIPE_EVENTS,
1218
+ description: "VibeCodeMax payments webhook",
1219
+ },
1220
+ });
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,
1320
+ method: "POST",
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
+ },
1341
+ });
1342
+ mergeEnvFile(envLocalPath, {
1343
+ LEMONSQUEEZY_WEBHOOK_SECRET: generatedSecret,
1344
+ });
1345
+ printJson({
1346
+ ok: true,
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"],
1377
+ });
1378
+ }
976
1379
  async function main() {
977
1380
  const { command, subcommand, flags } = parseArgs(process.argv.slice(2));
978
1381
  if (!command || command === "--help" || command === "help") {
@@ -985,6 +1388,12 @@ async function main() {
985
1388
  "storage setup-supabase",
986
1389
  "storage check-s3-context",
987
1390
  "storage setup-s3",
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",
988
1397
  "configure-site-redirects",
989
1398
  "configure-email-password",
990
1399
  "enable-google-provider",
@@ -1011,6 +1420,18 @@ async function main() {
1011
1420
  return checkS3Context(flags);
1012
1421
  if (command === "storage" && subcommand === "setup-s3")
1013
1422
  return setupS3Storage(flags);
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);
1014
1435
  if (command === "configure-site-redirects")
1015
1436
  return configureSiteRedirects(flags);
1016
1437
  if (command === "configure-email-password")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodemax/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "VibeCodeMax CLI — local provider setup for bootstrap and project configuration",
5
5
  "type": "module",
6
6
  "bin": {