@friggframework/core 2.0.0-next.121 → 2.0.0-next.122

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/CLAUDE.md CHANGED
@@ -224,7 +224,7 @@ packages/core/
224
224
  (`remove-entities-from-integration.js`) refuses to leave a module with no
225
225
  entity (`LAST_ENTITY_OF_MODULE`, 409). Re-adding an attached entity or
226
226
  removing a detached one changes nothing; a detached entity is not deleted.
227
- `integration-repository-entities-parity.test.js` runs every adapter against
227
+ `integration-repository-entities-parity.db.test.js` runs every adapter against
228
228
  real databases when `INTEGRATION_ENTITIES_PARITY_MONGO_URL` /
229
229
  `INTEGRATION_ENTITIES_PARITY_POSTGRES_URL` are set. The DocumentDB adapter
230
230
  writes only `Integration.entityIds` (as its `createIntegration` does), not
@@ -266,7 +266,7 @@ packages/core/
266
266
  mapping keys to leave out of the rows; never write such rows back. Path
267
267
  segments must match `^[A-Za-z_][A-Za-z0-9_]*$`. The PostgreSQL, MongoDB and
268
268
  DocumentDB adapters give the same pages and totals;
269
- `integration-mapping-repository-query-parity.test.js` checks that against
269
+ `integration-mapping-repository-query-parity.db.test.js` checks that against
270
270
  real databases when `QUERY_MAPPINGS_PARITY_MONGO_URL` /
271
271
  `QUERY_MAPPINGS_PARITY_POSTGRES_URL` are set. Two orderings still differ:
272
272
  strings compare by the database collation on PostgreSQL and by code point
@@ -67,9 +67,10 @@ class MyIntegration extends IntegrationBase {
67
67
  async hydrateFromExternalUser(externalUserId) {
68
68
  // Find integration context by external entity ID
69
69
  const result =
70
- await this.commands.findIntegrationContextByExternalEntityId(
71
- externalUserId
72
- );
70
+ await this.commands.findIntegrationContextByExternalEntityId({
71
+ externalId: externalUserId,
72
+ type: 'my-integration', // integration config.type to match
73
+ });
73
74
 
74
75
  if (result.error) {
75
76
  return { error: result.error };
@@ -231,9 +232,10 @@ Manage integration records and load full integration contexts.
231
232
  ```javascript
232
233
  // Find integration context by external entity ID
233
234
  // Returns { context, error } where context includes record + hydrated modules
234
- const result = await commands.findIntegrationContextByExternalEntityId(
235
- 'external-user-or-workspace-id'
236
- );
235
+ const result = await commands.findIntegrationContextByExternalEntityId({
236
+ externalId: 'external-user-or-workspace-id',
237
+ type: 'my-integration', // integration config.type to match
238
+ });
237
239
 
238
240
  if (!result.error) {
239
241
  integration.setIntegrationRecord(result.context);
@@ -299,7 +301,10 @@ try {
299
301
  For integration context operations, errors are returned in the result:
300
302
 
301
303
  ```javascript
302
- const result = await commands.findIntegrationContextByExternalEntityId(userId);
304
+ const result = await commands.findIntegrationContextByExternalEntityId({
305
+ externalId: userId,
306
+ type: 'my-integration',
307
+ });
303
308
 
304
309
  if (result.error) {
305
310
  return { error: result.error };
@@ -349,9 +354,10 @@ const result =
349
354
  const { createFriggCommands } = require('@friggframework/core');
350
355
 
351
356
  this.commands = createFriggCommands({ integrationClass: MyIntegration });
352
- const result = await this.commands.findIntegrationContextByExternalEntityId(
353
- userId
354
- );
357
+ const result = await this.commands.findIntegrationContextByExternalEntityId({
358
+ externalId: userId,
359
+ type: 'my-integration',
360
+ });
355
361
  ```
356
362
 
357
363
  ## Best Practices
@@ -433,7 +439,7 @@ if (!user) {
433
439
  | | `findEntitiesByIds(ids)` | Find multiple by IDs |
434
440
  | | `updateEntity(id, updates)` | Update entity properties |
435
441
  | | `deleteEntity(id)` | Delete entity |
436
- | **Integration** | `findIntegrationContextByExternalEntityId(externalId)` | Load integration + modules by external ID |
442
+ | **Integration** | `findIntegrationContextByExternalEntityId({ externalId, type })` | Load integration + modules by external ID |
437
443
  | | `loadIntegrationContextById(integrationId)` | Load integration + modules by ID |
438
444
 
439
445
  ## Support
@@ -1,4 +1,5 @@
1
1
  const { Cryptor } = require('../encrypt/Cryptor');
2
+ const { isEncryptedEnvelope } = require('./encryption/ciphertext-format');
2
3
  const {
3
4
  getEncryptedFields,
4
5
  getFieldsToEncryptOnWrite,
@@ -295,39 +296,7 @@ class DocumentDBEncryptionService {
295
296
  * _isEncryptedValue({}) // false
296
297
  */
297
298
  _isEncryptedValue(value) {
298
- // Must be string
299
- if (typeof value !== 'string') {
300
- return false;
301
- }
302
-
303
- // Must have exactly 4 colon-separated parts
304
- const parts = value.split(':');
305
- if (parts.length !== 4) {
306
- return false;
307
- }
308
-
309
- // Enhanced validation: check for base64 pattern
310
- // This prevents false positives on URLs, connection strings, etc.
311
- const base64Pattern = /^[A-Za-z0-9+/=]+$/;
312
-
313
- // All parts should be base64-encoded
314
- if (!parts.every(part => base64Pattern.test(part))) {
315
- return false;
316
- }
317
-
318
- // Encrypted values should be sufficiently long to be valid
319
- // Real encrypted values from Cryptor are always >50 chars due to envelope encryption format:
320
- // - keyId (base64): ~12 chars minimum
321
- // - iv (base64): ~24 chars for 16-byte IV
322
- // - ciphertext (base64): varies, minimum ~16 chars for small values
323
- // - encryptedKey (base64): ~44 chars for 32-byte data key
324
- // Total minimum: ~96 chars, so 50 is a safe lower bound
325
- // This prevents false positives on non-encrypted strings that happen to have 4 colons
326
- if (value.length < 50) {
327
- return false;
328
- }
329
-
330
- return true;
299
+ return isEncryptedEnvelope(value);
331
300
  }
332
301
  }
333
302
 
@@ -508,9 +508,9 @@ return entities.map((e) => ({
508
508
 
509
509
  **Verified:**
510
510
 
511
- - ✅ `postgres-relation-decryption.test.js` - Proves the bug exists
512
- - ✅ `postgres-decryption-fix-verification.test.js` - Verifies separate queries work
513
- - ✅ `mongo-decryption-fix-verification.test.js` - Verifies fix for MongoDB
511
+ - ✅ `postgres-relation-decryption.db.test.js` - Proves the bug exists
512
+ - ✅ `postgres-decryption-fix-verification.db.test.js` - Verifies separate queries work
513
+ - ✅ `mongo-decryption-fix-verification.db.test.js` - Verifies fix for MongoDB
514
514
 
515
515
  **Implementation Examples:**
516
516
 
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Recognises values produced by Cryptor.encrypt():
3
+ * keyId:iv:ciphertext:encryptedKey
4
+ * keyId and encryptedKey are base64, iv and ciphertext are hex, so all four
5
+ * parts match the base64 alphabet. Real envelopes are well over 50
6
+ * characters (the encrypted data key alone is longer).
7
+ *
8
+ * Shared by the Prisma (FieldEncryptionService) and DocumentDB paths so a
9
+ * plaintext value that happens to contain colons (`a:b:c:d`, a URL with
10
+ * userinfo and a port) is never mistaken for ciphertext: on write it gets
11
+ * encrypted, on read it is returned as plaintext.
12
+ */
13
+ const BASE64_PART = /^[A-Za-z0-9+/=]+$/;
14
+ const MIN_ENVELOPE_LENGTH = 50;
15
+
16
+ function isEncryptedEnvelope(value) {
17
+ if (typeof value !== 'string' || value.length < MIN_ENVELOPE_LENGTH) {
18
+ return false;
19
+ }
20
+ const parts = value.split(':');
21
+ return parts.length === 4 && parts.every((part) => BASE64_PART.test(part));
22
+ }
23
+
24
+ module.exports = { isEncryptedEnvelope };
@@ -146,6 +146,16 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
146
146
  }
147
147
 
148
148
  for (const field of credentialFields) {
149
+ // Validate entry by entry: one bad entry must not drop the
150
+ // module's (or any other module's) valid fields.
151
+ if (typeof field !== 'string' || field.trim() === '') {
152
+ logger.warn(
153
+ `Ignoring invalid encryption.credentialFields entry ${JSON.stringify(field)} ` +
154
+ `in module "${moduleDef.moduleName || moduleDef.name || 'unknown'}" ` +
155
+ '(expected a non-empty string)'
156
+ );
157
+ continue;
158
+ }
149
159
  const prefixedField = field.startsWith('data.') ? field : `data.${field}`;
150
160
  fields.push(prefixedField);
151
161
  }
@@ -174,26 +184,46 @@ function loadModuleEncryptionSchemas(integrations) {
174
184
  return;
175
185
  }
176
186
 
177
- const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
187
+ const { getModulesDefinitionFromIntegrationClasses } = require('../../integrations/utils/map-integration-dto');
188
+
189
+ // Integrations without modules contribute no credential fields.
190
+ const integrationsWithModules = integrations.filter(
191
+ (integration) => integration?.Definition?.modules
192
+ );
193
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(
194
+ integrationsWithModules
195
+ );
178
196
 
179
- const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
180
- const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
197
+ // Fields the core schema already encrypts (e.g. data.api_key) are
198
+ // covered; registering them again would fail validation and drop every
199
+ // other module field with them.
200
+ const coreCredentialFields = CORE_ENCRYPTION_SCHEMA.Credential.fields;
201
+ const credentialFields = extractCredentialFieldsFromModules(
202
+ moduleDefinitions
203
+ ).filter((field) => !coreCredentialFields.includes(field));
181
204
 
182
205
  if (credentialFields.length === 0) {
183
206
  return;
184
207
  }
185
208
 
186
- const moduleSchema = {
209
+ // Merge with whatever custom schema the app already registered rather
210
+ // than replacing it (registerCustomSchema overwrites).
211
+ const existingCredentialFields = customSchema.Credential?.fields || [];
212
+ const mergedSchema = {
213
+ ...customSchema,
187
214
  Credential: {
188
- fields: credentialFields
189
- }
215
+ ...customSchema.Credential,
216
+ fields: [
217
+ ...new Set([...existingCredentialFields, ...credentialFields]),
218
+ ],
219
+ },
190
220
  };
191
221
 
192
222
  logger.info(
193
223
  `Registering module-level encryption for ${credentialFields.length} credential fields`
194
224
  );
195
225
 
196
- registerCustomSchema(moduleSchema);
226
+ registerCustomSchema(mergedSchema);
197
227
  }
198
228
 
199
229
  /**
@@ -205,7 +235,40 @@ function loadModuleEncryptionSchemas(integrations) {
205
235
  *
206
236
  * Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
207
237
  */
238
+ /**
239
+ * Raised when an app's encryption configuration is invalid. In deployed
240
+ * stages this fails startup instead of silently writing plaintext.
241
+ */
242
+ class EncryptionSchemaError extends Error {
243
+ constructor(message, { cause } = {}) {
244
+ super(message, { cause });
245
+ this.name = 'EncryptionSchemaError';
246
+ }
247
+ }
248
+
249
+ /** Stages where field-level encryption is bypassed (see prisma.js). */
250
+ function isLocalStage() {
251
+ const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
252
+ return ['dev', 'test', 'local'].includes(stage.toLowerCase());
253
+ }
254
+
255
+ /**
256
+ * An app definition was found but part of its encryption configuration
257
+ * could not be applied. Deployed stages fail closed; local stages warn and
258
+ * carry on so development isn't blocked.
259
+ */
260
+ function reportInvalidEncryptionConfig(what, error) {
261
+ const message = `Invalid ${what}: ${error.message}`;
262
+ if (isLocalStage()) {
263
+ logger.warn(`${message} (ignored in local stage)`);
264
+ return;
265
+ }
266
+ logger.error(message);
267
+ throw new EncryptionSchemaError(message, { cause: error });
268
+ }
269
+
208
270
  function loadCustomEncryptionSchema() {
271
+ let appDefinition;
209
272
  try {
210
273
  // Lazy require to avoid circular dependency issues
211
274
  const path = require('node:path');
@@ -220,38 +283,54 @@ function loadCustomEncryptionSchema() {
220
283
  const backendIndexPath = path.join(backendDir, 'index.js');
221
284
 
222
285
  const backendModule = require(backendIndexPath);
223
- const appDefinition = backendModule?.Definition;
286
+ appDefinition = backendModule?.Definition;
287
+ } catch (error) {
288
+ // No loadable backend (tests, standalone usage): custom schema is
289
+ // optional, so this stays at debug level.
290
+ logger.debug('Could not load custom encryption schema:', error.message);
291
+ return;
292
+ }
224
293
 
225
- if (!appDefinition) {
226
- return; // No app definition found
227
- }
294
+ if (!appDefinition) {
295
+ return; // No app definition found
296
+ }
297
+
298
+ // From here an app definition exists, so failures are configuration
299
+ // errors and must be visible.
228
300
 
229
- // Load app-level custom schema
230
- const customSchema = appDefinition.encryption?.schema;
231
- if (customSchema && Object.keys(customSchema).length > 0) {
301
+ // Load app-level custom schema
302
+ const customSchema = appDefinition.encryption?.schema;
303
+ if (customSchema && Object.keys(customSchema).length > 0) {
304
+ try {
232
305
  registerCustomSchema(customSchema);
306
+ } catch (error) {
307
+ reportInvalidEncryptionConfig('app encryption.schema', error);
233
308
  }
309
+ }
234
310
 
235
- // Load app-level encryption opt-out — apps can declare fields they
236
- // don't want encrypted on write (decryption on read still works,
237
- // so legacy data remains readable).
238
- const disable = appDefinition.encryption?.disable;
239
- if (disable && Object.keys(disable).length > 0) {
311
+ // Load app-level encryption opt-out — apps can declare fields they
312
+ // don't want encrypted on write (decryption on read still works,
313
+ // so legacy data remains readable).
314
+ const disable = appDefinition.encryption?.disable;
315
+ if (disable && Object.keys(disable).length > 0) {
316
+ try {
240
317
  registerEncryptionOptOut(disable);
318
+ } catch (error) {
319
+ reportInvalidEncryptionConfig('app encryption.disable', error);
241
320
  }
321
+ }
242
322
 
243
- // Load module-level encryption schemas from integrations
244
- const integrations = appDefinition.integrations;
245
- if (integrations && Array.isArray(integrations)) {
323
+ // Load module-level encryption schemas from integrations
324
+ const integrations = appDefinition.integrations;
325
+ if (integrations && Array.isArray(integrations)) {
326
+ try {
246
327
  loadModuleEncryptionSchemas(integrations);
328
+ } catch (error) {
329
+ reportInvalidEncryptionConfig(
330
+ 'module encryption.credentialFields',
331
+ error
332
+ );
247
333
  }
248
- } catch (error) {
249
- // Silently ignore errors - custom schema is optional
250
- // This handles cases like:
251
- // - Backend package.json not found (tests, standalone usage)
252
- // - No appDefinition defined
253
- // - No custom encryption schema specified
254
- logger.debug('Could not load custom encryption schema:', error.message);
255
334
  }
256
335
  }
257
336
 
@@ -386,6 +465,7 @@ function resetCustomSchema() {
386
465
  }
387
466
 
388
467
  module.exports = {
468
+ EncryptionSchemaError,
389
469
  CORE_ENCRYPTION_SCHEMA,
390
470
  getEncryptedFields,
391
471
  getFieldsToEncryptOnWrite,
@@ -1,3 +1,5 @@
1
+ const { isEncryptedEnvelope } = require('./ciphertext-format');
2
+
1
3
  /**
2
4
  * Field Encryption Service
3
5
  *
@@ -147,12 +149,7 @@ class FieldEncryptionService {
147
149
  }
148
150
 
149
151
  _isEncrypted(value) {
150
- if (typeof value !== 'string') {
151
- return false;
152
- }
153
-
154
- const parts = value.split(':');
155
- return parts.length >= 4;
152
+ return isEncryptedEnvelope(value);
156
153
  }
157
154
 
158
155
  _getNestedValue(obj, path) {
@@ -218,22 +215,22 @@ class FieldEncryptionService {
218
215
  }
219
216
 
220
217
  /**
221
- * Serialize a value for encryption
222
- * Objects/arrays are JSON stringified, primitives are converted to strings
218
+ * Serialize a value for encryption.
219
+ * Every value is JSON-encoded, strings included, so the type survives
220
+ * the round trip: '123456' is stored as "\"123456\"" and reads back as a
221
+ * string, 1234 reads back as a number.
223
222
  * @private
224
223
  */
225
224
  _serializeForEncryption(value) {
226
- if (typeof value === 'object' && value !== null) {
227
- // JSON.stringify for objects and arrays
228
- return JSON.stringify(value);
229
- }
230
- // For primitives (string, number, boolean), convert to string
231
- return String(value);
225
+ return JSON.stringify(value);
232
226
  }
233
227
 
234
228
  /**
235
- * Deserialize a value after decryption
236
- * Attempts to parse as JSON, returns string if parsing fails
229
+ * Deserialize a value after decryption.
230
+ * Parses the JSON written by _serializeForEncryption. Values encrypted by
231
+ * earlier versions stored strings raw: those that aren't valid JSON come
232
+ * back unchanged; a raw legacy string that is also valid JSON (e.g.
233
+ * '123456') still parses as it always did.
237
234
  * @private
238
235
  */
239
236
  _deserializeAfterDecryption(value) {
@@ -1,7 +1,10 @@
1
1
  const {
2
2
  createEncryptionExtension,
3
3
  } = require('./encryption/prisma-encryption-extension');
4
- const { loadCustomEncryptionSchema } = require('./encryption/encryption-schema-registry');
4
+ const {
5
+ loadCustomEncryptionSchema,
6
+ EncryptionSchemaError,
7
+ } = require('./encryption/encryption-schema-registry');
5
8
  const { logger } = require('./encryption/logger');
6
9
  const { Cryptor } = require('../encrypt/Cryptor');
7
10
  const config = require('./config');
@@ -125,6 +128,12 @@ const prismaClientSingleton = () => {
125
128
  `Field-level encryption enabled using ${encryptionConfig.method.toUpperCase()}`
126
129
  );
127
130
  } catch (error) {
131
+ // An invalid encryption configuration must fail startup: carrying
132
+ // on without the extension would write encrypted fields in
133
+ // plaintext.
134
+ if (error instanceof EncryptionSchemaError) {
135
+ throw error;
136
+ }
128
137
  logger.error(
129
138
  'Failed to initialize encryption extension:',
130
139
  error
package/jest-setup.js CHANGED
@@ -1,2 +1,13 @@
1
- const {globalSetup} = require('@friggframework/test');
2
- module.exports = globalSetup;
1
+ // Core's unit suites must not need a database, so this global setup only
2
+ // sets the test environment. It deliberately does not start an in-memory
3
+ // MongoDB (as @friggframework/test's globalSetup does): nothing in the default
4
+ // suite talks to a live server, and a binary that failed to start used to stop
5
+ // every core suite from running. Suites that need a real database are
6
+ // `*.db.test.js` files, run by `npm run test:db` (see jest.db.config.js).
7
+ const { overrideEnvironment } = require('@friggframework/test');
8
+
9
+ module.exports = async function () {
10
+ if (!process.env.STAGE) {
11
+ overrideEnvironment({ STAGE: 'dev' });
12
+ }
13
+ };
package/jest-teardown.js CHANGED
@@ -1,2 +1,5 @@
1
- const {globalTeardown} = require('@friggframework/test');
2
- module.exports = globalTeardown;
1
+ const { restoreEnvironment } = require('@friggframework/test');
2
+
3
+ module.exports = async function () {
4
+ restoreEnvironment();
5
+ };
package/jest.config.js CHANGED
@@ -20,4 +20,8 @@ module.exports = {
20
20
  globalTeardown: './jest-teardown.js',
21
21
 
22
22
  setupFilesAfterEnv: ['./logs/jest-logger-setup.js'],
23
+
24
+ // `*.db.test.js` suites talk to a real MongoDB replica set / PostgreSQL
25
+ // and run separately via `npm run test:db` (jest.db.config.js).
26
+ testPathIgnorePatterns: ['/node_modules/', '\\.db\\.test\\.js$'],
23
27
  };
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Database integration suites (`*.db.test.js`).
3
+ *
4
+ * These talk to real databases instead of mocking Prisma, so they are kept
5
+ * out of the default `npm test` run and executed with `npm run test:db`.
6
+ * They need generated Prisma clients (`npm run prisma:generate`) and:
7
+ * - a MongoDB replica set (Prisma requires one for MongoDB writes):
8
+ * FRIGG_TEST_MONGODB_URL, default
9
+ * mongodb://localhost:27017/frigg?replicaSet=rs0
10
+ * - PostgreSQL with the prisma-postgresql migrations applied:
11
+ * FRIGG_TEST_POSTGRES_URL, default
12
+ * postgresql://postgres:postgres@localhost:5432/frigg?schema=public
13
+ * The repository parity suites skip themselves unless their own URLs are
14
+ * set (INTEGRATION_ENTITIES_PARITY_* / QUERY_MAPPINGS_PARITY_*).
15
+ */
16
+ const base = require('./jest.config');
17
+
18
+ module.exports = {
19
+ ...base,
20
+ coverageThreshold: undefined,
21
+ testMatch: ['**/*.db.test.js'],
22
+ testPathIgnorePatterns: ['/node_modules/'],
23
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0-next.121",
4
+ "version": "2.0.0-next.122",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -48,9 +48,9 @@
48
48
  }
49
49
  },
50
50
  "devDependencies": {
51
- "@friggframework/eslint-config": "2.0.0-next.121",
52
- "@friggframework/prettier-config": "2.0.0-next.121",
53
- "@friggframework/test": "2.0.0-next.121",
51
+ "@friggframework/eslint-config": "2.0.0-next.122",
52
+ "@friggframework/prettier-config": "2.0.0-next.122",
53
+ "@friggframework/test": "2.0.0-next.122",
54
54
  "@prisma/client": "^6.19.3",
55
55
  "@types/lodash": "4.17.15",
56
56
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -68,6 +68,7 @@
68
68
  "scripts": {
69
69
  "lint:fix": "prettier --write --loglevel error . && eslint . --fix",
70
70
  "test": "jest --passWithNoTests # TODO",
71
+ "test:db": "jest --config jest.db.config.js --runInBand",
71
72
  "prisma:generate:mongo": "npx prisma generate --schema ./prisma-mongodb/schema.prisma",
72
73
  "prisma:generate:postgres": "npx prisma generate --schema ./prisma-postgresql/schema.prisma",
73
74
  "prisma:generate": "npm run prisma:generate:mongo && npm run prisma:generate:postgres",
@@ -90,5 +91,5 @@
90
91
  "publishConfig": {
91
92
  "access": "public"
92
93
  },
93
- "gitHead": "311b30a60ef98880ceeede1594375ae2c10347eb"
94
+ "gitHead": "d9e2755c836c7cfb4f2de7a8e3d003c6d7167d03"
94
95
  }
@@ -97,16 +97,17 @@ const QueuerUtil = {
97
97
  Id: uuid(),
98
98
  MessageBody: JSON.stringify(entry),
99
99
  });
100
- // Sends 10, then purges the buffer
100
+ // Sends 10, then starts a fresh buffer. Each command gets its own
101
+ // array: clearing the shared buffer in place would also empty the
102
+ // Entries of a command that was already sent.
101
103
  if (buffer.length === batchSize) {
104
+ const batch = buffer.splice(0, buffer.length);
102
105
  const command = new SendMessageBatchCommand({
103
- Entries: buffer,
106
+ Entries: batch,
104
107
  QueueUrl: queueUrl,
105
108
  });
106
109
  const result = await sqs.send(command);
107
- inspectBatchResult(result, queueUrl, buffer);
108
- // Purge the buffer
109
- buffer.splice(0, buffer.length);
110
+ inspectBatchResult(result, queueUrl, batch);
110
111
  }
111
112
  }
112
113