@reventlessdev/reventless-core 3.0.0-alpha.297 → 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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
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.299 (2026-10-17)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-core
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.298 (2026-10-16)
15
+
16
+ * feat!(inbound): a translation slice can declare a signed webhook URL ([0ad6994](https://github.com/ReventlessDev/reventless-core/commit/0ad69949fcc91c2756c60ca8cb120dd468841d8b))
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ * InboundTranslationSlice.Spec requires `endpoint`. Specs under
21
+ @@reventless.spec get it from the PPX (republish reventless-ppx with this
22
+ release); hand-written specs add `let endpoint = None`.
23
+
24
+
25
+
6
26
  # 3.0.0-alpha.297 (2026-10-16)
7
27
 
8
28
  * fix(auth)!: only operators read the framework's own stores ([a2fd202](https://github.com/ReventlessDev/reventless-core/commit/a2fd2022b6876dae855b185df7f8263353fc971d))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.297",
3
+ "version": "3.0.0-alpha.299",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -28,22 +28,22 @@
28
28
  "dependencies": {
29
29
  "sury": "11.0.0",
30
30
  "uuid": "^13.0.0",
31
- "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
32
31
  "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
33
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
33
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.12",
34
34
  "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.22",
35
35
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
36
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
37
36
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.23",
38
37
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
39
- "@reventlessdev/reventless-interop": "3.0.0-alpha.40",
40
- "@reventlessdev/reventless-infra": "3.0.0-alpha.194",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.166"
38
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.196",
39
+ "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.168",
41
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.41"
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.102"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.103"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "^12.3.1"
@@ -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(
@@ -407,19 +407,22 @@ function Make(DcbEventLogStorage) {
407
407
  let inboundTranslationSliceData = inboundTranslationSlices.map(ITS => {
408
408
  let its = ITS.make(publishJsons, componentRuntime[ITS.Spec.name], opts);
409
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));
410
412
  let registerResolver = HooksConfig.hooks.inboundMutationResolverHook;
411
- if (registerResolver !== undefined) {
413
+ if (registerResolver !== undefined && exposed) {
412
414
  registerResolver(fieldName, ITS.Spec.externalInputSchema, inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf));
413
415
  }
414
416
  let bindReceive = HooksConfig.hooks.inboundMutationBindReceiveHook;
415
- if (bindReceive !== undefined) {
416
- 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)));
417
419
  }
418
420
  return [
419
421
  ITS.Spec.name,
420
422
  fieldName,
421
423
  its,
422
- ITS.Spec.externalInputSchema
424
+ ITS.Spec.externalInputSchema,
425
+ exposed
423
426
  ];
424
427
  });
425
428
  let inboundTranslationSlicesOutputs = Object.fromEntries(inboundTranslationSliceData.map(param => [
@@ -482,7 +485,7 @@ function Make(DcbEventLogStorage) {
482
485
  let receiveFn = receivers[event.fieldName];
483
486
  if (receiveFn !== undefined) {
484
487
  return Effect.promise(async () => {
485
- 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);
486
489
  return CommandTopic$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result));
487
490
  });
488
491
  } else {
@@ -507,8 +510,9 @@ function Make(DcbEventLogStorage) {
507
510
  });
508
511
  let dcbResources = Object.values(stateChangeSlicesOutputs).flatMap(outputs => outputs.resources).concat(Object.values(inboundTranslationSlicesOutputs).flatMap(outputs => outputs.resources.concat(outputs.queryDb.resources)));
509
512
  let asyncDcbResources = Object.values(asyncStateChangeSlicesOutputs).flatMap(outputs => outputs.resources);
510
- let inboundFieldNames = inboundTranslationSliceData.map(param => param[1]);
511
- 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]);
512
516
  let dcbMutationData = stateChangeSlices.flatMap(S => {
513
517
  let commandSchema = S.Spec.commandSchema;
514
518
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
@@ -578,7 +582,7 @@ function Make(DcbEventLogStorage) {
578
582
  consistencyRead: Stdlib_Option.flatMap(sliceDef, d => d.consistencyRead)
579
583
  };
580
584
  });
581
- let mutationEntriesFromInboundSlices = inboundTranslationSlices.map(ITS => {
585
+ let mutationEntriesFromInboundSlices = inboundTranslationSlices.filter(ITS => InboundTranslationSlice_Callback$ReventlessCore.exposesMutation(ITS.Spec.externalInputSchema)).map(ITS => {
582
586
  let fieldName = Api_Naming$ReventlessCore.sliceMutationField(name, ITS.Spec.name);
583
587
  let fieldPermissions = {};
584
588
  Stdlib_Option.forEach(inboundDoorPermission(ITS.Spec.commandSchema, ITS.Spec.authorizationOf), rule => {
@@ -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
  },
@@ -57,8 +57,13 @@ function Make(QueryDbStorage) {
57
57
  let publishJsonsFn = param[1];
58
58
  let queryDbOps = param[0];
59
59
  return {
60
- receive: async (inputJson, caller) => {
61
- 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);
62
67
  await syncToQueryDb(queryDbOps, InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result));
63
68
  return result;
64
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
  }
@@ -7,6 +7,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
7
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
8
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
9
  import * as Effect from "effect/Effect";
10
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
11
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
12
  import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
12
13
  import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
@@ -16,6 +17,9 @@ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types
16
17
  import * as Message$ReventlessCore from "../../Message.res.mjs";
17
18
  import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
18
19
  import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
20
+ import * as ApiNoApiHelpers$ReventlessCore from "../Api/ApiNoApiHelpers.res.mjs";
21
+ import * as InboundTranslationSlice$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/InboundTranslationSlice.res.mjs";
22
+ import * as InboundTranslationSlice_Webhook$ReventlessCore from "./InboundTranslationSlice_Webhook.res.mjs";
19
23
 
20
24
  let auditStatusSchema = Sury.union([
21
25
  Sury.literal("Success"),
@@ -28,7 +32,8 @@ let auditRowSchema = Sury.$schema(s => ({
28
32
  targetIds: s.m(Sury.$option(Sury.array(Sury.string))),
29
33
  commandCount: s.m(Sury.$option(Sury.int32)),
30
34
  error: s.m(Sury.$option(Sury.string)),
31
- receivedAt: s.m(Sury.string)
35
+ receivedAt: s.m(Sury.string),
36
+ door: s.m(Sury.$option(InboundTranslationSlice$ReventlessInfra.doorSchema))
32
37
  }));
33
38
 
34
39
  function receiveResultToOutcome(result) {
@@ -87,26 +92,35 @@ function doorCaller(identity) {
87
92
  return Stdlib_Option.getOr((identity == null) ? undefined : Primitive_option.some(identity), Identity$Reventless.anonymous);
88
93
  }
89
94
 
95
+ function exposesMutation(externalInputSchema) {
96
+ return !ApiNoApiHelpers$ReventlessCore.isNoApi(externalInputSchema);
97
+ }
98
+
99
+ function checkReachable(plugin, slice, mutation, webhook) {
100
+ if (!mutation && !webhook) {
101
+ return Stdlib_JsError.throwWithMessage(plugin + `: inbound translation slice ` + slice + ` has @noApi on externalInput and declares no endpoint, so nothing can reach it`);
102
+ }
103
+ }
104
+
90
105
  function Make(Spec) {
91
106
  return Translation => {
92
107
  let auditLog = {};
93
108
  let makeMeta = () => Message$ReventlessCore.generateMeta(Spec.targetName, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
94
109
  let messageOf = (exn, fallback) => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), fallback);
95
- let succeed = (requestId, inputJson, targetIds) => {
96
- let commandCount = targetIds.length;
110
+ let failWith = (requestId, input, door, errorCode, error) => {
97
111
  auditLog[requestId] = {
98
- input: JSON.stringify(inputJson),
99
- status: "Success",
100
- targetIds: targetIds,
101
- commandCount: commandCount,
102
- receivedAt: new Date().toISOString()
112
+ input: input,
113
+ status: "Failure",
114
+ error: error,
115
+ receivedAt: new Date().toISOString(),
116
+ door: door
103
117
  };
104
118
  return {
105
- TAG: "Ok",
119
+ TAG: "Error",
106
120
  _0: {
107
121
  requestId: requestId,
108
- targetIds: targetIds,
109
- commandCount: commandCount
122
+ error: error,
123
+ errorCode: errorCode
110
124
  }
111
125
  };
112
126
  };
@@ -129,21 +143,26 @@ function Make(Spec) {
129
143
  };
130
144
  }
131
145
  };
132
- let receive = async (publishJsons, inputJson, caller) => {
146
+ let receive = async (publishJsons, inputJson, caller, doorOpt) => {
147
+ let door = doorOpt !== undefined ? doorOpt : "Mutation";
133
148
  let requestId = Uuid.v4();
134
- let fail = (errorCode, error) => {
149
+ let fail = (errorCode, error) => failWith(requestId, JSON.stringify(inputJson), door, errorCode, error);
150
+ let succeed = extra => {
151
+ let commandCount = extra.length;
135
152
  auditLog[requestId] = {
136
153
  input: JSON.stringify(inputJson),
137
- status: "Failure",
138
- error: error,
139
- receivedAt: new Date().toISOString()
154
+ status: "Success",
155
+ targetIds: extra,
156
+ commandCount: commandCount,
157
+ receivedAt: new Date().toISOString(),
158
+ door: door
140
159
  };
141
160
  return {
142
- TAG: "Error",
161
+ TAG: "Ok",
143
162
  _0: {
144
163
  requestId: requestId,
145
- error: error,
146
- errorCode: errorCode
164
+ targetIds: extra,
165
+ commandCount: commandCount
147
166
  }
148
167
  };
149
168
  };
@@ -197,20 +216,24 @@ function Make(Spec) {
197
216
  return fail("Forbidden", Spec.name + `: the caller is not authorized for every command this input translates into`);
198
217
  }
199
218
  if (msgs.length === 0) {
200
- return succeed(requestId, inputJson, []);
219
+ return succeed([]);
201
220
  }
202
221
  try {
203
222
  await publishJsons(msgs);
204
- return succeed(requestId, inputJson, pairs.map(param => param[0]));
223
+ return succeed(pairs.map(param => param[0]));
205
224
  } catch (raw_exn$1) {
206
225
  let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
207
- return fail(undefined, messageOf(exn$1, "publish failed"));
226
+ return fail("PublishFailed", messageOf(exn$1, "publish failed"));
208
227
  }
209
228
  };
229
+ let refuse = (body, errorCode, error) => failWith(Uuid.v4(), body, "Webhook", errorCode, error);
230
+ let webhook = Stdlib_Option.map(Spec.endpoint, endpoint => InboundTranslationSlice_Webhook$ReventlessCore.routeOf(Spec.name, endpoint));
210
231
  return {
211
232
  Spec: Spec,
212
233
  auditLog: auditLog,
213
- receive: receive
234
+ receive: receive,
235
+ refuse: refuse,
236
+ webhook: webhook
214
237
  };
215
238
  };
216
239
  }
@@ -223,6 +246,8 @@ export {
223
246
  takeAuditRow,
224
247
  callerAdmits,
225
248
  doorCaller,
249
+ exposesMutation,
250
+ checkReachable,
226
251
  Make,
227
252
  }
228
253
  /* auditStatusSchema Not a pure module */
@@ -0,0 +1,231 @@
1
+ // The HTTP door of an inbound translation slice: find the slice a request's path
2
+ // names, check its signature over the raw body, and hand the parsed body to
3
+ // `receive` as the caller the endpoint stands for. Runtime-pure — the AWS command
4
+ // Lambda and the local registry both serve through it.
5
+
6
+ type request = {
7
+ method: string,
8
+ path: string,
9
+ /** Header names lower-cased. */
10
+ headers: dict<string>,
11
+ /** The body exactly as sent: a signature is over these bytes, not over a re-encoding. */
12
+ body: string,
13
+ }
14
+
15
+ type response = {status: int, body: string}
16
+
17
+ /** An endpoint with its slice's role erased to a role name. */
18
+ type route = {
19
+ slice: string,
20
+ path: string,
21
+ method: string,
22
+ auth: Reventless.InboundTranslationSlice.webhookAuth,
23
+ roles: array<Reventless.Role.name>,
24
+ }
25
+
26
+ /** A route and the slice operations that serve it. `receive` is the webhook door's. */
27
+ type slice = {
28
+ route: route,
29
+ receive: (
30
+ JSON.t,
31
+ ~caller: Reventless.Identity.t,
32
+ ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
33
+ refuse: (
34
+ ~body: string,
35
+ ~errorCode: string,
36
+ string,
37
+ ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
38
+ }
39
+
40
+ let normalizePath = (path: string): string =>
41
+ path->String.split("/")->Array.filter(segment => segment != "")->Array.join("/")
42
+
43
+ let routeOf = (
44
+ ~slice: string,
45
+ endpoint: Reventless.InboundTranslationSlice.endpointConfig<'role>,
46
+ ): route => {
47
+ slice,
48
+ path: normalizePath(endpoint.path),
49
+ method: (endpoint.method->Option.getOr(Post) :> string),
50
+ auth: endpoint.auth,
51
+ roles: switch endpoint.role {
52
+ | Some(role) =>
53
+ Reventless.Authorization.AllowRoles([role])
54
+ ->Reventless.Authorization.named
55
+ ->Reventless.Authorization.rolesOf
56
+ | None => []
57
+ },
58
+ }
59
+
60
+ /** The environment variable a named secret is read from. */
61
+ let secretEnvKey = (name: string): string =>
62
+ "REVENTLESS_WEBHOOK_SECRET_" ++ name->String.toUpperCase->String.replaceRegExp(/[^A-Z0-9]/g, "_")
63
+
64
+ /** The secrets a variable holds: several, comma-separated, while one is rotated. */
65
+ let secretsOf = (raw: string): array<string> =>
66
+ raw->String.split(",")->Array.map(String.trim)->Array.filter(secret => secret != "")
67
+
68
+ @val external env: dict<string> = "process.env"
69
+
70
+ let secretFromEnv = (name: string): option<string> => env->Dict.get(secretEnvKey(name))
71
+
72
+ // Compares every character whatever the first difference, so the time taken does
73
+ // not tell a forger how much of a guess was right.
74
+ let sameText = (a: string, b: string): bool => {
75
+ let code = (text, i) => text->String.charCodeAt(i)->Option.getOr(0)
76
+ a->String.length == b->String.length && {
77
+ let diff = ref(0)
78
+ for i in 0 to a->String.length - 1 {
79
+ diff := diff.contents->Int.bitwiseOr(code(a, i)->Int.bitwiseXor(code(b, i)))
80
+ }
81
+ diff.contents == 0
82
+ }
83
+ }
84
+
85
+ let hmacHex = (~secret: string, text: string): string =>
86
+ NodeCrypto.createHmac("sha256", secret)->NodeCrypto.hmacUpdate(text)->NodeCrypto.hmacDigest("hex")
87
+
88
+ /** Whether `body` carries a valid signature by one of `secrets`. */
89
+ let verify = (
90
+ signature: Reventless.InboundTranslationSlice.signature,
91
+ ~secrets: array<string>,
92
+ ~headers: dict<string>,
93
+ ~body: string,
94
+ ~nowSeconds: float,
95
+ ): result<unit, string> => {
96
+ let header = name => headers->Dict.get(name->String.toLowerCase)
97
+ let given = switch (header(signature.header), signature.prefix) {
98
+ | (None, _) => Error(`missing ${signature.header} header`)
99
+ | (Some(raw), None) => Ok(raw)
100
+ | (Some(raw), Some(prefix)) if raw->String.startsWith(prefix) =>
101
+ Ok(raw->String.slice(~start=prefix->String.length))
102
+ | (Some(_), Some(prefix)) => Error(`${signature.header} does not start with "${prefix}"`)
103
+ }
104
+ let signed = switch signature.timestampHeader {
105
+ | None => Ok(body)
106
+ | Some(name) =>
107
+ switch header(name) {
108
+ | None => Error(`missing ${name} header`)
109
+ | Some(raw) =>
110
+ switch raw->Float.fromString {
111
+ | None => Error(`${name} is not a time in seconds`)
112
+ | Some(at)
113
+ if Math.abs(nowSeconds -. at) >
114
+ signature.toleranceSeconds->Option.getOr(300)->Int.toFloat =>
115
+ Error(`${name} is outside the accepted window`)
116
+ | Some(_) => Ok(`${raw}.${body}`)
117
+ }
118
+ }
119
+ }
120
+ switch (given, signed) {
121
+ | (Error(msg), _) | (_, Error(msg)) => Error(msg)
122
+ | (Ok(given), Ok(text)) =>
123
+ let given = given->String.trim->String.toLowerCase
124
+ secrets->Array.some(secret => sameText(hmacHex(~secret, text), given))
125
+ ? Ok()
126
+ : Error(`${signature.header} does not match the body`)
127
+ }
128
+ }
129
+
130
+ /** Who a request that passed `route.auth` acts as. An open URL identifies nobody. */
131
+ let callerOf = (route: route, ~signed: bool): Reventless.Identity.t =>
132
+ signed
133
+ ? {
134
+ userId: `webhook:${route.slice}`,
135
+ username: route.slice,
136
+ groups: route.roles->Array.map(Reventless.Role.groupOf),
137
+ provider: Custom("webhook"),
138
+ }
139
+ : Reventless.Identity.anonymous
140
+
141
+ let json = (fields: array<(string, JSON.t)>): string =>
142
+ fields->Dict.fromArray->JSON.Encode.object->JSON.stringify
143
+
144
+ let plain = (status, error) => {status, body: json([("error", JSON.Encode.string(error))])}
145
+
146
+ /** The status a sender acts on: 2xx done, 5xx try again, any other 4xx do not. */
147
+ let statusOf = (result: ReventlessInfra.InboundTranslationSlice.receiveResult): int =>
148
+ switch result {
149
+ | Ok(_) => 200
150
+ | Error({errorCode: "Unsigned"}) => 401
151
+ | Error({errorCode: "InvalidBody"}) => 400
152
+ | Error({errorCode: "Forbidden"}) => 403
153
+ | Error({errorCode: "PublishFailed"}) => 503
154
+ | Error(_) => 422
155
+ }
156
+
157
+ let respond = (result: ReventlessInfra.InboundTranslationSlice.receiveResult): response => {
158
+ status: statusOf(result),
159
+ body: switch result {
160
+ | Ok({requestId, commandCount}) =>
161
+ json([
162
+ ("requestId", JSON.Encode.string(requestId)),
163
+ ("commandCount", JSON.Encode.int(commandCount)),
164
+ ])
165
+ | Error({requestId, error}) =>
166
+ json([("requestId", JSON.Encode.string(requestId)), ("error", JSON.Encode.string(error))])
167
+ },
168
+ }
169
+
170
+ /** Serve one request on its slice. A secret the platform does not have is a
171
+ misconfiguration, answered 500 and logged, never treated as open. */
172
+ let serve = async (
173
+ slice: slice,
174
+ ~secretOf: string => option<string>=secretFromEnv,
175
+ ~nowSeconds: float=Date.now() /. 1000.,
176
+ request: request,
177
+ ): response =>
178
+ if request.method->String.toUpperCase != slice.route.method {
179
+ plain(405, `${slice.route.path} accepts ${slice.route.method}`)
180
+ } else {
181
+ let signed = switch slice.route.auth {
182
+ | Open => Ok(Ok(false))
183
+ | SignedBody(signature) =>
184
+ switch secretOf(signature.secret)->Option.mapOr([], secretsOf) {
185
+ | [] => Error(secretEnvKey(signature.secret))
186
+ | secrets =>
187
+ Ok(
188
+ verify(
189
+ signature,
190
+ ~secrets,
191
+ ~headers=request.headers,
192
+ ~body=request.body,
193
+ ~nowSeconds,
194
+ )->Result.map(() => true),
195
+ )
196
+ }
197
+ }
198
+ switch signed {
199
+ | Error(key) =>
200
+ EffectLogger.logError(
201
+ ~comp=`InboundTranslationSlice(${slice.route.slice})`,
202
+ `webhook secret ${key} is not set; refusing every request`,
203
+ )->Effect.runSync
204
+ plain(500, "webhook is not configured")
205
+ | Ok(Error(msg)) => respond(await slice.refuse(~body=request.body, ~errorCode="Unsigned", msg))
206
+ | Ok(Ok(signed)) =>
207
+ switch try Some(JSON.parseOrThrow(request.body)) catch {
208
+ | _ => None
209
+ } {
210
+ | None =>
211
+ respond(
212
+ await slice.refuse(~body=request.body, ~errorCode="InvalidBody", "body is not JSON"),
213
+ )
214
+ | Some(input) => respond(await slice.receive(input, ~caller=callerOf(slice.route, ~signed)))
215
+ }
216
+ }
217
+ }
218
+
219
+ /** Serve a request on whichever slice its path names. */
220
+ let dispatch = async (
221
+ slices: array<slice>,
222
+ ~secretOf=?,
223
+ ~nowSeconds=?,
224
+ request: request,
225
+ ): response => {
226
+ let path = normalizePath(request.path)
227
+ switch slices->Array.find(slice => slice.route.path == path) {
228
+ | Some(slice) => await serve(slice, ~secretOf?, ~nowSeconds?, request)
229
+ | None => plain(404, `no webhook at /${path}`)
230
+ }
231
+ }
@@ -0,0 +1,294 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Nodecrypto from "node:crypto";
4
+ import * as Stdlib_Float from "@rescript/runtime/lib/es6/Stdlib_Float.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
7
+ import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
10
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
+ import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
12
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
13
+ import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
14
+
15
+ function normalizePath(path) {
16
+ return path.split("/").filter(segment => segment !== "").join("/");
17
+ }
18
+
19
+ function routeOf(slice, endpoint) {
20
+ let role = endpoint.role;
21
+ return {
22
+ slice: slice,
23
+ path: normalizePath(endpoint.path),
24
+ method: Stdlib_Option.getOr(endpoint.method, "POST"),
25
+ auth: endpoint.auth,
26
+ roles: role !== undefined ? Authorization$Reventless.rolesOf(Authorization$Reventless.named({
27
+ TAG: "AllowRoles",
28
+ _0: [Primitive_option.valFromOption(role)]
29
+ })) : []
30
+ };
31
+ }
32
+
33
+ function secretEnvKey(name) {
34
+ return "REVENTLESS_WEBHOOK_SECRET_" + name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
35
+ }
36
+
37
+ function secretsOf(raw) {
38
+ return raw.split(",").map(prim => prim.trim()).filter(secret => secret !== "");
39
+ }
40
+
41
+ function secretFromEnv(name) {
42
+ return process.env[secretEnvKey(name)];
43
+ }
44
+
45
+ function sameText(a, b) {
46
+ let code = (text, i) => Stdlib_Option.getOr(Stdlib_String.charCodeAt(text, i), 0);
47
+ if (a.length !== b.length) {
48
+ return false;
49
+ }
50
+ let diff = 0;
51
+ for (let i = 0, i_finish = a.length; i < i_finish; ++i) {
52
+ diff = diff | code(a, i) ^ code(b, i);
53
+ }
54
+ return diff === 0;
55
+ }
56
+
57
+ function hmacHex(secret, text) {
58
+ return Nodecrypto.createHmac("sha256", secret).update(text).digest("hex");
59
+ }
60
+
61
+ function verify(signature, secrets, headers, body, nowSeconds) {
62
+ let match = headers[signature.header.toLowerCase()];
63
+ let match$1 = signature.prefix;
64
+ let given = match !== undefined ? (
65
+ match$1 !== undefined ? (
66
+ match.startsWith(match$1) ? ({
67
+ TAG: "Ok",
68
+ _0: match.slice(match$1.length)
69
+ }) : ({
70
+ TAG: "Error",
71
+ _0: signature.header + ` does not start with "` + match$1 + `"`
72
+ })
73
+ ) : ({
74
+ TAG: "Ok",
75
+ _0: match
76
+ })
77
+ ) : ({
78
+ TAG: "Error",
79
+ _0: `missing ` + signature.header + ` header`
80
+ });
81
+ let name = signature.timestampHeader;
82
+ let signed;
83
+ if (name !== undefined) {
84
+ let raw = headers[name.toLowerCase()];
85
+ if (raw !== undefined) {
86
+ let at = Stdlib_Float.fromString(raw);
87
+ signed = at !== undefined ? (
88
+ Math.abs(nowSeconds - at) > Stdlib_Option.getOr(signature.toleranceSeconds, 300) ? ({
89
+ TAG: "Error",
90
+ _0: name + ` is outside the accepted window`
91
+ }) : ({
92
+ TAG: "Ok",
93
+ _0: raw + `.` + body
94
+ })
95
+ ) : ({
96
+ TAG: "Error",
97
+ _0: name + ` is not a time in seconds`
98
+ });
99
+ } else {
100
+ signed = {
101
+ TAG: "Error",
102
+ _0: `missing ` + name + ` header`
103
+ };
104
+ }
105
+ } else {
106
+ signed = {
107
+ TAG: "Ok",
108
+ _0: body
109
+ };
110
+ }
111
+ if (given.TAG !== "Ok") {
112
+ return {
113
+ TAG: "Error",
114
+ _0: given._0
115
+ };
116
+ }
117
+ if (signed.TAG !== "Ok") {
118
+ return {
119
+ TAG: "Error",
120
+ _0: signed._0
121
+ };
122
+ }
123
+ let text = signed._0;
124
+ let given$1 = given._0.trim().toLowerCase();
125
+ if (secrets.some(secret => sameText(hmacHex(secret, text), given$1))) {
126
+ return {
127
+ TAG: "Ok",
128
+ _0: undefined
129
+ };
130
+ } else {
131
+ return {
132
+ TAG: "Error",
133
+ _0: signature.header + ` does not match the body`
134
+ };
135
+ }
136
+ }
137
+
138
+ function callerOf(route, signed) {
139
+ if (signed) {
140
+ return {
141
+ userId: `webhook:` + route.slice,
142
+ username: route.slice,
143
+ groups: route.roles.map(Role$Reventless.groupOf),
144
+ provider: {
145
+ TAG: "Custom",
146
+ _0: "webhook"
147
+ }
148
+ };
149
+ } else {
150
+ return Identity$Reventless.anonymous;
151
+ }
152
+ }
153
+
154
+ function json(fields) {
155
+ return JSON.stringify(Object.fromEntries(fields));
156
+ }
157
+
158
+ function plain(status, error) {
159
+ return {
160
+ status: status,
161
+ body: json([[
162
+ "error",
163
+ error
164
+ ]])
165
+ };
166
+ }
167
+
168
+ function statusOf(result) {
169
+ if (result.TAG === "Ok") {
170
+ return 200;
171
+ }
172
+ switch (result._0.errorCode) {
173
+ case "Forbidden" :
174
+ return 403;
175
+ case "InvalidBody" :
176
+ return 400;
177
+ case "PublishFailed" :
178
+ return 503;
179
+ case "Unsigned" :
180
+ return 401;
181
+ default:
182
+ return 422;
183
+ }
184
+ }
185
+
186
+ function respond(result) {
187
+ let tmp;
188
+ if (result.TAG === "Ok") {
189
+ let match = result._0;
190
+ tmp = json([
191
+ [
192
+ "requestId",
193
+ match.requestId
194
+ ],
195
+ [
196
+ "commandCount",
197
+ match.commandCount
198
+ ]
199
+ ]);
200
+ } else {
201
+ let match$1 = result._0;
202
+ tmp = json([
203
+ [
204
+ "requestId",
205
+ match$1.requestId
206
+ ],
207
+ [
208
+ "error",
209
+ match$1.error
210
+ ]
211
+ ]);
212
+ }
213
+ return {
214
+ status: statusOf(result),
215
+ body: tmp
216
+ };
217
+ }
218
+
219
+ async function serve(slice, secretOfOpt, nowSecondsOpt, request) {
220
+ let secretOf = secretOfOpt !== undefined ? secretOfOpt : secretFromEnv;
221
+ let nowSeconds = nowSecondsOpt !== undefined ? nowSecondsOpt : Date.now() / 1000;
222
+ if (request.method.toUpperCase() !== slice.route.method) {
223
+ return plain(405, slice.route.path + ` accepts ` + slice.route.method);
224
+ }
225
+ let signature = slice.route.auth;
226
+ let signed;
227
+ if (typeof signature !== "object") {
228
+ signed = {
229
+ TAG: "Ok",
230
+ _0: {
231
+ TAG: "Ok",
232
+ _0: false
233
+ }
234
+ };
235
+ } else {
236
+ let signature$1 = signature._0;
237
+ let secrets = Stdlib_Option.mapOr(secretOf(signature$1.secret), [], secretsOf);
238
+ signed = secrets.length !== 0 ? ({
239
+ TAG: "Ok",
240
+ _0: Stdlib_Result.map(verify(signature$1, secrets, request.headers, request.body, nowSeconds), () => true)
241
+ }) : ({
242
+ TAG: "Error",
243
+ _0: secretEnvKey(signature$1.secret)
244
+ });
245
+ }
246
+ if (signed.TAG === "Ok") {
247
+ let msg = signed._0;
248
+ if (msg.TAG !== "Ok") {
249
+ return respond(await slice.refuse(request.body, "Unsigned", msg._0));
250
+ }
251
+ let input;
252
+ try {
253
+ input = JSON.parse(request.body);
254
+ } catch (exn) {
255
+ input = undefined;
256
+ }
257
+ if (input !== undefined) {
258
+ return respond(await slice.receive(input, callerOf(slice.route, msg._0)));
259
+ } else {
260
+ return respond(await slice.refuse(request.body, "InvalidBody", "body is not JSON"));
261
+ }
262
+ }
263
+ Effect.runSync(EffectLogger$ReventlessCore.logError(`InboundTranslationSlice(` + slice.route.slice + `)`, undefined, `webhook secret ` + signed._0 + ` is not set; refusing every request`));
264
+ return plain(500, "webhook is not configured");
265
+ }
266
+
267
+ async function dispatch(slices, secretOf, nowSeconds, request) {
268
+ let path = normalizePath(request.path);
269
+ let slice = slices.find(slice => slice.route.path === path);
270
+ if (slice !== undefined) {
271
+ return await serve(slice, secretOf, nowSeconds, request);
272
+ } else {
273
+ return plain(404, `no webhook at /` + path);
274
+ }
275
+ }
276
+
277
+ export {
278
+ normalizePath,
279
+ routeOf,
280
+ secretEnvKey,
281
+ secretsOf,
282
+ secretFromEnv,
283
+ sameText,
284
+ hmacHex,
285
+ verify,
286
+ callerOf,
287
+ json,
288
+ plain,
289
+ statusOf,
290
+ respond,
291
+ serve,
292
+ dispatch,
293
+ }
294
+ /* node:crypto Not a pure module */
@@ -146,6 +146,7 @@ module PsTwoRuleFeed = {
146
146
  @schema type command = AddProduct({sku: string}) | AddCategory({sku: string})
147
147
  let targetName = "AddProduct"
148
148
  let externalSystem = None
149
+ let endpoint = None
149
150
  type role = Reventless.Role.name
150
151
  let authorizationOf = (name: string): Reventless.Authorization.rule<role> =>
151
152
  switch name {
@@ -178,6 +178,8 @@ globalThis.describe("requiredAccess derived from the authorization rule", () =>
178
178
  });
179
179
  });
180
180
 
181
+ let name = "PsTwoRuleFeed";
182
+
181
183
  let moduleUrl = "ps-two-rule-feed://spec";
182
184
 
183
185
  let externalInputSchema = Sury.$schema(s => ({
@@ -195,6 +197,8 @@ let commandSchema = Sury.union([
195
197
  }))
196
198
  ]);
197
199
 
200
+ let targetName = "AddProduct";
201
+
198
202
  function authorizationOf(name) {
199
203
  switch (name) {
200
204
  case "AddCategory" :
@@ -215,12 +219,13 @@ function authorizationOf(name) {
215
219
  let traits = [];
216
220
 
217
221
  let PsTwoRuleFeed = {
218
- name: "PsTwoRuleFeed",
222
+ name: name,
219
223
  moduleUrl: moduleUrl,
220
224
  externalInputSchema: externalInputSchema,
221
225
  commandSchema: commandSchema,
222
- targetName: "AddProduct",
226
+ targetName: targetName,
223
227
  externalSystem: undefined,
228
+ endpoint: undefined,
224
229
  authorizationOf: authorizationOf,
225
230
  traits: traits
226
231
  };
@@ -241,8 +246,19 @@ function make$3(param, param$1, param$2) {
241
246
  return 0;
242
247
  }
243
248
 
249
+ let PsTwoRuleFeedSlice_Spec = {
250
+ name: name,
251
+ moduleUrl: moduleUrl,
252
+ externalInputSchema: externalInputSchema,
253
+ commandSchema: commandSchema,
254
+ targetName: targetName,
255
+ externalSystem: undefined,
256
+ authorizationOf: authorizationOf,
257
+ endpoint: undefined
258
+ };
259
+
244
260
  let PsTwoRuleFeedSlice = {
245
- Spec: PsTwoRuleFeed,
261
+ Spec: PsTwoRuleFeedSlice_Spec,
246
262
  Translation: Translation,
247
263
  queryDbName: "PsTwoRuleFeedAudit",
248
264
  make: make$3