@reventlessdev/reventless-aws 3.0.0-alpha.383 → 3.0.0-alpha.385
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 +25 -0
- package/package.json +9 -9
- package/src/Platform.res +5 -0
- package/src/Platform.res.mjs +5 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +25 -4
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +96 -35
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +65 -18
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +46 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +29 -1
- package/src/components/Api/AppSync_Adapter.res +6 -9
- package/src/components/Api/AppSync_Adapter.res.mjs +9 -6
- package/src/components/InboundTranslationSlice_Builder.res +6 -0
- package/src/components/InboundTranslationSlice_Builder.res.mjs +9 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +25 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +25 -1
- package/tests/AppSync_AdapterTest.res +52 -0
- package/tests/AppSync_AdapterTest.res.mjs +35 -0
- package/tests/EpInboundTestSlice.res +1 -0
- package/tests/EpInboundTestSlice.res.mjs +3 -0
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.385 (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.384 (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.383 (2026-10-16)
|
|
7
32
|
|
|
8
33
|
* feat!(automation): schedules set by events ([53bc436](https://github.com/ReventlessDev/reventless-core/commit/53bc4363dffce81ef36475728bf0dae704716021))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.385",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -23,18 +23,18 @@
|
|
|
23
23
|
"uuid": "^13.0.0",
|
|
24
24
|
"yaml": "^2.8.3",
|
|
25
25
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.32",
|
|
26
|
-
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
27
26
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.12",
|
|
28
|
-
"@reventlessdev/rescript-
|
|
27
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.0",
|
|
29
28
|
"@reventlessdev/rescript-node": "2.0.0-alpha.22",
|
|
30
|
-
"@reventlessdev/rescript-pulumi-
|
|
29
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.30",
|
|
31
30
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
|
|
31
|
+
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
32
32
|
"@reventlessdev/rescript-web": "1.0.0-alpha.7",
|
|
33
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
34
|
-
"@reventlessdev/reventless-
|
|
35
|
-
"@reventlessdev/reventless-
|
|
36
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
37
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
33
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.195",
|
|
34
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.298",
|
|
35
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.41",
|
|
36
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.162",
|
|
37
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.167"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"rescript": "12.3.1",
|
package/src/Platform.res
CHANGED
|
@@ -148,6 +148,11 @@ let groupOf = Reventless.Role.groupOf
|
|
|
148
148
|
/** `groupOf(Role.admin)`. */
|
|
149
149
|
let adminGroup = Reventless.Role.adminGroup
|
|
150
150
|
|
|
151
|
+
/** The plugin's webhook base URL — each inbound slice's endpoint is its `path`
|
|
152
|
+
under it — or `""` when no slice declares one. Read after `deployPlugin`, to
|
|
153
|
+
export it and register it with the sender. */
|
|
154
|
+
let webhookUrl = (): Pulumi.Output.t<string> => PluginRuntime_Builder.webhookUrlRef.contents
|
|
155
|
+
|
|
151
156
|
module MakeWithConfig = (
|
|
152
157
|
Config: {
|
|
153
158
|
let splitApi: bool
|
package/src/Platform.res.mjs
CHANGED
|
@@ -171,6 +171,10 @@ function loadProvidedGroups() {
|
|
|
171
171
|
return Platform_ProvidedGroups$ReventlessAws.load(platformStackReference());
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
function webhookUrl() {
|
|
175
|
+
return PluginRuntime_Builder$ReventlessAws.webhookUrlRef.contents;
|
|
176
|
+
}
|
|
177
|
+
|
|
174
178
|
function MakeWithConfig(Config) {
|
|
175
179
|
OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
|
|
176
180
|
Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
|
|
@@ -3008,6 +3012,7 @@ export {
|
|
|
3008
3012
|
loadProvidedGroups,
|
|
3009
3013
|
groupOf,
|
|
3010
3014
|
adminGroup,
|
|
3015
|
+
webhookUrl,
|
|
3011
3016
|
MakeWithConfig,
|
|
3012
3017
|
Make,
|
|
3013
3018
|
}
|
|
@@ -30,7 +30,7 @@ import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessde
|
|
|
30
30
|
// per-slice handler building (functor + decode + handleCommands), all
|
|
31
31
|
// compiler-checked against the framework signatures (see the module header and
|
|
32
32
|
// docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md).
|
|
33
|
-
import { deriveScope, commandTypeNames, makeStorageOps, buildSliceHandler, buildInboundReceiver } from "./DcbCommandTopicEntryPoint_Ops.res.mjs";
|
|
33
|
+
import { deriveScope, commandTypeNames, makeStorageOps, buildSliceHandler, buildInboundReceiver, buildWebhookSlice, serveWebhook } from "./DcbCommandTopicEntryPoint_Ops.res.mjs";
|
|
34
34
|
import { makeDynamoQueryDbOps } from "./QueryDbEntryPoint_Ops.res.mjs";
|
|
35
35
|
|
|
36
36
|
const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
|
|
@@ -177,6 +177,8 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
177
177
|
// follow-up, so on that backend the receiver runs without persistence.
|
|
178
178
|
const inboundModules = config.inboundTranslationSliceModules || [];
|
|
179
179
|
const inboundReceiversByField = {};
|
|
180
|
+
// Route W registry: the slices whose spec declares an endpoint.
|
|
181
|
+
const webhookSlices = [];
|
|
180
182
|
await Promise.all(inboundModules.map(async ({ spec, translation, auditTableName }) => {
|
|
181
183
|
const specModule = await loadModule(spec);
|
|
182
184
|
const translationModule = await loadModule(translation);
|
|
@@ -203,6 +205,8 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
203
205
|
publishJsons,
|
|
204
206
|
auditOps,
|
|
205
207
|
);
|
|
208
|
+
const webhook = buildWebhookSlice(specModule, translationModule, publishJsons, auditOps);
|
|
209
|
+
if (webhook != null) webhookSlices.push(webhook);
|
|
206
210
|
}));
|
|
207
211
|
|
|
208
212
|
// Sync (default): inline-dispatch the command via the same composite handler
|
|
@@ -276,8 +280,9 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
276
280
|
// Elements past the first two are additive — existing callers (the integration
|
|
277
281
|
// tests) keep destructuring the first two. No deploy-time `plugin` fragment
|
|
278
282
|
// needed here: this Lambda serves exactly one plugin and HANDLER_CONFIG already
|
|
279
|
-
// names it. The 5th element is the Route 0 inbound-translation registry
|
|
280
|
-
|
|
283
|
+
// names it. The 5th element is the Route 0 inbound-translation registry, the
|
|
284
|
+
// 6th the Route W webhook slices.
|
|
285
|
+
return [sqsHandler, cmdGenHandler, dcbComp(config.pluginName), config.pluginName, inboundReceiversByField, webhookSlices];
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
async function buildHandler() {
|
|
@@ -313,7 +318,23 @@ initPromise.catch(() => {});
|
|
|
313
318
|
|
|
314
319
|
export async function handler(event, context) {
|
|
315
320
|
setRequestId(context?.awsRequestId);
|
|
316
|
-
const [sqsHandler, cmdGenHandler, comp, plugin, inboundReceivers] = await initPromise;
|
|
321
|
+
const [sqsHandler, cmdGenHandler, comp, plugin, inboundReceivers, webhookSlices] = await initPromise;
|
|
322
|
+
|
|
323
|
+
// Route W: a request on the Lambda's Function URL, for an inbound slice's
|
|
324
|
+
// webhook. The body stays raw until its signature is checked.
|
|
325
|
+
if (event.requestContext?.http != null && typeof event.rawPath === "string") {
|
|
326
|
+
const body = event.body == null
|
|
327
|
+
? ""
|
|
328
|
+
: event.isBase64Encoded ? Buffer.from(event.body, "base64").toString("utf8") : event.body;
|
|
329
|
+
const headers = {};
|
|
330
|
+
for (const [name, value] of Object.entries(event.headers ?? {})) headers[name.toLowerCase()] = value;
|
|
331
|
+
return await serveWebhook(webhookSlices ?? [], {
|
|
332
|
+
method: event.requestContext.http.method,
|
|
333
|
+
path: event.rawPath,
|
|
334
|
+
headers,
|
|
335
|
+
body,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
317
338
|
|
|
318
339
|
// Route 0: InboundTranslationSlice mutation — the AppSync resolver invokes this
|
|
319
340
|
// Lambda with `{__inboundTranslation: true, fieldName, arguments, identity}`.
|
|
@@ -258,7 +258,14 @@ type inboundCallback = {
|
|
|
258
258
|
ReventlessInfra.CommandTopic.publishJsons,
|
|
259
259
|
JSON.t,
|
|
260
260
|
~caller: Reventless.Identity.t=?,
|
|
261
|
+
~door: ReventlessInfra.InboundTranslationSlice.door=?,
|
|
261
262
|
) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
|
|
263
|
+
refuse: (
|
|
264
|
+
~body: string,
|
|
265
|
+
~errorCode: string,
|
|
266
|
+
string,
|
|
267
|
+
) => ReventlessInfra.InboundTranslationSlice.receiveResult,
|
|
268
|
+
webhook: option<ReventlessCore.InboundTranslationSlice_Webhook.route>,
|
|
262
269
|
auditLog: dict<ReventlessCore.InboundTranslationSlice_Callback.auditRow>,
|
|
263
270
|
}
|
|
264
271
|
@module(
|
|
@@ -266,9 +273,49 @@ type inboundCallback = {
|
|
|
266
273
|
)
|
|
267
274
|
external makeInboundCallback: specModule => translationModule => inboundCallback = "Make"
|
|
268
275
|
|
|
276
|
+
// The request's audit row to its table, when one was threaded. A failed write is
|
|
277
|
+
// logged, not raised: the commands are published.
|
|
278
|
+
let persistAudit = async (
|
|
279
|
+
callback: inboundCallback,
|
|
280
|
+
auditQueryDbOps: option<ReventlessCore.QueryDb_Adapter.operations>,
|
|
281
|
+
result: ReventlessInfra.InboundTranslationSlice.receiveResult,
|
|
282
|
+
) =>
|
|
283
|
+
switch auditQueryDbOps {
|
|
284
|
+
| Some(ops) =>
|
|
285
|
+
let id = result->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
|
|
286
|
+
switch callback.auditLog->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(id) {
|
|
287
|
+
| Some(row) =>
|
|
288
|
+
let json =
|
|
289
|
+
row->Reventless.Util_Sury.toJson(
|
|
290
|
+
ReventlessCore.InboundTranslationSlice_Callback.auditRowSchema,
|
|
291
|
+
)
|
|
292
|
+
// `save` resolves `Error(_)` rather than throwing, so its result is read.
|
|
293
|
+
try {
|
|
294
|
+
switch await ops.save(id, json, ReventlessCore.QueryDb.Overwrite, None) {
|
|
295
|
+
| Ok() => ()
|
|
296
|
+
| Error(err) =>
|
|
297
|
+
ReventlessCore.EffectLogger.logError(
|
|
298
|
+
~comp="InboundTranslationSlice.audit",
|
|
299
|
+
`failed to persist audit row ${id}: ${err->ReventlessCore.QueryDb.storageErrorToString}`,
|
|
300
|
+
)->Effect.runSync
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
| exn =>
|
|
304
|
+
let detail =
|
|
305
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown error")
|
|
306
|
+
ReventlessCore.EffectLogger.logError(
|
|
307
|
+
~comp="InboundTranslationSlice.audit",
|
|
308
|
+
`failed to persist audit row ${id}: ${detail}`,
|
|
309
|
+
)->Effect.runSync
|
|
310
|
+
}
|
|
311
|
+
| None => ()
|
|
312
|
+
}
|
|
313
|
+
| None => ()
|
|
314
|
+
}
|
|
315
|
+
|
|
269
316
|
// Builds the inbound receive handler: `receive` for the resolver's caller, then
|
|
270
|
-
// the request's audit row
|
|
271
|
-
//
|
|
317
|
+
// the request's audit row. Answers the `commandOutcome` JSON the AppSync
|
|
318
|
+
// direct-invocation route answers with.
|
|
272
319
|
let buildInboundReceiver = (
|
|
273
320
|
spec: specModule,
|
|
274
321
|
translation: translationModule,
|
|
@@ -282,42 +329,56 @@ let buildInboundReceiver = (
|
|
|
282
329
|
args,
|
|
283
330
|
~caller=ReventlessCore.InboundTranslationSlice_Callback.doorCaller(identity),
|
|
284
331
|
)
|
|
285
|
-
|
|
286
|
-
| Some(ops) =>
|
|
287
|
-
let id = result->ReventlessCore.InboundTranslationSlice_Callback.requestIdOf
|
|
288
|
-
switch callback.auditLog->ReventlessCore.InboundTranslationSlice_Callback.takeAuditRow(id) {
|
|
289
|
-
| Some(row) =>
|
|
290
|
-
let json =
|
|
291
|
-
row->Reventless.Util_Sury.toJson(
|
|
292
|
-
ReventlessCore.InboundTranslationSlice_Callback.auditRowSchema,
|
|
293
|
-
)
|
|
294
|
-
// A failed audit write is logged, not raised: the commands are published.
|
|
295
|
-
// `save` resolves `Error(_)` rather than throwing, so its result is read.
|
|
296
|
-
try {
|
|
297
|
-
switch await ops.save(id, json, ReventlessCore.QueryDb.Overwrite, None) {
|
|
298
|
-
| Ok() => ()
|
|
299
|
-
| Error(err) =>
|
|
300
|
-
ReventlessCore.EffectLogger.logError(
|
|
301
|
-
~comp="InboundTranslationSlice.audit",
|
|
302
|
-
`failed to persist audit row ${id}: ${err->ReventlessCore.QueryDb.storageErrorToString}`,
|
|
303
|
-
)->Effect.runSync
|
|
304
|
-
}
|
|
305
|
-
} catch {
|
|
306
|
-
| exn =>
|
|
307
|
-
let detail =
|
|
308
|
-
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown error")
|
|
309
|
-
ReventlessCore.EffectLogger.logError(
|
|
310
|
-
~comp="InboundTranslationSlice.audit",
|
|
311
|
-
`failed to persist audit row ${id}: ${detail}`,
|
|
312
|
-
)->Effect.runSync
|
|
313
|
-
}
|
|
314
|
-
| None => ()
|
|
315
|
-
}
|
|
316
|
-
| None => ()
|
|
317
|
-
}
|
|
332
|
+
await persistAudit(callback, auditQueryDbOps, result)
|
|
318
333
|
result
|
|
319
334
|
->ReventlessCore.InboundTranslationSlice_Callback.receiveResultToOutcome
|
|
320
335
|
// `CommandTopic_Helpers`, not `CommandTopic`, which imports `@pulumi/pulumi`.
|
|
321
336
|
->ReventlessCore.CommandTopic_Helpers.commandOutcomeToJson
|
|
322
337
|
}
|
|
323
338
|
}
|
|
339
|
+
|
|
340
|
+
// The slice's webhook door, when its spec declares an endpoint: both operations
|
|
341
|
+
// persist the audit row they write.
|
|
342
|
+
let buildWebhookSlice = (
|
|
343
|
+
spec: specModule,
|
|
344
|
+
translation: translationModule,
|
|
345
|
+
~publishJsons: ReventlessInfra.CommandTopic.publishJsons,
|
|
346
|
+
~auditQueryDbOps: option<ReventlessCore.QueryDb_Adapter.operations>,
|
|
347
|
+
): Nullable.t<ReventlessCore.InboundTranslationSlice_Webhook.slice> => {
|
|
348
|
+
let callback = makeInboundCallback(spec)(translation)
|
|
349
|
+
let persisted = async result => {
|
|
350
|
+
await persistAudit(callback, auditQueryDbOps, result)
|
|
351
|
+
result
|
|
352
|
+
}
|
|
353
|
+
callback.webhook
|
|
354
|
+
->Option.map((route): ReventlessCore.InboundTranslationSlice_Webhook.slice => {
|
|
355
|
+
route,
|
|
356
|
+
receive: async (input, ~caller) =>
|
|
357
|
+
await persisted(await callback.receive(publishJsons, input, ~caller, ~door=Webhook)),
|
|
358
|
+
refuse: async (~body, ~errorCode, error) =>
|
|
359
|
+
await persisted(callback.refuse(~body, ~errorCode, error)),
|
|
360
|
+
})
|
|
361
|
+
->Nullable.fromOption
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Serve a Function URL request on the plugin's webhook slices. Answers the
|
|
365
|
+
response shape a Function URL expects. */
|
|
366
|
+
let serveWebhook = async (
|
|
367
|
+
slices: array<ReventlessCore.InboundTranslationSlice_Webhook.slice>,
|
|
368
|
+
request: ReventlessCore.InboundTranslationSlice_Webhook.request,
|
|
369
|
+
): JSON.t => {
|
|
370
|
+
let {status, body} = await ReventlessCore.InboundTranslationSlice_Webhook.dispatch(
|
|
371
|
+
slices,
|
|
372
|
+
request,
|
|
373
|
+
)
|
|
374
|
+
Dict.fromArray([
|
|
375
|
+
("statusCode", JSON.Encode.int(status)),
|
|
376
|
+
(
|
|
377
|
+
"headers",
|
|
378
|
+
Dict.fromArray([
|
|
379
|
+
("content-type", JSON.Encode.string("application/json")),
|
|
380
|
+
])->JSON.Encode.object,
|
|
381
|
+
),
|
|
382
|
+
("body", JSON.Encode.string(body)),
|
|
383
|
+
])->JSON.Encode.object
|
|
384
|
+
}
|
|
@@ -5,6 +5,7 @@ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.
|
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import * as Stream from "effect/Stream";
|
|
8
|
+
import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
8
9
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
9
10
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
10
11
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
@@ -13,6 +14,7 @@ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Mess
|
|
|
13
14
|
import * as QueryDb$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb.res.mjs";
|
|
14
15
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
15
16
|
import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
17
|
+
import * as InboundTranslationSlice_Webhook$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Webhook.res.mjs";
|
|
16
18
|
import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
|
|
17
19
|
import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
18
20
|
import * as DcbEventLogStorage_Postgres_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs";
|
|
@@ -118,31 +120,73 @@ function buildSliceHandler(spec, behavior, scope, dcbEventLog) {
|
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
async function persistAudit(callback, auditQueryDbOps, result) {
|
|
124
|
+
if (auditQueryDbOps === undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
let id = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result);
|
|
128
|
+
let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(callback.auditLog, id);
|
|
129
|
+
if (row === undefined) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let json = Util_Sury$Reventless.toJson(row, InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema);
|
|
133
|
+
try {
|
|
134
|
+
let err = await auditQueryDbOps.save(id, json, "Overwrite", undefined);
|
|
135
|
+
if (err.TAG === "Ok") {
|
|
136
|
+
return;
|
|
137
|
+
} else {
|
|
138
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError("InboundTranslationSlice.audit", undefined, `failed to persist audit row ` + id + `: ` + QueryDb$ReventlessCore.storageErrorToString(err._0)));
|
|
139
|
+
}
|
|
140
|
+
} catch (raw_exn) {
|
|
141
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
142
|
+
let detail = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown error");
|
|
143
|
+
return Effect.runSync(EffectLogger$ReventlessCore.logError("InboundTranslationSlice.audit", undefined, `failed to persist audit row ` + id + `: ` + detail));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
121
147
|
function buildInboundReceiver(spec, translation, publishJsons, auditQueryDbOps) {
|
|
122
148
|
let callback = InboundTranslationSlice_CallbackResMjs.Make(spec)(translation);
|
|
123
149
|
return async (args, identity) => {
|
|
124
|
-
let result = await callback.receive(publishJsons, args, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(identity));
|
|
125
|
-
|
|
126
|
-
let id = InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result);
|
|
127
|
-
let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(callback.auditLog, id);
|
|
128
|
-
if (row !== undefined) {
|
|
129
|
-
let json = Util_Sury$Reventless.toJson(row, InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema);
|
|
130
|
-
try {
|
|
131
|
-
let err = await auditQueryDbOps.save(id, json, "Overwrite", undefined);
|
|
132
|
-
if (err.TAG !== "Ok") {
|
|
133
|
-
Effect.runSync(EffectLogger$ReventlessCore.logError("InboundTranslationSlice.audit", undefined, `failed to persist audit row ` + id + `: ` + QueryDb$ReventlessCore.storageErrorToString(err._0)));
|
|
134
|
-
}
|
|
135
|
-
} catch (raw_exn) {
|
|
136
|
-
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
137
|
-
let detail = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown error");
|
|
138
|
-
Effect.runSync(EffectLogger$ReventlessCore.logError("InboundTranslationSlice.audit", undefined, `failed to persist audit row ` + id + `: ` + detail));
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
150
|
+
let result = await callback.receive(publishJsons, args, InboundTranslationSlice_Callback$ReventlessCore.doorCaller(identity), undefined);
|
|
151
|
+
await persistAudit(callback, auditQueryDbOps, result);
|
|
142
152
|
return CommandTopic_Helpers$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result));
|
|
143
153
|
};
|
|
144
154
|
}
|
|
145
155
|
|
|
156
|
+
function buildWebhookSlice(spec, translation, publishJsons, auditQueryDbOps) {
|
|
157
|
+
let callback = InboundTranslationSlice_CallbackResMjs.Make(spec)(translation);
|
|
158
|
+
let persisted = async result => {
|
|
159
|
+
await persistAudit(callback, auditQueryDbOps, result);
|
|
160
|
+
return result;
|
|
161
|
+
};
|
|
162
|
+
return Stdlib_Nullable.fromOption(Stdlib_Option.map(callback.webhook, route => ({
|
|
163
|
+
route: route,
|
|
164
|
+
receive: async (input, caller) => await persisted(await callback.receive(publishJsons, input, caller, "Webhook")),
|
|
165
|
+
refuse: async (body, errorCode, error) => await persisted(callback.refuse(body, errorCode, error))
|
|
166
|
+
})));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function serveWebhook(slices, request) {
|
|
170
|
+
let match = await InboundTranslationSlice_Webhook$ReventlessCore.dispatch(slices, undefined, undefined, request);
|
|
171
|
+
return Object.fromEntries([
|
|
172
|
+
[
|
|
173
|
+
"statusCode",
|
|
174
|
+
match.status
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
"headers",
|
|
178
|
+
Object.fromEntries([[
|
|
179
|
+
"content-type",
|
|
180
|
+
"application/json"
|
|
181
|
+
]])
|
|
182
|
+
],
|
|
183
|
+
[
|
|
184
|
+
"body",
|
|
185
|
+
match.body
|
|
186
|
+
]
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
|
|
146
190
|
export {
|
|
147
191
|
deriveScope,
|
|
148
192
|
commandTypeNames,
|
|
@@ -150,6 +194,9 @@ export {
|
|
|
150
194
|
makePostgresStorageOps,
|
|
151
195
|
makeStorageOps,
|
|
152
196
|
buildSliceHandler,
|
|
197
|
+
persistAudit,
|
|
153
198
|
buildInboundReceiver,
|
|
199
|
+
buildWebhookSlice,
|
|
200
|
+
serveWebhook,
|
|
154
201
|
}
|
|
155
202
|
/* Stdlib_JsExn Not a pure module */
|
|
@@ -41,6 +41,11 @@ let forDcbCommandTopic = (
|
|
|
41
41
|
// the per-flavor commandHandlerConfig below (whichever is higher wins).
|
|
42
42
|
~sliceMemoryFloor: int=0,
|
|
43
43
|
~sliceTimeoutFloor: int=0,
|
|
44
|
+
// The webhook secrets, by environment variable, when some inbound slice declares
|
|
45
|
+
// an endpoint: the sync Lambda then gets a public Function URL, handed to
|
|
46
|
+
// `onWebhookUrl`. The entry point routes its requests by path.
|
|
47
|
+
~webhookSecrets: option<dict<Pulumi.Output.t<string>>>=?,
|
|
48
|
+
~onWebhookUrl: Pulumi.Output.t<string> => unit=_ => (),
|
|
44
49
|
~connect,
|
|
45
50
|
dcbCommandTopic,
|
|
46
51
|
) =>
|
|
@@ -107,6 +112,12 @@ let forDcbCommandTopic = (
|
|
|
107
112
|
}
|
|
108
113
|
})
|
|
109
114
|
)
|
|
115
|
+
let webhookSecrets = isAsync ? None : webhookSecrets
|
|
116
|
+
webhookSecrets->Option.forEach(secrets =>
|
|
117
|
+
secrets->Dict.forEachWithKey((value, key) =>
|
|
118
|
+
envVars->Dict.set(key, value->Pulumi.Output.asInput)
|
|
119
|
+
)
|
|
120
|
+
)
|
|
110
121
|
// First-class log-level override → LOG_LEVEL, winning over any generic
|
|
111
122
|
// envVars entry. When unset, makeFromCodeAsset applies the tier default.
|
|
112
123
|
cfg.logLevel->Option.forEach(level =>
|
|
@@ -319,6 +330,41 @@ let forDcbCommandTopic = (
|
|
|
319
330
|
)
|
|
320
331
|
})
|
|
321
332
|
|
|
333
|
+
if webhookSecrets->Option.isSome {
|
|
334
|
+
let customOpts =
|
|
335
|
+
opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
336
|
+
let functionName =
|
|
337
|
+
runtime.parts.lambda->Pulumi.Output.flatMap(lambda => lambda.name)->Pulumi.Output.asInput
|
|
338
|
+
let url = PulumiAws.FunctionUrl.make(
|
|
339
|
+
~name=`${name}-webhook`,
|
|
340
|
+
~args={authorizationType: None, functionName},
|
|
341
|
+
~opts=customOpts,
|
|
342
|
+
)
|
|
343
|
+
// A public URL needs both grants: the URL invoke, and the function invoke
|
|
344
|
+
// limited to calls arriving through that URL.
|
|
345
|
+
let _ = PulumiAws.Lambda.Permission.make(
|
|
346
|
+
~name=`${name}-webhookUrl`,
|
|
347
|
+
~args={
|
|
348
|
+
action: "lambda:InvokeFunctionUrl",
|
|
349
|
+
function: functionName,
|
|
350
|
+
principal: "*",
|
|
351
|
+
functionUrlAuthType: "NONE",
|
|
352
|
+
},
|
|
353
|
+
~opts=customOpts,
|
|
354
|
+
)
|
|
355
|
+
let _ = PulumiAws.Lambda.Permission.make(
|
|
356
|
+
~name=`${name}-webhookInvoke`,
|
|
357
|
+
~args={
|
|
358
|
+
action: "lambda:InvokeFunction",
|
|
359
|
+
function: functionName,
|
|
360
|
+
principal: "*",
|
|
361
|
+
invokedViaFunctionUrl: true,
|
|
362
|
+
},
|
|
363
|
+
~opts=customOpts,
|
|
364
|
+
)
|
|
365
|
+
onWebhookUrl(url.functionUrl)
|
|
366
|
+
}
|
|
367
|
+
|
|
322
368
|
connect(~runtime)
|
|
323
369
|
CommandTopicChannel.SQS.clearBatchSize()
|
|
324
370
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Aws from "@pulumi/aws";
|
|
4
4
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
5
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
7
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
8
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
@@ -11,16 +12,18 @@ import * as DcbBackend$ReventlessAws from "../DcbEventLog/DcbBackend.res.mjs";
|
|
|
11
12
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
13
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
13
14
|
import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
|
|
15
|
+
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
14
16
|
import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
|
|
15
17
|
import * as CommandTopicChannel_SQS$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS.res.mjs";
|
|
16
18
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
|
|
17
19
|
|
|
18
20
|
let log = Logger$ReventlessCore.fromEnv();
|
|
19
21
|
|
|
20
|
-
function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginName, syncConfig, asyncConfig, sliceMemoryFloorOpt, sliceTimeoutFloorOpt, connect, dcbCommandTopic) {
|
|
22
|
+
function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginName, syncConfig, asyncConfig, sliceMemoryFloorOpt, sliceTimeoutFloorOpt, webhookSecrets, onWebhookUrlOpt, connect, dcbCommandTopic) {
|
|
21
23
|
let inboundSlices = inboundSlicesOpt !== undefined ? inboundSlicesOpt : [];
|
|
22
24
|
let sliceMemoryFloor = sliceMemoryFloorOpt !== undefined ? sliceMemoryFloorOpt : 0;
|
|
23
25
|
let sliceTimeoutFloor = sliceTimeoutFloorOpt !== undefined ? sliceTimeoutFloorOpt : 0;
|
|
26
|
+
let onWebhookUrl = onWebhookUrlOpt !== undefined ? onWebhookUrlOpt : param => {};
|
|
24
27
|
if (slicePaths.length === 0 && inboundSlices.length === 0) {
|
|
25
28
|
return log.warn("StateChangeSliceRuntime_Builder_Single", undefined, "forDcbCommandTopic skipped (no slice specs)");
|
|
26
29
|
}
|
|
@@ -54,6 +57,10 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
|
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
56
59
|
}));
|
|
60
|
+
let webhookSecrets$1 = isAsync ? undefined : webhookSecrets;
|
|
61
|
+
Stdlib_Option.forEach(webhookSecrets$1, secrets => Stdlib_Dict.forEachWithKey(secrets, (value, key) => {
|
|
62
|
+
envVars[key] = value;
|
|
63
|
+
}));
|
|
57
64
|
Stdlib_Option.forEach(cfg.logLevel, level => {
|
|
58
65
|
envVars["LOG_LEVEL"] = Pulumi.output(level);
|
|
59
66
|
});
|
|
@@ -129,6 +136,27 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
|
|
|
129
136
|
}])),
|
|
130
137
|
role: runtime.parts.lambdaRole.id
|
|
131
138
|
}));
|
|
139
|
+
if (Stdlib_Option.isSome(webhookSecrets$1)) {
|
|
140
|
+
let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
141
|
+
let functionName = Output$Pulumi.flatMap(runtime.parts.lambda, lambda => lambda.name);
|
|
142
|
+
let url = new (Aws.lambda.FunctionUrl)(name + `-webhook`, {
|
|
143
|
+
authorizationType: "NONE",
|
|
144
|
+
functionName: functionName
|
|
145
|
+
}, customOpts);
|
|
146
|
+
new (Aws.lambda.Permission)(name + `-webhookUrl`, {
|
|
147
|
+
action: "lambda:InvokeFunctionUrl",
|
|
148
|
+
function: functionName,
|
|
149
|
+
principal: "*",
|
|
150
|
+
functionUrlAuthType: "NONE"
|
|
151
|
+
}, customOpts);
|
|
152
|
+
new (Aws.lambda.Permission)(name + `-webhookInvoke`, {
|
|
153
|
+
action: "lambda:InvokeFunction",
|
|
154
|
+
function: functionName,
|
|
155
|
+
principal: "*",
|
|
156
|
+
invokedViaFunctionUrl: true
|
|
157
|
+
}, customOpts);
|
|
158
|
+
onWebhookUrl(url.functionUrl);
|
|
159
|
+
}
|
|
132
160
|
connect(runtime);
|
|
133
161
|
CommandTopicChannel_SQS$ReventlessAws.clearBatchSize();
|
|
134
162
|
}
|
|
@@ -309,21 +309,18 @@ let injectAwsAuth = (
|
|
|
309
309
|
iamQueryFieldPrefixes->Array.push(entry.listFieldName)
|
|
310
310
|
iamTypePrefixes->Array.push(entry.returnTypeName)
|
|
311
311
|
}
|
|
312
|
+
// Every door the entry emits: the resolvers here do not check the rule, so a
|
|
313
|
+
// by-ids or by-index field left to the open directive reads the gated rows.
|
|
314
|
+
let fieldNames = ReventlessCore.GraphQL_FragmentGenerator.queryFieldNamesOf(entry)
|
|
312
315
|
switch entry.authorization {
|
|
313
|
-
| Some({group}) =>
|
|
314
|
-
queryAuthMap->Dict.set(entry.singleFieldName, [group])
|
|
315
|
-
queryAuthMap->Dict.set(entry.listFieldName, [group])
|
|
316
|
+
| Some({group}) => fieldNames->Array.forEach(f => queryAuthMap->Dict.set(f, [group]))
|
|
316
317
|
| None => ()
|
|
317
318
|
}
|
|
318
319
|
switch entry.permission {
|
|
319
320
|
| Some(permission) =>
|
|
320
321
|
switch AppSync_SdlDecorate.permissionToGate(permission) {
|
|
321
|
-
| Groups(groups) =>
|
|
322
|
-
|
|
323
|
-
queryAuthMap->Dict.set(entry.listFieldName, groups)
|
|
324
|
-
| AnyAuthenticated =>
|
|
325
|
-
queryAuthMap->Dict.delete(entry.singleFieldName)
|
|
326
|
-
queryAuthMap->Dict.delete(entry.listFieldName)
|
|
322
|
+
| Groups(groups) => fieldNames->Array.forEach(f => queryAuthMap->Dict.set(f, groups))
|
|
323
|
+
| AnyAuthenticated => fieldNames->Array.forEach(f => queryAuthMap->Dict.delete(f))
|
|
327
324
|
| Anonymous =>
|
|
328
325
|
// Both derived field names, because the spec declares one permission and
|
|
329
326
|
// the generator emits two fields from it; naming only one would send the
|
|
@@ -154,11 +154,13 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
154
154
|
iamQueryFieldPrefixes.push(entry.listFieldName);
|
|
155
155
|
iamTypePrefixes.push(entry.returnTypeName);
|
|
156
156
|
}
|
|
157
|
+
let fieldNames = GraphQL_FragmentGenerator$ReventlessCore.queryFieldNamesOf(entry);
|
|
157
158
|
let match = entry.authorization;
|
|
158
159
|
if (match !== undefined) {
|
|
159
160
|
let group = match.group;
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
fieldNames.forEach(f => {
|
|
162
|
+
queryAuthMap[f] = [group];
|
|
163
|
+
});
|
|
162
164
|
}
|
|
163
165
|
let permission = entry.permission;
|
|
164
166
|
if (permission === undefined) {
|
|
@@ -167,16 +169,17 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
167
169
|
let groups = AppSync_SdlDecorate$ReventlessAws.permissionToGate(permission);
|
|
168
170
|
if (typeof groups !== "object") {
|
|
169
171
|
if (groups === "AnyAuthenticated") {
|
|
170
|
-
Stdlib_Dict.$$delete(queryAuthMap,
|
|
171
|
-
return
|
|
172
|
+
fieldNames.forEach(f => Stdlib_Dict.$$delete(queryAuthMap, f));
|
|
173
|
+
return;
|
|
172
174
|
}
|
|
173
175
|
anonymousFields.push(entry.singleFieldName);
|
|
174
176
|
anonymousFields.push(entry.listFieldName);
|
|
175
177
|
return;
|
|
176
178
|
} else {
|
|
177
179
|
let groups$1 = groups._0;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
fieldNames.forEach(f => {
|
|
181
|
+
queryAuthMap[f] = groups$1;
|
|
182
|
+
});
|
|
180
183
|
return;
|
|
181
184
|
}
|
|
182
185
|
});
|
|
@@ -25,6 +25,12 @@ module Make = (
|
|
|
25
25
|
~specPath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
|
|
26
26
|
~translationPath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
|
|
27
27
|
)
|
|
28
|
+
switch Spec.endpoint {
|
|
29
|
+
| Some({auth: SignedBody({secret})}) =>
|
|
30
|
+
PluginRuntime_Builder.registerWebhook(~specName=Spec.name, ~secret=Some(secret))
|
|
31
|
+
| Some({auth: Open}) => PluginRuntime_Builder.registerWebhook(~specName=Spec.name, ~secret=None)
|
|
32
|
+
| None => ()
|
|
33
|
+
}
|
|
28
34
|
module InnerMake = Inner.Make(Spec, Translation)
|
|
29
35
|
module Spec = Spec
|
|
30
36
|
module Translation = Translation
|
|
@@ -15,6 +15,15 @@ function Make(Api) {
|
|
|
15
15
|
})(Api);
|
|
16
16
|
let Make$1 = Spec => (Translation => {
|
|
17
17
|
PluginRuntime_Builder$ReventlessAws.registerInboundTranslationSliceSpec(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl));
|
|
18
|
+
let match = Spec.endpoint;
|
|
19
|
+
if (match !== undefined) {
|
|
20
|
+
let match$1 = match.auth;
|
|
21
|
+
if (typeof match$1 !== "object") {
|
|
22
|
+
PluginRuntime_Builder$ReventlessAws.registerWebhook(Spec.name, undefined);
|
|
23
|
+
} else {
|
|
24
|
+
PluginRuntime_Builder$ReventlessAws.registerWebhook(Spec.name, match$1._0.secret);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
18
27
|
let InnerMake = Inner.Make(Spec)(Translation);
|
|
19
28
|
let make = (publishJsons, runtime, opts) => {
|
|
20
29
|
let component = InnerMake.make(publishJsons, runtime, opts);
|
|
@@ -161,6 +161,29 @@ let registerInboundAuditTableName = (~specName: string, tableName: Pulumi.Output
|
|
|
161
161
|
)
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// Webhook endpoints. Each slice's route is read off its spec at run time, so the
|
|
165
|
+
// deploy needs only whether any slice has one (the sync command Lambda then gets a
|
|
166
|
+
// Function URL) and the secrets they name, which become that Lambda's environment.
|
|
167
|
+
// A secret is read when the slice registers, outside any `apply`, so a missing one
|
|
168
|
+
// fails `pulumi preview` too.
|
|
169
|
+
let webhookSlices: array<string> = []
|
|
170
|
+
let webhookSecrets: dict<Pulumi.Output.t<string>> = Dict.make()
|
|
171
|
+
|
|
172
|
+
let registerWebhook = (~specName: string, ~secret: option<string>) => {
|
|
173
|
+
if !(webhookSlices->Array.includes(specName)) {
|
|
174
|
+
webhookSlices->Array.push(specName)
|
|
175
|
+
}
|
|
176
|
+
secret->Option.forEach(secret =>
|
|
177
|
+
webhookSecrets->Dict.set(
|
|
178
|
+
ReventlessCore.InboundTranslationSlice_Webhook.secretEnvKey(secret),
|
|
179
|
+
Pulumi.Config.make(Some("webhook"))->Pulumi.Config.requireSecret(secret),
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The plugin's webhook base URL, once its command Lambda has one; "" until then.
|
|
185
|
+
let webhookUrlRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
|
|
186
|
+
|
|
164
187
|
/**
|
|
165
188
|
Redundant with the seams that populate `dcbConfigRef` automatically:
|
|
166
189
|
`pluginName` is set by `registerPluginName` (called from `Plugin_Builder.make`
|
|
@@ -933,6 +956,8 @@ module Make = (
|
|
|
933
956
|
StateChangeSliceRuntime_Builder_Single.forDcbCommandTopic(
|
|
934
957
|
~slicePaths,
|
|
935
958
|
~inboundSlices,
|
|
959
|
+
~webhookSecrets=?webhookSlices->Array.length > 0 ? Some(webhookSecrets) : None,
|
|
960
|
+
~onWebhookUrl=url => webhookUrlRef := url,
|
|
936
961
|
~dcbTableName=dcbConfig.dcbTableName,
|
|
937
962
|
~pluginName=dcbConfig.pluginName,
|
|
938
963
|
~syncConfig=syncStateChangesConfigRef.contents,
|
|
@@ -22,6 +22,7 @@ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/s
|
|
|
22
22
|
import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
|
|
23
23
|
import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
|
|
24
24
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
25
|
+
import * as InboundTranslationSlice_Webhook$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Webhook.res.mjs";
|
|
25
26
|
import * as StateChangeSliceRuntime_Builder_Single$ReventlessAws from "../../adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs";
|
|
26
27
|
|
|
27
28
|
let log = Logger$ReventlessCore.fromEnv();
|
|
@@ -139,6 +140,23 @@ function registerInboundAuditTableName(specName, tableName) {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
let webhookSlices = [];
|
|
144
|
+
|
|
145
|
+
let webhookSecrets = {};
|
|
146
|
+
|
|
147
|
+
function registerWebhook(specName, secret) {
|
|
148
|
+
if (!webhookSlices.includes(specName)) {
|
|
149
|
+
webhookSlices.push(specName);
|
|
150
|
+
}
|
|
151
|
+
Stdlib_Option.forEach(secret, secret => {
|
|
152
|
+
webhookSecrets[InboundTranslationSlice_Webhook$ReventlessCore.secretEnvKey(secret)] = new Pulumi.Config("webhook").requireSecret(secret);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let webhookUrlRef = {
|
|
157
|
+
contents: Pulumi.output("")
|
|
158
|
+
};
|
|
159
|
+
|
|
142
160
|
function registerDcbConfig(pluginName, dcbTableName, stateChangeSliceModulePathsOpt, param) {
|
|
143
161
|
let stateChangeSliceModulePaths = stateChangeSliceModulePathsOpt !== undefined ? stateChangeSliceModulePathsOpt : [];
|
|
144
162
|
dcbConfigRef.contents = {
|
|
@@ -542,7 +560,9 @@ function Make(EventCollectorChannel) {
|
|
|
542
560
|
};
|
|
543
561
|
}
|
|
544
562
|
});
|
|
545
|
-
StateChangeSliceRuntime_Builder_Single$ReventlessAws.forDcbCommandTopic(slicePaths, inboundSlices, dcbConfig.dcbTableName, dcbConfig.pluginName, syncStateChangesConfigRef.contents, asyncStateChangesConfigRef.contents, memorySize, timeout,
|
|
563
|
+
StateChangeSliceRuntime_Builder_Single$ReventlessAws.forDcbCommandTopic(slicePaths, inboundSlices, dcbConfig.dcbTableName, dcbConfig.pluginName, syncStateChangesConfigRef.contents, asyncStateChangesConfigRef.contents, memorySize, timeout, webhookSlices.length !== 0 ? webhookSecrets : undefined, url => {
|
|
564
|
+
webhookUrlRef.contents = url;
|
|
565
|
+
}, connect, dcbCommandTopic);
|
|
546
566
|
};
|
|
547
567
|
let finish = () => {};
|
|
548
568
|
return {
|
|
@@ -577,6 +597,10 @@ export {
|
|
|
577
597
|
registeredInboundSlices,
|
|
578
598
|
registerInboundTranslationSliceSpec,
|
|
579
599
|
registerInboundAuditTableName,
|
|
600
|
+
webhookSlices,
|
|
601
|
+
webhookSecrets,
|
|
602
|
+
registerWebhook,
|
|
603
|
+
webhookUrlRef,
|
|
580
604
|
registerDcbConfig,
|
|
581
605
|
registerDcbTableName,
|
|
582
606
|
heartbeatConfigRef,
|
|
@@ -606,6 +606,58 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
|
606
606
|
)
|
|
607
607
|
},
|
|
608
608
|
)
|
|
609
|
+
|
|
610
|
+
// The resolvers check no rule, so a door the gate misses reads the gated rows.
|
|
611
|
+
// Run through the real generator, so a door it adds later is covered too.
|
|
612
|
+
testSync("a roles-gated entry's every generated door carries its groups", () => {
|
|
613
|
+
let byCategory: Reventless.ReadModel.indexConfig = {
|
|
614
|
+
index: "byCategory",
|
|
615
|
+
type_: "S",
|
|
616
|
+
idField: "categoryId",
|
|
617
|
+
projectionType: ALL,
|
|
618
|
+
}
|
|
619
|
+
let gated = (
|
|
620
|
+
~single,
|
|
621
|
+
~list,
|
|
622
|
+
~subIdField=?,
|
|
623
|
+
~indexQueries=?,
|
|
624
|
+
): ReventlessInfra.Api.querySchemaEntry => {
|
|
625
|
+
singleFieldName: single,
|
|
626
|
+
listFieldName: list,
|
|
627
|
+
returnTypeName: single,
|
|
628
|
+
stateSchema: S.unknown,
|
|
629
|
+
authorization: None,
|
|
630
|
+
permission: AllowRoles([Reventless.Role.make("Admin")]),
|
|
631
|
+
?subIdField,
|
|
632
|
+
?indexQueries,
|
|
633
|
+
}
|
|
634
|
+
let entries = [
|
|
635
|
+
gated(~single="p_Item", ~list="p_Items", ~indexQueries=[byCategory]),
|
|
636
|
+
gated(~single="p_Line", ~list="p_Lines", ~subIdField="lineId"),
|
|
637
|
+
]
|
|
638
|
+
let frag = ReventlessCore.GraphQL_FragmentGenerator.generate(
|
|
639
|
+
~mutationEntries=[],
|
|
640
|
+
~queryEntries=entries,
|
|
641
|
+
)
|
|
642
|
+
let queries = (
|
|
643
|
+
AppSync_Adapter.injectAwsAuth(
|
|
644
|
+
frag,
|
|
645
|
+
~mutationEntries=[],
|
|
646
|
+
~queryEntries=entries,
|
|
647
|
+
)->decodeFragment
|
|
648
|
+
).queries
|
|
649
|
+
let names = queries->Array.map(ReventlessCore.GraphQL_Stitcher.extractLeadingName)
|
|
650
|
+
expect(
|
|
651
|
+
["p_ItemsByIds", "p_ItemsRefs", "p_ItemByCategory", "p_LineItems"]->Array.every(
|
|
652
|
+
door => names->Array.includes(door),
|
|
653
|
+
),
|
|
654
|
+
)->toBe(true)
|
|
655
|
+
expect(
|
|
656
|
+
queries->Array.filter(
|
|
657
|
+
field => !(field->String.includes(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)),
|
|
658
|
+
),
|
|
659
|
+
)->toEqual([])
|
|
660
|
+
})
|
|
609
661
|
})
|
|
610
662
|
|
|
611
663
|
// ── Type-level dual-auth ─────────────────────────────────────────────────────
|
|
@@ -12,6 +12,7 @@ import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core
|
|
|
12
12
|
import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
|
|
13
13
|
import * as AppSync_SdlDecorate$ReventlessAws from "../src/components/Api/AppSync_SdlDecorate.res.mjs";
|
|
14
14
|
import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
|
|
15
|
+
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
15
16
|
|
|
16
17
|
globalThis.describe("AppSync_Adapter.sha256Hex", () => {
|
|
17
18
|
globalThis.test("returns a 64-character hex string", () => {
|
|
@@ -596,6 +597,40 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
|
|
|
596
597
|
}
|
|
597
598
|
});
|
|
598
599
|
});
|
|
600
|
+
globalThis.test("a roles-gated entry's every generated door carries its groups", () => {
|
|
601
|
+
let gated = (single, list, subIdField, indexQueries) => ({
|
|
602
|
+
singleFieldName: single,
|
|
603
|
+
listFieldName: list,
|
|
604
|
+
returnTypeName: single,
|
|
605
|
+
stateSchema: Sury.unknown,
|
|
606
|
+
authorization: undefined,
|
|
607
|
+
permission: {
|
|
608
|
+
TAG: "AllowRoles",
|
|
609
|
+
_0: ["Admin"]
|
|
610
|
+
},
|
|
611
|
+
subIdField: subIdField,
|
|
612
|
+
indexQueries: indexQueries
|
|
613
|
+
});
|
|
614
|
+
let entries = [
|
|
615
|
+
gated("p_Item", "p_Items", undefined, [{
|
|
616
|
+
index: "byCategory",
|
|
617
|
+
type_: "S",
|
|
618
|
+
idField: "categoryId",
|
|
619
|
+
projectionType: "ALL"
|
|
620
|
+
}]),
|
|
621
|
+
gated("p_Line", "p_Lines", "lineId", undefined)
|
|
622
|
+
];
|
|
623
|
+
let frag = GraphQL_FragmentGenerator$ReventlessCore.generate([], entries);
|
|
624
|
+
let queries = GraphQL_Stitcher$ReventlessCore.decode(AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], entries)).queries;
|
|
625
|
+
let names = queries.map(GraphQL_Stitcher$ReventlessCore.extractLeadingName);
|
|
626
|
+
globalThis.expect([
|
|
627
|
+
"p_ItemsByIds",
|
|
628
|
+
"p_ItemsRefs",
|
|
629
|
+
"p_ItemByCategory",
|
|
630
|
+
"p_LineItems"
|
|
631
|
+
].every(door => names.includes(door))).toBe(true);
|
|
632
|
+
globalThis.expect(queries.filter(field => !field.includes(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`))).toEqual([]);
|
|
633
|
+
});
|
|
599
634
|
});
|
|
600
635
|
|
|
601
636
|
globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
|
|
@@ -15,6 +15,7 @@ type command = AddThing({thingId: string})
|
|
|
15
15
|
|
|
16
16
|
let targetName = "AddThing"
|
|
17
17
|
let externalSystem: option<string> = Some("TestFeed")
|
|
18
|
+
let endpoint = None
|
|
18
19
|
|
|
19
20
|
type role = Reventless.Role.name
|
|
20
21
|
let authorizationOf = (_: string): Reventless.Authorization.rule<role> => AllowRoles([
|
|
@@ -27,6 +27,8 @@ let targetName = "AddThing";
|
|
|
27
27
|
|
|
28
28
|
let externalSystem = "TestFeed";
|
|
29
29
|
|
|
30
|
+
let endpoint;
|
|
31
|
+
|
|
30
32
|
export {
|
|
31
33
|
name,
|
|
32
34
|
moduleUrl,
|
|
@@ -34,6 +36,7 @@ export {
|
|
|
34
36
|
commandSchema,
|
|
35
37
|
targetName,
|
|
36
38
|
externalSystem,
|
|
39
|
+
endpoint,
|
|
37
40
|
authorizationOf,
|
|
38
41
|
}
|
|
39
42
|
/* externalInputSchema Not a pure module */
|