@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.
@@ -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 */
@@ -52,7 +52,7 @@ module Make = (
52
52
  let config = Reventless.ReadModel.config()
53
53
  let subIdConfig: option<Reventless.ReadModel.subIdConfig<state>> = None
54
54
  type role = Reventless.Role.name
55
- let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
55
+ let authorization: Reventless.Authorization.rule<role> = Reventless.OwnerScope.operators()
56
56
  let visibility: Reventless.Visibility.t = Public
57
57
  }
58
58