@reventlessdev/reventless-core 3.0.0-alpha.290 → 3.0.0-alpha.292

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.
@@ -1,6 +1,6 @@
1
- // InboundTranslationSlice callback — receives external input and translates to commands.
2
- //
3
- // Maintains an audit log QueryDb and delegates translation to Translation.translate.
1
+ // InboundTranslationSlice callback: parses external input, translates it into
2
+ // commands, checks each against its own rule, and publishes them. Runtime-pure,
3
+ // since the deployed DCB command Lambda imports it.
4
4
 
5
5
  @schema
6
6
  type auditStatus =
@@ -17,21 +17,8 @@ type auditRow = {
17
17
  receivedAt: string,
18
18
  }
19
19
 
20
- /**
21
- Map a `receive` outcome onto the `CommandResult` union the slice's mutation field
22
- declares, so both surfaces feed `CommandTopic.commandOutcomeToJson` rather than
23
- hand-rolling a second `__typename` writer.
24
-
25
- A translation can fan out across several targets; the mutation response reports
26
- the first target as `entityId` and the fan-out count as `eventCount`. The
27
- per-target detail stays queryable through the slice's audit read model. When the
28
- translation legitimately produced no command, `entityId` is omitted (encoding as
29
- `null`) rather than reporting a target that does not exist.
30
-
31
- Lives here rather than in `InboundTranslationSlice.res` because the deployed DCB
32
- command Lambda calls it at runtime, and the base module imports `@pulumi/pulumi`
33
- at deploy time — keeping this runtime-pure keeps Pulumi out of the Lambda graph.
34
- */
20
+ /** The `CommandResult` a `receive` outcome answers with: the first target as
21
+ `entityId` (omitted when nothing was produced), the fan-out as `eventCount`. */
35
22
  let receiveResultToOutcome = (
36
23
  result: ReventlessInfra.InboundTranslationSlice.receiveResult,
37
24
  ): CommandTopic.commandOutcome =>
@@ -41,37 +28,54 @@ let receiveResultToOutcome = (
41
28
  | Some(entityId) => Accepted({msgId: requestId, entityId, eventCount: commandCount})
42
29
  | None => Accepted({msgId: requestId, eventCount: commandCount})
43
30
  }
44
- | Error({requestId, error}) =>
45
- Rejected({msgId: requestId, errorCode: "TranslationFailed", errorDetail: Some(error)})
31
+ | Error({requestId, error, ?errorCode}) =>
32
+ Rejected({
33
+ msgId: requestId,
34
+ errorCode: errorCode->Option.getOr("TranslationFailed"),
35
+ errorDetail: Some(error),
36
+ })
46
37
  }
47
38
 
48
- /**
49
- The request a `receive` outcome belongs to, from either arm. Both carry the id
50
- that keys the outcome's audit row, so a caller draining the log does not have to
51
- match on the result to find its own row.
52
- */
39
+ /** The request a `receive` outcome belongs to, from either arm. */
53
40
  let requestIdOf = (result: ReventlessInfra.InboundTranslationSlice.receiveResult): string =>
54
41
  switch result {
55
42
  | Ok({requestId}) | Error({requestId}) => requestId
56
43
  }
57
44
 
58
- /**
59
- Remove one request's audit row from the in-memory log and hand it back.
60
-
61
- `auditLog` is a hand-off buffer between `receive` and whoever persists the row,
62
- not a log in its own right — the QueryDb it drains into is the durable one. It
63
- has to be emptied as it drains: the dict lives as long as the process (a warm
64
- Lambda container serves many requests), so a drain that walked the whole dict
65
- would rewrite every row the container had ever seen on every request — quadratic
66
- writes, unbounded retention, and, because the write is an overwrite by row id,
67
- resurrection of rows deleted from the table since.
68
- */
45
+ /** Remove one request's audit row from the in-memory log and hand it back. The
46
+ log outlives a request in a warm process, so draining all of it would rewrite
47
+ every row it ever held. */
69
48
  let takeAuditRow = (auditLog: Dict.t<auditRow>, requestId: string): option<auditRow> => {
70
49
  let row = auditLog->Dict.get(requestId)
71
50
  auditLog->Dict.delete(requestId)
72
51
  row
73
52
  }
74
53
 
54
+ /** Whether `caller` may send a command under `rule`. No caller, or a system
55
+ (IAM) caller, is the platform acting for itself and passes every rule. */
56
+ let callerAdmits = (rule: Reventless.Authorization.permission, caller) =>
57
+ switch caller {
58
+ | None => true
59
+ | Some(identity) =>
60
+ switch Reventless.OwnerScope.classify(identity, ~elevated=[]) {
61
+ | System => true
62
+ | _ => Reventless.Authorization.isAllowed(rule, identity)
63
+ }
64
+ }
65
+
66
+ /** What an inbound mutation's resolver sends a deployed handler. `identity` is
67
+ built by the resolver, and is null where the transport identified nobody. */
68
+ type doorInvocation = {
69
+ fieldName: string,
70
+ arguments: JSON.t,
71
+ identity: Nullable.t<Reventless.Identity.t>,
72
+ }
73
+
74
+ /** The caller a door hands on: what its transport identified, else anonymous, so
75
+ a door that lost the identity refuses rather than acts as the platform. */
76
+ let doorCaller = (identity: Nullable.t<Reventless.Identity.t>): Reventless.Identity.t =>
77
+ identity->Nullable.toOption->Option.getOr(Reventless.Identity.anonymous)
78
+
75
79
  module type T = {
76
80
  module Spec: Reventless.InboundTranslationSlice.Spec
77
81
  module Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec
@@ -83,6 +87,7 @@ module type T = {
83
87
  let receive: (
84
88
  ReventlessInfra.CommandTopic.publishJsons,
85
89
  JSON.t,
90
+ ~caller: Reventless.Identity.t=?,
86
91
  ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>
87
92
  }
88
93
 
@@ -97,149 +102,102 @@ module Make = (
97
102
 
98
103
  let now = () => Date.make()->Date.toISOString
99
104
 
100
- // InboundTranslationSlice ingests external (non-Reventless) messages — there's
101
- // no upstream Reventless meta to derive from, so emitted commands are roots
102
- // of a fresh correlation chain. `traceparent` populated from an inbound HTTP
103
- // header would need to be threaded in here by the API/ingress adapter.
104
- // `service` names the command's TARGET, as on the API path — see the note in
105
- // `AutomationSlice_Callback`. The target's events are dispatched on it.
105
+ // External input has no upstream meta, so each command starts a correlation
106
+ // chain. `service` names the command's target, on which its events dispatch.
106
107
  let makeMeta = (): Reventless.Message.meta => Message.generateMeta(~service=Spec.targetName)
107
108
 
109
+ let messageOf = (exn, fallback) =>
110
+ exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr(fallback)
111
+
112
+ // 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
+ )
118
+ Error({ReventlessInfra.InboundTranslationSlice.requestId, error, ?errorCode})
119
+ }
120
+
121
+ let succeed = (~requestId, ~inputJson, targetIds) => {
122
+ let commandCount = targetIds->Array.length
123
+ auditLog->Dict.set(
124
+ requestId,
125
+ {
126
+ input: inputJson->JSON.stringify,
127
+ status: Success,
128
+ targetIds,
129
+ commandCount,
130
+ receivedAt: now(),
131
+ },
132
+ )
133
+ Ok({ReventlessInfra.InboundTranslationSlice.requestId, targetIds, commandCount})
134
+ }
135
+
136
+ // Encoded with the command's schema, the one its target decodes with.
137
+ let encode = ((targetId, cmd): (string, Spec.command)): result<
138
+ Reventless.Message.commandJson,
139
+ string,
140
+ > =>
141
+ try Ok({
142
+ id: targetId,
143
+ meta: makeMeta(),
144
+ commandJson: cmd->Reventless.Util_Sury.toJson(Spec.commandSchema),
145
+ }) catch {
146
+ | exn =>
147
+ EffectLogger.logError(
148
+ ~comp=`InboundTranslationSlice(${Spec.name})`,
149
+ `failed to encode command: ${messageOf(exn, "unknown")}`,
150
+ )->Effect.runSync
151
+ Error("failed to encode command")
152
+ }
153
+
108
154
  let receive = async (
109
155
  publishJsons: ReventlessInfra.CommandTopic.publishJsons,
110
156
  inputJson: JSON.t,
157
+ ~caller: option<Reventless.Identity.t>=?,
111
158
  ): ReventlessInfra.InboundTranslationSlice.receiveResult => {
112
159
  let requestId = Uuid.v4()
113
-
114
- // Parse the external input
115
- let input = try inputJson->S.parseOrThrow(~to=Spec.externalInputSchema)->Ok catch {
116
- | exn =>
117
- let msg =
118
- exn
119
- ->JsExn.fromException
120
- ->Option.flatMap(JsExn.message)
121
- ->Option.getOr("invalid input")
122
- Error(msg)
160
+ let fail = (~errorCode=?, error) => failWith(~requestId, ~inputJson, ~errorCode?, error)
161
+ let parsed = try Ok(inputJson->S.parseOrThrow(~to=Spec.externalInputSchema)) catch {
162
+ | exn => Error(messageOf(exn, "invalid input"))
123
163
  }
124
164
 
125
- switch input {
126
- | Error(msg) =>
127
- auditLog->Dict.set(
128
- requestId,
129
- {
130
- input: inputJson->JSON.stringify,
131
- status: Failure,
132
- error: msg,
133
- receivedAt: now(),
134
- },
135
- )
136
- Error({requestId, error: msg})
137
-
165
+ switch parsed {
166
+ | Error(msg) => fail(msg)
138
167
  | Ok(input) =>
139
168
  switch Translation.translate(input) {
169
+ | Error(msg) => fail(msg)
140
170
  | Ok(pairs) =>
141
- if pairs->Array.length === 0 {
142
- auditLog->Dict.set(
143
- requestId,
144
- {
145
- input: inputJson->JSON.stringify,
146
- status: Success,
147
- targetIds: [],
148
- commandCount: 0,
149
- receivedAt: now(),
150
- },
171
+ // Each command against its own rule; one refused refuses the message.
172
+ let refused =
173
+ pairs->Array.find(((_, cmd)) =>
174
+ !callerAdmits(Spec.commandAuthorization(cmd)->Reventless.Authorization.named, caller)
151
175
  )
152
- Ok({requestId, targetIds: [], commandCount: 0})
153
- } else {
154
- // Encode all commands; abort on first encoding failure
155
- let msgs = ref([])
156
- let encodeError = ref(None)
157
- pairs->Array.forEach(pair => {
158
- let (targetId, cmd) = pair
159
- if encodeError.contents->Option.isNone {
160
- try {
161
- let commandJson = cmd->JSON.stringifyAny->Option.getOrThrow->JSON.parseOrThrow
162
- let msg: Reventless.Message.commandJson = {
163
- id: targetId,
164
- meta: makeMeta(),
165
- commandJson,
166
- }
167
- msgs.contents = msgs.contents->Array.concat([msg])
168
- } catch {
169
- | exn =>
170
- let errMsg =
171
- exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
172
- EffectLogger.logError(
173
- ~comp=`InboundTranslationSlice(${Spec.name})`,
174
- `failed to encode command: ${errMsg}`,
175
- )->Effect.runSync
176
- encodeError := Some("failed to encode command")
177
- }
176
+ switch refused {
177
+ | Some(_) =>
178
+ fail(
179
+ ~errorCode="Forbidden",
180
+ `${Spec.name}: the caller is not authorized for every command this input translates into`,
181
+ )
182
+ | None =>
183
+ switch pairs->Array.reduce(Ok([]), (acc, pair) =>
184
+ switch (acc, encode(pair)) {
185
+ | (Ok(msgs), Ok(msg)) => Ok(msgs->Array.concat([msg]))
186
+ | (Error(_) as failed, _) => failed
187
+ | (_, Error(msg)) => Error(msg)
178
188
  }
179
- })
180
-
181
- switch encodeError.contents {
182
- | Some(msg) =>
183
- auditLog->Dict.set(
184
- requestId,
185
- {
186
- input: inputJson->JSON.stringify,
187
- status: Failure,
188
- error: msg,
189
- receivedAt: now(),
190
- },
191
- )
192
- Error({requestId, error: msg})
193
- | None =>
189
+ ) {
190
+ | Error(msg) => fail(msg)
191
+ | Ok([]) => succeed(~requestId, ~inputJson, [])
192
+ | Ok(msgs) =>
194
193
  try {
195
- await publishJsons(msgs.contents)
196
- let targetIds = pairs->Array.map(pair => {
197
- let (targetId, _) = pair
198
- targetId
199
- })
200
- auditLog->Dict.set(
201
- requestId,
202
- {
203
- input: inputJson->JSON.stringify,
204
- status: Success,
205
- targetIds,
206
- commandCount: pairs->Array.length,
207
- receivedAt: now(),
208
- },
209
- )
210
- Ok({requestId, targetIds, commandCount: pairs->Array.length})
194
+ await publishJsons(msgs)
195
+ succeed(~requestId, ~inputJson, pairs->Array.map(((targetId, _)) => targetId))
211
196
  } catch {
212
- | exn =>
213
- let msg =
214
- exn
215
- ->JsExn.fromException
216
- ->Option.flatMap(JsExn.message)
217
- ->Option.getOr("publish failed")
218
- auditLog->Dict.set(
219
- requestId,
220
- {
221
- input: inputJson->JSON.stringify,
222
- status: Failure,
223
- error: msg,
224
- receivedAt: now(),
225
- },
226
- )
227
- Error({requestId, error: msg})
197
+ | exn => fail(messageOf(exn, "publish failed"))
228
198
  }
229
199
  }
230
200
  }
231
-
232
- | Error(msg) =>
233
- auditLog->Dict.set(
234
- requestId,
235
- {
236
- input: inputJson->JSON.stringify,
237
- status: Failure,
238
- error: msg,
239
- receivedAt: now(),
240
- },
241
- )
242
- Error({requestId, error: msg})
243
201
  }
244
202
  }
245
203
  }
@@ -3,11 +3,17 @@
3
3
  import * as Sury from "sury";
4
4
  import * as Uuid from "uuid";
5
5
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
6
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
7
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
9
  import * as Effect from "effect/Effect";
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";
9
12
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
13
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
14
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
10
15
  import * as Message$ReventlessCore from "../../Message.res.mjs";
16
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
11
17
  import * as EffectLogger$ReventlessCore from "../../util/EffectLogger.res.mjs";
12
18
 
13
19
  let auditStatusSchema = Sury.union([
@@ -49,7 +55,7 @@ function receiveResultToOutcome(result) {
49
55
  return {
50
56
  TAG: "Rejected",
51
57
  msgId: match$1.requestId,
52
- errorCode: "TranslationFailed",
58
+ errorCode: Stdlib_Option.getOr(match$1.errorCode, "TranslationFailed"),
53
59
  errorDetail: match$1.error
54
60
  };
55
61
  }
@@ -64,158 +70,141 @@ function takeAuditRow(auditLog, requestId) {
64
70
  return row;
65
71
  }
66
72
 
73
+ function callerAdmits(rule, caller) {
74
+ if (caller === undefined) {
75
+ return true;
76
+ }
77
+ let match = OwnerScope$Reventless.classify(caller, []);
78
+ if (typeof match !== "object") {
79
+ return true;
80
+ } else {
81
+ return Authorization$Reventless.isAllowed(rule, caller);
82
+ }
83
+ }
84
+
85
+ function doorCaller(identity) {
86
+ return Stdlib_Option.getOr((identity == null) ? undefined : Primitive_option.some(identity), Identity$Reventless.anonymous);
87
+ }
88
+
67
89
  function Make(Spec) {
68
90
  return Translation => {
69
91
  let auditLog = {};
70
92
  let makeMeta = () => Message$ReventlessCore.generateMeta(Spec.targetName, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
71
- let receive = async (publishJsons, inputJson) => {
72
- let requestId = Uuid.v4();
73
- let input;
93
+ let messageOf = (exn, fallback) => Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), fallback);
94
+ let succeed = (requestId, inputJson, targetIds) => {
95
+ let commandCount = targetIds.length;
96
+ auditLog[requestId] = {
97
+ input: JSON.stringify(inputJson),
98
+ status: "Success",
99
+ targetIds: targetIds,
100
+ commandCount: commandCount,
101
+ receivedAt: new Date().toISOString()
102
+ };
103
+ return {
104
+ TAG: "Ok",
105
+ _0: {
106
+ requestId: requestId,
107
+ targetIds: targetIds,
108
+ commandCount: commandCount
109
+ }
110
+ };
111
+ };
112
+ let encode = param => {
74
113
  try {
75
- input = {
114
+ return {
76
115
  TAG: "Ok",
77
- _0: Sury.parseOrThrow(inputJson, Spec.externalInputSchema)
116
+ _0: {
117
+ id: param[0],
118
+ meta: makeMeta(),
119
+ commandJson: Util_Sury$Reventless.toJson(param[1], Spec.commandSchema)
120
+ }
78
121
  };
79
122
  } catch (raw_exn) {
80
123
  let exn = Primitive_exceptions.internalToException(raw_exn);
81
- let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "invalid input");
82
- input = {
124
+ Effect.runSync(EffectLogger$ReventlessCore.logError(`InboundTranslationSlice(` + Spec.name + `)`, undefined, `failed to encode command: ` + messageOf(exn, "unknown")));
125
+ return {
83
126
  TAG: "Error",
84
- _0: msg
127
+ _0: "failed to encode command"
85
128
  };
86
129
  }
87
- if (input.TAG === "Ok") {
88
- let pairs = Translation.translate(input._0);
89
- if (pairs.TAG === "Ok") {
90
- let pairs$1 = pairs._0;
91
- if (pairs$1.length === 0) {
92
- auditLog[requestId] = {
93
- input: JSON.stringify(inputJson),
94
- status: "Success",
95
- targetIds: [],
96
- commandCount: 0,
97
- receivedAt: new Date().toISOString()
98
- };
99
- return {
100
- TAG: "Ok",
101
- _0: {
102
- requestId: requestId,
103
- targetIds: [],
104
- commandCount: 0
105
- }
106
- };
107
- }
108
- let msgs = {
109
- contents: []
110
- };
111
- let encodeError = {
112
- contents: undefined
113
- };
114
- pairs$1.forEach(pair => {
115
- if (!Stdlib_Option.isNone(encodeError.contents)) {
116
- return;
117
- }
118
- try {
119
- let commandJson = JSON.parse(Stdlib_Option.getOrThrow(JSON.stringify(pair[1]), undefined));
120
- let msg_id = pair[0];
121
- let msg_meta = makeMeta();
122
- let msg = {
123
- id: msg_id,
124
- meta: msg_meta,
125
- commandJson: commandJson
126
- };
127
- msgs.contents = msgs.contents.concat([msg]);
128
- return;
129
- } catch (raw_exn) {
130
- let exn = Primitive_exceptions.internalToException(raw_exn);
131
- let errMsg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
132
- Effect.runSync(EffectLogger$ReventlessCore.logError(`InboundTranslationSlice(` + Spec.name + `)`, undefined, `failed to encode command: ` + errMsg));
133
- encodeError.contents = "failed to encode command";
134
- return;
135
- }
136
- });
137
- let msg$1 = encodeError.contents;
138
- if (msg$1 !== undefined) {
139
- auditLog[requestId] = {
140
- input: JSON.stringify(inputJson),
141
- status: "Failure",
142
- error: msg$1,
143
- receivedAt: new Date().toISOString()
144
- };
145
- return {
146
- TAG: "Error",
147
- _0: {
148
- requestId: requestId,
149
- error: msg$1
150
- }
151
- };
152
- }
153
- try {
154
- await publishJsons(msgs.contents);
155
- let targetIds = pairs$1.map(pair => pair[0]);
156
- auditLog[requestId] = {
157
- input: JSON.stringify(inputJson),
158
- status: "Success",
159
- targetIds: targetIds,
160
- commandCount: pairs$1.length,
161
- receivedAt: new Date().toISOString()
162
- };
163
- return {
164
- TAG: "Ok",
165
- _0: {
166
- requestId: requestId,
167
- targetIds: targetIds,
168
- commandCount: pairs$1.length
169
- }
170
- };
171
- } catch (raw_exn$1) {
172
- let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
173
- let msg$2 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn$1), Stdlib_JsExn.message), "publish failed");
174
- auditLog[requestId] = {
175
- input: JSON.stringify(inputJson),
176
- status: "Failure",
177
- error: msg$2,
178
- receivedAt: new Date().toISOString()
179
- };
180
- return {
181
- TAG: "Error",
182
- _0: {
183
- requestId: requestId,
184
- error: msg$2
185
- }
186
- };
187
- }
188
- } else {
189
- let msg$3 = pairs._0;
190
- auditLog[requestId] = {
191
- input: JSON.stringify(inputJson),
192
- status: "Failure",
193
- error: msg$3,
194
- receivedAt: new Date().toISOString()
195
- };
196
- return {
197
- TAG: "Error",
198
- _0: {
199
- requestId: requestId,
200
- error: msg$3
201
- }
202
- };
203
- }
204
- } else {
205
- let msg$4 = input._0;
130
+ };
131
+ let receive = async (publishJsons, inputJson, caller) => {
132
+ let requestId = Uuid.v4();
133
+ let fail = (errorCode, error) => {
206
134
  auditLog[requestId] = {
207
135
  input: JSON.stringify(inputJson),
208
136
  status: "Failure",
209
- error: msg$4,
137
+ error: error,
210
138
  receivedAt: new Date().toISOString()
211
139
  };
212
140
  return {
213
141
  TAG: "Error",
214
142
  _0: {
215
143
  requestId: requestId,
216
- error: msg$4
144
+ error: error,
145
+ errorCode: errorCode
217
146
  }
218
147
  };
148
+ };
149
+ let parsed;
150
+ try {
151
+ parsed = {
152
+ TAG: "Ok",
153
+ _0: Sury.parseOrThrow(inputJson, Spec.externalInputSchema)
154
+ };
155
+ } catch (raw_exn) {
156
+ let exn = Primitive_exceptions.internalToException(raw_exn);
157
+ parsed = {
158
+ TAG: "Error",
159
+ _0: messageOf(exn, "invalid input")
160
+ };
161
+ }
162
+ if (parsed.TAG !== "Ok") {
163
+ return fail(undefined, parsed._0);
164
+ }
165
+ let msg = Translation.translate(parsed._0);
166
+ if (msg.TAG !== "Ok") {
167
+ return fail(undefined, msg._0);
168
+ }
169
+ let pairs = msg._0;
170
+ let refused = pairs.find(param => !callerAdmits(Authorization$Reventless.named(Spec.commandAuthorization(param[1])), caller));
171
+ if (refused !== undefined) {
172
+ return fail("Forbidden", Spec.name + `: the caller is not authorized for every command this input translates into`);
173
+ }
174
+ let msg$1 = Stdlib_Array.reduce(pairs, {
175
+ TAG: "Ok",
176
+ _0: []
177
+ }, (acc, pair) => {
178
+ let match = encode(pair);
179
+ if (acc.TAG === "Ok") {
180
+ if (match.TAG === "Ok") {
181
+ return {
182
+ TAG: "Ok",
183
+ _0: acc._0.concat([match._0])
184
+ };
185
+ } else {
186
+ return {
187
+ TAG: "Error",
188
+ _0: match._0
189
+ };
190
+ }
191
+ } else {
192
+ return acc;
193
+ }
194
+ });
195
+ if (msg$1.TAG !== "Ok") {
196
+ return fail(undefined, msg$1._0);
197
+ }
198
+ let msgs = msg$1._0;
199
+ if (msgs.length === 0) {
200
+ return succeed(requestId, inputJson, []);
201
+ }
202
+ try {
203
+ await publishJsons(msgs);
204
+ return succeed(requestId, inputJson, pairs.map(param => param[0]));
205
+ } catch (raw_exn$1) {
206
+ let exn$1 = Primitive_exceptions.internalToException(raw_exn$1);
207
+ return fail(undefined, messageOf(exn$1, "publish failed"));
219
208
  }
220
209
  };
221
210
  return {
@@ -232,6 +221,8 @@ export {
232
221
  receiveResultToOutcome,
233
222
  requestIdOf,
234
223
  takeAuditRow,
224
+ callerAdmits,
225
+ doorCaller,
235
226
  Make,
236
227
  }
237
228
  /* auditStatusSchema Not a pure module */
@@ -1017,12 +1017,20 @@ type platformHooks = {
1017
1017
  ) => unit,
1018
1018
  // Phase 2: bind generateCommand inside Output.apply.
1019
1019
  mutationBindHook?: (~field: string, ~generateCommand: CommandGenerator.commandGenerator) => unit,
1020
- // InboundTranslationSlice — phase 1: register SDL + stub.
1021
- inboundMutationResolverHook?: (~fieldName: string, ~externalInputSchema: S.t<unknown>) => unit,
1020
+ // InboundTranslationSlice — phase 1: register SDL + stub. `permission` is the
1021
+ // door's rule (`Plugin_Structure.inboundDoorPermission`); `None` admits nobody.
1022
+ inboundMutationResolverHook?: (
1023
+ ~fieldName: string,
1024
+ ~externalInputSchema: S.t<unknown>,
1025
+ ~permission: Reventless.Authorization.permission=?,
1026
+ ) => unit,
1022
1027
  // InboundTranslationSlice — phase 2: bind receive.
1023
1028
  inboundMutationBindReceiveHook?: (
1024
1029
  ~fieldName: string,
1025
- ~receive: JSON.t => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
1030
+ ~receive: (
1031
+ JSON.t,
1032
+ ~caller: Reventless.Identity.t=?,
1033
+ ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
1026
1034
  ) => unit,
1027
1035
  // GraphQL type definitions.
1028
1036
  schemaTypeRegistrationHook?: array<string> => unit,