@reventlessdev/reventless-core 3.0.0-alpha.296 → 3.0.0-alpha.298

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,31 @@
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.298 (2026-10-16)
7
+
8
+ * feat!(inbound): a translation slice can declare a signed webhook URL ([0ad6994](https://github.com/ReventlessDev/reventless-core/commit/0ad69949fcc91c2756c60ca8cb120dd468841d8b))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * InboundTranslationSlice.Spec requires `endpoint`. Specs under
13
+ @@reventless.spec get it from the PPX (republish reventless-ppx with this
14
+ release); hand-written specs add `let endpoint = None`.
15
+
16
+
17
+
18
+ # 3.0.0-alpha.297 (2026-10-16)
19
+
20
+ * fix(auth)!: only operators read the framework's own stores ([a2fd202](https://github.com/ReventlessDev/reventless-core/commit/a2fd2022b6876dae855b185df7f8263353fc971d))
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ * a caller outside the elevated groups is refused the
25
+ framework's to-do, schedule and audit stores. On AWS, a roles-gated
26
+ view's …ByIds, …Refs, …Items and …By<Index> fields refuse callers
27
+ outside its roles.
28
+
29
+
30
+
6
31
  # 3.0.0-alpha.296 (2026-10-16)
7
32
 
8
33
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.296",
3
+ "version": "3.0.0-alpha.298",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,21 +29,21 @@
29
29
  "sury": "11.0.0",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
32
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.22",
33
- "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
34
- "@reventlessdev/rescript-node": "2.0.0-alpha.22",
35
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
36
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
33
+ "@reventlessdev/rescript-node": "2.0.0-alpha.22",
34
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
35
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.22",
36
+ "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.23",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
39
- "@reventlessdev/reventless-interop": "3.0.0-alpha.40",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.165",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.193"
39
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.41",
40
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.195",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.167"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.1",
45
45
  "sury-ppx": "11.0.0",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.101"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.102"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "^12.3.1"
@@ -777,6 +777,21 @@ let mutationArgTypes = (~fieldName: string, variantSchema: S.t<unknown>): option
777
777
  out
778
778
  })
779
779
 
780
+ /** Every query field `generate` emits for an entry, under the same conditions.
781
+ A platform gating an entry gates each of them: a door left out is open. */
782
+ let queryFieldNamesOf = (entry: querySchemaEntry): array<string> => {
783
+ let byIds = entry.includeIdParam->Option.getOr(true) && entry.subIdField === None
784
+ let indexFields =
785
+ entry.indexQueries
786
+ ->Option.getOr([])
787
+ ->Array.filter(ic => !Reventless.ReadModel.isDerivedIndex(ic))
788
+ ->Array.map(ic => indexQueryFieldName(~singleFieldName=entry.singleFieldName, ~index=ic.index))
789
+ [entry.singleFieldName, entry.listFieldName]
790
+ ->Array.concat(byIds ? [entry.listFieldName ++ "ByIds", entry.listFieldName ++ "Refs"] : [])
791
+ ->Array.concat(entry.subIdField->Option.isSome ? [entry.singleFieldName ++ "Items"] : [])
792
+ ->Array.concat(indexFields)
793
+ }
794
+
780
795
  // ── Main generate function ─────────────────────────────────────────────────
781
796
 
782
797
  let generate = (
@@ -529,6 +529,18 @@ function mutationArgTypes(fieldName, variantSchema) {
529
529
  });
530
530
  }
531
531
 
532
+ function queryFieldNamesOf(entry) {
533
+ let byIds = Stdlib_Option.getOr(entry.includeIdParam, true) && entry.subIdField === undefined;
534
+ let indexFields = Stdlib_Option.getOr(entry.indexQueries, []).filter(ic => !ReadModel$Reventless.isDerivedIndex(ic)).map(ic => indexQueryFieldName(entry.singleFieldName, ic.index));
535
+ return [
536
+ entry.singleFieldName,
537
+ entry.listFieldName
538
+ ].concat(byIds ? [
539
+ entry.listFieldName + "ByIds",
540
+ entry.listFieldName + "Refs"
541
+ ] : []).concat(Stdlib_Option.isSome(entry.subIdField) ? [entry.singleFieldName + "Items"] : []).concat(indexFields);
542
+ }
543
+
532
544
  function generate(mutationEntries, queryEntries) {
533
545
  let types = [];
534
546
  let mutations = [];
@@ -729,6 +741,7 @@ export {
729
741
  deriveIndexQueryField,
730
742
  deriveMutationFieldFromObject,
731
743
  mutationArgTypes,
744
+ queryFieldNamesOf,
732
745
  generate,
733
746
  }
734
747
  /* log Not a pure module */
@@ -63,7 +63,7 @@ module Make = (
63
63
  let config = Reventless.ReadModel.config()
64
64
  let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
65
65
  type role = Reventless.Role.name
66
- let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
66
+ let authorization: Reventless.Authorization.rule<role> = Reventless.OwnerScope.operators()
67
67
  let visibility: Reventless.Visibility.t = Public
68
68
  }
69
69
 
@@ -81,7 +81,7 @@ module Make = (
81
81
  let config = Reventless.ReadModel.config()
82
82
  let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
83
83
  type role = Reventless.Role.name
84
- let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
84
+ let authorization: Reventless.Authorization.rule<role> = Reventless.OwnerScope.operators()
85
85
  let visibility: Reventless.Visibility.t = Public
86
86
  }
87
87
  module SchedulesQueryDb = QueryDb_Builder.Make(
@@ -15,6 +15,7 @@ import * as Clock$Reventless from "@reventlessdev/reventless-spec/src/components
15
15
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
16
16
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
17
17
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
18
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
18
19
  import * as Component$ReventlessCore from "../Component.res.mjs";
19
20
  import * as EventTopic$ReventlessCore from "../EventTopic/EventTopic.res.mjs";
20
21
  import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
@@ -36,6 +37,7 @@ function Make(RuntimeEnvironment) {
36
37
  let readsClock = mappedSources.includes(Clock$Reventless.name);
37
38
  let moduleUrl = import.meta.url;
38
39
  let config = ReadModel$Reventless.config(undefined, undefined, undefined);
40
+ let authorization = OwnerScope$Reventless.operators();
39
41
  let SpecificQueryDb = QueryDb_Builder$ReventlessCore.Make({
40
42
  Id: Id$Reventless.$$String,
41
43
  name: queryDbName,
@@ -43,13 +45,14 @@ function Make(RuntimeEnvironment) {
43
45
  stateSchema: AutomationSlice_Callback$ReventlessCore.todoRowSchema,
44
46
  config: config,
45
47
  subIdConfig: undefined,
46
- authorization: "AllowAuthenticated",
48
+ authorization: authorization,
47
49
  visibility: "Public"
48
50
  })(QueryDbStorage)(QueryDbResolvers);
49
51
  let schedulesStoreName = Spec.name + "Schedule";
50
52
  let schedulesDbName = readsClock ? schedulesStoreName : undefined;
51
53
  let moduleUrl$1 = import.meta.url;
52
54
  let config$1 = ReadModel$Reventless.config(undefined, undefined, undefined);
55
+ let authorization$1 = OwnerScope$Reventless.operators();
53
56
  let SchedulesQueryDb = QueryDb_Builder$ReventlessCore.Make({
54
57
  Id: Id$Reventless.$$String,
55
58
  name: schedulesStoreName,
@@ -57,7 +60,7 @@ function Make(RuntimeEnvironment) {
57
60
  stateSchema: AutomationSlice_Schedules$ReventlessCore.rowSchema,
58
61
  config: config$1,
59
62
  subIdConfig: undefined,
60
- authorization: "AllowAuthenticated",
63
+ authorization: authorization$1,
61
64
  visibility: "Public"
62
65
  })(QueryDbStorage)(QueryDbResolvers);
63
66
  let syncSchedules = async (ops, stored) => {
@@ -759,38 +759,51 @@ module Make = (
759
759
  ~opts,
760
760
  )
761
761
  let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
762
+ // `@noApi` on `externalInput` withholds the mutation; the webhook is
763
+ // then the only door, and a slice with neither could never be reached.
764
+ let exposed = InboundTranslationSlice_Callback.exposesMutation(
765
+ ITS.Spec.externalInputSchema->S.castToUnknown,
766
+ )
767
+ InboundTranslationSlice_Callback.checkReachable(
768
+ ~plugin=name,
769
+ ~slice=ITS.Spec.name,
770
+ ~mutation=exposed,
771
+ ~webhook=ITS.Spec.endpoint->Option.isSome,
772
+ )
762
773
 
763
774
  switch HooksConfig.hooks.inboundMutationResolverHook {
764
- | Some(registerResolver) =>
775
+ | Some(registerResolver) if exposed =>
765
776
  registerResolver(
766
777
  ~fieldName,
767
778
  ~externalInputSchema=ITS.Spec.externalInputSchema->S.castToUnknown,
768
779
  ~permission=?inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf),
769
780
  )
770
- | None => ()
781
+ | _ => ()
771
782
  }
772
783
 
773
784
  switch HooksConfig.hooks.inboundMutationBindReceiveHook {
774
- | Some(bindReceive) =>
785
+ | Some(bindReceive) if exposed =>
775
786
  let _ =
776
787
  its
777
788
  ->Component.operations
778
- ->Pulumi.Output.apply(ops => bindReceive(~fieldName, ~receive=ops.receive))
779
- | None => ()
789
+ ->Pulumi.Output.apply(ops =>
790
+ bindReceive(~fieldName, ~receive=(input, ~caller=?) => ops.receive(input, ~caller?))
791
+ )
792
+ | _ => ()
780
793
  }
781
794
 
782
- (ITS.Spec.name, fieldName, its, ITS.Spec.externalInputSchema->S.castToUnknown)
795
+ (ITS.Spec.name, fieldName, its, ITS.Spec.externalInputSchema->S.castToUnknown, exposed)
783
796
  })
784
797
 
785
798
  let inboundTranslationSlicesOutputs =
786
799
  inboundTranslationSliceData
787
- ->Array.map(((specName, _, its, _)) => (specName, its->Component.outputs))
800
+ ->Array.map(((specName, _, its, _, _)) => (specName, its->Component.outputs))
788
801
  ->Dict.fromArray
789
802
 
790
803
  // Collect InboundTranslationSlice receive functions for composite handler routing
791
804
  let inboundReceiversOutput =
792
805
  inboundTranslationSliceData
793
- ->Array.map(((_, fieldName, its, _)) =>
806
+ ->Array.map(((_, fieldName, its, _, _)) =>
794
807
  its
795
808
  ->Component.operations
796
809
  ->Pulumi.Output.apply(ops => (fieldName, ops.receive))
@@ -942,9 +955,10 @@ module Make = (
942
955
  ->Dict.valuesToArray
943
956
  ->Array.flatMap(outputs => outputs.resources)
944
957
 
945
- let inboundFieldNames =
946
- inboundTranslationSliceData->Array.map(((_, fieldName, _, _)) => fieldName)
947
- let inboundSchemas = inboundTranslationSliceData->Array.map(((_, _, _, schema)) => schema)
958
+ let exposedInbound =
959
+ inboundTranslationSliceData->Array.filter(((_, _, _, _, exposed)) => exposed)
960
+ let inboundFieldNames = exposedInbound->Array.map(((_, fieldName, _, _, _)) => fieldName)
961
+ let inboundSchemas = exposedInbound->Array.map(((_, _, _, schema, _)) => schema)
948
962
 
949
963
  // Collect DCB mutation field names + TAGs for AppSync resolver creation
950
964
  // (excludes @noApi slices — includes both sync and async)
@@ -1092,7 +1106,13 @@ module Make = (
1092
1106
  })
1093
1107
 
1094
1108
  let mutationEntriesFromInboundSlices =
1095
- inboundTranslationSlices->Array.map((module(ITS: InboundTranslationSlice.T)) => {
1109
+ inboundTranslationSlices
1110
+ ->Array.filter((module(ITS: InboundTranslationSlice.T)) =>
1111
+ InboundTranslationSlice_Callback.exposesMutation(
1112
+ ITS.Spec.externalInputSchema->S.castToUnknown,
1113
+ )
1114
+ )
1115
+ ->Array.map((module(ITS: InboundTranslationSlice.T)) => {
1096
1116
  let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
1097
1117
  let fieldPermissions = Dict.make()
1098
1118
  inboundDoorPermission(
@@ -1138,6 +1158,9 @@ module Make = (
1138
1158
  }
1139
1159
  })
1140
1160
 
1161
+ // What the framework keeps for a slice is read by the deployment's operators.
1162
+ let operators = Reventless.OwnerScope.operators()
1163
+
1141
1164
  let automationEntries = automationSlices->Array.map((module(A: AutomationSlice.T)) => {
1142
1165
  let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName=A.queryDbName)
1143
1166
  {
@@ -1148,6 +1171,7 @@ module Make = (
1148
1171
  A.Spec.todoItemSchema,
1149
1172
  )->S.castToUnknown,
1150
1173
  authorization: None,
1174
+ permission: operators,
1151
1175
  connectionSpec: true,
1152
1176
  }
1153
1177
  })
@@ -1162,6 +1186,7 @@ module Make = (
1162
1186
  returnTypeName: qn.returnTypeName,
1163
1187
  stateSchema: AutomationSlice_Schedules.rowSchema->S.castToUnknown,
1164
1188
  authorization: None,
1189
+ permission: operators,
1165
1190
  connectionSpec: true,
1166
1191
  }
1167
1192
  })
@@ -1181,6 +1206,7 @@ module Make = (
1181
1206
  O.Spec.outboundItemSchema,
1182
1207
  )->S.castToUnknown,
1183
1208
  authorization: None,
1209
+ permission: operators,
1184
1210
  connectionSpec: true,
1185
1211
  }
1186
1212
  })
@@ -1197,6 +1223,7 @@ module Make = (
1197
1223
  returnTypeName: qn.returnTypeName,
1198
1224
  stateSchema: InboundTranslationSlice_Callback.auditRowSchema->S.castToUnknown,
1199
1225
  authorization: None,
1226
+ permission: operators,
1200
1227
  connectionSpec: true,
1201
1228
  }
1202
1229
  })
@@ -13,6 +13,7 @@ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js
13
13
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
14
14
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
15
15
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
16
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
16
17
  import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
17
18
  import * as Component$ReventlessCore from "../Component.res.mjs";
18
19
  import * as DcbValidation$Reventless from "@reventlessdev/reventless-spec/src/components/DcbValidation.res.mjs";
@@ -406,19 +407,22 @@ function Make(DcbEventLogStorage) {
406
407
  let inboundTranslationSliceData = inboundTranslationSlices.map(ITS => {
407
408
  let its = ITS.make(publishJsons, componentRuntime[ITS.Spec.name], opts);
408
409
  let fieldName = Api_Naming$ReventlessCore.sliceMutationField(name, ITS.Spec.name);
410
+ let exposed = InboundTranslationSlice_Callback$ReventlessCore.exposesMutation(ITS.Spec.externalInputSchema);
411
+ InboundTranslationSlice_Callback$ReventlessCore.checkReachable(name, ITS.Spec.name, exposed, Stdlib_Option.isSome(ITS.Spec.endpoint));
409
412
  let registerResolver = HooksConfig.hooks.inboundMutationResolverHook;
410
- if (registerResolver !== undefined) {
413
+ if (registerResolver !== undefined && exposed) {
411
414
  registerResolver(fieldName, ITS.Spec.externalInputSchema, inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf));
412
415
  }
413
416
  let bindReceive = HooksConfig.hooks.inboundMutationBindReceiveHook;
414
- if (bindReceive !== undefined) {
415
- Component$ReventlessCore.operations(its).apply(ops => bindReceive(fieldName, ops.receive));
417
+ if (bindReceive !== undefined && exposed) {
418
+ Component$ReventlessCore.operations(its).apply(ops => bindReceive(fieldName, (input, caller) => ops.receive(input, caller, undefined)));
416
419
  }
417
420
  return [
418
421
  ITS.Spec.name,
419
422
  fieldName,
420
423
  its,
421
- ITS.Spec.externalInputSchema
424
+ ITS.Spec.externalInputSchema,
425
+ exposed
422
426
  ];
423
427
  });
424
428
  let inboundTranslationSlicesOutputs = Object.fromEntries(inboundTranslationSliceData.map(param => [
@@ -481,7 +485,7 @@ function Make(DcbEventLogStorage) {
481
485
  let receiveFn = receivers[event.fieldName];
482
486
  if (receiveFn !== undefined) {
483
487
  return Effect.promise(async () => {
484
- let result = await receiveFn(event.arguments, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(event.identity));
488
+ let result = await receiveFn(event.arguments, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(event.identity), undefined);
485
489
  return CommandTopic$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result));
486
490
  });
487
491
  } else {
@@ -506,8 +510,9 @@ function Make(DcbEventLogStorage) {
506
510
  });
507
511
  let dcbResources = Object.values(stateChangeSlicesOutputs).flatMap(outputs => outputs.resources).concat(Object.values(inboundTranslationSlicesOutputs).flatMap(outputs => outputs.resources.concat(outputs.queryDb.resources)));
508
512
  let asyncDcbResources = Object.values(asyncStateChangeSlicesOutputs).flatMap(outputs => outputs.resources);
509
- let inboundFieldNames = inboundTranslationSliceData.map(param => param[1]);
510
- let inboundSchemas = inboundTranslationSliceData.map(param => param[3]);
513
+ let exposedInbound = inboundTranslationSliceData.filter(param => param[4]);
514
+ let inboundFieldNames = exposedInbound.map(param => param[1]);
515
+ let inboundSchemas = exposedInbound.map(param => param[3]);
511
516
  let dcbMutationData = stateChangeSlices.flatMap(S => {
512
517
  let commandSchema = S.Spec.commandSchema;
513
518
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
@@ -577,7 +582,7 @@ function Make(DcbEventLogStorage) {
577
582
  consistencyRead: Stdlib_Option.flatMap(sliceDef, d => d.consistencyRead)
578
583
  };
579
584
  });
580
- let mutationEntriesFromInboundSlices = inboundTranslationSlices.map(ITS => {
585
+ let mutationEntriesFromInboundSlices = inboundTranslationSlices.filter(ITS => InboundTranslationSlice_Callback$ReventlessCore.exposesMutation(ITS.Spec.externalInputSchema)).map(ITS => {
581
586
  let fieldName = Api_Naming$ReventlessCore.sliceMutationField(name, ITS.Spec.name);
582
587
  let fieldPermissions = {};
583
588
  Stdlib_Option.forEach(inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf), rule => {
@@ -612,6 +617,7 @@ function Make(DcbEventLogStorage) {
612
617
  resolvedFields: resolvedFields
613
618
  };
614
619
  });
620
+ let operators = OwnerScope$Reventless.operators();
615
621
  let automationEntries = automationSlices.map(A => {
616
622
  let qn = Api_Naming$ReventlessCore.queryFieldNamesForSliceQueryDb(name, A.queryDbName, undefined);
617
623
  return {
@@ -620,6 +626,7 @@ function Make(DcbEventLogStorage) {
620
626
  returnTypeName: qn.returnTypeName,
621
627
  stateSchema: AutomationSlice_Callback$ReventlessCore.todoRowSchemaFor(A.Spec.todoItemSchema),
622
628
  authorization: undefined,
629
+ permission: operators,
623
630
  connectionSpec: true
624
631
  };
625
632
  });
@@ -631,6 +638,7 @@ function Make(DcbEventLogStorage) {
631
638
  returnTypeName: qn.returnTypeName,
632
639
  stateSchema: AutomationSlice_Schedules$ReventlessCore.rowSchema,
633
640
  authorization: undefined,
641
+ permission: operators,
634
642
  connectionSpec: true
635
643
  };
636
644
  }));
@@ -642,6 +650,7 @@ function Make(DcbEventLogStorage) {
642
650
  returnTypeName: qn.returnTypeName,
643
651
  stateSchema: OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchemaFor(O.Spec.outboundItemSchema),
644
652
  authorization: undefined,
653
+ permission: operators,
645
654
  connectionSpec: true
646
655
  };
647
656
  });
@@ -653,6 +662,7 @@ function Make(DcbEventLogStorage) {
653
662
  returnTypeName: qn.returnTypeName,
654
663
  stateSchema: InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema,
655
664
  authorization: undefined,
665
+ permission: operators,
656
666
  connectionSpec: true
657
667
  };
658
668
  });
@@ -36,7 +36,7 @@ module Make = (
36
36
  let config = Reventless.ReadModel.config()
37
37
  let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
38
38
  type role = Reventless.Role.name
39
- let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
39
+ let authorization: Reventless.Authorization.rule<role> = Reventless.OwnerScope.operators()
40
40
  let visibility: Reventless.Visibility.t = Public
41
41
  }
42
42
 
@@ -81,8 +81,13 @@ module Make = (
81
81
  ->Pulumi.Output.all2
82
82
  ->Pulumi.Output.apply(((queryDbOps, publishJsonsFn)) => {
83
83
  let ops: InboundTranslationSlice.operations = {
84
- receive: async (inputJson, ~caller=?) => {
85
- let result = await Callback.receive(publishJsonsFn, inputJson, ~caller?)
84
+ receive: async (inputJson, ~caller=?, ~door=?) => {
85
+ let result = await Callback.receive(publishJsonsFn, inputJson, ~caller?, ~door?)
86
+ await syncToQueryDb(queryDbOps, result->InboundTranslationSlice_Callback.requestIdOf)
87
+ result
88
+ },
89
+ refuse: async (~body, ~errorCode, error) => {
90
+ let result = Callback.refuse(~body, ~errorCode, error)
86
91
  await syncToQueryDb(queryDbOps, result->InboundTranslationSlice_Callback.requestIdOf)
87
92
  result
88
93
  },
@@ -3,6 +3,7 @@
3
3
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
4
4
  import * as Pulumi from "@pulumi/pulumi";
5
5
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
6
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
6
7
  import * as Component$ReventlessCore from "../Component.res.mjs";
7
8
  import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
8
9
  import * as QueryDb_Builder$ReventlessCore from "../QueryDb/QueryDb_Builder.res.mjs";
@@ -16,6 +17,7 @@ function Make(QueryDbStorage) {
16
17
  let queryDbName = Spec.name + "Audit";
17
18
  let moduleUrl = import.meta.url;
18
19
  let config = ReadModel$Reventless.config(undefined, undefined, undefined);
20
+ let authorization = OwnerScope$Reventless.operators();
19
21
  let SpecificQueryDb = QueryDb_Builder$ReventlessCore.Make({
20
22
  Id: Id$Reventless.$$String,
21
23
  name: queryDbName,
@@ -23,7 +25,7 @@ function Make(QueryDbStorage) {
23
25
  stateSchema: InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema,
24
26
  config: config,
25
27
  subIdConfig: undefined,
26
- authorization: "AllowAuthenticated",
28
+ authorization: authorization,
27
29
  visibility: "Public"
28
30
  })(QueryDbStorage)(QueryDbResolvers);
29
31
  let syncToQueryDb = async (queryDbOps, requestId) => {
@@ -55,8 +57,13 @@ function Make(QueryDbStorage) {
55
57
  let publishJsonsFn = param[1];
56
58
  let queryDbOps = param[0];
57
59
  return {
58
- receive: async (inputJson, caller) => {
59
- let result = await Callback.receive(publishJsonsFn, inputJson, caller);
60
+ receive: async (inputJson, caller, door) => {
61
+ let result = await Callback.receive(publishJsonsFn, inputJson, caller, door);
62
+ await syncToQueryDb(queryDbOps, InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result));
63
+ return result;
64
+ },
65
+ refuse: async (body, errorCode, error) => {
66
+ let result = Callback.refuse(body, errorCode, error);
60
67
  await syncToQueryDb(queryDbOps, InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result));
61
68
  return result;
62
69
  }
@@ -15,6 +15,8 @@ type auditRow = {
15
15
  commandCount?: int,
16
16
  error?: string,
17
17
  receivedAt: string,
18
+ /** Absent on rows written before the slice had a second door. */
19
+ door?: ReventlessInfra.InboundTranslationSlice.door,
18
20
  }
19
21
 
20
22
  /** The `CommandResult` a `receive` outcome answers with: the first target as
@@ -76,6 +78,19 @@ type doorInvocation = {
76
78
  let doorCaller = (identity: Nullable.t<Reventless.Identity.t>): Reventless.Identity.t =>
77
79
  identity->Nullable.toOption->Option.getOr(Reventless.Identity.anonymous)
78
80
 
81
+ /** Whether a slice's input is offered as a mutation: not when `externalInput`
82
+ carries `@noApi`, which leaves the webhook as its only door. */
83
+ let exposesMutation = (externalInputSchema: S.t<unknown>): bool =>
84
+ !ApiNoApiHelpers.isNoApi(externalInputSchema)
85
+
86
+ /** Refuse a slice with neither door, which nothing could ever reach. */
87
+ let checkReachable = (~plugin: string, ~slice: string, ~mutation: bool, ~webhook: bool) =>
88
+ if !mutation && !webhook {
89
+ JsError.throwWithMessage(
90
+ `${plugin}: inbound translation slice ${slice} has @noApi on externalInput and declares no endpoint, so nothing can reach it`,
91
+ )
92
+ }
93
+
79
94
  module type T = {
80
95
  module Spec: Reventless.InboundTranslationSlice.Spec
81
96
  module Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec
@@ -88,7 +103,18 @@ module type T = {
88
103
  ReventlessInfra.CommandTopic.publishJsons,
89
104
  JSON.t,
90
105
  ~caller: Reventless.Identity.t=?,
106
+ ~door: ReventlessInfra.InboundTranslationSlice.door=?,
91
107
  ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>
108
+
109
+ /** Record a webhook request refused before it could be read, and reject it. */
110
+ let refuse: (
111
+ ~body: string,
112
+ ~errorCode: string,
113
+ string,
114
+ ) => ReventlessInfra.InboundTranslationSlice.receiveResult
115
+
116
+ /** The slice's webhook route, if its spec declares an endpoint. */
117
+ let webhook: option<InboundTranslationSlice_Webhook.route>
92
118
  }
93
119
 
94
120
  module Make = (
@@ -110,15 +136,12 @@ module Make = (
110
136
  exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr(fallback)
111
137
 
112
138
  // One audit row per request, whatever the outcome.
113
- let failWith = (~requestId, ~inputJson, ~errorCode=?, error) => {
114
- auditLog->Dict.set(
115
- requestId,
116
- {input: inputJson->JSON.stringify, status: Failure, error, receivedAt: now()},
117
- )
139
+ let failWith = (~requestId, ~input, ~door, ~errorCode=?, error) => {
140
+ auditLog->Dict.set(requestId, {input, status: Failure, error, receivedAt: now(), door})
118
141
  Error({ReventlessInfra.InboundTranslationSlice.requestId, error, ?errorCode})
119
142
  }
120
143
 
121
- let succeed = (~requestId, ~inputJson, targetIds) => {
144
+ let succeed = (~requestId, ~inputJson, ~door, targetIds) => {
122
145
  let commandCount = targetIds->Array.length
123
146
  auditLog->Dict.set(
124
147
  requestId,
@@ -128,6 +151,7 @@ module Make = (
128
151
  targetIds,
129
152
  commandCount,
130
153
  receivedAt: now(),
154
+ door,
131
155
  },
132
156
  )
133
157
  Ok({ReventlessInfra.InboundTranslationSlice.requestId, targetIds, commandCount})
@@ -155,9 +179,12 @@ module Make = (
155
179
  publishJsons: ReventlessInfra.CommandTopic.publishJsons,
156
180
  inputJson: JSON.t,
157
181
  ~caller: option<Reventless.Identity.t>=?,
182
+ ~door: ReventlessInfra.InboundTranslationSlice.door=Mutation,
158
183
  ): ReventlessInfra.InboundTranslationSlice.receiveResult => {
159
184
  let requestId = Uuid.v4()
160
- let fail = (~errorCode=?, error) => failWith(~requestId, ~inputJson, ~errorCode?, error)
185
+ let fail = (~errorCode=?, error) =>
186
+ failWith(~requestId, ~input=inputJson->JSON.stringify, ~door, ~errorCode?, error)
187
+ let succeed = succeed(~requestId, ~inputJson, ~door, ...)
161
188
  let parsed = try Ok(inputJson->S.parseOrThrow(~to=Spec.externalInputSchema)) catch {
162
189
  | exn => Error(messageOf(exn, "invalid input"))
163
190
  }
@@ -192,16 +219,24 @@ module Make = (
192
219
  ~errorCode="Forbidden",
193
220
  `${Spec.name}: the caller is not authorized for every command this input translates into`,
194
221
  )
195
- | Ok([]) => succeed(~requestId, ~inputJson, [])
222
+ | Ok([]) => succeed([])
196
223
  | Ok(msgs) =>
197
224
  try {
198
225
  await publishJsons(msgs)
199
- succeed(~requestId, ~inputJson, pairs->Array.map(((targetId, _)) => targetId))
226
+ succeed(pairs->Array.map(((targetId, _)) => targetId))
200
227
  } catch {
201
- | exn => fail(messageOf(exn, "publish failed"))
228
+ | exn => fail(~errorCode="PublishFailed", messageOf(exn, "publish failed"))
202
229
  }
203
230
  }
204
231
  }
205
232
  }
206
233
  }
234
+
235
+ let refuse = (~body, ~errorCode, error) =>
236
+ failWith(~requestId=Uuid.v4(), ~input=body, ~door=Webhook, ~errorCode, error)
237
+
238
+ let webhook =
239
+ Spec.endpoint->Option.map(endpoint =>
240
+ InboundTranslationSlice_Webhook.routeOf(~slice=Spec.name, endpoint)
241
+ )
207
242
  }