@reventlessdev/reventless-local 3.0.0-alpha.298 → 3.0.0-alpha.299

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 (55) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/package.json +10 -10
  3. package/src/EmitCapabilities.res +3 -3
  4. package/src/LocalPlatformRegistry.res +1 -1
  5. package/src/LocalSeedTarget.res +1 -1
  6. package/src/Platform.res +57 -3
  7. package/src/Platform.res.mjs +61 -6
  8. package/src/ShellConfig.res +11 -1
  9. package/src/ShellConfig.res.mjs +10 -4
  10. package/src/adapter/Auth/Auth_GraphqlContext.res +18 -2
  11. package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +8 -1
  12. package/src/adapter/Auth/UserStore.res +102 -3
  13. package/src/adapter/Auth/UserStore.res.mjs +128 -2
  14. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +33 -0
  15. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +26 -0
  16. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +11 -3
  17. package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +3 -0
  18. package/src/adapter/DomainGraphQL_Server.res +1 -1
  19. package/src/adapter/GraphQL_CallerError.res +4 -0
  20. package/src/adapter/GraphQL_CallerError.res.mjs +9 -0
  21. package/src/adapter/LocalAnonymousLimit.res +35 -0
  22. package/src/adapter/LocalAnonymousLimit.res.mjs +52 -0
  23. package/src/adapter/LocalCapabilities.res +14 -2
  24. package/src/adapter/LocalCapabilities.res.mjs +21 -5
  25. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +22 -0
  26. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +7 -0
  27. package/src/components/InboundTranslationSlice_Builder.res +1 -0
  28. package/src/components/InboundTranslationSlice_Builder.res.mjs +4 -1
  29. package/tests/EmitCapabilitiesTest.res +4 -6
  30. package/tests/EmitCapabilitiesTest.res.mjs +1 -2
  31. package/tests/HostShellDistTest.res.mjs +1 -1
  32. package/tests/LocalPlatformRegistryTest.res +3 -5
  33. package/tests/LocalPlatformRegistryTest.res.mjs +2 -4
  34. package/tests/LocalPlatformStartTest.res +2 -2
  35. package/tests/LocalPlatformStartTest.res.mjs +1 -2
  36. package/tests/ShellConfigTest.res.mjs +27 -27
  37. package/tests/adapter/InboundTranslationMutationTest.res +13 -2
  38. package/tests/adapter/InboundTranslationMutationTest.res.mjs +7 -4
  39. package/tests/adapter/LocalAnonymousLimitTest.res +70 -0
  40. package/tests/adapter/LocalAnonymousLimitTest.res.mjs +85 -0
  41. package/tests/adapter/LocalAuthLoginTest.res +2 -2
  42. package/tests/adapter/LocalAuthUserStoreTest.res +88 -0
  43. package/tests/adapter/LocalAuthUserStoreTest.res.mjs +83 -0
  44. package/tests/capability/LocalCapabilitiesTest.res +2 -1
  45. package/tests/capability/LocalCapabilitiesTest.res.mjs +3 -2
  46. package/tests/components/inboundtranslationslice/InboundTranslationCredentialTest.res +136 -0
  47. package/tests/components/inboundtranslationslice/InboundTranslationCredentialTest.res.mjs +183 -0
  48. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +143 -11
  49. package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +129 -22
  50. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +18 -5
  51. package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +12 -3
  52. package/tests/components/inboundtranslationslice/InboundTranslationWebhookTest.res +26 -6
  53. package/tests/components/inboundtranslationslice/InboundTranslationWebhookTest.res.mjs +48 -13
  54. package/tests/plugin/Plugin_Fixtures.res +0 -4
  55. package/tests/plugin/Plugin_Fixtures.res.mjs +13 -33
@@ -32,7 +32,11 @@ describe("InboundTranslationSlice Callback", () => {
32
32
  "status": "completed",
33
33
  }->Obj.magic
34
34
 
35
- let result = await Callback.receive(mockPublish, inputJson)
35
+ let result = await Callback.receive(
36
+ mockPublish,
37
+ inputJson,
38
+ ~capabilities=Reventless.Capabilities.none,
39
+ )
36
40
 
37
41
  switch result {
38
42
  | Ok({requestId, targetIds, commandCount}) =>
@@ -69,7 +73,11 @@ describe("InboundTranslationSlice Callback", () => {
69
73
  "status": "pending",
70
74
  }->Obj.magic
71
75
 
72
- let result = await Callback.receive(mockPublish, inputJson)
76
+ let result = await Callback.receive(
77
+ mockPublish,
78
+ inputJson,
79
+ ~capabilities=Reventless.Capabilities.none,
80
+ )
73
81
 
74
82
  switch result {
75
83
  | Error({error}) => expect(error)->toBe("Unknown payment status: pending")
@@ -92,7 +100,11 @@ describe("InboundTranslationSlice Callback", () => {
92
100
  // Missing required fields
93
101
  let inputJson = {"unexpected": "data"}->Obj.magic
94
102
 
95
- let result = await Callback.receive(mockPublish, inputJson)
103
+ let result = await Callback.receive(
104
+ mockPublish,
105
+ inputJson,
106
+ ~capabilities=Reventless.Capabilities.none,
107
+ )
96
108
 
97
109
  switch result {
98
110
  | Error(_) => expect(true)->toBe(true)
@@ -119,7 +131,11 @@ describe("InboundTranslationSlice Callback", () => {
119
131
  "status": "completed",
120
132
  }->Obj.magic
121
133
 
122
- let result = await Callback.receive(failingPublish, inputJson)
134
+ let result = await Callback.receive(
135
+ failingPublish,
136
+ inputJson,
137
+ ~capabilities=Reventless.Capabilities.none,
138
+ )
123
139
 
124
140
  switch result {
125
141
  | Error({error}) => expect(error)->toBe("publish failed")
@@ -147,7 +163,11 @@ describe("InboundTranslationSlice Callback", () => {
147
163
  let input = status =>
148
164
  {"paymentId": "pay-1", "orderId": "ord-1", "status": status}->Obj.magic
149
165
 
150
- let first = await Callback.receive(mockPublish, input("completed"))
166
+ let first = await Callback.receive(
167
+ mockPublish,
168
+ input("completed"),
169
+ ~capabilities=Reventless.Capabilities.none,
170
+ )
151
171
  let firstId = first->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
152
172
  expect(
153
173
  Callback.auditLog
@@ -158,7 +178,11 @@ describe("InboundTranslationSlice Callback", () => {
158
178
 
159
179
  // A second request drains only its own row — the first one is gone, so it
160
180
  // cannot be written a second time.
161
- let second = await Callback.receive(mockPublish, input("completed"))
181
+ let second = await Callback.receive(
182
+ mockPublish,
183
+ input("completed"),
184
+ ~capabilities=Reventless.Capabilities.none,
185
+ )
162
186
  let secondId = second->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
163
187
  expect(secondId == firstId)->toBe(false)
164
188
  expect(Callback.auditLog->Dict.toArray->Array.length)->toBe(1)
@@ -182,6 +206,7 @@ describe("InboundTranslationSlice Callback", () => {
182
206
  let result = await Callback.receive(
183
207
  mockPublish,
184
208
  {"paymentId": "pay-1", "orderId": "ord-1", "status": "pending"}->Obj.magic,
209
+ ~capabilities=Reventless.Capabilities.none,
185
210
  )
186
211
  let id = result->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
187
212
  switch Callback.auditLog->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(id) {
@@ -215,9 +240,14 @@ describe("InboundTranslationSlice Callback", () => {
215
240
 
216
241
  let targetName = "ConfirmPayment"
217
242
  let externalSystem = None
243
+ let capabilityNeeds: array<Reventless.CapabilityNeed.t> = []
218
244
  let endpoint = None
219
245
 
220
- let translate = (input: externalInput) => Ok(
246
+ let translate = async (
247
+ input: externalInput,
248
+ ~capabilities as _: Reventless.Capabilities.t,
249
+ ~caller as _: option<Reventless.Identity.t>,
250
+ ) => Ok(
221
251
  input.items->Array.map(
222
252
  item => (input.orderId, ConfirmPayment({orderId: input.orderId, paymentId: item})),
223
253
  ),
@@ -248,7 +278,11 @@ describe("InboundTranslationSlice Callback", () => {
248
278
  "items": ["pay-1", "pay-2", "pay-3"],
249
279
  }->Obj.magic
250
280
 
251
- let result = await MultiCallback.receive(mockPublish, inputJson)
281
+ let result = await MultiCallback.receive(
282
+ mockPublish,
283
+ inputJson,
284
+ ~capabilities=Reventless.Capabilities.none,
285
+ )
252
286
 
253
287
  switch result {
254
288
  | Ok({targetIds, commandCount}) =>
@@ -286,9 +320,14 @@ describe("InboundTranslationSlice Callback", () => {
286
320
 
287
321
  let targetName = "ConfirmPayment"
288
322
  let externalSystem = None
323
+ let capabilityNeeds: array<Reventless.CapabilityNeed.t> = []
289
324
  let endpoint = None
290
325
 
291
- let translate = (_input: externalInput) => Ok([])
326
+ let translate = async (
327
+ _input: externalInput,
328
+ ~capabilities as _: Reventless.Capabilities.t,
329
+ ~caller as _: option<Reventless.Identity.t>,
330
+ ) => Ok([])
292
331
 
293
332
  type role = Reventless.Role.name
294
333
 
@@ -312,7 +351,11 @@ describe("InboundTranslationSlice Callback", () => {
312
351
 
313
352
  let inputJson = {"orderId": "ord-1"}->Obj.magic
314
353
 
315
- let result = await EmptyCallback.receive(mockPublish, inputJson)
354
+ let result = await EmptyCallback.receive(
355
+ mockPublish,
356
+ inputJson,
357
+ ~capabilities=Reventless.Capabilities.none,
358
+ )
316
359
 
317
360
  switch result {
318
361
  | Ok({targetIds, commandCount}) =>
@@ -360,6 +403,7 @@ describe("receive checks each command against its own rule", () => {
360
403
  let result = await CatalogFeedCallback.receive(
361
404
  publish,
362
405
  catalogInput("both"),
406
+ ~capabilities=Reventless.Capabilities.none,
363
407
  ~caller=caller(["Merchandiser"]),
364
408
  )
365
409
  switch result {
@@ -379,6 +423,7 @@ describe("receive checks each command against its own rule", () => {
379
423
  let result = await CatalogFeedCallback.receive(
380
424
  publish,
381
425
  catalogInput("both"),
426
+ ~capabilities=Reventless.Capabilities.none,
382
427
  ~caller=caller(["Merchandiser", "Admin"]),
383
428
  )
384
429
  expect(result->Result.isOk)->toBe(true)
@@ -389,6 +434,7 @@ describe("receive checks each command against its own rule", () => {
389
434
  let result = await CatalogFeedCallback.receive(
390
435
  publish,
391
436
  catalogInput("product"),
437
+ ~capabilities=Reventless.Capabilities.none,
392
438
  ~caller=caller(["Merchandiser"]),
393
439
  )
394
440
  expect(result->Result.isOk)->toBe(true)
@@ -396,7 +442,11 @@ describe("receive checks each command against its own rule", () => {
396
442
  })
397
443
 
398
444
  testPromise("no caller is the platform itself, and publishes", async () => {
399
- let result = await CatalogFeedCallback.receive(publish, catalogInput("both"))
445
+ let result = await CatalogFeedCallback.receive(
446
+ publish,
447
+ catalogInput("both"),
448
+ ~capabilities=Reventless.Capabilities.none,
449
+ )
400
450
  expect(result->Result.isOk)->toBe(true)
401
451
  expect(published.contents->Array.length)->toBe(2)
402
452
  })
@@ -405,6 +455,7 @@ describe("receive checks each command against its own rule", () => {
405
455
  let result = await CatalogFeedCallback.receive(
406
456
  publish,
407
457
  catalogInput("both"),
458
+ ~capabilities=Reventless.Capabilities.none,
408
459
  ~caller=Reventless.Identity.anonymous,
409
460
  )
410
461
  switch result->ReventlessCore.InboundTranslationSlice_Callback.receiveResultToOutcome {
@@ -430,6 +481,7 @@ describe("receive encodes commands with their schema", () => {
430
481
  let _ = await NoteFeedCallback.receive(
431
482
  publish,
432
483
  Dict.fromArray([("orderId", JSON.Encode.string("ord-1"))])->JSON.Encode.object,
484
+ ~capabilities=Reventless.Capabilities.none,
433
485
  )
434
486
 
435
487
  let commandJson = (published.contents->Array.getUnsafe(0)).commandJson
@@ -439,3 +491,83 @@ describe("receive encodes commands with their schema", () => {
439
491
  expect(commandJson->JSON.stringify->String.includes(`"note":null`))->toBe(true)
440
492
  })
441
493
  })
494
+
495
+ // A door that looks at its caller before it decides: only an administrator may
496
+ // send it. It refuses before reaching a capability, and the refusal is recorded
497
+ // like any other translation error, naming who was refused.
498
+ module AdminOnlyTranslation = {
499
+ let translate = async (input, ~capabilities, ~caller: option<Reventless.Identity.t>) =>
500
+ switch caller {
501
+ | None => Error("this door needs a caller")
502
+ | Some(identity) if !(identity->Reventless.Identity.hasGroup("Admin")) =>
503
+ Error("only an administrator may send this")
504
+ | Some(_) => await PaymentWebhookSpec.translate(input, ~capabilities, ~caller)
505
+ }
506
+ let moduleUrl = PaymentWebhookSpec.moduleUrl
507
+ }
508
+ module AdminOnlyCallback = ReventlessCore.InboundTranslationSlice_Callback.Make(
509
+ PaymentWebhookSpec,
510
+ AdminOnlyTranslation,
511
+ )
512
+
513
+ describe("receive hands the door its caller", () => {
514
+ let payment = {"paymentId": "pay-1", "orderId": "ord-1", "status": "completed"}->Obj.magic
515
+ let identity = (~userId, ~groups): Reventless.Identity.t => {
516
+ ...Reventless.Identity.anonymous,
517
+ userId,
518
+ groups,
519
+ }
520
+
521
+ let receive = async (~caller=?) => {
522
+ AdminOnlyCallback.auditLog
523
+ ->Dict.keysToArray
524
+ ->Array.forEach(k => AdminOnlyCallback.auditLog->Dict.delete(k))
525
+ let published = ref([])
526
+ let publish: ReventlessInfra.CommandTopic.publishJsons = async cmds => published := cmds
527
+ let result = await AdminOnlyCallback.receive(
528
+ publish,
529
+ payment,
530
+ ~capabilities=Reventless.Capabilities.none,
531
+ ~caller?,
532
+ )
533
+ let row = AdminOnlyCallback.auditLog->Dict.valuesToArray->Array.getUnsafe(0)
534
+ (result, published.contents, row)
535
+ }
536
+
537
+ testPromise("with no caller, the door refuses and nothing is published", async () => {
538
+ let (result, published, row) = await receive()
539
+ expect(result->Result.isError)->toBe(true)
540
+ expect(published)->toEqual([])
541
+ expect(row.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Failure)
542
+ expect(row.error)->toEqual(Some("this door needs a caller"))
543
+ expect(row.caller)->toEqual(None)
544
+ })
545
+
546
+ testPromise(
547
+ "a caller lacking the role is refused, and the row names who was refused",
548
+ async () => {
549
+ let (result, published, row) = await receive(
550
+ ~caller=identity(~userId="u_visitor", ~groups=["Shopper"]),
551
+ )
552
+ switch result {
553
+ | Error({error, ?errorCode}) =>
554
+ expect(error)->toBe("only an administrator may send this")
555
+ // The door's refusal, not the platform's per-command gate.
556
+ expect(errorCode)->toEqual(None)
557
+ | Ok(_) => expect(true)->toBe(false)
558
+ }
559
+ expect(published)->toEqual([])
560
+ expect(row.caller)->toEqual(Some("u_visitor"))
561
+ },
562
+ )
563
+
564
+ testPromise("a caller holding the role is let through, and named", async () => {
565
+ let (result, published, row) = await receive(
566
+ ~caller=identity(~userId="u_admin", ~groups=["Admin"]),
567
+ )
568
+ expect(result->Result.isOk)->toBe(true)
569
+ expect(published->Array.length)->toBe(1)
570
+ expect(row.status)->toBe(ReventlessCore.InboundTranslationSlice_Callback.Success)
571
+ expect(row.caller)->toEqual(Some("u_admin"))
572
+ })
573
+ })
@@ -7,6 +7,7 @@ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
7
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
8
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
9
9
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
10
+ import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
10
11
  import * as InboundTranslationSliceFixtures$ReventlessLocal from "./InboundTranslationSliceFixtures.res.mjs";
11
12
  import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
12
13
 
@@ -27,7 +28,8 @@ let Callback = InboundTranslationSlice_Callback$ReventlessCore.Make({
27
28
  targetName: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.targetName,
28
29
  externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalSystem,
29
30
  authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.authorizationOf,
30
- endpoint: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.endpoint
31
+ endpoint: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.endpoint,
32
+ capabilityNeeds: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.capabilityNeeds
31
33
  })(PaymentWebhookTranslation);
32
34
 
33
35
  globalThis.describe("InboundTranslationSlice Callback", () => {
@@ -47,7 +49,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
47
49
  orderId: "ord-1",
48
50
  status: "completed"
49
51
  };
50
- let result = await Callback.receive(mockPublish, inputJson, undefined, undefined);
52
+ let result = await Callback.receive(mockPublish, inputJson, Capabilities$Reventless.none, undefined, undefined);
51
53
  if (result.TAG === "Ok") {
52
54
  let match = result._0;
53
55
  globalThis.expect(match.targetIds).toEqual(["ord-1"]);
@@ -78,7 +80,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
78
80
  orderId: "ord-1",
79
81
  status: "pending"
80
82
  };
81
- let result = await Callback.receive(mockPublish, inputJson, undefined, undefined);
83
+ let result = await Callback.receive(mockPublish, inputJson, Capabilities$Reventless.none, undefined, undefined);
82
84
  if (result.TAG === "Ok") {
83
85
  globalThis.expect(true).toBe(false);
84
86
  } else {
@@ -95,7 +97,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
95
97
  let inputJson = {
96
98
  unexpected: "data"
97
99
  };
98
- let result = await Callback.receive(mockPublish, inputJson, undefined, undefined);
100
+ let result = await Callback.receive(mockPublish, inputJson, Capabilities$Reventless.none, undefined, undefined);
99
101
  if (result.TAG === "Ok") {
100
102
  globalThis.expect(true).toBe(false);
101
103
  } else {
@@ -113,7 +115,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
113
115
  orderId: "ord-1",
114
116
  status: "completed"
115
117
  };
116
- let result = await Callback.receive(failingPublish, inputJson, undefined, undefined);
118
+ let result = await Callback.receive(failingPublish, inputJson, Capabilities$Reventless.none, undefined, undefined);
117
119
  if (result.TAG === "Ok") {
118
120
  globalThis.expect(true).toBe(false);
119
121
  } else {
@@ -132,7 +134,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
132
134
  paymentId: "pay-1",
133
135
  orderId: "ord-1",
134
136
  status: "completed"
135
- }, undefined, undefined);
137
+ }, Capabilities$Reventless.none, undefined, undefined);
136
138
  let firstId = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(first);
137
139
  globalThis.expect(Stdlib_Option.isSome(InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(Callback.auditLog, firstId))).toBe(true);
138
140
  globalThis.expect(Object.entries(Callback.auditLog).length).toBe(0);
@@ -140,7 +142,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
140
142
  paymentId: "pay-1",
141
143
  orderId: "ord-1",
142
144
  status: "completed"
143
- }, undefined, undefined);
145
+ }, Capabilities$Reventless.none, undefined, undefined);
144
146
  let secondId = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(second);
145
147
  globalThis.expect(secondId === firstId).toBe(false);
146
148
  globalThis.expect(Object.entries(Callback.auditLog).length).toBe(1);
@@ -153,7 +155,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
153
155
  paymentId: "pay-1",
154
156
  orderId: "ord-1",
155
157
  status: "pending"
156
- }, undefined, undefined);
158
+ }, Capabilities$Reventless.none, undefined, undefined);
157
159
  let id = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result);
158
160
  let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(Callback.auditLog, id);
159
161
  if (row !== undefined) {
@@ -176,7 +178,8 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
176
178
  orderId: s.m(DcbTag$Reventless.string),
177
179
  paymentId: s.m(Sury.string)
178
180
  }));
179
- let translate = input => ({
181
+ let capabilityNeeds = [];
182
+ let translate = async (input, param, param$1) => ({
180
183
  TAG: "Ok",
181
184
  _0: input.items.map(item => [
182
185
  input.orderId,
@@ -200,7 +203,8 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
200
203
  targetName: "ConfirmPayment",
201
204
  externalSystem: undefined,
202
205
  authorizationOf: authorizationOf,
203
- endpoint: undefined
206
+ endpoint: undefined,
207
+ capabilityNeeds: capabilityNeeds
204
208
  })(MultiTranslation);
205
209
  let publishedCommands = {
206
210
  contents: []
@@ -216,7 +220,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
216
220
  "pay-3"
217
221
  ]
218
222
  };
219
- let result = await MultiCallback.receive(mockPublish, inputJson, undefined, undefined);
223
+ let result = await MultiCallback.receive(mockPublish, inputJson, Capabilities$Reventless.none, undefined, undefined);
220
224
  if (result.TAG === "Ok") {
221
225
  let match = result._0;
222
226
  globalThis.expect(match.targetIds).toEqual([
@@ -246,7 +250,8 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
246
250
  orderId: s.m(DcbTag$Reventless.string),
247
251
  paymentId: s.m(Sury.string)
248
252
  }));
249
- let translate = _input => ({
253
+ let capabilityNeeds = [];
254
+ let translate = async (_input, param, param$1) => ({
250
255
  TAG: "Ok",
251
256
  _0: []
252
257
  });
@@ -263,7 +268,8 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
263
268
  targetName: "ConfirmPayment",
264
269
  externalSystem: undefined,
265
270
  authorizationOf: authorizationOf,
266
- endpoint: undefined
271
+ endpoint: undefined,
272
+ capabilityNeeds: capabilityNeeds
267
273
  })(EmptyTranslation);
268
274
  let publishedCommands = {
269
275
  contents: []
@@ -274,7 +280,7 @@ globalThis.describe("InboundTranslationSlice Callback", () => {
274
280
  let inputJson = {
275
281
  orderId: "ord-1"
276
282
  };
277
- let result = await EmptyCallback.receive(mockPublish, inputJson, undefined, undefined);
283
+ let result = await EmptyCallback.receive(mockPublish, inputJson, Capabilities$Reventless.none, undefined, undefined);
278
284
  if (result.TAG === "Ok") {
279
285
  let match = result._0;
280
286
  globalThis.expect(match.targetIds).toEqual([]);
@@ -304,7 +310,8 @@ let CatalogFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
304
310
  targetName: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.targetName,
305
311
  externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.externalSystem,
306
312
  authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.authorizationOf,
307
- endpoint: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.endpoint
313
+ endpoint: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.endpoint,
314
+ capabilityNeeds: InboundTranslationSliceFixtures$ReventlessLocal.CatalogFeedSpec.capabilityNeeds
308
315
  })(CatalogFeedTranslation);
309
316
 
310
317
  function caller(groups) {
@@ -340,7 +347,7 @@ globalThis.describe("receive checks each command against its own rule", () => {
340
347
  published.contents = [];
341
348
  });
342
349
  globalThis.test("a caller lacking one command's role is refused, and nothing is published", async () => {
343
- let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), caller(["Merchandiser"]), undefined);
350
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), Capabilities$Reventless.none, caller(["Merchandiser"]), undefined);
344
351
  if (result.TAG === "Ok") {
345
352
  globalThis.expect(true).toBe(false);
346
353
  } else {
@@ -352,7 +359,7 @@ globalThis.describe("receive checks each command against its own rule", () => {
352
359
  globalThis.expect(published.contents.length).toBe(0);
353
360
  });
354
361
  globalThis.test("a caller holding every command's role publishes them all", async () => {
355
- let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), caller([
362
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), Capabilities$Reventless.none, caller([
356
363
  "Merchandiser",
357
364
  "Admin"
358
365
  ]), undefined);
@@ -360,17 +367,17 @@ globalThis.describe("receive checks each command against its own rule", () => {
360
367
  globalThis.expect(published.contents.length).toBe(2);
361
368
  });
362
369
  globalThis.test("the role one command needs is enough when the input yields only it", async () => {
363
- let result = await CatalogFeedCallback.receive(publish, catalogInput("product"), caller(["Merchandiser"]), undefined);
370
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("product"), Capabilities$Reventless.none, caller(["Merchandiser"]), undefined);
364
371
  globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
365
372
  globalThis.expect(published.contents.length).toBe(1);
366
373
  });
367
374
  globalThis.test("no caller is the platform itself, and publishes", async () => {
368
- let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), undefined, undefined);
375
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), Capabilities$Reventless.none, undefined, undefined);
369
376
  globalThis.expect(Stdlib_Result.isOk(result)).toBe(true);
370
377
  globalThis.expect(published.contents.length).toBe(2);
371
378
  });
372
379
  globalThis.test("a refusal answers CommandRejected with Forbidden", async () => {
373
- let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), Identity$Reventless.anonymous, undefined);
380
+ let result = await CatalogFeedCallback.receive(publish, catalogInput("both"), Capabilities$Reventless.none, Identity$Reventless.anonymous, undefined);
374
381
  let match = InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result);
375
382
  switch (match.TAG) {
376
383
  case "Rejected" :
@@ -401,7 +408,8 @@ let NoteFeedCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
401
408
  targetName: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.targetName,
402
409
  externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.externalSystem,
403
410
  authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.authorizationOf,
404
- endpoint: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.endpoint
411
+ endpoint: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.endpoint,
412
+ capabilityNeeds: InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.capabilityNeeds
405
413
  })(NoteFeedTranslation);
406
414
 
407
415
  globalThis.describe("receive encodes commands with their schema", () => {
@@ -415,7 +423,7 @@ globalThis.describe("receive encodes commands with their schema", () => {
415
423
  await NoteFeedCallback.receive(publish, Object.fromEntries([[
416
424
  "orderId",
417
425
  "ord-1"
418
- ]]), undefined, undefined);
426
+ ]]), Capabilities$Reventless.none, undefined, undefined);
419
427
  let commandJson = published.contents[0].commandJson;
420
428
  let decoded = Sury.parseOrThrow(commandJson, InboundTranslationSliceFixtures$ReventlessLocal.NoteFeedSpec.commandSchema);
421
429
  globalThis.expect(decoded).toEqual({
@@ -427,6 +435,103 @@ globalThis.describe("receive encodes commands with their schema", () => {
427
435
  });
428
436
  });
429
437
 
438
+ async function translate$3(input, capabilities, caller) {
439
+ if (caller !== undefined) {
440
+ if (Identity$Reventless.hasGroup(caller, "Admin")) {
441
+ return await InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.translate(input, capabilities, caller);
442
+ } else {
443
+ return {
444
+ TAG: "Error",
445
+ _0: "only an administrator may send this"
446
+ };
447
+ }
448
+ } else {
449
+ return {
450
+ TAG: "Error",
451
+ _0: "this door needs a caller"
452
+ };
453
+ }
454
+ }
455
+
456
+ let moduleUrl$3 = InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.moduleUrl;
457
+
458
+ let AdminOnlyTranslation = {
459
+ translate: translate$3,
460
+ moduleUrl: moduleUrl$3
461
+ };
462
+
463
+ let AdminOnlyCallback = InboundTranslationSlice_Callback$ReventlessCore.Make({
464
+ name: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.name,
465
+ moduleUrl: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.moduleUrl,
466
+ externalInputSchema: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalInputSchema,
467
+ commandSchema: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.commandSchema,
468
+ targetName: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.targetName,
469
+ externalSystem: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.externalSystem,
470
+ authorizationOf: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.authorizationOf,
471
+ endpoint: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.endpoint,
472
+ capabilityNeeds: InboundTranslationSliceFixtures$ReventlessLocal.PaymentWebhookSpec.capabilityNeeds
473
+ })(AdminOnlyTranslation);
474
+
475
+ globalThis.describe("receive hands the door its caller", () => {
476
+ let payment = {
477
+ paymentId: "pay-1",
478
+ orderId: "ord-1",
479
+ status: "completed"
480
+ };
481
+ let identity = (userId, groups) => {
482
+ let newrecord = {...Identity$Reventless.anonymous};
483
+ newrecord.groups = groups;
484
+ newrecord.userId = userId;
485
+ return newrecord;
486
+ };
487
+ let receive = async caller => {
488
+ Object.keys(AdminOnlyCallback.auditLog).forEach(k => Stdlib_Dict.$$delete(AdminOnlyCallback.auditLog, k));
489
+ let published = {
490
+ contents: []
491
+ };
492
+ let publish = async cmds => {
493
+ published.contents = cmds;
494
+ };
495
+ let result = await AdminOnlyCallback.receive(publish, payment, Capabilities$Reventless.none, caller, undefined);
496
+ let row = Object.values(AdminOnlyCallback.auditLog)[0];
497
+ return [
498
+ result,
499
+ published.contents,
500
+ row
501
+ ];
502
+ };
503
+ globalThis.test("with no caller, the door refuses and nothing is published", async () => {
504
+ let match = await receive(undefined);
505
+ let row = match[2];
506
+ globalThis.expect(Stdlib_Result.isError(match[0])).toBe(true);
507
+ globalThis.expect(match[1]).toEqual([]);
508
+ globalThis.expect(row.status).toBe("Failure");
509
+ globalThis.expect(row.error).toEqual("this door needs a caller");
510
+ globalThis.expect(row.caller).toEqual(undefined);
511
+ });
512
+ globalThis.test("a caller lacking the role is refused, and the row names who was refused", async () => {
513
+ let match = await receive(identity("u_visitor", ["Shopper"]));
514
+ let result = match[0];
515
+ if (result.TAG === "Ok") {
516
+ globalThis.expect(true).toBe(false);
517
+ } else {
518
+ let match$1 = result._0;
519
+ globalThis.expect(match$1.error).toBe("only an administrator may send this");
520
+ globalThis.expect(match$1.errorCode).toEqual(undefined);
521
+ }
522
+ globalThis.expect(match[1]).toEqual([]);
523
+ globalThis.expect(match[2].caller).toEqual("u_visitor");
524
+ });
525
+ globalThis.test("a caller holding the role is let through, and named", async () => {
526
+ let match = await receive(identity("u_admin", ["Admin"]));
527
+ let row = match[2];
528
+ globalThis.expect(Stdlib_Result.isOk(match[0])).toBe(true);
529
+ globalThis.expect(match[1].length).toBe(1);
530
+ globalThis.expect(row.status).toBe("Success");
531
+ globalThis.expect(row.caller).toEqual("u_admin");
532
+ });
533
+ });
534
+
430
535
  export {
431
536
  PaymentWebhookTranslation,
432
537
  Callback,
@@ -436,5 +541,7 @@ export {
436
541
  catalogInput,
437
542
  NoteFeedTranslation,
438
543
  NoteFeedCallback,
544
+ AdminOnlyTranslation,
545
+ AdminOnlyCallback,
439
546
  }
440
547
  /* Callback Not a pure module */
@@ -28,9 +28,14 @@ module PaymentWebhookSpec = {
28
28
 
29
29
  let targetName = "ConfirmPayment"
30
30
  let externalSystem = None
31
+ let capabilityNeeds: array<Reventless.CapabilityNeed.t> = []
31
32
  let endpoint = None
32
33
 
33
- let translate = (input: externalInput) =>
34
+ let translate = async (
35
+ input: externalInput,
36
+ ~capabilities as _: Reventless.Capabilities.t,
37
+ ~caller as _: option<Reventless.Identity.t>,
38
+ ) =>
34
39
  switch input.status {
35
40
  | "completed" =>
36
41
  Ok([(input.orderId, ConfirmPayment({orderId: input.orderId, paymentId: input.paymentId}))])
@@ -53,6 +58,7 @@ module CatalogFeedSpec = {
53
58
 
54
59
  let targetName = "AddProduct"
55
60
  let externalSystem = None
61
+ let capabilityNeeds: array<Reventless.CapabilityNeed.t> = []
56
62
  let endpoint = None
57
63
 
58
64
  type role = Reventless.Role.name
@@ -65,7 +71,11 @@ module CatalogFeedSpec = {
65
71
  | _ => DenyAll
66
72
  }
67
73
 
68
- let translate = (input: externalInput) =>
74
+ let translate = async (
75
+ input: externalInput,
76
+ ~capabilities as _: Reventless.Capabilities.t,
77
+ ~caller as _: option<Reventless.Identity.t>,
78
+ ) =>
69
79
  switch input.kind {
70
80
  | "product" => Ok([(input.id, AddProduct({productId: input.id}))])
71
81
  | "both" =>
@@ -91,9 +101,12 @@ module NoteFeedSpec = {
91
101
 
92
102
  let targetName = "Annotate"
93
103
  let externalSystem = None
104
+ let capabilityNeeds: array<Reventless.CapabilityNeed.t> = []
94
105
  let endpoint = None
95
106
 
96
- let translate = (input: externalInput) => Ok([
97
- (input.orderId, Annotate({orderId: input.orderId, note: None})),
98
- ])
107
+ let translate = async (
108
+ input: externalInput,
109
+ ~capabilities as _: Reventless.Capabilities.t,
110
+ ~caller as _: option<Reventless.Identity.t>,
111
+ ) => Ok([(input.orderId, Annotate({orderId: input.orderId, note: None}))])
99
112
  }