@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 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.383",
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-pulumi-aws": "3.0.0-alpha.29",
27
+ "@reventlessdev/rescript-effect": "1.0.0-alpha.0",
29
28
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
30
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.23",
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.193",
34
- "@reventlessdev/reventless-interop": "3.0.0-alpha.40",
35
- "@reventlessdev/reventless-core": "3.0.0-alpha.296",
36
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.160",
37
- "@reventlessdev/reventless-spec": "3.0.0-alpha.165"
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
@@ -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
- return [sqsHandler, cmdGenHandler, dcbComp(config.pluginName), config.pluginName, inboundReceiversByField];
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 to its table when one was threaded. Answers the
271
- // `commandOutcome` JSON the AppSync direct-invocation route answers with.
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
- switch auditQueryDbOps {
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
- if (auditQueryDbOps !== undefined) {
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
- queryAuthMap->Dict.set(entry.singleFieldName, groups)
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
- queryAuthMap[entry.singleFieldName] = [group];
161
- queryAuthMap[entry.listFieldName] = [group];
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, entry.singleFieldName);
171
- return Stdlib_Dict.$$delete(queryAuthMap, entry.listFieldName);
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
- queryAuthMap[entry.singleFieldName] = groups$1;
179
- queryAuthMap[entry.listFieldName] = groups$1;
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, connect, dcbCommandTopic);
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 */