@plusscommunities/pluss-core-aws 2.0.26-beta.0 → 2.0.27

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.
@@ -2,25 +2,24 @@ const AWS = require("aws-sdk");
2
2
 
3
3
  module.exports = (accessKey, secretKey) => {
4
4
  return new Promise((resolve, reject) => {
5
- if (accessKey && secretKey) {
6
- AWS.config.update({
7
- accessKeyId: accessKey,
8
- secretAccessKey: secretKey,
9
- });
10
-
11
- const sesv2 = new AWS.SESV2();
12
- const params = { PageSize: 1 };
13
- sesv2.listEmailIdentities(params, (err, data) => {
14
- if (err) return reject(err);
15
- const { EmailIdentities } = data;
16
- return resolve(
17
- EmailIdentities && EmailIdentities.length > 0
18
- ? EmailIdentities[0]
19
- : null,
20
- );
21
- });
22
- } else {
23
- resolve(null);
24
- }
5
+ // Without explicit credentials the execution role is used, scoped to
6
+ // this client so the global SDK config is never mutated
7
+ const sesv2 =
8
+ accessKey && secretKey
9
+ ? new AWS.SESV2({
10
+ accessKeyId: accessKey,
11
+ secretAccessKey: secretKey,
12
+ })
13
+ : new AWS.SESV2();
14
+ const params = { PageSize: 1 };
15
+ sesv2.listEmailIdentities(params, (err, data) => {
16
+ if (err) return reject(err);
17
+ const { EmailIdentities } = data;
18
+ return resolve(
19
+ EmailIdentities && EmailIdentities.length > 0
20
+ ? EmailIdentities[0]
21
+ : null,
22
+ );
23
+ });
25
24
  });
26
25
  };
@@ -2,20 +2,19 @@ const AWS = require("aws-sdk");
2
2
 
3
3
  module.exports = (accessKey, secretKey) => {
4
4
  return new Promise((resolve, reject) => {
5
- if (accessKey && secretKey) {
6
- AWS.config.update({
7
- accessKeyId: accessKey,
8
- secretAccessKey: secretKey,
9
- });
10
-
11
- const sesv2 = new AWS.SESV2();
12
- const params = {};
13
- sesv2.getAccount(params, (err, data) => {
14
- if (err) return reject(err);
15
- return resolve(data);
16
- });
17
- } else {
18
- resolve(null);
19
- }
5
+ // Without explicit credentials the execution role is used, scoped to
6
+ // this client so the global SDK config is never mutated
7
+ const sesv2 =
8
+ accessKey && secretKey
9
+ ? new AWS.SESV2({
10
+ accessKeyId: accessKey,
11
+ secretAccessKey: secretKey,
12
+ })
13
+ : new AWS.SESV2();
14
+ const params = {};
15
+ sesv2.getAccount(params, (err, data) => {
16
+ if (err) return reject(err);
17
+ return resolve(data);
18
+ });
20
19
  });
21
20
  };
@@ -8,26 +8,33 @@ module.exports = async (accessKey, secretKey) => {
8
8
  sender: null,
9
9
  };
10
10
  const { emailConfig } = getConfig();
11
- const service = await getEmailService(accessKey, secretKey);
12
- console.log("Amazon SES status", {
13
- service,
14
- allwoSandbox: emailConfig.allwoSandbox,
15
- forceFallback: emailConfig.forceFallback,
16
- });
11
+ // Fallback-only configurations must not require any SES permissions
17
12
  if (emailConfig.forceFallback) return result;
18
-
19
- if (
20
- service &&
21
- service.SendingEnabled &&
22
- (emailConfig.allwoSandbox || service.ProductionAccessEnabled) &&
23
- service.SendQuota.Max24HourSend > service.SendQuota.SentLast24Hours
24
- ) {
25
- result.enabled = true;
26
- const sender = await getDefaultEmailAddress(accessKey, secretKey);
27
- console.log("Amazon SES sender", sender);
28
- if (sender) {
29
- result.sender = sender.IdentityName;
13
+ try {
14
+ const service = await getEmailService(accessKey, secretKey);
15
+ console.log("Amazon SES status", {
16
+ service,
17
+ allwoSandbox: emailConfig.allwoSandbox,
18
+ });
19
+ if (
20
+ service &&
21
+ service.SendingEnabled &&
22
+ (emailConfig.allwoSandbox || service.ProductionAccessEnabled) &&
23
+ service.SendQuota.Max24HourSend > service.SendQuota.SentLast24Hours
24
+ ) {
25
+ const sender = await getDefaultEmailAddress(accessKey, secretKey);
26
+ console.log("Amazon SES sender", sender);
27
+ // Enabled only once the sender lookup has succeeded, so partial SES
28
+ // permissions degrade to the fallback sender
29
+ result.enabled = true;
30
+ if (sender) {
31
+ result.sender = sender.IdentityName;
32
+ }
30
33
  }
34
+ } catch (e) {
35
+ // A role without the SES grants degrades to the fallback sender
36
+ // instead of failing the send
37
+ console.log("Amazon SES unavailable", e.message || e);
31
38
  }
32
39
  return result;
33
40
  };
package/aws/sendEmail.js CHANGED
@@ -1,15 +1,16 @@
1
1
  const AWS = require("aws-sdk");
2
- const { getConfig } = require("../config");
3
2
 
4
3
  module.exports = (from, to, subject, content, access = null, bcc = null) => {
5
4
  return new Promise((resolve, reject) => {
6
- const { serverlessConfig } = getConfig();
7
- AWS.config.update({
8
- accessKeyId: access ? access.key : serverlessConfig.key,
9
- secretAccessKey: access ? access.secret : serverlessConfig.secret,
10
- });
11
-
12
- const sesv2 = new AWS.SESV2();
5
+ // Default to the execution role; explicit credentials are only for
6
+ // cross-account sends. Scoped to this client so the global SDK config
7
+ // is never mutated
8
+ const sesv2 = access
9
+ ? new AWS.SESV2({
10
+ accessKeyId: access.key,
11
+ secretAccessKey: access.secret,
12
+ })
13
+ : new AWS.SESV2();
13
14
  const params = {
14
15
  Content: {
15
16
  Simple: {
@@ -3,7 +3,8 @@ const moment = require("moment");
3
3
  const checkActivityExists = require("./checkActivityExists");
4
4
  const updateRef = require("../common/updateRef");
5
5
  const getRole = require("../users/getRole");
6
- const { getMultiRowId, getRowId } = require("../../helper");
6
+ const getUser = require("../users/getUser");
7
+ const { getMultiRowId, getRowId, log } = require("../../helper");
7
8
 
8
9
  /**
9
10
  * Creates an analytics entry
@@ -42,33 +43,56 @@ module.exports = async (
42
43
  }
43
44
  }
44
45
 
45
- // Enrich analytics events with the user's site-specific role type and category.
46
- // Always resolved here via getRole rather than expecting callers to provide them because:
47
- // 1. Most callers can't — stream handlers only have minimal embedded objects with no
48
- // type/category fields.
49
- // 2. user.type only carries the primary role, not the site-specific role.
50
- // 3. user.category doesn't exist on the user object — category (staff/primary/linked)
51
- // lives on the role record.
52
- // Centralising here avoids burdening call sites with role resolution and guarantees
53
- // accuracy (always reads current role, not potentially stale caller-provided values).
54
- // getRole is a DynamoDB point-read (~5ms) and this runs async, so no user-facing latency impact.
46
+ // Never fail on a missing user persist with null enrichment.
47
+ const actor = user || {};
48
+
49
+ // Type/category are per-site role facts, so resolve them here — callers only
50
+ // carry minimal user objects.
55
51
  let userType = null;
56
52
  let userCategory = null;
53
+ let enrichmentFailed = false;
57
54
 
58
55
  try {
59
- const role = await getRole(site, user.id);
56
+ const role = await getRole(site, actor.id);
60
57
  if (role) {
61
58
  userType = role.type || null;
62
59
  userCategory = role.category || null;
63
60
  }
64
61
  } catch (e) {
65
- // getRole failed fall through to master fallback below
62
+ // Log so an outage is distinguishable from a missing role, then fall
63
+ // through to the master fallback.
64
+ enrichmentFailed = true;
65
+ log("logActivity", "RoleLookupFailed", {
66
+ site,
67
+ userId: actor.id || null,
68
+ err: e,
69
+ });
66
70
  }
67
71
 
68
- // Best-effort fallback: user.type is set from primary role, may not always be present
69
- if (!userType && user.type === "master") {
72
+ // Master fallback: previews omit type (undefined the system user's explicit
73
+ // null), so resolve it from the user record.
74
+ let primaryType = actor.type;
75
+ if (!userType && primaryType === undefined && actor.id) {
76
+ try {
77
+ const fullUser = await getUser(actor.id);
78
+ primaryType = fullUser ? fullUser.type : null;
79
+ } catch (e) {
80
+ // Best-effort — log so a lookup failure is distinguishable from a
81
+ // genuinely uncategorised user, but never block the activity write.
82
+ enrichmentFailed = true;
83
+ log("logActivity", "EnrichmentLookupFailed", {
84
+ site,
85
+ userId: actor.id,
86
+ err: e,
87
+ });
88
+ }
89
+ }
90
+ if (!userType && primaryType === "master") {
70
91
  userType = "master";
71
- userCategory = "master";
92
+ }
93
+ // Masters count as staff for category filtering.
94
+ if (userType === "master" && !userCategory) {
95
+ userCategory = "staff";
72
96
  }
73
97
 
74
98
  // Log the activity
@@ -78,8 +102,8 @@ module.exports = async (
78
102
  Timestamp: timestamp || moment.utc().valueOf(),
79
103
  EntityType: entityType,
80
104
  SubjectId: entityId,
81
- UserId: user.id,
82
- User: user,
105
+ UserId: actor.id || null,
106
+ User: user || null,
83
107
  Site: site,
84
108
  EntityId: getRowId(entityId, entityType),
85
109
  ActivityId: getMultiRowId([entityId, entityType, actionType]),
@@ -88,6 +112,14 @@ module.exports = async (
88
112
  userCategory,
89
113
  };
90
114
 
115
+ // An unresolved lookup failure means enrichment didn't happen — omit the
116
+ // keys so aggregation applies pre-enrichment totals semantics rather than
117
+ // recording a falsely "enriched but uncategorised" event.
118
+ if (enrichmentFailed && userType === null && userCategory === null) {
119
+ delete activity.userType;
120
+ delete activity.userCategory;
121
+ }
122
+
91
123
  await updateRef(`analytics`, activity);
92
124
  return true;
93
125
  };
@@ -63,8 +63,7 @@ module.exports = async (
63
63
  rateLimitId = null,
64
64
  } = {},
65
65
  ) => {
66
- const { communityConfig, serverlessConfig, emailConfig, accessConfig } =
67
- getConfig();
66
+ const { communityConfig, emailConfig, accessConfig } = getConfig();
68
67
  const now = moment.utc();
69
68
  let rateLimitIdToUse;
70
69
 
@@ -120,10 +119,7 @@ module.exports = async (
120
119
  </div>`;
121
120
  }
122
121
 
123
- const sesInfo = await getEmailServiceInfo(
124
- serverlessConfig.key,
125
- serverlessConfig.secret,
126
- );
122
+ const sesInfo = await getEmailServiceInfo();
127
123
 
128
124
  const mailOptions = {
129
125
  from: fromEmail || `${communityConfig.name} <${sesInfo.sender}>`,
@@ -142,7 +138,7 @@ module.exports = async (
142
138
  mailOptions.to,
143
139
  mailOptions.subject,
144
140
  mailOptions.html,
145
- serverlessConfig,
141
+ null,
146
142
  mailOptions.bcc,
147
143
  );
148
144
  log("sendEmail", "Success", { toEmail, subject }, logId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plusscommunities/pluss-core-aws",
3
- "version": "2.0.26-beta.0",
3
+ "version": "2.0.27",
4
4
  "description": "Core extension package for Pluss Communities platform",
5
5
  "scripts": {
6
6
  "betapatch": "npm version prepatch --preid=beta",