@infuro/cms-core 1.0.60 → 1.0.61

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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { emitOrderNotificationTrigger, fireOrderNotificationTrigger } from './chunk-O2642WVS.js';
2
- import { checkMultiVendorEnabled, loadUserVendorContext, getPublicSettingsGroup, BlogGeneratorService } from './chunk-JAEUYYGA.js';
3
- export { Address, Attendee, Attribute, BLOG_GENERATOR_AGENT_NAME, BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION, BLOG_GENERATOR_DEFAULT_VALIDATION_RULES, BLOG_GENERATOR_LLM_AGENT_SLUG, BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR, BLOG_METADATA_ENRICHER_AGENT_NAME, BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION, BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES, BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, Blog, BlogGeneratorService, Brand, CMS_ENTITY_MAP, Cart, CartItem, Category, ChatConversation, ChatMessage, Collection, Combo, ComboItem, Comment, Config, Contact, Currency, CurrencyExchange, Customer, Customer_Contacts, Discount, DiscountRules, Event, EventProduct, Form, FormField, FormSubmission, JobSchedule, JobScheduleRun, KnowledgeBaseChunk, KnowledgeBaseDocument, LlmAgent, LlmAgentKnowledgeDocument, Media, MessageTemplate, Order, OrderAddresses, OrderDiscounts, OrderItem, OrderNotificationBinding, OrderNotificationTrigger, OtpChallenge, Page, PasswordResetToken, Payment, Permission, Product, ProductAttribute, ProductCategory, ProductConfig, ProductVariant, RefundPolicy, RefundRequest, RssArticle, RssFeed, Seo, Tag, Tax, User, UserDeviceToken, UserGroup, Vendor, VendorCustomer, VendorRole, VendorRolePermission, VendorUser, VendorUserProfile, Wishlist, WishlistItem, ZIP_MIME_TYPES, applyApprovalStatusSideEffects, applyEventApprovalStatusSideEffects, applyRotatingVendorInvite, applyVendorCustomersContactFilter, applyVendorEventCreateApproval, applyVendorProductCreateApproval, assertCaptchaOk, assertContactAllowedForVendorOrder, assertEventApprovalUpdate, assertProductApprovalUpdate, autoExpireOrderIfPaymentTimeExceeded, buildBlogMetadataUserPrompt, buildCronFromSchedule, buildRssUserPromptFromFeeds, buildUserInviteLink, buildVendorInviteLink, calculateOrderRefundPreview, calculateRefundFromPolicy, checkAndIncrementDiscountUsage, checkEventsEnabled, checkMultiVendorEnabled, completeUserInviteAccept, consumeAppliedDiscountUsages, contactIsVendorCustomer, countDiscountOrdersForContact, countRecentOtpSends, createAnalyticsHandlers, createBlogBySlugHandler, createChangePasswordHandler, createCmsApiHandler, createCmsApp, createCmsAppWithMessaging, createCrudByIdHandler, createCrudHandler, createDashboardStatsHandler, createEcommerceAnalyticsHandler, createEventOrderMessageTemplateHandlers, createForgotPasswordHandler, createFormBySlugHandler, createInviteAcceptHandler, createJobScheduleHandlers, createLlmAgentKnowledgeHandlers, createMediaZipExtractHandler, createMessageTemplateRowLoader, createOtpChallenge, createSetPasswordHandler, createSettingsApiHandlers, createSocialMediaHandlers, createStorefrontApiHandler, createUploadHandler, createUserAuthApiRouter, createUserAvatarHandler, createUserProfileHandler, createUsersApiHandlers, createVendorDashboardHandler, createVendorOnboardHandlers, daysBeforeEventStart, decrementDiscountUsage, describeEventTierPolicy, ensureMessagingPluginsOnCms, ensureScheduleQueueWorker, ensureVendorCustomerForOrderContact, ensureVendorCustomersForOrder, findActiveRefundPolicyForVendor, findUserByInviteToken, findVendorByInviteToken, formatTierRange, generateNumericOtp, getPublicSettingsGroup, getRequireEventApproval, getRequireProductApproval, getRssArticleSummaryFromItem, getVendorCatalogCreateFlags, hashOtpCode, hydrateVendorSessionUser, invalidateEventsCache, invalidateMultiVendorCache, invalidateRequireEventApprovalCache, invalidateRequireProductApprovalCache, invalidateVendorCatalogCreateFlagsCache, isCustomerTypeContact, isMaxPerUserUsageReached, isMaxTotalUsageReached, isZipMedia, llmAgentToChatAgentOptions, loadSettingsGroupFromDb, loadUserVendorContext, mergeGuardrailsIntoSystemPrompt, messagingPlugins, metaFetchUserManagedPages, metaPostPageFeed, metaPostPagePhoto, metaResolvePageAccessToken, newUserInviteToken, normalizePhoneE164, normalizeRefundTiers, overlayCmsPlugins, parseBlogGeneratorAgentContent, parseBlogGeneratorModelOutput, parseBlogMetadataEnrichmentJson, parseLlmAgentValidationRules, pgBossScheduleNameForId, queueErpCreateContactIfEnabled, queueJobScheduleNow, queuePlugin, queueSms, recordDiscountUsage, registerJobRunnerWorker, registerMessagingQueueProcessors, registerSmsQueueProcessor, relativePathFromMediaParentId, resolveBlogCategoryIdByName, resolveSettingsEncryptionKey, resolveVendorIdForContactCheck, sanitizeMediaFolderPath, sanitizeStorageSegment, sendVendorOnboardEmails, simpleDecrypt, simpleEncrypt, syncJobScheduleToPgBoss, validateRefundTiers, validateScheduleInput, validateUserMessageAgainstAgentRules, validateUserMessageAgainstStructuredRules, verifyAndConsumeOtpChallenge, verifyOtpCodeHash, whatsappPlugin, wrapGetCmsWithMessaging } from './chunk-JAEUYYGA.js';
2
+ import { checkMultiVendorEnabled, loadUserVendorContext, getPublicSettingsGroup, BlogGeneratorService } from './chunk-UHT4INVW.js';
3
+ export { Address, Attendee, Attribute, BLOG_GENERATOR_AGENT_NAME, BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION, BLOG_GENERATOR_DEFAULT_VALIDATION_RULES, BLOG_GENERATOR_LLM_AGENT_SLUG, BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR, BLOG_METADATA_ENRICHER_AGENT_NAME, BLOG_METADATA_ENRICHER_DEFAULT_SYSTEM_INSTRUCTION, BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES, BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, Blog, BlogGeneratorService, Brand, CMS_ENTITY_MAP, Cart, CartItem, Category, ChatConversation, ChatMessage, Collection, Combo, ComboItem, Comment, Config, Contact, Currency, CurrencyExchange, Customer, Customer_Contacts, Discount, DiscountRules, Event, EventProduct, Form, FormField, FormSubmission, JobSchedule, JobScheduleRun, KnowledgeBaseChunk, KnowledgeBaseDocument, LlmAgent, LlmAgentKnowledgeDocument, Media, MessageTemplate, Order, OrderAddresses, OrderDiscounts, OrderItem, OrderNotificationBinding, OrderNotificationTrigger, OtpChallenge, Page, PasswordResetToken, Payment, Permission, Product, ProductAttribute, ProductCategory, ProductConfig, ProductVariant, RefundPolicy, RefundRequest, RssArticle, RssFeed, Seo, Tag, Tax, User, UserDeviceToken, UserGroup, Vendor, VendorCustomer, VendorRole, VendorRolePermission, VendorUser, VendorUserProfile, Wishlist, WishlistItem, ZIP_MIME_TYPES, applyApprovalStatusSideEffects, applyEventApprovalStatusSideEffects, applyRotatingVendorInvite, applyVendorCustomersContactFilter, applyVendorEventCreateApproval, applyVendorProductCreateApproval, assertCaptchaOk, assertContactAllowedForVendorOrder, assertEventApprovalUpdate, assertProductApprovalUpdate, autoExpireOrderIfPaymentTimeExceeded, buildBlogMetadataUserPrompt, buildCronFromSchedule, buildRssUserPromptFromFeeds, buildUserInviteLink, buildVendorInviteLink, calculateOrderRefundPreview, calculateRefundFromPolicy, checkAndIncrementDiscountUsage, checkEventsEnabled, checkMultiVendorEnabled, completeUserInviteAccept, consumeAppliedDiscountUsages, contactIsVendorCustomer, countDiscountOrdersForContact, countRecentOtpSends, createAnalyticsHandlers, createBlogBySlugHandler, createChangePasswordHandler, createCmsApiHandler, createCmsApp, createCmsAppWithMessaging, createCrudByIdHandler, createCrudHandler, createDashboardStatsHandler, createEcommerceAnalyticsHandler, createEventOrderMessageTemplateHandlers, createForgotPasswordHandler, createFormBySlugHandler, createInviteAcceptHandler, createJobScheduleHandlers, createLlmAgentKnowledgeHandlers, createMediaZipExtractHandler, createMessageTemplateRowLoader, createOtpChallenge, createSetPasswordHandler, createSettingsApiHandlers, createSocialMediaHandlers, createStorefrontApiHandler, createUploadHandler, createUserAuthApiRouter, createUserAvatarHandler, createUserProfileHandler, createUsersApiHandlers, createVendorDashboardHandler, createVendorOnboardHandlers, daysBeforeEventStart, decrementDiscountUsage, describeEventTierPolicy, ensureMessagingPluginsOnCms, ensureScheduleQueueWorker, ensureVendorCustomerForOrderContact, ensureVendorCustomersForOrder, findActiveRefundPolicyForVendor, findUserByInviteToken, findVendorByInviteToken, formatTierRange, generateNumericOtp, getPublicSettingsGroup, getRequireEventApproval, getRequireProductApproval, getRssArticleSummaryFromItem, getVendorCatalogCreateFlags, hashOtpCode, hydrateVendorSessionUser, invalidateEventsCache, invalidateMultiVendorCache, invalidateRequireEventApprovalCache, invalidateRequireProductApprovalCache, invalidateVendorCatalogCreateFlagsCache, isCustomerTypeContact, isMaxPerUserUsageReached, isMaxTotalUsageReached, isZipMedia, llmAgentToChatAgentOptions, loadSettingsGroupFromDb, loadUserVendorContext, mergeGuardrailsIntoSystemPrompt, messagingPlugins, metaFetchUserManagedPages, metaPostPageFeed, metaPostPagePhoto, metaResolvePageAccessToken, newUserInviteToken, normalizePhoneE164, normalizeRefundTiers, overlayCmsPlugins, parseBlogGeneratorAgentContent, parseBlogGeneratorModelOutput, parseBlogMetadataEnrichmentJson, parseLlmAgentValidationRules, pgBossScheduleNameForId, queueErpCreateContactIfEnabled, queueJobScheduleNow, queuePlugin, queueSms, recordDiscountUsage, registerJobRunnerWorker, registerMessagingQueueProcessors, registerSmsQueueProcessor, relativePathFromMediaParentId, resolveBlogCategoryIdByName, resolveSettingsEncryptionKey, resolveVendorIdForContactCheck, sanitizeMediaFolderPath, sanitizeStorageSegment, sendVendorOnboardEmails, simpleDecrypt, simpleEncrypt, syncJobScheduleToPgBoss, validateRefundTiers, validateScheduleInput, validateUserMessageAgainstAgentRules, validateUserMessageAgainstStructuredRules, verifyAndConsumeOtpChallenge, verifyOtpCodeHash, whatsappPlugin, wrapGetCmsWithMessaging } from './chunk-UHT4INVW.js';
4
4
  import { PgBossService } from './chunk-GO7PPYNU.js';
5
5
  export { JOB_RUNNER_QUEUE, PgBossService } from './chunk-GO7PPYNU.js';
6
6
  export { deductInventoryForConfirmedOrder, mergeInventoryLines, orderInventorySnapshotFromRow, orderStatusHoldsStock, reconcileOrderInventoryBetweenSnapshots, restoreInventoryForCancelledOrder, validateInventoryForConfirmedOrderLines, validateInventoryForOrderBecomingConfirmed } from './chunk-3K5AIEIZ.js';
@@ -1050,37 +1050,91 @@ var LOG = "[s3]";
1050
1050
  function serializeS3Error(err) {
1051
1051
  if (!err || typeof err !== "object") {
1052
1052
  return {
1053
- error: String(err)
1053
+ error: String(err),
1054
+ errorType: typeof err
1054
1055
  };
1055
1056
  }
1056
1057
  const e = err;
1057
- return {
1058
+ const out = {
1058
1059
  name: e.name,
1059
1060
  message: e.message,
1060
1061
  code: e.Code ?? e.code,
1062
+ fault: e.$fault,
1061
1063
  httpStatus: e.$metadata?.httpStatusCode,
1062
1064
  requestId: e.$metadata?.requestId,
1063
1065
  extendedRequestId: e.$metadata?.extendedRequestId,
1064
- cfId: e.$metadata?.cfId
1066
+ cfId: e.$metadata?.cfId,
1067
+ attempts: e.$metadata?.attempts,
1068
+ totalRetryDelay: e.$metadata?.totalRetryDelay,
1069
+ stack: e.stack
1065
1070
  };
1071
+ if (e.cause != null) {
1072
+ out.cause = serializeS3Error(e.cause);
1073
+ }
1074
+ try {
1075
+ const extras = {};
1076
+ for (const k of Object.keys(e)) {
1077
+ if (k === "name" || k === "message" || k === "Code" || k === "code" || k === "$fault" || k === "$metadata" || k === "stack" || k === "cause") {
1078
+ continue;
1079
+ }
1080
+ const v = e[k];
1081
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v == null) {
1082
+ extras[k] = v;
1083
+ } else {
1084
+ extras[k] = String(v);
1085
+ }
1086
+ }
1087
+ if (Object.keys(extras).length > 0) out.extras = extras;
1088
+ } catch {
1089
+ }
1090
+ return out;
1066
1091
  }
1067
1092
  __name(serializeS3Error, "serializeS3Error");
1068
1093
  function publicObjectUrl(config, fullKey) {
1094
+ console.info(LOG, "build public URL", {
1095
+ fullKey,
1096
+ hasBaseUrl: Boolean(config.baseUrl),
1097
+ baseUrl: config.baseUrl ?? null,
1098
+ bucket: config.bucket,
1099
+ region: config.region
1100
+ });
1069
1101
  if (config.baseUrl) {
1070
- return `${config.baseUrl.replace(/\/$/, "")}/${fullKey}`;
1102
+ const url2 = `${config.baseUrl.replace(/\/$/, "")}/${fullKey}`;
1103
+ console.info(LOG, "public URL from baseUrl", {
1104
+ fullKey,
1105
+ url: url2
1106
+ });
1107
+ return url2;
1071
1108
  }
1072
- return `https://s3.${config.region}.amazonaws.com/${config.bucket}/${fullKey}`;
1109
+ const url = `https://s3.${config.region}.amazonaws.com/${config.bucket}/${fullKey}`;
1110
+ console.info(LOG, "public URL from default S3 endpoint", {
1111
+ fullKey,
1112
+ url
1113
+ });
1114
+ return url;
1073
1115
  }
1074
1116
  __name(publicObjectUrl, "publicObjectUrl");
1075
1117
  function describeS3Config(config) {
1076
- const base = config.baseUrl ? new URL(config.baseUrl).host : null;
1118
+ let baseUrlHost = null;
1119
+ try {
1120
+ baseUrlHost = config.baseUrl ? new URL(config.baseUrl).host : null;
1121
+ } catch (err) {
1122
+ console.error(LOG, "invalid baseUrl while describing config", {
1123
+ baseUrl: config.baseUrl,
1124
+ ...serializeS3Error(err)
1125
+ });
1126
+ baseUrlHost = config.baseUrl ?? null;
1127
+ }
1077
1128
  return {
1078
1129
  bucket: config.bucket,
1079
1130
  region: config.region,
1080
1131
  prefix: config.prefix ?? "uploads",
1081
1132
  hasBaseUrl: Boolean(config.baseUrl),
1082
- baseUrlHost: base,
1083
- accessKeyIdLast4: config.accessKeyId.slice(-4),
1133
+ baseUrl: config.baseUrl ?? null,
1134
+ baseUrlHost,
1135
+ accessKeyIdLast4: config.accessKeyId ? config.accessKeyId.slice(-4) : null,
1136
+ accessKeyIdLength: config.accessKeyId?.length ?? 0,
1137
+ secretAccessKeyLength: config.secretAccessKey?.length ?? 0,
1084
1138
  envSummary: {
1085
1139
  AWS_BUCKET_NAME: Boolean(config.bucket),
1086
1140
  AWS_REGION: Boolean(config.region),
@@ -1091,7 +1145,11 @@ function describeS3Config(config) {
1091
1145
  };
1092
1146
  }
1093
1147
  __name(describeS3Config, "describeS3Config");
1094
- async function probePublicUrl(url) {
1148
+ async function probePublicUrl(url, fullKey) {
1149
+ console.info(LOG, "probe public URL start", {
1150
+ url,
1151
+ fullKey
1152
+ });
1095
1153
  try {
1096
1154
  const head = await fetch(url, {
1097
1155
  method: "HEAD",
@@ -1099,15 +1157,25 @@ async function probePublicUrl(url) {
1099
1157
  });
1100
1158
  const headInfo = {
1101
1159
  url,
1160
+ fullKey,
1102
1161
  status: head.status,
1103
1162
  ok: head.ok,
1104
1163
  contentType: head.headers.get("content-type"),
1105
- location: head.headers.get("location")
1164
+ contentLength: head.headers.get("content-length"),
1165
+ location: head.headers.get("location"),
1166
+ cacheControl: head.headers.get("cache-control"),
1167
+ etag: head.headers.get("etag")
1106
1168
  };
1169
+ console.info(LOG, "probe HEAD result", headInfo);
1107
1170
  if (head.ok) {
1108
1171
  console.info(LOG, "public URL reachable (HEAD)", headInfo);
1109
1172
  return;
1110
1173
  }
1174
+ console.warn(LOG, "probe HEAD not ok \u2014 trying ranged GET", {
1175
+ url,
1176
+ fullKey,
1177
+ status: head.status
1178
+ });
1111
1179
  const getRes = await fetch(url, {
1112
1180
  method: "GET",
1113
1181
  headers: {
@@ -1118,19 +1186,26 @@ async function probePublicUrl(url) {
1118
1186
  let bodySnippet = "";
1119
1187
  try {
1120
1188
  bodySnippet = (await getRes.text()).slice(0, 400);
1121
- } catch {
1189
+ } catch (bodyErr) {
1190
+ console.error(LOG, "probe GET body read failed", {
1191
+ url,
1192
+ fullKey,
1193
+ ...serializeS3Error(bodyErr)
1194
+ });
1122
1195
  }
1123
1196
  console.error(LOG, "public URL not readable \u2014 admin images will not display", {
1124
1197
  ...headInfo,
1125
1198
  getStatus: getRes.status,
1126
1199
  getOk: getRes.ok,
1127
1200
  getContentType: getRes.headers.get("content-type"),
1201
+ getContentLength: getRes.headers.get("content-length"),
1128
1202
  bodySnippet
1129
1203
  });
1130
1204
  } catch (err) {
1131
1205
  console.error(LOG, "public URL probe failed", {
1132
1206
  url,
1133
- error: err instanceof Error ? err.message : String(err)
1207
+ fullKey,
1208
+ ...serializeS3Error(err)
1134
1209
  });
1135
1210
  }
1136
1211
  }
@@ -1138,9 +1213,69 @@ __name(probePublicUrl, "probePublicUrl");
1138
1213
  function getS3Storage(config) {
1139
1214
  const prefix = (config.prefix ?? "uploads").replace(/\/$/, "");
1140
1215
  const keyPrefix = prefix ? `${prefix}/` : "";
1216
+ console.info(LOG, "storage service created", {
1217
+ ...describeS3Config(config),
1218
+ keyPrefix,
1219
+ resolvedPrefix: prefix
1220
+ });
1141
1221
  return {
1142
1222
  async upload(buffer, key, contentType) {
1143
- const { S3Client, PutObjectCommand } = await import('@aws-sdk/client-s3');
1223
+ const uploadId = `upl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1224
+ const started = Date.now();
1225
+ console.info(LOG, "upload() called", {
1226
+ uploadId,
1227
+ incomingKey: key,
1228
+ contentType,
1229
+ bytes: buffer?.length ?? null,
1230
+ isBuffer: Buffer.isBuffer(buffer),
1231
+ ...describeS3Config(config)
1232
+ });
1233
+ if (!buffer || !Buffer.isBuffer(buffer)) {
1234
+ console.error(LOG, "upload aborted \u2014 invalid buffer", {
1235
+ uploadId,
1236
+ incomingKey: key,
1237
+ bufferType: typeof buffer,
1238
+ isBuffer: Buffer.isBuffer(buffer)
1239
+ });
1240
+ throw new Error("S3 upload: invalid buffer");
1241
+ }
1242
+ if (!key || typeof key !== "string") {
1243
+ console.error(LOG, "upload aborted \u2014 invalid key", {
1244
+ uploadId,
1245
+ key,
1246
+ keyType: typeof key
1247
+ });
1248
+ throw new Error("S3 upload: invalid key");
1249
+ }
1250
+ console.info(LOG, "importing @aws-sdk/client-s3", {
1251
+ uploadId,
1252
+ key
1253
+ });
1254
+ let S3Client;
1255
+ let PutObjectCommand;
1256
+ let HeadObjectCommand;
1257
+ try {
1258
+ const sdk = await import('@aws-sdk/client-s3');
1259
+ S3Client = sdk.S3Client;
1260
+ PutObjectCommand = sdk.PutObjectCommand;
1261
+ HeadObjectCommand = sdk.HeadObjectCommand;
1262
+ console.info(LOG, "AWS SDK import ok", {
1263
+ uploadId,
1264
+ key
1265
+ });
1266
+ } catch (err) {
1267
+ console.error(LOG, "AWS SDK import failed", {
1268
+ uploadId,
1269
+ key,
1270
+ ...serializeS3Error(err)
1271
+ });
1272
+ throw err;
1273
+ }
1274
+ console.info(LOG, "creating S3Client", {
1275
+ uploadId,
1276
+ region: config.region,
1277
+ accessKeyIdLast4: config.accessKeyId.slice(-4)
1278
+ });
1144
1279
  const client = new S3Client({
1145
1280
  region: config.region,
1146
1281
  credentials: {
@@ -1151,47 +1286,114 @@ function getS3Storage(config) {
1151
1286
  const rawKey = key.replace(/^\/+/, "");
1152
1287
  const fullKey = keyPrefix + rawKey;
1153
1288
  const publicUrl = publicObjectUrl(config, fullKey);
1154
- const started = Date.now();
1155
- console.info(LOG, "upload start", {
1156
- ...describeS3Config(config),
1157
- key: rawKey,
1289
+ console.info(LOG, "resolved S3 object key", {
1290
+ uploadId,
1291
+ incomingKey: key,
1292
+ rawKey,
1293
+ keyPrefix,
1158
1294
  fullKey,
1295
+ bucket: config.bucket,
1296
+ region: config.region,
1159
1297
  publicUrl,
1160
1298
  contentType,
1161
- bytes: buffer.length
1299
+ bytes: buffer.length,
1300
+ note: "PutObject will use Bucket + Key (= fullKey)"
1301
+ });
1302
+ const putInput = {
1303
+ Bucket: config.bucket,
1304
+ Key: fullKey,
1305
+ Body: buffer,
1306
+ ContentType: contentType
1307
+ };
1308
+ console.info(LOG, "PutObjectCommand prepared", {
1309
+ uploadId,
1310
+ Bucket: putInput.Bucket,
1311
+ Key: putInput.Key,
1312
+ ContentType: putInput.ContentType,
1313
+ BodyBytes: buffer.length,
1314
+ BodyIsBuffer: Buffer.isBuffer(putInput.Body)
1162
1315
  });
1316
+ let putResult;
1163
1317
  try {
1164
- await client.send(new PutObjectCommand({
1165
- Bucket: config.bucket,
1166
- Key: fullKey,
1167
- Body: buffer,
1168
- ContentType: contentType
1169
- }));
1318
+ console.info(LOG, "PutObject send start", {
1319
+ uploadId,
1320
+ bucket: config.bucket,
1321
+ fullKey,
1322
+ msElapsed: Date.now() - started
1323
+ });
1324
+ putResult = await client.send(new PutObjectCommand(putInput));
1325
+ console.info(LOG, "PutObject send returned", {
1326
+ uploadId,
1327
+ bucket: config.bucket,
1328
+ fullKey,
1329
+ ETag: putResult?.ETag,
1330
+ VersionId: putResult?.VersionId,
1331
+ httpStatus: putResult?.$metadata?.httpStatusCode,
1332
+ requestId: putResult?.$metadata?.requestId,
1333
+ ms: Date.now() - started
1334
+ });
1170
1335
  } catch (err) {
1171
- const errorDump = serializeS3Error(err);
1172
1336
  console.error(LOG, "PutObject failed", {
1337
+ uploadId,
1173
1338
  ...describeS3Config(config),
1174
- key: rawKey,
1339
+ incomingKey: key,
1340
+ rawKey,
1341
+ keyPrefix,
1175
1342
  fullKey,
1176
1343
  publicUrl,
1177
1344
  contentType,
1178
1345
  bytes: buffer.length,
1179
- ...errorDump,
1346
+ ms: Date.now() - started,
1347
+ ...serializeS3Error(err),
1180
1348
  rawError: err instanceof Error ? err.message : String(err)
1181
1349
  });
1182
1350
  throw err;
1183
1351
  }
1184
- console.info(LOG, "PutObject ok", {
1352
+ console.info(LOG, "PutObject ok \u2014 verifying with HeadObject", {
1353
+ uploadId,
1354
+ bucket: config.bucket,
1355
+ fullKey,
1356
+ publicUrl
1357
+ });
1358
+ try {
1359
+ const head = await client.send(new HeadObjectCommand({
1360
+ Bucket: config.bucket,
1361
+ Key: fullKey
1362
+ }));
1363
+ console.info(LOG, "HeadObject ok \u2014 object exists in bucket", {
1364
+ uploadId,
1365
+ bucket: config.bucket,
1366
+ fullKey,
1367
+ contentType: head.ContentType,
1368
+ contentLength: head.ContentLength,
1369
+ etag: head.ETag,
1370
+ lastModified: head.LastModified?.toISOString?.() ?? head.LastModified,
1371
+ httpStatus: head.$metadata?.httpStatusCode,
1372
+ requestId: head.$metadata?.requestId
1373
+ });
1374
+ } catch (headErr) {
1375
+ console.error(LOG, "HeadObject failed after PutObject \u2014 key may be wrong or ACL/permission issue", {
1376
+ uploadId,
1377
+ bucket: config.bucket,
1378
+ fullKey,
1379
+ publicUrl,
1380
+ ...serializeS3Error(headErr)
1381
+ });
1382
+ }
1383
+ console.info(LOG, "upload complete", {
1384
+ uploadId,
1185
1385
  ...describeS3Config(config),
1186
- key: rawKey,
1386
+ incomingKey: key,
1387
+ rawKey,
1187
1388
  fullKey,
1188
1389
  publicUrl,
1189
1390
  contentType,
1190
1391
  bytes: buffer.length,
1191
1392
  ms: Date.now() - started,
1393
+ ETag: putResult?.ETag,
1192
1394
  hasCustomBaseUrl: Boolean(config.baseUrl)
1193
1395
  });
1194
- void probePublicUrl(publicUrl);
1396
+ void probePublicUrl(publicUrl, fullKey);
1195
1397
  return publicUrl;
1196
1398
  }
1197
1399
  };
@@ -1202,26 +1404,66 @@ function s3StoragePlugin(config) {
1202
1404
  name: "storage",
1203
1405
  version: "1.0.0",
1204
1406
  async init(context) {
1407
+ console.info(LOG, "plugin init start", {
1408
+ configKeys: Object.keys(config ?? {}),
1409
+ hasContextConfig: Boolean(context?.config),
1410
+ contextConfigKeys: context?.config ? Object.keys(context.config) : []
1411
+ });
1205
1412
  const region = config.region ?? context.config.AWS_REGION ?? "";
1206
1413
  const accessKeyId = config.accessKeyId ?? context.config.AWS_ACCESS_KEY_ID ?? "";
1207
1414
  const secretAccessKey = config.secretAccessKey ?? context.config.AWS_SECRET_ACCESS_KEY ?? "";
1208
1415
  const bucket = config.bucket ?? context.config.AWS_BUCKET_NAME ?? "";
1209
1416
  const baseUrl = config.baseUrl ?? context.config.S3_MEDIA_URL ?? context.config.S3_CUSTOM_DOMAIN;
1417
+ console.info(LOG, "plugin init resolved raw values", {
1418
+ region: region || "(empty)",
1419
+ bucket: bucket || "(empty)",
1420
+ prefix: config.prefix ?? "uploads",
1421
+ hasAccessKeyId: Boolean(accessKeyId),
1422
+ accessKeyIdLast4: accessKeyId ? accessKeyId.slice(-4) : null,
1423
+ hasSecretAccessKey: Boolean(secretAccessKey),
1424
+ secretAccessKeyLength: secretAccessKey?.length ?? 0,
1425
+ baseUrlRaw: baseUrl ?? null,
1426
+ fromPluginConfig: {
1427
+ region: Boolean(config.region),
1428
+ accessKeyId: Boolean(config.accessKeyId),
1429
+ secretAccessKey: Boolean(config.secretAccessKey),
1430
+ bucket: Boolean(config.bucket),
1431
+ baseUrl: Boolean(config.baseUrl)
1432
+ },
1433
+ fromContext: {
1434
+ AWS_REGION: Boolean(context.config.AWS_REGION),
1435
+ AWS_ACCESS_KEY_ID: Boolean(context.config.AWS_ACCESS_KEY_ID),
1436
+ AWS_SECRET_ACCESS_KEY: Boolean(context.config.AWS_SECRET_ACCESS_KEY),
1437
+ AWS_BUCKET_NAME: Boolean(context.config.AWS_BUCKET_NAME),
1438
+ S3_MEDIA_URL: Boolean(context.config.S3_MEDIA_URL),
1439
+ S3_CUSTOM_DOMAIN: Boolean(context.config.S3_CUSTOM_DOMAIN)
1440
+ }
1441
+ });
1210
1442
  const resolvedBaseUrl = typeof baseUrl === "string" && baseUrl ? baseUrl.startsWith("http") ? baseUrl : `https://${baseUrl}` : void 0;
1443
+ console.info(LOG, "plugin init baseUrl resolution", {
1444
+ baseUrlRaw: baseUrl ?? null,
1445
+ resolvedBaseUrl: resolvedBaseUrl ?? null
1446
+ });
1211
1447
  if (!region || !accessKeyId || !secretAccessKey || !bucket) {
1212
1448
  console.error(LOG, "plugin init missing config", {
1213
1449
  hasRegion: Boolean(region),
1214
1450
  hasAccessKeyId: Boolean(accessKeyId),
1215
1451
  hasSecretAccessKey: Boolean(secretAccessKey),
1216
1452
  hasBucket: Boolean(bucket),
1217
- hasBaseUrl: Boolean(resolvedBaseUrl)
1453
+ hasBaseUrl: Boolean(resolvedBaseUrl),
1454
+ regionValue: region || null,
1455
+ bucketValue: bucket || null
1218
1456
  });
1219
1457
  throw new Error("S3 storage plugin: missing region, accessKeyId, secretAccessKey or bucket");
1220
1458
  }
1221
1459
  let baseUrlHost = null;
1222
1460
  try {
1223
1461
  baseUrlHost = resolvedBaseUrl ? new URL(resolvedBaseUrl).host : null;
1224
- } catch {
1462
+ } catch (err) {
1463
+ console.error(LOG, "plugin init invalid resolvedBaseUrl", {
1464
+ resolvedBaseUrl,
1465
+ ...serializeS3Error(err)
1466
+ });
1225
1467
  baseUrlHost = resolvedBaseUrl ?? null;
1226
1468
  }
1227
1469
  console.info(LOG, "plugin ready", {
@@ -1229,6 +1471,7 @@ function s3StoragePlugin(config) {
1229
1471
  bucket,
1230
1472
  prefix: config.prefix ?? "uploads",
1231
1473
  hasBaseUrl: Boolean(resolvedBaseUrl),
1474
+ baseUrl: resolvedBaseUrl ?? null,
1232
1475
  baseUrlHost,
1233
1476
  accessKeyIdLast4: accessKeyId.slice(-4),
1234
1477
  envSummary: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infuro/cms-core",
3
- "version": "1.0.60",
3
+ "version": "1.0.61",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",