@reventlessdev/reventless-aws 3.0.0-alpha.377 → 3.0.0-alpha.379

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/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.379 (2026-10-10)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **pulumi-aws:** a list's last page no longer promises another ([00b141f](https://github.com/ReventlessDev/reventless-core/commit/00b141fbfdbf44544fbea8ab979356831381f48a))
11
+ * feat!: a command's rule is looked up by its name, without a cast ([5f02c40](https://github.com/ReventlessDev/reventless-core/commit/5f02c4081d077bf4a6951d8c9b2b16553a8ca87b))
12
+
13
+ ### BREAKING CHANGES
14
+
15
+ * a spec that writes `commandAuthorization` by hand writes
16
+ `authorizationOf = (name: string) => …` instead; the PPX refuses the old name.
17
+
18
+
19
+
20
+ # 3.0.0-alpha.378 (2026-10-10)
21
+
22
+ ### Bug Fixes
23
+
24
+ * an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
25
+
26
+
6
27
  # 3.0.0-alpha.377 (2026-10-08)
7
28
 
8
29
  **Note:** Version bump only for package @reventlessdev/reventless-aws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.377",
3
+ "version": "3.0.0-alpha.379",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -23,18 +23,18 @@
23
23
  "uuid": "^13.0.0",
24
24
  "yaml": "^2.8.3",
25
25
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.30",
26
- "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
27
- "@reventlessdev/rescript-node": "2.0.0-alpha.22",
28
- "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.25",
29
26
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
27
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.27",
28
+ "@reventlessdev/rescript-node": "2.0.0-alpha.22",
29
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
30
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
30
31
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
31
32
  "@reventlessdev/rescript-web": "1.0.0-alpha.7",
32
- "@reventlessdev/reventless-core": "3.0.0-alpha.291",
33
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
34
- "@reventlessdev/reventless-infra": "3.0.0-alpha.188",
35
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.155",
33
+ "@reventlessdev/reventless-core": "3.0.0-alpha.293",
34
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.190",
36
35
  "@reventlessdev/reventless-interop": "3.0.0-alpha.39",
37
- "@reventlessdev/reventless-spec": "3.0.0-alpha.160"
36
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.157",
37
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.162"
38
38
  },
39
39
  "devDependencies": {
40
40
  "rescript": "12.3.1",
@@ -485,7 +485,7 @@ function MakeWithConfig(Config) {
485
485
  errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
486
486
  eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
487
487
  commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
488
- commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
488
+ authorizationOf: UiFragmentRegistry$ReventlessCore.authorizationOf,
489
489
  commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
490
490
  traits: UiFragmentRegistry$ReventlessCore.traits,
491
491
  readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
@@ -682,7 +682,7 @@ function MakeWithConfig(Config) {
682
682
  errorSchema: PluginSpec$ReventlessCore.errorSchema,
683
683
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
684
684
  moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
685
- commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
685
+ authorizationOf: PluginSpec$ReventlessCore.authorizationOf,
686
686
  commandTransition: PluginSpec$ReventlessCore.commandTransition,
687
687
  traits: PluginSpec$ReventlessCore.traits
688
688
  })({
@@ -1901,7 +1901,7 @@ function Make($star) {
1901
1901
  errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
1902
1902
  eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
1903
1903
  commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
1904
- commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
1904
+ authorizationOf: UiFragmentRegistry$ReventlessCore.authorizationOf,
1905
1905
  commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
1906
1906
  traits: UiFragmentRegistry$ReventlessCore.traits,
1907
1907
  readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
@@ -2098,7 +2098,7 @@ function Make($star) {
2098
2098
  errorSchema: PluginSpec$ReventlessCore.errorSchema,
2099
2099
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
2100
2100
  moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
2101
- commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
2101
+ authorizationOf: PluginSpec$ReventlessCore.authorizationOf,
2102
2102
  commandTransition: PluginSpec$ReventlessCore.commandTransition,
2103
2103
  traits: PluginSpec$ReventlessCore.traits
2104
2104
  })({
@@ -168,7 +168,7 @@ export async function buildHandlersForConfig(config, opts = {}) {
168
168
 
169
169
  // Route 0 registry: InboundTranslationSlice receive handlers keyed by their
170
170
  // AppSync mutation field name. The resolver invokes this Lambda with
171
- // {__inboundTranslation, fieldName, arguments}; the field name is
171
+ // {__inboundTranslation, fieldName, arguments, identity}; the field name is
172
172
  // `<pluginName>_<sliceName>` (Api_Naming.sliceMutationField — the deploy side
173
173
  // uses the same `name` that HANDLER_CONFIG.pluginName carries). The spec +
174
174
  // translation modules are the one untyped seam (dynamic import); the typed
@@ -316,11 +316,8 @@ export async function handler(event, context) {
316
316
  const [sqsHandler, cmdGenHandler, comp, plugin, inboundReceivers] = await initPromise;
317
317
 
318
318
  // Route 0: InboundTranslationSlice mutation — the AppSync resolver invokes this
319
- // Lambda with `{__inboundTranslation: true, fieldName, arguments}` (no `command`,
320
- // no `Records`). Dispatch to the field's receive handler, which translates +
321
- // publishes and returns a commandOutcome JSON byte-compatible with Route 1's
322
- // `commandOutcomeToJson`. Without this branch the payload fell through to Route 2
323
- // and crashed on `event.records` being undefined.
319
+ // Lambda with `{__inboundTranslation: true, fieldName, arguments, identity}`.
320
+ // The field's receiver checks each translated command against `identity`.
324
321
  if (event.__inboundTranslation === true) {
325
322
  const fieldName = event.fieldName;
326
323
  const receiver = (inboundReceivers || {})[fieldName];
@@ -329,7 +326,7 @@ export async function handler(event, context) {
329
326
  throw new Error("no inbound translation receiver for field: " + fieldName);
330
327
  }
331
328
  log.debug("InboundTranslation invocation (" + fieldName + ")", { comp: "DcbCommandTopicRuntime" });
332
- return await receiver(event.arguments);
329
+ return await receiver(event.arguments, event.identity ?? null);
333
330
  }
334
331
 
335
332
  // Route 1: AppSync direct invocation — payload carries the CommandGenerator.payload
@@ -246,13 +246,8 @@ let buildSliceHandler = (
246
246
  }
247
247
 
248
248
  // ── Inbound translation receiver wiring (Route 0) ───────────────────────────
249
- // The DCB command Lambda is also the target of every InboundTranslationSlice
250
- // mutation on the plugin's API — its AppSync resolver invokes this Lambda with an
251
- // `{__inboundTranslation, fieldName, arguments}` payload. Building the per-field
252
- // `receive` here keeps the curried `InboundTranslationSlice_Callback.Make` functor
253
- // call compiler-checked (same rationale as `buildSliceHandler`) and mirrors the
254
- // in-process composite handler in `Dcb_Builder.res`, so the deployed surface and
255
- // the local surface encode the same `commandOutcome`.
249
+ // Every inbound mutation's resolver invokes this Lambda with a `doorInvocation`.
250
+ // The functor call lives here to be compiler-checked, as in `buildSliceHandler`.
256
251
 
257
252
  // The dynamically-imported Translation module — opaque; a clean pass-through to
258
253
  // the functor (only `translate` is read, inside the compiled callback).
@@ -262,6 +257,7 @@ type inboundCallback = {
262
257
  receive: (
263
258
  ReventlessInfra.CommandTopic.publishJsons,
264
259
  JSON.t,
260
+ ~caller: Reventless.Identity.t=?,
265
261
  ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
266
262
  auditLog: dict<ReventlessCore.InboundTranslationSlice_Callback.auditRow>,
267
263
  }
@@ -270,12 +266,9 @@ type inboundCallback = {
270
266
  )
271
267
  external makeInboundCallback: specModule => translationModule => inboundCallback = "Make"
272
268
 
273
- // Builds the inbound receive handler: run the slice's `receive` (which validates
274
- // the external input against the spec schema, translates, and publishes the mapped
275
- // commands via `publishJsons`), then — when an audit table name was threaded —
276
- // drain the in-memory audit log to that table (best-effort, matching
277
- // `InboundTranslationSlice_Builder`'s inline `syncToQueryDb`). Returns the
278
- // `commandOutcome` JSON, byte-compatible with the AppSync direct-invocation route.
269
+ // Builds the inbound receive handler: `receive` for the resolver's caller, then
270
+ // the request's audit row to its table when one was threaded. Answers the
271
+ // `commandOutcome` JSON the AppSync direct-invocation route answers with.
279
272
  let buildInboundReceiver = (
280
273
  spec: specModule,
281
274
  translation: translationModule,
@@ -283,8 +276,12 @@ let buildInboundReceiver = (
283
276
  ~auditQueryDbOps: option<ReventlessCore.QueryDb_Adapter.operations>,
284
277
  ) => {
285
278
  let callback = makeInboundCallback(spec)(translation)
286
- async (args: JSON.t): JSON.t => {
287
- let result = await callback.receive(publishJsons, args)
279
+ async (args: JSON.t, identity: Nullable.t<Reventless.Identity.t>): JSON.t => {
280
+ let result = await callback.receive(
281
+ publishJsons,
282
+ args,
283
+ ~caller=ReventlessCore.InboundTranslationSlice_Callback.doorCaller(identity),
284
+ )
288
285
  switch auditQueryDbOps {
289
286
  | Some(ops) =>
290
287
  let id = result->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
@@ -294,12 +291,8 @@ let buildInboundReceiver = (
294
291
  row->Reventless.Util_Sury.toJson(
295
292
  ReventlessCore.InboundTranslationSlice_Callback.auditRowSchema,
296
293
  )
297
- // A failed audit write must not fail the mutation (the command was
298
- // already published), but it must not vanish either. `save` RESOLVES
299
- // with `Error(_)` on a storage failure (it does not throw), so the
300
- // result must be inspected — a bare `let _ = await ops.save(...)` would
301
- // swallow the failure and leave the audit view permanently, silently
302
- // empty. `try` still guards an unexpected reject.
294
+ // A failed audit write is logged, not raised: the commands are published.
295
+ // `save` resolves `Error(_)` rather than throwing, so its result is read.
303
296
  try {
304
297
  switch await ops.save(id, json, ReventlessCore.QueryDb.Overwrite, None) {
305
298
  | Ok() => ()
@@ -324,9 +317,7 @@ let buildInboundReceiver = (
324
317
  }
325
318
  result
326
319
  ->ReventlessCore.InboundTranslationSlice_Callback.receiveResultToOutcome
327
- // `CommandTopic_Helpers`, not `CommandTopic`: the latter imports `Adapter`
328
- // (→ `@pulumi/pulumi`) for a deploy-time helper, which would crash this
329
- // runtime Lambda's cold start. The encoder itself lives in the pure Helpers.
320
+ // `CommandTopic_Helpers`, not `CommandTopic`, which imports `@pulumi/pulumi`.
330
321
  ->ReventlessCore.CommandTopic_Helpers.commandOutcomeToJson
331
322
  }
332
323
  }
@@ -120,8 +120,8 @@ function buildSliceHandler(spec, behavior, scope, dcbEventLog) {
120
120
 
121
121
  function buildInboundReceiver(spec, translation, publishJsons, auditQueryDbOps) {
122
122
  let callback = InboundTranslationSlice_CallbackResMjs.Make(spec)(translation);
123
- return async args => {
124
- let result = await callback.receive(publishJsons, args);
123
+ return async (args, identity) => {
124
+ let result = await callback.receive(publishJsons, args, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(identity));
125
125
  if (auditQueryDbOps !== undefined) {
126
126
  let id = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result);
127
127
  let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(callback.auditLog, id);
@@ -149,6 +149,19 @@ describe("AppSync_Adapter.generateFragment", () => {
149
149
  // `@authorize` PPX annotations) into `@aws_cognito_user_pools(cognito_groups: [...])`
150
150
  // directives on the corresponding SDL fields.
151
151
 
152
+ // Two constructors under different roles, the case one field gate must widen for.
153
+ module TwoRuleFeed = {
154
+ @schema
155
+ type command = AddProduct({sku: string}) | AddCategory({sku: string})
156
+
157
+ let authorizationOf = (name: string): Reventless.Authorization.rule<Reventless.Role.name> =>
158
+ switch name {
159
+ | "AddProduct" => AllowRoles([Reventless.Role.make("Merchandiser")])
160
+ | "AddCategory" => AllowRoles([Reventless.Role.make("Admin")])
161
+ | _ => DenyAll
162
+ }
163
+ }
164
+
152
165
  describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
153
166
  // Minimal mutation fragment with two fields. Field names must match what
154
167
  // the entries declare so the stitcher's extractLeadingName can pair them.
@@ -225,6 +238,22 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
225
238
  )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`)
226
239
  })
227
240
 
241
+ // An inbound slice has one field for every constructor, so its gate admits any
242
+ // constructor's roles; each command is checked against its own in `receive`.
243
+ testSync("an inbound slice's field carries every constructor's groups", () => {
244
+ let permission = ReventlessCore.Dcb_Builder.inboundDoorPermission(
245
+ TwoRuleFeed.commandSchema,
246
+ TwoRuleFeed.authorizationOf,
247
+ )
248
+ let fp = Dict.fromArray(permission->Option.mapOr([], p => [("p_TwoRuleFeed", p)]))
249
+ let entry = mutationEntry(~fieldNames=["p_TwoRuleFeed"], ~fieldPermissions=fp)
250
+ let frag = makeFragment(["p_TwoRuleFeed(sku: String!): String"], [])
251
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
252
+ expect(
253
+ decodeFragment(aug).mutations->Array.getUnsafe(0),
254
+ )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Merchandiser", "Admin"])`)
255
+ })
256
+
228
257
  testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
229
258
  let fp = Dict.fromArray([("p_Add", Reventless.Authorization.AllowAuthenticated)])
230
259
  let entry = mutationEntry(~fieldNames=["p_Add"], ~fieldPermissions=fp)
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
7
  import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
8
8
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
9
+ import * as Dcb_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/Dcb/Dcb_Builder.res.mjs";
9
10
  import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
10
11
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
11
12
  import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
@@ -109,6 +110,39 @@ globalThis.describe("AppSync_Adapter.generateFragment", () => {
109
110
  });
110
111
  });
111
112
 
113
+ let commandSchema = Sury.union([
114
+ Sury.$schema(s => ({
115
+ TAG: "AddProduct",
116
+ sku: s.m(Sury.string)
117
+ })),
118
+ Sury.$schema(s => ({
119
+ TAG: "AddCategory",
120
+ sku: s.m(Sury.string)
121
+ }))
122
+ ]);
123
+
124
+ function authorizationOf(name) {
125
+ switch (name) {
126
+ case "AddCategory" :
127
+ return {
128
+ TAG: "AllowRoles",
129
+ _0: ["Admin"]
130
+ };
131
+ case "AddProduct" :
132
+ return {
133
+ TAG: "AllowRoles",
134
+ _0: ["Merchandiser"]
135
+ };
136
+ default:
137
+ return "DenyAll";
138
+ }
139
+ }
140
+
141
+ let TwoRuleFeed = {
142
+ commandSchema: commandSchema,
143
+ authorizationOf: authorizationOf
144
+ };
145
+
112
146
  globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
113
147
  let makeFragment = (mutationFields, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
114
148
  types: [],
@@ -169,6 +203,17 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
169
203
  Role$Reventless.clearGroups();
170
204
  globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`);
171
205
  });
206
+ globalThis.test("an inbound slice's field carries every constructor's groups", () => {
207
+ let permission = Dcb_Builder$ReventlessCore.inboundDoorPermission(commandSchema, authorizationOf);
208
+ let fp = Object.fromEntries(Stdlib_Option.mapOr(permission, [], p => [[
209
+ "p_TwoRuleFeed",
210
+ p
211
+ ]]));
212
+ let entry = mutationEntry(["p_TwoRuleFeed"], fp);
213
+ let frag = makeFragment(["p_TwoRuleFeed(sku: String!): String"], []);
214
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
215
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Merchandiser", "Admin"])`);
216
+ });
172
217
  globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
173
218
  let fp = Object.fromEntries([[
174
219
  "p_Add",
@@ -875,5 +920,6 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
875
920
 
876
921
  export {
877
922
  decodeFragment,
923
+ TwoRuleFeed,
878
924
  }
879
925
  /* Not a pure module */
@@ -0,0 +1,12 @@
1
+ // `buildInboundReceiver` over the EpInboundTestSlice fixture, publishing into an
2
+ // array. Answers `[outcome, publishedCount]`.
3
+ import { buildInboundReceiver } from "@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs";
4
+ import * as spec from "./EpInboundTestSlice.res.mjs";
5
+ import * as translation from "./EpInboundTestSliceTranslation.res.mjs";
6
+
7
+ export async function receiveWithStubPublish(args, identity) {
8
+ const published = [];
9
+ const receive = buildInboundReceiver(spec, translation, async (cmds) => { published.push(...cmds); }, undefined);
10
+ const outcome = await receive(args, identity);
11
+ return [outcome, published.length];
12
+ }
@@ -39,10 +39,33 @@ let routeInbound: JSON.t => promise<JSON.t> = %raw(`
39
39
  // Mirror handler's Route 0 dispatch.
40
40
  const receiver = (inboundReceivers || {})[event.fieldName];
41
41
  if (receiver === undefined) throw new Error("no inbound receiver for " + event.fieldName);
42
- return await receiver(event.arguments);
42
+ return await receiver(event.arguments, event.identity ?? null);
43
43
  }
44
44
  `)
45
45
 
46
+ // The receiver over the fixture modules with a publish that records, so an
47
+ // admitted caller's commands are seen without SQS.
48
+ @module("./DcbInboundReceiverStub.mjs")
49
+ external receiveWithStubPublish: (
50
+ JSON.t,
51
+ Nullable.t<Reventless.Identity.t>,
52
+ ) => promise<(JSON.t, int)> = "receiveWithStubPublish"
53
+
54
+ let usdArguments: JSON.t =
55
+ Dict.fromArray([
56
+ ("sku", "SKU-1"->JSON.Encode.string),
57
+ ("currency", "USD"->JSON.Encode.string),
58
+ ])->JSON.Encode.object
59
+
60
+ let cognito = (groups): Reventless.Identity.t => {
61
+ userId: "u-1",
62
+ username: "alice",
63
+ groups,
64
+ provider: Cognito,
65
+ }
66
+
67
+ let typenameOf = (outcome: JSON.t) => outcome->JSON.stringify
68
+
46
69
  let inboundEvent = (~fieldName, ~currency): JSON.t => {
47
70
  let arguments = Dict.fromArray([
48
71
  ("sku", "SKU-1"->JSON.Encode.string),
@@ -70,4 +93,29 @@ describe("DcbCommandTopicEntryPoint Route 0 (InboundTranslation)", () => {
70
93
  expect(s->String.includes("Unsupported currency"))->toBe(true)
71
94
  },
72
95
  )
96
+
97
+ test("the payload's caller reaches the per-command check", async () => {
98
+ let (outcome, count) = await receiveWithStubPublish(
99
+ usdArguments,
100
+ Nullable.make(cognito(["Ops"])),
101
+ )
102
+ expect(typenameOf(outcome)->String.includes("Forbidden"))->toBe(true)
103
+ expect(count)->toBe(0)
104
+ })
105
+
106
+ test("a caller holding the command's role publishes", async () => {
107
+ let (outcome, count) = await receiveWithStubPublish(
108
+ usdArguments,
109
+ Nullable.make(cognito(["Admin"])),
110
+ )
111
+ expect(typenameOf(outcome)->String.includes("CommandAccepted"))->toBe(true)
112
+ expect(count)->toBe(1)
113
+ })
114
+
115
+ // A resolver that identified nobody must not be taken for the platform itself.
116
+ test("a payload with no identity is refused", async () => {
117
+ let (outcome, count) = await receiveWithStubPublish(usdArguments, Nullable.null)
118
+ expect(typenameOf(outcome)->String.includes("Forbidden"))->toBe(true)
119
+ expect(count)->toBe(0)
120
+ })
73
121
  })
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as DcbInboundReceiverStubMjs from "./DcbInboundReceiverStub.mjs";
4
5
 
5
6
  let routeInbound = (async (event) => {
6
7
  const { buildHandlersForConfig } = await import(
@@ -24,9 +25,37 @@ let routeInbound = (async (event) => {
24
25
  // Mirror handler's Route 0 dispatch.
25
26
  const receiver = (inboundReceivers || {})[event.fieldName];
26
27
  if (receiver === undefined) throw new Error("no inbound receiver for " + event.fieldName);
27
- return await receiver(event.arguments);
28
+ return await receiver(event.arguments, event.identity ?? null);
28
29
  });
29
30
 
31
+ function receiveWithStubPublish(prim0, prim1) {
32
+ return DcbInboundReceiverStubMjs.receiveWithStubPublish(prim0, prim1);
33
+ }
34
+
35
+ let usdArguments = Object.fromEntries([
36
+ [
37
+ "sku",
38
+ "SKU-1"
39
+ ],
40
+ [
41
+ "currency",
42
+ "USD"
43
+ ]
44
+ ]);
45
+
46
+ function cognito(groups) {
47
+ return {
48
+ userId: "u-1",
49
+ username: "alice",
50
+ groups: groups,
51
+ provider: "Cognito"
52
+ };
53
+ }
54
+
55
+ function typenameOf(outcome) {
56
+ return JSON.stringify(outcome);
57
+ }
58
+
30
59
  function inboundEvent(fieldName, currency) {
31
60
  let $$arguments = Object.fromEntries([
32
61
  [
@@ -62,10 +91,29 @@ globalThis.describe("DcbCommandTopicEntryPoint Route 0 (InboundTranslation)", ()
62
91
  globalThis.expect(s.includes("CommandRejected")).toBe(true);
63
92
  globalThis.expect(s.includes("Unsupported currency")).toBe(true);
64
93
  });
94
+ globalThis.test("the payload's caller reaches the per-command check", async () => {
95
+ let match = await DcbInboundReceiverStubMjs.receiveWithStubPublish(usdArguments, cognito(["Ops"]));
96
+ globalThis.expect(JSON.stringify(match[0]).includes("Forbidden")).toBe(true);
97
+ globalThis.expect(match[1]).toBe(0);
98
+ });
99
+ globalThis.test("a caller holding the command's role publishes", async () => {
100
+ let match = await DcbInboundReceiverStubMjs.receiveWithStubPublish(usdArguments, cognito(["Admin"]));
101
+ globalThis.expect(JSON.stringify(match[0]).includes("CommandAccepted")).toBe(true);
102
+ globalThis.expect(match[1]).toBe(1);
103
+ });
104
+ globalThis.test("a payload with no identity is refused", async () => {
105
+ let match = await DcbInboundReceiverStubMjs.receiveWithStubPublish(usdArguments, null);
106
+ globalThis.expect(JSON.stringify(match[0]).includes("Forbidden")).toBe(true);
107
+ globalThis.expect(match[1]).toBe(0);
108
+ });
65
109
  });
66
110
 
67
111
  export {
68
112
  routeInbound,
113
+ receiveWithStubPublish,
114
+ usdArguments,
115
+ cognito,
116
+ typenameOf,
69
117
  inboundEvent,
70
118
  }
71
119
  /* routeInbound Not a pure module */
@@ -1,7 +1,5 @@
1
- // Minimal InboundTranslationSlice spec fixture for the DCB entry-point Route 0
2
- // routing test (DcbInboundTranslationRoutingTest). Hand-written — not a plugin
3
- // component, so no folder-based ppx; only the fields
4
- // InboundTranslationSlice_Callback.Make reads at runtime are provided.
1
+ // Inbound spec fixture for DcbInboundTranslationRoutingTest. Hand-written, so it
2
+ // declares the members the PPX would otherwise inject.
5
3
 
6
4
  let name = "EpInboundTest"
7
5
  let moduleUrl = "ep-inbound-test://spec"
@@ -17,3 +15,8 @@ type command = AddThing({thingId: string})
17
15
 
18
16
  let targetName = "AddThing"
19
17
  let externalSystem: option<string> = Some("TestFeed")
18
+
19
+ type role = Reventless.Role.name
20
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowRoles([
21
+ Reventless.Role.make("Admin"),
22
+ ])
@@ -12,6 +12,13 @@ let commandSchema = Sury.$schema(s => ({
12
12
  thingId: s.m(Sury.string)
13
13
  }));
14
14
 
15
+ function authorizationOf(param) {
16
+ return {
17
+ TAG: "AllowRoles",
18
+ _0: ["Admin"]
19
+ };
20
+ }
21
+
15
22
  let name = "EpInboundTest";
16
23
 
17
24
  let moduleUrl = "ep-inbound-test://spec";
@@ -27,5 +34,6 @@ export {
27
34
  commandSchema,
28
35
  targetName,
29
36
  externalSystem,
37
+ authorizationOf,
30
38
  }
31
39
  /* externalInputSchema Not a pure module */
@@ -58,9 +58,9 @@ describe("listAllItemsConnection — paging", () => {
58
58
 
59
59
  testSync("a filtered read examines more rows than it serves", () => {
60
60
  // The budget reads as deep as the page needs, which for a backward cut is
61
- // `_upTo` rather than `_first + _from`.
61
+ // `_upTo` rather than one row past `_first + _from`.
62
62
  expect(
63
- code->String.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from) > 1000"),
63
+ code->String.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from + 1) > 1000"),
64
64
  )->toBe(true)
65
65
  expect(
66
66
  code->String.includes("const _page = _backward ? _rest : _rest.slice(0, _first);"),
@@ -37,7 +37,7 @@ globalThis.describe("listAllItemsConnection — paging", () => {
37
37
  globalThis.expect(code.includes("ctx.args.after + '_' + i")).toBe(false);
38
38
  });
39
39
  globalThis.test("a filtered read examines more rows than it serves", () => {
40
- globalThis.expect(code.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from) > 1000")).toBe(true);
40
+ globalThis.expect(code.includes("parts.length > 0 ? ((_backward ? _upTo : _first + _from + 1) > 1000")).toBe(true);
41
41
  globalThis.expect(code.includes("const _page = _backward ? _rest : _rest.slice(0, _first);")).toBe(true);
42
42
  globalThis.expect(code.includes("hasNextPage: _more || !!_next,")).toBe(true);
43
43
  });
@@ -5,7 +5,7 @@
5
5
  // wired into reventless-core's rescript.json, not reventless-aws's), exposing
6
6
  // exactly the fields the compiled `@@reventless.spec` output would: name, Id
7
7
  // (patched in at runtime by patchSpecId), commandSchema/eventSchema/errorSchema
8
- // (via sury-ppx), moduleUrl, commandAuthorization.
8
+ // (via sury-ppx), moduleUrl, authorizationOf.
9
9
 
10
10
  @schema
11
11
  type command = Add({name: string})
@@ -19,6 +19,6 @@ type error = AlreadyExists
19
19
  let name = "AggTestAggregate"
20
20
  let moduleUrl = "agg-test://AggTestAggregate"
21
21
  type role = Reventless.Role.name
22
- let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAnonymous
22
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAnonymous
23
23
  type lifecycleState = unit
24
24
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
@@ -14,7 +14,7 @@ let eventSchema = Sury.$schema(s => ({
14
14
 
15
15
  let errorSchema = Sury.literal("AlreadyExists");
16
16
 
17
- function commandAuthorization(param) {
17
+ function authorizationOf(param) {
18
18
  return "AllowAnonymous";
19
19
  }
20
20
 
@@ -32,7 +32,7 @@ export {
32
32
  errorSchema,
33
33
  name,
34
34
  moduleUrl,
35
- commandAuthorization,
35
+ authorizationOf,
36
36
  commandTransition,
37
37
  }
38
38
  /* commandSchema Not a pure module */
@@ -64,7 +64,7 @@ type event =
64
64
  let name = "EpCompositeSlice"
65
65
  let moduleUrl = "ep-test://EpCompositeSlice"
66
66
  type role = Reventless.Role.name
67
- let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAnonymous
67
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAnonymous
68
68
  type lifecycleState = unit
69
69
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
70
70
  let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
@@ -47,7 +47,7 @@ let eventSchema = Sury.union([
47
47
  }))
48
48
  ]);
49
49
 
50
- function commandAuthorization(param) {
50
+ function authorizationOf(param) {
51
51
  return "AllowAnonymous";
52
52
  }
53
53
 
@@ -68,7 +68,7 @@ export {
68
68
  eventSchema,
69
69
  name,
70
70
  moduleUrl,
71
- commandAuthorization,
71
+ authorizationOf,
72
72
  commandTransition,
73
73
  readConsistency,
74
74
  }
@@ -22,7 +22,7 @@ type event = WidgetAdded({widgetId: @s.matches(Reventless.DcbTag.partition) stri
22
22
  let name = "EpTestSlice"
23
23
  let moduleUrl = "ep-test://EpTestSlice"
24
24
  type role = Reventless.Role.name
25
- let commandAuthorization = (_: command): Reventless.Authorization.rule<role> => AllowAnonymous
25
+ let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowAnonymous
26
26
  type lifecycleState = unit
27
27
  let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
28
28
  let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
@@ -20,7 +20,7 @@ let eventSchema = Sury.$schema(s => ({
20
20
  widgetId: s.m(DcbTag$Reventless.partition)
21
21
  }));
22
22
 
23
- function commandAuthorization(param) {
23
+ function authorizationOf(param) {
24
24
  return "AllowAnonymous";
25
25
  }
26
26
 
@@ -41,7 +41,7 @@ export {
41
41
  eventSchema,
42
42
  name,
43
43
  moduleUrl,
44
- commandAuthorization,
44
+ authorizationOf,
45
45
  commandTransition,
46
46
  readConsistency,
47
47
  }