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

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,13 @@
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.378 (2026-10-10)
7
+
8
+ ### Bug Fixes
9
+
10
+ * an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
11
+
12
+
6
13
  # 3.0.0-alpha.377 (2026-10-08)
7
14
 
8
15
  **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.378",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -24,17 +24,17 @@
24
24
  "yaml": "^2.8.3",
25
25
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.30",
26
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
27
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
30
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
31
- "@reventlessdev/rescript-web": "1.0.0-alpha.7",
32
- "@reventlessdev/reventless-core": "3.0.0-alpha.291",
28
+ "@reventlessdev/rescript-node": "2.0.0-alpha.22",
29
+ "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.26",
33
30
  "@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",
31
+ "@reventlessdev/rescript-web": "1.0.0-alpha.7",
32
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
33
+ "@reventlessdev/reventless-core": "3.0.0-alpha.292",
34
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.156",
36
35
  "@reventlessdev/reventless-interop": "3.0.0-alpha.39",
37
- "@reventlessdev/reventless-spec": "3.0.0-alpha.160"
36
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.189",
37
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.161"
38
38
  },
39
39
  "devDependencies": {
40
40
  "rescript": "12.3.1",
@@ -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,20 @@ 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 commandAuthorization = (command: command): Reventless.Authorization.rule<
158
+ Reventless.Role.name,
159
+ > =>
160
+ switch command {
161
+ | AddProduct(_) => AllowRoles([Reventless.Role.make("Merchandiser")])
162
+ | AddCategory(_) => AllowRoles([Reventless.Role.make("Admin")])
163
+ }
164
+ }
165
+
152
166
  describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
153
167
  // Minimal mutation fragment with two fields. Field names must match what
154
168
  // the entries declare so the stitcher's extractLeadingName can pair them.
@@ -225,6 +239,22 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
225
239
  )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`)
226
240
  })
227
241
 
242
+ // An inbound slice has one field for every constructor, so its gate admits any
243
+ // constructor's roles; each command is checked against its own in `receive`.
244
+ testSync("an inbound slice's field carries every constructor's groups", () => {
245
+ let permission = ReventlessCore.Dcb_Builder.inboundDoorPermission(
246
+ TwoRuleFeed.commandSchema,
247
+ TwoRuleFeed.commandAuthorization,
248
+ )
249
+ let fp = Dict.fromArray(permission->Option.mapOr([], p => [("p_TwoRuleFeed", p)]))
250
+ let entry = mutationEntry(~fieldNames=["p_TwoRuleFeed"], ~fieldPermissions=fp)
251
+ let frag = makeFragment(["p_TwoRuleFeed(sku: String!): String"], [])
252
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
253
+ expect(
254
+ decodeFragment(aug).mutations->Array.getUnsafe(0),
255
+ )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Merchandiser", "Admin"])`)
256
+ })
257
+
228
258
  testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
229
259
  let fp = Dict.fromArray([("p_Add", Reventless.Authorization.AllowAuthenticated)])
230
260
  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,36 @@ 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 commandAuthorization(command) {
125
+ if (command.TAG === "AddProduct") {
126
+ return {
127
+ TAG: "AllowRoles",
128
+ _0: ["Merchandiser"]
129
+ };
130
+ } else {
131
+ return {
132
+ TAG: "AllowRoles",
133
+ _0: ["Admin"]
134
+ };
135
+ }
136
+ }
137
+
138
+ let TwoRuleFeed = {
139
+ commandSchema: commandSchema,
140
+ commandAuthorization: commandAuthorization
141
+ };
142
+
112
143
  globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
113
144
  let makeFragment = (mutationFields, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
114
145
  types: [],
@@ -169,6 +200,17 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
169
200
  Role$Reventless.clearGroups();
170
201
  globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`);
171
202
  });
203
+ globalThis.test("an inbound slice's field carries every constructor's groups", () => {
204
+ let permission = Dcb_Builder$ReventlessCore.inboundDoorPermission(commandSchema, commandAuthorization);
205
+ let fp = Object.fromEntries(Stdlib_Option.mapOr(permission, [], p => [[
206
+ "p_TwoRuleFeed",
207
+ p
208
+ ]]));
209
+ let entry = mutationEntry(["p_TwoRuleFeed"], fp);
210
+ let frag = makeFragment(["p_TwoRuleFeed(sku: String!): String"], []);
211
+ let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
212
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Merchandiser", "Admin"])`);
213
+ });
172
214
  globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
173
215
  let fp = Object.fromEntries([[
174
216
  "p_Add",
@@ -875,5 +917,6 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
875
917
 
876
918
  export {
877
919
  decodeFragment,
920
+ TwoRuleFeed,
878
921
  }
879
922
  /* 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 commandAuthorization = (_: command): 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 commandAuthorization(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
+ commandAuthorization,
30
38
  }
31
39
  /* externalInputSchema Not a pure module */