@reventlessdev/reventless-gwt 1.0.0-alpha.232 → 1.0.0-alpha.233

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,21 @@
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
+ # 1.0.0-alpha.233 (2026-10-01)
7
+
8
+ * feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * AllowGroups and AdminGroup are removed — write AllowRoles over
13
+ the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
14
+ types declare `type role`, and their rules are Authorization.rule<role>; a spec
15
+ that writes its own rule declares `type role` (Reventless.Role.name for a rule
16
+ over Authorization.permission). Role.name is a private string: make one from a
17
+ string with Role.make.
18
+
19
+
20
+
6
21
  # 1.0.0-alpha.232 (2026-10-01)
7
22
 
8
23
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.232",
3
+ "version": "1.0.0-alpha.233",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -16,15 +16,15 @@
16
16
  "sury": "11.0.0",
17
17
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
18
18
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
19
- "@reventlessdev/reventless-core": "3.0.0-alpha.285",
20
- "@reventlessdev/reventless-infra": "3.0.0-alpha.183",
21
19
  "@reventlessdev/rescript-node": "2.0.0-alpha.20",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.155"
20
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.184",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.286",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.156"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
26
26
  "sury-ppx": "11.0.0",
27
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.95"
27
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.96"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "rescript": "^12.3.1"
@@ -23,18 +23,40 @@ open ReventlessCore
23
23
  // differs). Pick the matching entry point.
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
- // Who issues the command, for slices and aggregates that mark `@owner` on the
27
- // events they decide on. `asCaller` names the caller before `whenCmd`; the
28
- // command is then checked against the owners the given events record, exactly
29
- // as the handler checks it before `decide`. Without `asCaller` the command
30
- // carries no caller claim, which the handler reads as the platform acting for
31
- // itself — so every scenario written before this existed is unchanged.
26
+ // Who issues the command. `asCaller` names the caller before `whenCmd`; the
27
+ // command is then checked as production checks it: its `@authorize` rule against
28
+ // the caller's roles, then — for slices and aggregates that mark `@owner` on the
29
+ // events they decide on — against the owners the given events record. Without
30
+ // `asCaller` the command carries no caller claim, which the handler reads as the
31
+ // platform acting for itself — so every scenario written before this existed is
32
+ // unchanged.
32
33
  // ---------------------------------------------------------------------------
33
34
  module Caller = {
34
- external unsafeAsString: 'id => string = "%identity"
35
+ external unsafeAsString: 'a => string = "%identity"
36
+
37
+ type t = {
38
+ claim: Message.CallerClaim.t,
39
+ /** The roles held, by name. A scenario names them by its plugin's own
40
+ `Roles.t`, whose cases are their names at run time. */
41
+ roles: array<Reventless.Role.name>,
42
+ signedIn: bool,
43
+ }
44
+
45
+ // A plugin's roles are payload-less variants, strings at run time; anything
46
+ // else would be compared with a rule's role names and never match.
47
+ let roleNames = (roles: array<'role>): array<Reventless.Role.name> =>
48
+ roles->Array.map(role =>
49
+ if Reventless.OwnerScope.isJsString(role) {
50
+ Reventless.Role.make(role->unsafeAsString)
51
+ } else {
52
+ JsError.throwWithMessage(
53
+ "Caller: expected roles of the plugin's Roles.t (payload-less, so strings at runtime)",
54
+ )
55
+ }
56
+ )
35
57
 
36
58
  /**
37
- A caller who owns only what records their id.
59
+ A caller who owns only what records their id, holding `roles` (none by default).
38
60
 
39
61
  Takes the id as the scenario has it — `Caller.owner(c1)` with `c1: CustomerId.t`,
40
62
  or a plain string — because every `Id.Make` identity is a string on the wire.
@@ -42,18 +64,52 @@ module Caller = {
42
64
  recorded owner, so it can only be refused, which fails the scenario that
43
65
  expected the owner to act. A value that is not a string at all is refused here.
44
66
  */
45
- let owner = (id: 'id): Message.CallerClaim.t =>
67
+ let owner = (id: 'id, ~roles: array<'role>=[]): t =>
46
68
  if Reventless.OwnerScope.isJsString(id) {
47
- Owned({userId: id->unsafeAsString})
69
+ {claim: Owned({userId: id->unsafeAsString}), roles: roleNames(roles), signedIn: true}
48
70
  } else {
49
71
  JsError.throwWithMessage(
50
72
  "Caller.owner: expected an id (a string at runtime, such as an Id.Make identity)",
51
73
  )
52
74
  }
53
- /** A caller in an elevated group, acting on anyone's behalf. */
54
- let operator: Message.CallerClaim.t = Exempt
75
+
76
+ /** A signed-in caller holding `roles`, who owns nothing the given events record. */
77
+ let inRoles = (roles: array<'role>): t => {
78
+ claim: Owned({userId: "caller-in-roles"}),
79
+ roles: roleNames(roles),
80
+ signedIn: true,
81
+ }
82
+
83
+ /** A caller exempt from ownership rules, acting on anyone's behalf. Holds no
84
+ role: elevation does not grant a command whose rule the caller fails. */
85
+ let operator: t = {claim: Exempt, roles: [], signedIn: true}
55
86
  /** A caller with no identity. */
56
- let anonymous: Message.CallerClaim.t = Unidentified
87
+ let anonymous: t = {claim: Unidentified, roles: [], signedIn: false}
88
+ }
89
+
90
+ /** `Caller` as a scenario sees it: roles typed by the spec's own `role`, so they
91
+ are written bare (`Caller.inRoles([Merchandiser])`) and another plugin's role
92
+ does not compile. */
93
+ module type Callers = {
94
+ type role
95
+ type t = Caller.t
96
+ let owner: ('id, ~roles: array<role>=?) => t
97
+ let inRoles: array<role> => t
98
+ let operator: t
99
+ let anonymous: t
100
+ }
101
+
102
+ module Callers = (
103
+ R: {
104
+ type role
105
+ },
106
+ ): (Callers with type role = R.role) => {
107
+ type role = R.role
108
+ type t = Caller.t
109
+ let owner = (id, ~roles: array<role>=[]) => Caller.owner(id, ~roles)
110
+ let inRoles = (roles: array<role>) => Caller.inRoles(roles)
111
+ let operator = Caller.operator
112
+ let anonymous = Caller.anonymous
57
113
  }
58
114
 
59
115
  module type ActingSpec = {
@@ -62,28 +118,58 @@ module type ActingSpec = {
62
118
  type history
63
119
  @schema
64
120
  type command
121
+ type role
122
+ let commandAuthorization: command => Reventless.Authorization.rule<role>
65
123
  }
66
124
 
67
125
  module Acting = (Spec: ActingSpec) => {
68
- let caller: ref<option<Message.CallerClaim.t>> = ref(None)
126
+ let caller: ref<option<Caller.t>> = ref(None)
69
127
  let refusal: ref<option<CommandTopic_Helpers.rejectedResult>> = ref(None)
70
128
 
71
129
  let ownerFieldsByEventType = Reventless.Owner.fieldNamesByVariant(
72
130
  Spec.historySchema->S.castToUnknown,
73
131
  )
74
132
 
75
- let asCaller = (history, claim) => {
76
- caller := Some(claim)
133
+ let asCaller = (history, who: Caller.t) => {
134
+ caller := Some(who)
77
135
  history
78
136
  }
79
137
 
138
+ let commandName = command =>
139
+ command->Message.encode(Spec.commandSchema)->Message.variantNameOfJson
140
+
141
+ // The rule first, as production checks it, against the roles alone: which group
142
+ // a role maps to is a deployment's fact, not the scenario's.
143
+ let refusedByRule = (who: Caller.t, command: Spec.command): bool =>
144
+ if (
145
+ Spec.commandAuthorization(command)
146
+ ->Reventless.Authorization.named
147
+ ->Reventless.Authorization.admits(~signedIn=who.signedIn, ~holds=role =>
148
+ who.roles->Array.includes(role)
149
+ )
150
+ ) {
151
+ false
152
+ } else {
153
+ refusal :=
154
+ Some({
155
+ errorCode: "Forbidden",
156
+ errorDetail: `${Spec.name}.${commandName(
157
+ command,
158
+ )}: the caller holds no role this command's rule admits`,
159
+ })
160
+ true
161
+ }
162
+
80
163
  // Every given event is the command's own history here, so all of them count.
81
164
  // The caller is consumed: the next scenario starts with no claim.
82
165
  let refuses = (history: array<Spec.history>, command: Spec.command): bool => {
83
- let claim = caller.contents
166
+ let who = caller.contents
84
167
  caller := None
85
168
  refusal := None
86
- if ownerFieldsByEventType->Dict.keysToArray->Array.length == 0 {
169
+ let claim = who->Option.map(w => w.claim)
170
+ if who->Option.mapOr(false, refusedByRule(_, command)) {
171
+ true
172
+ } else if ownerFieldsByEventType->Dict.keysToArray->Array.length == 0 {
87
173
  false
88
174
  } else {
89
175
  let owners = history->Array.reduce([], (owners, event) => {
@@ -95,10 +181,7 @@ module Acting = (Spec: ActingSpec) => {
95
181
  })
96
182
  refusal :=
97
183
  Reventless.OwnerScope.decideActing(~claim, ~recordedOwners=Some(owners))
98
- ->CommandTopic_Helpers.ownershipRefusal(
99
- ~component=Spec.name,
100
- ~command=command->Message.encode(Spec.commandSchema)->Message.variantNameOfJson,
101
- )
184
+ ->CommandTopic_Helpers.ownershipRefusal(~component=Spec.name, ~command=commandName(command))
102
185
  ->Option.map(r => r.rejected)
103
186
  refusal.contents->Option.isSome
104
187
  }
@@ -155,6 +238,9 @@ module type BehaviorSpec = {
155
238
 
156
239
  @schema
157
240
  type event
241
+
242
+ type role
243
+ let commandAuthorization: command => Reventless.Authorization.rule<role>
158
244
  }
159
245
 
160
246
  module type Behavior = {
@@ -176,8 +262,8 @@ module type T = {
176
262
  let givenEvents: array<Spec.consumedEvent> => array<Spec.consumedEvent>
177
263
 
178
264
  // Who issues the next `whenCmd`; see `Caller`.
179
- module Caller: module type of Caller
180
- let asCaller: (array<Spec.consumedEvent>, Message.CallerClaim.t) => array<Spec.consumedEvent>
265
+ module Caller: Callers with type role = Spec.role
266
+ let asCaller: (array<Spec.consumedEvent>, Caller.t) => array<Spec.consumedEvent>
181
267
 
182
268
  let whenCmd: (array<Spec.consumedEvent>, Spec.command) => array<Spec.event>
183
269
 
@@ -187,7 +273,8 @@ module type T = {
187
273
  let thenEventWithError: (array<Spec.event>, Spec.event, Spec.error) => Outcome.outcome
188
274
  let thenEventsWithError: (array<Spec.event>, array<Spec.event>, Spec.error) => Outcome.outcome
189
275
  let thenError: (array<Spec.event>, Spec.error) => Outcome.outcome
190
- // Refused before `decide` because the caller does not own what it acts on.
276
+ // Refused before `decide`: by the command's rule, or because the caller does not
277
+ // own what it acts on.
191
278
  let thenRefused: array<Spec.event> => Outcome.outcome
192
279
 
193
280
  // DCB optimistic-concurrency assertions.
@@ -363,13 +450,17 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
363
450
  module Core = AssertionCore(Spec)
364
451
  let errors = Core.errors
365
452
 
366
- module Caller = Caller
453
+ module Caller = Callers({
454
+ type role = Spec.role
455
+ })
367
456
  module Guard = Acting({
368
457
  let name = Spec.name
369
458
  type history = Spec.consumedEvent
370
459
  let historySchema = Spec.consumedEventSchema
371
460
  type command = Spec.command
372
461
  let commandSchema = Spec.commandSchema
462
+ type role = Spec.role
463
+ let commandAuthorization = Spec.commandAuthorization
373
464
  })
374
465
  let asCaller = Guard.asCaller
375
466
 
@@ -637,6 +728,8 @@ module type AggregateSpec = {
637
728
  type event
638
729
  @schema
639
730
  type error
731
+ type role
732
+ let commandAuthorization: command => Reventless.Authorization.rule<role>
640
733
  }
641
734
 
642
735
  module type AggregateT = {
@@ -648,8 +741,8 @@ module type AggregateT = {
648
741
  let givenEvents: array<Spec.event> => array<Spec.event>
649
742
 
650
743
  // Who issues the next `whenCmd`; see `Caller`.
651
- module Caller: module type of Caller
652
- let asCaller: (array<Spec.event>, Message.CallerClaim.t) => array<Spec.event>
744
+ module Caller: Callers with type role = Spec.role
745
+ let asCaller: (array<Spec.event>, Caller.t) => array<Spec.event>
653
746
 
654
747
  let whenCmd: (array<Spec.event>, Spec.command) => array<Spec.event>
655
748
 
@@ -669,7 +762,8 @@ module type AggregateT = {
669
762
  ) => Outcome.outcome
670
763
  let thenEventsWithError: (array<Spec.event>, array<Spec.event>, Spec.error) => Outcome.outcome
671
764
  let thenError: (array<Spec.event>, Spec.error) => Outcome.outcome
672
- // Refused before `decide` because the caller does not own what it acts on.
765
+ // Refused before `decide`: by the command's rule, or because the caller does not
766
+ // own what it acts on.
673
767
  let thenRefused: array<Spec.event> => Outcome.outcome
674
768
  }
675
769
 
@@ -686,13 +780,17 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
686
780
  module Core = AssertionCore(Spec)
687
781
  let errors = Core.errors
688
782
 
689
- module Caller = Caller
783
+ module Caller = Callers({
784
+ type role = Spec.role
785
+ })
690
786
  module Guard = Acting({
691
787
  let name = Spec.name
692
788
  type history = Spec.event
693
789
  let historySchema = Spec.eventSchema
694
790
  type command = Spec.command
695
791
  let commandSchema = Spec.commandSchema
792
+ type role = Spec.role
793
+ let commandAuthorization = Spec.commandAuthorization
696
794
  })
697
795
  let asCaller = Guard.asCaller
698
796
 
@@ -13,26 +13,85 @@ import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
13
13
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
14
14
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
15
15
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
16
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
16
17
  import * as DcbScopeInference$Reventless from "@reventlessdev/reventless-spec/src/components/DcbScopeInference.res.mjs";
17
18
  import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
18
19
 
19
- function owner(id) {
20
+ function roleNames(roles) {
21
+ return roles.map(role => {
22
+ if (OwnerScope$Reventless.isJsString(role)) {
23
+ return role;
24
+ } else {
25
+ return Stdlib_JsError.throwWithMessage("Caller: expected roles of the plugin's Roles.t (payload-less, so strings at runtime)");
26
+ }
27
+ });
28
+ }
29
+
30
+ function owner(id, rolesOpt) {
31
+ let roles = rolesOpt !== undefined ? rolesOpt : [];
20
32
  if (OwnerScope$Reventless.isJsString(id)) {
21
33
  return {
22
- TAG: "Owned",
23
- userId: id
34
+ claim: {
35
+ TAG: "Owned",
36
+ userId: id
37
+ },
38
+ roles: roleNames(roles),
39
+ signedIn: true
24
40
  };
25
41
  } else {
26
42
  return Stdlib_JsError.throwWithMessage("Caller.owner: expected an id (a string at runtime, such as an Id.Make identity)");
27
43
  }
28
44
  }
29
45
 
46
+ function inRoles(roles) {
47
+ return {
48
+ claim: {
49
+ TAG: "Owned",
50
+ userId: "caller-in-roles"
51
+ },
52
+ roles: roleNames(roles),
53
+ signedIn: true
54
+ };
55
+ }
56
+
57
+ let operator_roles = [];
58
+
59
+ let operator = {
60
+ claim: "Exempt",
61
+ roles: operator_roles,
62
+ signedIn: true
63
+ };
64
+
65
+ let anonymous_roles = [];
66
+
67
+ let anonymous = {
68
+ claim: "Unidentified",
69
+ roles: anonymous_roles,
70
+ signedIn: false
71
+ };
72
+
30
73
  let Caller = {
74
+ roleNames: roleNames,
31
75
  owner: owner,
32
- operator: "Exempt",
33
- anonymous: "Unidentified"
76
+ inRoles: inRoles,
77
+ operator: operator,
78
+ anonymous: anonymous
34
79
  };
35
80
 
81
+ function Callers(R) {
82
+ let owner$1 = (id, rolesOpt) => {
83
+ let roles = rolesOpt !== undefined ? rolesOpt : [];
84
+ return owner(id, roles);
85
+ };
86
+ let inRoles$1 = inRoles;
87
+ return {
88
+ owner: owner$1,
89
+ inRoles: inRoles$1,
90
+ operator: operator,
91
+ anonymous: anonymous
92
+ };
93
+ }
94
+
36
95
  function Acting(Spec) {
37
96
  let caller = {
38
97
  contents: undefined
@@ -41,14 +100,30 @@ function Acting(Spec) {
41
100
  contents: undefined
42
101
  };
43
102
  let ownerFieldsByEventType = Owner$Reventless.fieldNamesByVariant(Spec.historySchema);
44
- let asCaller = (history, claim) => {
45
- caller.contents = claim;
103
+ let asCaller = (history, who) => {
104
+ caller.contents = who;
46
105
  return history;
47
106
  };
107
+ let commandName = command => Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, Spec.commandSchema));
108
+ let refusedByRule = (who, command) => {
109
+ if (Authorization$Reventless.admits(Authorization$Reventless.named(Spec.commandAuthorization(command)), who.signedIn, role => who.roles.includes(role))) {
110
+ return false;
111
+ } else {
112
+ refusal.contents = {
113
+ errorCode: "Forbidden",
114
+ errorDetail: Spec.name + `.` + Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, Spec.commandSchema)) + `: the caller holds no role this command's rule admits`
115
+ };
116
+ return true;
117
+ }
118
+ };
48
119
  let refuses = (history, command) => {
49
- let claim = caller.contents;
120
+ let who = caller.contents;
50
121
  caller.contents = undefined;
51
122
  refusal.contents = undefined;
123
+ let claim = Stdlib_Option.map(who, w => w.claim);
124
+ if (Stdlib_Option.mapOr(who, false, __x => refusedByRule(__x, command))) {
125
+ return true;
126
+ }
52
127
  if (Object.keys(ownerFieldsByEventType).length === 0) {
53
128
  return false;
54
129
  }
@@ -101,6 +176,8 @@ function Acting(Spec) {
101
176
  refusal: refusal,
102
177
  ownerFieldsByEventType: ownerFieldsByEventType,
103
178
  asCaller: asCaller,
179
+ commandName: commandName,
180
+ refusedByRule: refusedByRule,
104
181
  refuses: refuses,
105
182
  encRefusal: encRefusal,
106
183
  unexpectedRefusal: unexpectedRefusal,
@@ -285,9 +362,21 @@ function Make(Spec) {
285
362
  });
286
363
  }
287
364
  };
365
+ let owner$1 = (id, rolesOpt) => {
366
+ let roles = rolesOpt !== undefined ? rolesOpt : [];
367
+ return owner(id, roles);
368
+ };
369
+ let inRoles$1 = inRoles;
370
+ let Caller = {
371
+ owner: owner$1,
372
+ inRoles: inRoles$1,
373
+ operator: operator,
374
+ anonymous: anonymous
375
+ };
288
376
  let name = Spec.name;
289
377
  let historySchema = Spec.consumedEventSchema;
290
378
  let commandSchema = Spec.commandSchema;
379
+ let commandAuthorization = Spec.commandAuthorization;
291
380
  let caller = {
292
381
  contents: undefined
293
382
  };
@@ -295,14 +384,28 @@ function Make(Spec) {
295
384
  contents: undefined
296
385
  };
297
386
  let ownerFieldsByEventType = Owner$Reventless.fieldNamesByVariant(historySchema);
298
- let asCaller = (history, claim) => {
299
- caller.contents = claim;
387
+ let asCaller = (history, who) => {
388
+ caller.contents = who;
300
389
  return history;
301
390
  };
302
391
  let refuses = (history, command) => {
303
- let claim = caller.contents;
392
+ let who = caller.contents;
304
393
  caller.contents = undefined;
305
394
  refusal.contents = undefined;
395
+ let claim = Stdlib_Option.map(who, w => w.claim);
396
+ if (Stdlib_Option.mapOr(who, false, __x => {
397
+ if (Authorization$Reventless.admits(Authorization$Reventless.named(commandAuthorization(command)), __x.signedIn, role => __x.roles.includes(role))) {
398
+ return false;
399
+ } else {
400
+ refusal.contents = {
401
+ errorCode: "Forbidden",
402
+ errorDetail: name + `.` + Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema)) + `: the caller holds no role this command's rule admits`
403
+ };
404
+ return true;
405
+ }
406
+ })) {
407
+ return true;
408
+ }
306
409
  if (Object.keys(ownerFieldsByEventType).length === 0) {
307
410
  return false;
308
411
  }
@@ -630,9 +733,21 @@ function MakeFromAggregate(Spec) {
630
733
  });
631
734
  }
632
735
  };
736
+ let owner$1 = (id, rolesOpt) => {
737
+ let roles = rolesOpt !== undefined ? rolesOpt : [];
738
+ return owner(id, roles);
739
+ };
740
+ let inRoles$1 = inRoles;
741
+ let Caller = {
742
+ owner: owner$1,
743
+ inRoles: inRoles$1,
744
+ operator: operator,
745
+ anonymous: anonymous
746
+ };
633
747
  let name = Spec.name;
634
748
  let historySchema = Spec.eventSchema;
635
749
  let commandSchema = Spec.commandSchema;
750
+ let commandAuthorization = Spec.commandAuthorization;
636
751
  let caller = {
637
752
  contents: undefined
638
753
  };
@@ -640,14 +755,28 @@ function MakeFromAggregate(Spec) {
640
755
  contents: undefined
641
756
  };
642
757
  let ownerFieldsByEventType = Owner$Reventless.fieldNamesByVariant(historySchema);
643
- let asCaller = (history, claim) => {
644
- caller.contents = claim;
758
+ let asCaller = (history, who) => {
759
+ caller.contents = who;
645
760
  return history;
646
761
  };
647
762
  let refuses = (history, command) => {
648
- let claim = caller.contents;
763
+ let who = caller.contents;
649
764
  caller.contents = undefined;
650
765
  refusal.contents = undefined;
766
+ let claim = Stdlib_Option.map(who, w => w.claim);
767
+ if (Stdlib_Option.mapOr(who, false, __x => {
768
+ if (Authorization$Reventless.admits(Authorization$Reventless.named(commandAuthorization(command)), __x.signedIn, role => __x.roles.includes(role))) {
769
+ return false;
770
+ } else {
771
+ refusal.contents = {
772
+ errorCode: "Forbidden",
773
+ errorDetail: name + `.` + Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema)) + `: the caller holds no role this command's rule admits`
774
+ };
775
+ return true;
776
+ }
777
+ })) {
778
+ return true;
779
+ }
651
780
  if (Object.keys(ownerFieldsByEventType).length === 0) {
652
781
  return false;
653
782
  }
@@ -772,6 +901,7 @@ function MakeFromAggregate(Spec) {
772
901
 
773
902
  export {
774
903
  Caller,
904
+ Callers,
775
905
  Acting,
776
906
  encodeTag,
777
907
  encodeQueryItem,
@@ -21,3 +21,8 @@ type error = CategoryAlreadyExists
21
21
 
22
22
  @schema
23
23
  type event = CategoryAdded({categoryId: @s.matches(Reventless.DcbTag.string) string, name: string})
24
+
25
+ // What `@@reventless.spec` injects into a real spec: the default rule.
26
+ type role = Reventless.Role.name
27
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
28
+ Reventless.Authorization.AllowAuthenticated
@@ -22,6 +22,10 @@ let eventSchema = Sury.$schema(s => ({
22
22
  name: s.m(Sury.string)
23
23
  }));
24
24
 
25
+ function commandAuthorization(param) {
26
+ return "AllowAuthenticated";
27
+ }
28
+
25
29
  let name = "ExternalAddCategory";
26
30
 
27
31
  export {
@@ -30,5 +34,6 @@ export {
30
34
  commandSchema,
31
35
  errorSchema,
32
36
  eventSchema,
37
+ commandAuthorization,
33
38
  }
34
39
  /* consumedEventSchema Not a pure module */
@@ -17,3 +17,8 @@ type error = CategoryAlreadyExists
17
17
 
18
18
  @schema
19
19
  type event = CategoryAdded({categoryId: @s.matches(Reventless.DcbTag.string) string, name: string})
20
+
21
+ // What `@@reventless.spec` injects into a real spec: the default rule.
22
+ type role = Reventless.Role.name
23
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
24
+ Reventless.Authorization.AllowAuthenticated
@@ -19,6 +19,10 @@ let eventSchema = Sury.$schema(s => ({
19
19
  name: s.m(Sury.string)
20
20
  }));
21
21
 
22
+ function commandAuthorization(param) {
23
+ return "AllowAuthenticated";
24
+ }
25
+
22
26
  let name = "WithFixtures";
23
27
 
24
28
  export {
@@ -27,5 +31,6 @@ export {
27
31
  commandSchema,
28
32
  errorSchema,
29
33
  eventSchema,
34
+ commandAuthorization,
30
35
  }
31
36
  /* consumedEventSchema Not a pure module */
@@ -18,3 +18,8 @@ type error = CategoryAlreadyExists
18
18
 
19
19
  @schema
20
20
  type event = CategoryAdded({categoryId: @s.matches(Reventless.DcbTag.string) string, name: string})
21
+
22
+ // What `@@reventless.spec` injects into a real spec: the default rule.
23
+ type role = Reventless.Role.name
24
+ let commandAuthorization = (_: command): Reventless.Authorization.rule<role> =>
25
+ Reventless.Authorization.AllowAuthenticated
@@ -19,6 +19,10 @@ let eventSchema = Sury.$schema(s => ({
19
19
  name: s.m(Sury.string)
20
20
  }));
21
21
 
22
+ function commandAuthorization(param) {
23
+ return "AllowAuthenticated";
24
+ }
25
+
22
26
  let name = "WithManualOpen";
23
27
 
24
28
  export {
@@ -27,5 +31,6 @@ export {
27
31
  commandSchema,
28
32
  errorSchema,
29
33
  eventSchema,
34
+ commandAuthorization,
30
35
  }
31
36
  /* consumedEventSchema Not a pure module */