@reventlessdev/reventless-aws 3.0.0-alpha.304 → 3.0.0-alpha.306

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 (69) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +9 -9
  3. package/src/Platform.res +1 -1
  4. package/src/adapter/Api/AppSync_Resolver_Native.res +1 -1
  5. package/src/adapter/Api/AppSync_Resolver_Retrying.res +2 -2
  6. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +69 -11
  7. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +19 -1
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +2 -2
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +1 -1
  10. package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res +1 -1
  11. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +1 -1
  12. package/src/adapter/Postgres/PgProjectionFeed.res +1 -1
  13. package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
  14. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +29 -15
  15. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +6 -5
  16. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +1 -1
  17. package/src/adapter/Runtime/AggregateEntryPoint.mjs +1 -1
  18. package/src/adapter/Runtime/AggregateEntryPoint_Ops.res +1 -1
  19. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +1 -1
  20. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +1 -1
  21. package/src/adapter/Runtime/CounterEntryPoint.mjs +1 -1
  22. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +1 -1
  23. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +44 -6
  24. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +1 -1
  25. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +8 -9
  26. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +36 -8
  27. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +18 -7
  28. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +1 -1
  29. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +1 -1
  30. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +1 -1
  31. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +1 -1
  32. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +1 -1
  33. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +1 -1
  34. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +1 -1
  35. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +1 -1
  36. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +1 -1
  37. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +1 -1
  38. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +1 -1
  39. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -1
  40. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +1 -1
  41. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +1 -1
  42. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +3 -1
  43. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +4 -1
  44. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +1 -1
  45. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +1 -1
  46. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +1 -1
  47. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +1 -1
  48. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +1 -1
  49. package/src/plugin/runtime/PluginRuntime_Builder.res +46 -22
  50. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +16 -11
  51. package/src/util/Util_Bundle.res +1 -1
  52. package/src/util/Util_DynamoDbStream_Runtime.res +1 -1
  53. package/src/util/Util_LambdaEnvBudget.res +74 -0
  54. package/src/util/Util_LambdaEnvBudget.res.mjs +37 -0
  55. package/tests/AWS_TagsTest.res +2 -2
  56. package/tests/Auth_ActiveRolePoolAttachmentTest.res +140 -1
  57. package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +117 -2
  58. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +2 -2
  59. package/tests/EventCollectorEntryPoint_OpsTest.res +13 -4
  60. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +19 -13
  61. package/tests/EventLogStorage_DynamoDb_RuntimeTest.res +1 -1
  62. package/tests/QueryDbResolvers_AppSyncTest.res +2 -2
  63. package/tests/Util_BundleEsmLoaderTest.res +1 -1
  64. package/tests/Util_BundleRuntimeExtensionTest.res +1 -1
  65. package/tests/Util_LambdaEnvBudgetTest.res +78 -0
  66. package/tests/Util_LambdaEnvBudgetTest.res.mjs +55 -0
  67. package/tests/integration/AggregateEntryPoint_IntegrationTest.res +1 -1
  68. package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res +1 -1
  69. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +2 -2
@@ -0,0 +1,74 @@
1
+ // Deploy-time check on a Lambda's environment size.
2
+ //
3
+ // AWS caps the whole environment of a function at 4096 bytes and rejects the
4
+ // UpdateFunctionConfiguration call that exceeds it. That rejection lands in the
5
+ // middle of a `pulumi up`, after the surrounding resources have already moved,
6
+ // so the stack is left half-updated and the message names a byte count rather
7
+ // than the field that grew. This has now bitten three times: `pluginDefinition`
8
+ // inline in HANDLER_CONFIG, the DCB slice registry, and the EventCollector's
9
+ // publishToAggregates map plus the PTA_ vars it names.
10
+ //
11
+ // The check runs where HANDLER_CONFIG is still a plain string — inside the
12
+ // builders' `Pulumi.Output.apply` — because that is the last point anything is
13
+ // measurable. By the time the env reaches `RuntimeEnvironment_Lambda`, the big
14
+ // values are Outputs, and totalling them there would mean a bulk resolve of the
15
+ // very dict `PluginRuntime_Builder` documents as unsafe to resolve in bulk.
16
+ //
17
+ // So the total is part exact, part estimated:
18
+ // * exact — the fixed framework vars, HANDLER_CONFIG, and every env var NAME
19
+ // * estimated — one allowance per Output-valued env var, since the value (an SQS
20
+ // URL) is not resolved yet
21
+ //
22
+ // Exceeding the limit on the exact part alone is a certainty, and fails the
23
+ // deploy. Exceeding it only once the allowances are added is a likelihood, and
24
+ // warns: a wrong estimate must not block a deploy that would have succeeded.
25
+
26
+ let limit = 4096
27
+
28
+ // Environment, NODE_OPTIONS, ESM_FALLBACK_DIRS, LOG_LEVEL,
29
+ // REVENTLESS_ELEVATED_GROUPS — set for every Lambda by RuntimeEnvironment_Lambda.
30
+ let frameworkVarsBytes = 191
31
+
32
+ // An SQS queue URL: https://sqs.<region>.amazonaws.com/<account>/<name>[.fifo].
33
+ // Rounded up from the longest in the example platforms, so the estimate errs
34
+ // toward warning early rather than reporting a limit breach as headroom.
35
+ let outputValueAllowance = 100
36
+
37
+ let log = ReventlessCore.Logger.fromEnv()
38
+
39
+ /**
40
+ * Check one Lambda's environment against the 4KB limit.
41
+ *
42
+ * `~outputValuedKeys` are env vars whose value is still an unresolved Output —
43
+ * counted by name plus `outputValueAllowance`. Raises when the exact portion
44
+ * alone is over the limit; warns when only the estimate is.
45
+ */
46
+ let check = (
47
+ ~lambdaName: string,
48
+ ~handlerConfigJson: string,
49
+ ~outputValuedKeys: array<string>=[],
50
+ ) => {
51
+ let handlerConfigBytes = String.length("HANDLER_CONFIG") + String.length(handlerConfigJson)
52
+ let keyBytes = outputValuedKeys->Array.reduce(0, (acc, k) => acc + String.length(k))
53
+ let exact = frameworkVarsBytes + handlerConfigBytes + keyBytes
54
+ let estimated = exact + outputValuedKeys->Array.length * outputValueAllowance
55
+
56
+ if exact > limit {
57
+ JsError.throwWithMessage(
58
+ `${lambdaName}: Lambda environment is ${exact->Int.toString} bytes before the ` ++
59
+ `${outputValuedKeys->Array.length->Int.toString} queue-URL vars are resolved, over the ` ++
60
+ `${limit->Int.toString}-byte limit. HANDLER_CONFIG is ${handlerConfigBytes->Int.toString} ` ++
61
+ `bytes of it — move the term that grows with the plugin into a code-archive asset ` ++
62
+ `(Util_Bundle.buildCodeArchive ~extraStringAssets), as pluginDefinition.json and ` ++
63
+ `sliceModules.json already are.`,
64
+ )
65
+ } else if estimated > limit {
66
+ log.warn(
67
+ ~comp="Util_LambdaEnvBudget",
68
+ `${lambdaName}: Lambda environment is about ${estimated->Int.toString} bytes ` ++
69
+ `(${exact->Int.toString} exact + ${outputValuedKeys->Array.length->Int.toString} queue-URL ` ++
70
+ `vars), at or over the ${limit->Int.toString}-byte limit. The deploy may fail on ` ++
71
+ `UpdateFunctionConfiguration; move a plugin-sized term out of HANDLER_CONFIG.`,
72
+ )
73
+ }
74
+ }
@@ -0,0 +1,37 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
6
+
7
+ let log = Logger$ReventlessCore.fromEnv();
8
+
9
+ function check(lambdaName, handlerConfigJson, outputValuedKeysOpt) {
10
+ let outputValuedKeys = outputValuedKeysOpt !== undefined ? outputValuedKeysOpt : [];
11
+ let handlerConfigBytes = "HANDLER_CONFIG".length + handlerConfigJson.length | 0;
12
+ let keyBytes = Stdlib_Array.reduce(outputValuedKeys, 0, (acc, k) => acc + k.length | 0);
13
+ let exact = (191 + handlerConfigBytes | 0) + keyBytes | 0;
14
+ let estimated = exact + (outputValuedKeys.length * 100 | 0) | 0;
15
+ if (exact > 4096) {
16
+ return Stdlib_JsError.throwWithMessage(lambdaName + `: Lambda environment is ` + exact.toString() + ` bytes before the ` + (outputValuedKeys.length.toString() + ` queue-URL vars are resolved, over the `) + ((4096).toString() + `-byte limit. HANDLER_CONFIG is ` + handlerConfigBytes.toString() + ` `) + `bytes of it — move the term that grows with the plugin into a code-archive asset (Util_Bundle.buildCodeArchive ~extraStringAssets), as pluginDefinition.json and sliceModules.json already are.`);
17
+ } else if (estimated > 4096) {
18
+ return log.warn("Util_LambdaEnvBudget", undefined, lambdaName + `: Lambda environment is about ` + estimated.toString() + ` bytes ` + (`(` + exact.toString() + ` exact + ` + outputValuedKeys.length.toString() + ` queue-URL `) + (`vars), at or over the ` + (4096).toString() + `-byte limit. The deploy may fail on `) + `UpdateFunctionConfiguration; move a plugin-sized term out of HANDLER_CONFIG.`);
19
+ } else {
20
+ return;
21
+ }
22
+ }
23
+
24
+ let limit = 4096;
25
+
26
+ let frameworkVarsBytes = 191;
27
+
28
+ let outputValueAllowance = 100;
29
+
30
+ export {
31
+ limit,
32
+ frameworkVarsBytes,
33
+ outputValueAllowance,
34
+ log,
35
+ check,
36
+ }
37
+ /* log Not a pure module */
@@ -3,7 +3,7 @@ open JestGlobals
3
3
  // Guards the resource-attribution tag schema. A downstream inventory answers
4
4
  // "which model component owns this resource, in what role, at what scope" purely
5
5
  // from these keys, so the key set and its namespacing are load-bearing.
6
- // See docs/plans/resource-attribution-tag-schema.md.
6
+ // See docs/plans/done/resource-attribution-tag-schema.md.
7
7
 
8
8
  module Attribution = ReventlessCore.ResourceAttribution
9
9
 
@@ -95,7 +95,7 @@ describe("AWS_Tags — owner overrides the piece adapter's own kind", () => {
95
95
  // The piece adapters pass their OWN ComponentType as ~kind, which names the
96
96
  // piece and so collapses onto ~role. A QueryDb is instantiated by six different
97
97
  // component kinds, so without the owner a reader cannot tell a ReadModel's
98
- // table from a StateViewSlice's. See docs/plans/resource-attribution-owner-kind.md.
98
+ // table from a StateViewSlice's. See docs/plans/done/resource-attribution-owner-kind.md.
99
99
  let queryDbTagsFor = (~owner=?) =>
100
100
  AWS_Tags.make(
101
101
  ~name="ProductsQueryDb",
@@ -76,6 +76,11 @@ let triggerArn = "arn:aws:lambda:eu-west-1:1:function:ActiveRoleTrigger"
76
76
  let lambdaConfigOf = input =>
77
77
  input->Dict.get("LambdaConfig")->Option.flatMap(JSON.Decode.object)
78
78
 
79
+ /** The `PreTokenGenerationConfig` an attach is expected to write: our ARN, at the
80
+ only version the trigger handler implements. */
81
+ let v1Config = arn =>
82
+ Dict.fromArray([("LambdaArn", str(arn)), ("LambdaVersion", str("V1_0"))])->JSON.Encode.object
83
+
79
84
  describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — settings survive the attach", () => {
80
85
  let input = merged(~preTokenGenerationArn=Some(triggerArn))
81
86
 
@@ -192,8 +197,105 @@ describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — a pool with nothin
192
197
  ~preTokenGenerationArn=Some(triggerArn),
193
198
  )
194
199
  expect(lambdaConfigOf(input))->toEqual(
195
- Some(Dict.fromArray([("PreTokenGeneration", str(triggerArn))])),
200
+ Some(
201
+ Dict.fromArray([
202
+ ("PreTokenGeneration", str(triggerArn)),
203
+ ("PreTokenGenerationConfig", v1Config(triggerArn)),
204
+ ]),
205
+ ),
206
+ )
207
+ })
208
+ })
209
+
210
+ // 🚨 The pool shape that took the deploy down. Cognito carries this one trigger
211
+ // in two fields, and `UpdateUserPool` rejects the call when both are present
212
+ // naming different functions. Nothing in this suite carried a
213
+ // `PreTokenGenerationConfig` before, which is exactly why it shipped.
214
+ describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — a pool already on the V2 field", () => {
215
+ let theirArn = "arn:aws:lambda:eu-west-1:1:function:TheirOwnPreToken"
216
+
217
+ let describedWithConfig = (~version) =>
218
+ Dict.fromArray([
219
+ ("Name", str("CustomerPool")),
220
+ (
221
+ "LambdaConfig",
222
+ Dict.fromArray([
223
+ ("PreSignUp", str("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp")),
224
+ ("PreTokenGeneration", str(theirArn)),
225
+ (
226
+ "PreTokenGenerationConfig",
227
+ Dict.fromArray([
228
+ ("LambdaArn", str(theirArn)),
229
+ ("LambdaVersion", str(version)),
230
+ ])->JSON.Encode.object,
231
+ ),
232
+ ])->JSON.Encode.object,
233
+ ),
234
+ ])
235
+
236
+ let attachOnto = (~version) =>
237
+ Attachment.mergedUpdateInput(
238
+ ~described=describedWithConfig(~version),
239
+ ~userPoolId="eu-west-1_abc123",
240
+ ~preTokenGenerationArn=Some(triggerArn),
241
+ )
242
+
243
+ // The property AWS enforces, asserted directly rather than through its error
244
+ // text: the two fields must name the same function.
245
+ testSync("both fields name our trigger, and the same one", () => {
246
+ let config = lambdaConfigOf(attachOnto(~version="V1_0"))
247
+ expect((
248
+ config->Option.flatMap(c => c->Dict.get("PreTokenGeneration")),
249
+ config
250
+ ->Option.flatMap(c => c->Dict.get("PreTokenGenerationConfig"))
251
+ ->Option.flatMap(JSON.Decode.object)
252
+ ->Option.flatMap(c => c->Dict.get("LambdaArn")),
253
+ ))->toEqual((Some(str(triggerArn)), Some(str(triggerArn))))
254
+ })
255
+
256
+ // A pool left on V2_0 does not fail — the handler answers in V1_0 shape and
257
+ // Cognito ignores it, so tokens mint un-narrowed and nothing reports it.
258
+ testSync("a pool that arrived on V2_0 is pinned back to the version we implement", () =>
259
+ expect(
260
+ lambdaConfigOf(attachOnto(~version="V2_0"))
261
+ ->Option.flatMap(c => c->Dict.get("PreTokenGenerationConfig"))
262
+ ->Option.flatMap(JSON.Decode.object)
263
+ ->Option.flatMap(c => c->Dict.get("LambdaVersion")),
264
+ )->toEqual(Some(str("V1_0")))
265
+ )
266
+
267
+ testSync("the customer's other triggers still survive", () =>
268
+ expect(
269
+ lambdaConfigOf(attachOnto(~version="V1_0"))->Option.flatMap(c => c->Dict.get("PreSignUp")),
270
+ )->toEqual(Some(str("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp")))
271
+ )
272
+
273
+ testSync("detaching removes both fields, not just the legacy one", () => {
274
+ let config = lambdaConfigOf(
275
+ Attachment.mergedUpdateInput(
276
+ ~described=describedWithConfig(~version="V1_0"),
277
+ ~userPoolId="eu-west-1_abc123",
278
+ ~preTokenGenerationArn=None,
279
+ ),
280
+ )
281
+ expect((
282
+ config->Option.flatMap(c => c->Dict.get("PreTokenGeneration")),
283
+ config->Option.flatMap(c => c->Dict.get("PreTokenGenerationConfig")),
284
+ ))->toEqual((None, None))
285
+ })
286
+
287
+ // The fixpoint property, and the one that would have caught this: AWS
288
+ // materialises whichever field the merge did not write, so the merge has to
289
+ // accept its own output unchanged or a later deploy fails with nothing altered
290
+ // but the service normalising its own record.
291
+ testSync("feeding the merge its own output back changes nothing", () => {
292
+ let once = attachOnto(~version="V1_0")
293
+ let twice = Attachment.mergedUpdateInput(
294
+ ~described=once,
295
+ ~userPoolId="eu-west-1_abc123",
296
+ ~preTokenGenerationArn=Some(triggerArn),
196
297
  )
298
+ expect(lambdaConfigOf(twice))->toEqual(lambdaConfigOf(once))
197
299
  })
198
300
  })
199
301
 
@@ -208,6 +310,43 @@ describe("Auth_ActiveRolePoolAttachment.attachedTrigger", () => {
208
310
  None,
209
311
  )
210
312
  )
313
+
314
+ testSync("reads the V2 field on a pool that only carries that one", () =>
315
+ expect(
316
+ Attachment.attachedTrigger(
317
+ ~described=Dict.fromArray([
318
+ (
319
+ "LambdaConfig",
320
+ Dict.fromArray([("PreTokenGenerationConfig", v1Config(triggerArn))])->JSON.Encode.object,
321
+ ),
322
+ ]),
323
+ ),
324
+ )->toEqual(Some(triggerArn))
325
+ )
326
+
327
+ // Our own ARN, at a version the handler does not speak. Reporting it as
328
+ // attached would leave a pool minting un-narrowed tokens looking correct
329
+ // forever; reporting none makes it drift the next `up` repairs.
330
+ testSync("reports none for our trigger held at a version we do not implement", () =>
331
+ expect(
332
+ Attachment.attachedTrigger(
333
+ ~described=Dict.fromArray([
334
+ (
335
+ "LambdaConfig",
336
+ Dict.fromArray([
337
+ (
338
+ "PreTokenGenerationConfig",
339
+ Dict.fromArray([
340
+ ("LambdaArn", str(triggerArn)),
341
+ ("LambdaVersion", str("V2_0")),
342
+ ])->JSON.Encode.object,
343
+ ),
344
+ ])->JSON.Encode.object,
345
+ ),
346
+ ]),
347
+ ),
348
+ )->toEqual(None)
349
+ )
211
350
  })
212
351
 
213
352
  // The pre-attach check. Its whole job is to keep a trigger that cannot run off a
@@ -119,6 +119,19 @@ function lambdaConfigOf(input) {
119
119
  return Stdlib_Option.flatMap(input["LambdaConfig"], Stdlib_JSON.Decode.object);
120
120
  }
121
121
 
122
+ function v1Config(arn) {
123
+ return Object.fromEntries([
124
+ [
125
+ "LambdaArn",
126
+ arn
127
+ ],
128
+ [
129
+ "LambdaVersion",
130
+ "V1_0"
131
+ ]
132
+ ]);
133
+ }
134
+
122
135
  globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — settings survive the attach", () => {
123
136
  let input = merged(triggerArn);
124
137
  globalThis.test("MFA stays on rather than defaulting off", () => {
@@ -200,10 +213,84 @@ globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — a pool
200
213
  "Name",
201
214
  "Bare"
202
215
  ]]), "eu-west-1_bare", triggerArn);
203
- globalThis.expect(lambdaConfigOf(input)).toEqual(Object.fromEntries([[
216
+ globalThis.expect(lambdaConfigOf(input)).toEqual(Object.fromEntries([
217
+ [
204
218
  "PreTokenGeneration",
205
219
  triggerArn
206
- ]]));
220
+ ],
221
+ [
222
+ "PreTokenGenerationConfig",
223
+ v1Config(triggerArn)
224
+ ]
225
+ ]));
226
+ });
227
+ });
228
+
229
+ globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — a pool already on the V2 field", () => {
230
+ let theirArn = "arn:aws:lambda:eu-west-1:1:function:TheirOwnPreToken";
231
+ let describedWithConfig = version => Object.fromEntries([
232
+ [
233
+ "Name",
234
+ "CustomerPool"
235
+ ],
236
+ [
237
+ "LambdaConfig",
238
+ Object.fromEntries([
239
+ [
240
+ "PreSignUp",
241
+ "arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp"
242
+ ],
243
+ [
244
+ "PreTokenGeneration",
245
+ theirArn
246
+ ],
247
+ [
248
+ "PreTokenGenerationConfig",
249
+ Object.fromEntries([
250
+ [
251
+ "LambdaArn",
252
+ theirArn
253
+ ],
254
+ [
255
+ "LambdaVersion",
256
+ version
257
+ ]
258
+ ])
259
+ ]
260
+ ])
261
+ ]
262
+ ]);
263
+ let attachOnto = version => Auth_ActiveRolePoolAttachment$ReventlessAws.mergedUpdateInput(describedWithConfig(version), "eu-west-1_abc123", triggerArn);
264
+ globalThis.test("both fields name our trigger, and the same one", () => {
265
+ let config = lambdaConfigOf(attachOnto("V1_0"));
266
+ globalThis.expect([
267
+ Stdlib_Option.flatMap(config, c => c["PreTokenGeneration"]),
268
+ Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(config, c => c["PreTokenGenerationConfig"]), Stdlib_JSON.Decode.object), c => c["LambdaArn"])
269
+ ]).toEqual([
270
+ triggerArn,
271
+ triggerArn
272
+ ]);
273
+ });
274
+ globalThis.test("a pool that arrived on V2_0 is pinned back to the version we implement", () => {
275
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(lambdaConfigOf(attachOnto("V2_0")), c => c["PreTokenGenerationConfig"]), Stdlib_JSON.Decode.object), c => c["LambdaVersion"])).toEqual("V1_0");
276
+ });
277
+ globalThis.test("the customer's other triggers still survive", () => {
278
+ globalThis.expect(Stdlib_Option.flatMap(lambdaConfigOf(attachOnto("V1_0")), c => c["PreSignUp"])).toEqual("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp");
279
+ });
280
+ globalThis.test("detaching removes both fields, not just the legacy one", () => {
281
+ let config = lambdaConfigOf(Auth_ActiveRolePoolAttachment$ReventlessAws.mergedUpdateInput(describedWithConfig("V1_0"), "eu-west-1_abc123", undefined));
282
+ globalThis.expect([
283
+ Stdlib_Option.flatMap(config, c => c["PreTokenGeneration"]),
284
+ Stdlib_Option.flatMap(config, c => c["PreTokenGenerationConfig"])
285
+ ]).toEqual([
286
+ undefined,
287
+ undefined
288
+ ]);
289
+ });
290
+ globalThis.test("feeding the merge its own output back changes nothing", () => {
291
+ let once = attachOnto("V1_0");
292
+ let twice = Auth_ActiveRolePoolAttachment$ReventlessAws.mergedUpdateInput(once, "eu-west-1_abc123", triggerArn);
293
+ globalThis.expect(lambdaConfigOf(twice)).toEqual(lambdaConfigOf(once));
207
294
  });
208
295
  });
209
296
 
@@ -217,6 +304,33 @@ globalThis.describe("Auth_ActiveRolePoolAttachment.attachedTrigger", () => {
217
304
  "Bare"
218
305
  ]]))).toEqual(undefined);
219
306
  });
307
+ globalThis.test("reads the V2 field on a pool that only carries that one", () => {
308
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTrigger(Object.fromEntries([[
309
+ "LambdaConfig",
310
+ Object.fromEntries([[
311
+ "PreTokenGenerationConfig",
312
+ v1Config(triggerArn)
313
+ ]])
314
+ ]]))).toEqual(triggerArn);
315
+ });
316
+ globalThis.test("reports none for our trigger held at a version we do not implement", () => {
317
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTrigger(Object.fromEntries([[
318
+ "LambdaConfig",
319
+ Object.fromEntries([[
320
+ "PreTokenGenerationConfig",
321
+ Object.fromEntries([
322
+ [
323
+ "LambdaArn",
324
+ triggerArn
325
+ ],
326
+ [
327
+ "LambdaVersion",
328
+ "V2_0"
329
+ ]
330
+ ])
331
+ ]])
332
+ ]]))).toEqual(undefined);
333
+ });
220
334
  });
221
335
 
222
336
  globalThis.describe("Auth_ActiveRolePoolAttachment.probeVerdict", () => {
@@ -259,5 +373,6 @@ export {
259
373
  merged,
260
374
  triggerArn,
261
375
  lambdaConfigOf,
376
+ v1Config,
262
377
  }
263
378
  /* Not a pure module */
@@ -440,7 +440,7 @@ describe("Runtime.buildConditionalTransactItems — composite partition fences o
440
440
  // pluginName}. The read is one exact `tag_composite` match, so the fence must be
441
441
  // a SINGLE composite-key fence — not one per member. Per-member fencing made the
442
442
  // low-cardinality prefix (`environment`, `platformName`) hot under a deploy
443
- // fan-out. Plan: docs/plans/Backlog/dcb-hot-tag-fence-contention.md.
443
+ // fan-out. Plan: docs/plans/done/dcb-hot-tag-fence-contention.md.
444
444
  let event = (eventType, tags): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
445
445
  eventType,
446
446
  data: JSON.Object(Dict.make()),
@@ -563,7 +563,7 @@ describe("Runtime.toItem — an empty tag value skips its GSI attribute", () =>
563
563
  // `tag_<key>` is a GSI hash key, and DynamoDB rejects an empty string for a key
564
564
  // attribute — writing one fails the whole append. An empty value is legitimate
565
565
  // (an absent composite-partition member), so the attribute is skipped and that
566
- // index goes sparse. See docs/plans/dcb-empty-tag-values-break-append.md.
566
+ // index goes sparse. See docs/plans/done/dcb-empty-tag-values-break-append.md.
567
567
  let tagOf = (key, value): Reventless.DcbTag.tag => {key, value}
568
568
  let tags = [tagOf("environment", "prod"), tagOf("componentName", "")]
569
569
  let storedEvent: ReventlessCore.DcbEventLog_Adapter.rawStoredEvent = {
@@ -107,7 +107,6 @@ let minimalConfig = (extras: array<(string, JSON.t)>): string =>
107
107
  ("schedulerQueueName", str("")),
108
108
  ("extensionPoints", JSON.Encode.array([])),
109
109
  ("extensions", JSON.Encode.array([])),
110
- ("publishToAggregates", obj([])),
111
110
  ],
112
111
  extras,
113
112
  )
@@ -139,7 +138,6 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
139
138
  let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(minimalConfig([]))
140
139
  expect(c.queueUrl)->toBe("q-url")
141
140
  expect(c.connectExtension->Option.isNone)->toBe(true) // absent → None
142
- expect(c.readModelQueueUrls->Dict.keysToArray->Array.length)->toBe(0)
143
141
  expect(c.readModelNamesForSourceName->Dict.keysToArray->Array.length)->toBe(0)
144
142
  })
145
143
 
@@ -198,7 +196,6 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
198
196
  ]),
199
197
  ]),
200
198
  ),
201
- ("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
202
199
  (
203
200
  "readModelNamesForSourceName",
204
201
  obj([("Ordering.Orders", JSON.Encode.array([str("ProductDemands")]))]),
@@ -208,9 +205,21 @@ describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
208
205
  let ep = c.extensionPoints->Array.getUnsafe(0)
209
206
  expect(ep.eventTopicArn)->toBe("arn:ep")
210
207
  expect(ep.aggregateNames)->toEqual(["Plugin"])
211
- expect(c.publishToAggregates->Dict.get("Plugin"))->toEqual(Some("PTA_Plugin_QUEUE_URL"))
212
208
  expect(c.readModelNamesForSourceName->Dict.get("Ordering.Orders"))->toEqual(
213
209
  Some(["ProductDemands"]),
214
210
  )
215
211
  })
212
+
213
+ // The target→env-var maps left HANDLER_CONFIG for queueUrls.json, so a config
214
+ // still carrying them must parse rather than trip the required-field check —
215
+ // that is what a Lambda running an older archive sends.
216
+ testSync("ignores publishToAggregates left over in HANDLER_CONFIG", () => {
217
+ let c = EventCollectorEntryPoint_Ops.parseHandlerConfig(
218
+ minimalConfig([
219
+ ("publishToAggregates", obj([("Plugin", str("PTA_Plugin_QUEUE_URL"))])),
220
+ ("readModelQueueUrls", obj([("ProductDemands", str("PRM_ProductDemands_QUEUE_URL"))])),
221
+ ]),
222
+ )
223
+ expect(c.queueUrl)->toBe("q-url")
224
+ })
216
225
  })
@@ -210,10 +210,6 @@ function minimalConfig(extras) {
210
210
  [
211
211
  "extensions",
212
212
  []
213
- ],
214
- [
215
- "publishToAggregates",
216
- Object.fromEntries([])
217
213
  ]
218
214
  ].concat(extras)));
219
215
  }
@@ -238,7 +234,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
238
234
  let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([]));
239
235
  globalThis.expect(c.queueUrl).toBe("q-url");
240
236
  globalThis.expect(Stdlib_Option.isNone(c.connectExtension)).toBe(true);
241
- globalThis.expect(Object.keys(c.readModelQueueUrls).length).toBe(0);
242
237
  globalThis.expect(Object.keys(c.readModelNamesForSourceName).length).toBe(0);
243
238
  });
244
239
  globalThis.test("connectExtension: JSON null (admin) maps to None; a record decodes", () => {
@@ -304,13 +299,6 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
304
299
  ]
305
300
  ])]
306
301
  ],
307
- [
308
- "publishToAggregates",
309
- Object.fromEntries([[
310
- "Plugin",
311
- "PTA_Plugin_QUEUE_URL"
312
- ]])
313
- ],
314
302
  [
315
303
  "readModelNamesForSourceName",
316
304
  Object.fromEntries([[
@@ -322,9 +310,27 @@ globalThis.describe("EventCollectorEntryPoint_Ops.parseHandlerConfig", () => {
322
310
  let ep = c.extensionPoints[0];
323
311
  globalThis.expect(ep.eventTopicArn).toBe("arn:ep");
324
312
  globalThis.expect(ep.aggregateNames).toEqual(["Plugin"]);
325
- globalThis.expect(c.publishToAggregates["Plugin"]).toEqual("PTA_Plugin_QUEUE_URL");
326
313
  globalThis.expect(c.readModelNamesForSourceName["Ordering.Orders"]).toEqual(["ProductDemands"]);
327
314
  });
315
+ globalThis.test("ignores publishToAggregates left over in HANDLER_CONFIG", () => {
316
+ let c = EventCollectorEntryPoint_Ops$ReventlessAws.parseHandlerConfig(minimalConfig([
317
+ [
318
+ "publishToAggregates",
319
+ Object.fromEntries([[
320
+ "Plugin",
321
+ "PTA_Plugin_QUEUE_URL"
322
+ ]])
323
+ ],
324
+ [
325
+ "readModelQueueUrls",
326
+ Object.fromEntries([[
327
+ "ProductDemands",
328
+ "PRM_ProductDemands_QUEUE_URL"
329
+ ]])
330
+ ]
331
+ ]));
332
+ globalThis.expect(c.queueUrl).toBe("q-url");
333
+ });
328
334
  });
329
335
 
330
336
  let project = EventCollectorEntryPoint_Ops$ReventlessAws.projectPluginRow;
@@ -57,7 +57,7 @@ describe("Runtime.buildTransactItems", () => {
57
57
  })
58
58
  })
59
59
 
60
- // ─── Aggregate snapshots (docs/plans/aggregate-snapshotting.md) ───
60
+ // ─── Aggregate snapshots (docs/plans/done/aggregate-snapshotting.md) ───
61
61
 
62
62
  describe("Runtime.replayQueryInput", () => {
63
63
  testSync("default bounds cover exactly the zero-padded event range", () => {
@@ -1,6 +1,6 @@
1
1
  open JestGlobals
2
2
 
3
- // Regression guard for docs/plans/platform-plugins-admin-connection-null-rows.md:
3
+ // Regression guard for docs/plans/done/platform-plugins-admin-connection-null-rows.md:
4
4
  // the Plugins admin RM shares its DynamoDB table with internal bookkeeping rows
5
5
  // (`deploy-schema:*`, `plugin-info:*`, `deploy-schema-hash:*`) that carry no `name`.
6
6
  // The auto-generated Connection Scan must exclude them, or `name: String!` resolves
@@ -39,7 +39,7 @@ describe("AppSync_Resolver_Retrying.Functions.listAllItemsConnection", () => {
39
39
  })
40
40
  })
41
41
 
42
- // Tripwires for docs/plans/aws-scan-connection-cursor-roundtrip.md. The Scan
42
+ // Tripwires for docs/plans/done/aws-scan-connection-cursor-roundtrip.md. The Scan
43
43
  // resolver's cursor must round-trip DynamoDB's own continuation token, not a
44
44
  // synthetic index — every list past page 1 was unreachable before this.
45
45
  describe("listAllItemsConnection — Scan cursor round-trip (paging fixes 1–3)", () => {
@@ -4,7 +4,7 @@ open JestGlobals
4
4
  // load-bearing contract: every code archive that ships the loader files MUST set
5
5
  // NODE_OPTIONS=--import pointing at the shipped register-hook.mjs, and the two
6
6
  // files must keep cross-referencing each other by the exact filenames used here.
7
- // See docs/plans/deployed-lambda-esm-self-containment.md.
7
+ // See docs/plans/done/deployed-lambda-esm-self-containment.md.
8
8
 
9
9
  describe("Util_Bundle ESM loader — env-var contract", () => {
10
10
  testSync("NODE_OPTIONS --imports the shipped register-hook file by name", () => {
@@ -4,7 +4,7 @@ open JestGlobals
4
4
  // packages its runtime import graph reaches (`companionModuleUrls`), the
5
5
  // bundler carries them into the archive, and the archive build fails loudly on
6
6
  // any statically imported package that would not resolve at cold start. See
7
- // docs/plans/runtime-extension-companion-packages.md.
7
+ // docs/plans/done/runtime-extension-companion-packages.md.
8
8
 
9
9
  // realpath because macOS hands out /var/... symlinks for tmpdir while the
10
10
  // package-root walk reports paths as given.