@reventlessdev/reventless-aws 3.0.0-alpha.345 → 3.0.0-alpha.347

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.
Files changed (112) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/package.json +19 -12
  3. package/rescript.json +2 -1
  4. package/run-bake-manifest.mjs +3 -0
  5. package/run-deploy-app.mjs +3 -0
  6. package/run-provision-accounts.mjs +3 -0
  7. package/run-provision-admin.mjs +3 -0
  8. package/scripts/BakeManifest.res +395 -0
  9. package/scripts/BakeManifest.res.mjs +487 -0
  10. package/scripts/DeployApp.res +708 -0
  11. package/scripts/DeployApp.res.mjs +1015 -0
  12. package/scripts/DeployManifest.res +84 -0
  13. package/scripts/DeployManifest.res.mjs +112 -0
  14. package/scripts/ProvisionAccounts.res +339 -0
  15. package/scripts/ProvisionAccounts.res.mjs +351 -0
  16. package/scripts/ProvisionAdmin.res +275 -0
  17. package/scripts/ProvisionAdmin.res.mjs +289 -0
  18. package/scripts/ProvisionCognito.res +149 -0
  19. package/scripts/ProvisionCognito.res.mjs +98 -0
  20. package/scripts/ProvisionIdentity.res +76 -11
  21. package/scripts/ProvisionIdentity.res.mjs +72 -7
  22. package/scripts/ProvisionProvider.res +104 -0
  23. package/scripts/ProvisionProvider.res.mjs +123 -0
  24. package/scripts/PulumiCli.res +43 -0
  25. package/scripts/PulumiCli.res.mjs +106 -0
  26. package/src/Platform.res +51 -4
  27. package/src/Platform.res.mjs +32 -14
  28. package/src/Platform_Stack.res +119 -9
  29. package/src/Platform_Stack.res.mjs +42 -4
  30. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
  31. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  32. package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
  33. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
  34. package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
  35. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
  36. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
  37. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
  38. package/src/adapter/Auth/Auth_LoginIdentifier.res +66 -0
  39. package/src/adapter/Auth/Auth_LoginIdentifier.res.mjs +70 -0
  40. package/src/adapter/Auth/Auth_SignUpMode.res +78 -0
  41. package/src/adapter/Auth/Auth_SignUpMode.res.mjs +56 -0
  42. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
  43. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
  44. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
  45. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
  46. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
  47. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
  48. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
  49. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
  50. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  51. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
  52. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +5 -0
  53. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +5 -1
  54. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
  55. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
  56. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
  57. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
  58. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
  59. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
  60. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
  61. package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
  62. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
  63. package/src/adapter/Task/TaskBucket_S3.res +1 -3
  64. package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
  65. package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
  66. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
  67. package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
  68. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
  69. package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
  70. package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
  71. package/src/components/Api/AppSync_Adapter.res +56 -72
  72. package/src/components/Api/AppSync_Adapter.res.mjs +26 -60
  73. package/src/components/Api/AppSync_SdlDecorate.res +82 -1
  74. package/src/components/Api/AppSync_SdlDecorate.res.mjs +45 -0
  75. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
  76. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
  77. package/src/plugin/stack/Plugin_Stack.res +4 -0
  78. package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
  79. package/src/util/Util_DeadLetterQueue.res +1 -5
  80. package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
  81. package/src/util/Util_StoreLayout.res +15 -2
  82. package/src/util/Util_StoreLayout.res.mjs +11 -3
  83. package/tests/AppSync_AdapterTest.res +83 -0
  84. package/tests/AppSync_AdapterTest.res.mjs +69 -0
  85. package/tests/Auth_LoginIdentifierTest.res +51 -0
  86. package/tests/Auth_LoginIdentifierTest.res.mjs +51 -0
  87. package/tests/Auth_SignUpModeTest.res +56 -0
  88. package/tests/Auth_SignUpModeTest.res.mjs +53 -0
  89. package/tests/BakeManifestTest.res +204 -0
  90. package/tests/BakeManifestTest.res.mjs +259 -0
  91. package/tests/DcbColdStartPartitionTest.res +32 -0
  92. package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
  93. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
  94. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
  95. package/tests/DcbUnresolvedPartitionSlice.res +28 -0
  96. package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
  97. package/tests/DeployAppTest.res +90 -0
  98. package/tests/DeployAppTest.res.mjs +133 -0
  99. package/tests/LambdaLayerLookupTest.res +104 -0
  100. package/tests/LambdaLayerLookupTest.res.mjs +117 -0
  101. package/tests/ProvisionAccountsTest.res +168 -0
  102. package/tests/ProvisionAccountsTest.res.mjs +184 -0
  103. package/tests/ProvisionAdminTest.res +55 -0
  104. package/tests/ProvisionAdminTest.res.mjs +66 -0
  105. package/tests/ProvisionIdentityTest.res +89 -3
  106. package/tests/ProvisionIdentityTest.res.mjs +66 -1
  107. package/tests/Util_OwnerScopeEnvTest.res +25 -0
  108. package/tests/Util_OwnerScopeEnvTest.res.mjs +13 -0
  109. package/tests/Util_StoreLayoutTest.res +6 -0
  110. package/tests/Util_StoreLayoutTest.res.mjs +7 -4
  111. package/tests/dcbColdStartPartition.mjs +66 -0
  112. package/tests/setup/layerArn.cjs +5 -0
@@ -183,14 +183,32 @@ let rec waitForMergeSuccess = async (
183
183
  // When both are present on the same field, the spec-level permission wins
184
184
  // (it is more specific). `AllowGroups([g1, g2, ...])` emits
185
185
  // `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`.
186
- // `AllowAuthenticated` / `AllowAnonymous` emit the group-less
187
- // `@aws_cognito_user_pools` — same reachability (with Cognito as primary auth
188
- // any reaching request is already authenticated), but stated rather than left
186
+ // `AllowAuthenticated` emits the group-less `@aws_cognito_user_pools` — the same
187
+ // reachability an undirectived field already has, but stated rather than left
189
188
  // implicit, so `assertGateable` can tell "deliberately open" from "nobody
190
189
  // stamped this". `AllowGroups([])` and `DenyAll` emit a sentinel `__deny_all__`
191
190
  // group that no Cognito user can belong to — effectively blocking the field at
192
191
  // the API layer.
193
192
  //
193
+ // `AllowAnonymous` is REFUSED at deploy time, and this used to be the bug:
194
+ // it shared `AllowAuthenticated`'s arm and emitted the same group-less
195
+ // directive, which on a Cognito-primary API means *any authenticated caller* —
196
+ // the opposite of what the spec says. AppSync has no anonymous mode to emit
197
+ // instead (`authenticationType` is API_KEY | AWS_IAM | AMAZON_COGNITO_USER_POOLS
198
+ // | OPENID_CONNECT, and this adapter provisions Cognito primary with AWS_IAM
199
+ // additional), so there is no directive that means "no auth". The old behaviour
200
+ // deployed green, passed `assertGateable`, worked on the local platform — whose
201
+ // resolvers call `Authorization.isAllowed` and honour the rule — and refused
202
+ // anonymous callers only on AWS. It failed CLOSED, which is why nothing
203
+ // surfaced it.
204
+ //
205
+ // Refusing follows `Auth_LoginIdentifier`'s rule for an unrecognised spelling:
206
+ // a silent contradiction of the source is worse than a stopped deploy, because
207
+ // only one of the two is discoverable. AppSync mutations have no runtime
208
+ // authorization check to fall back on — the only `Authorization.isAllowed` call
209
+ // in this package is on the Postgres query path — so the directive is the whole
210
+ // enforcement, and a directive that cannot say this must not pretend to.
211
+ //
194
212
  // NOT `@aws_auth(...)`: that is the single-mode form, which AppSync ignores on a
195
213
  // multi-auth API — which every API this adapter provisions is. See
196
214
  // `AppSync_SdlDecorate.formatCognitoGroupsDirective` for the full rationale and
@@ -209,62 +227,13 @@ let rec waitForMergeSuccess = async (
209
227
  // and a least-privilege deploy-role policy — see
210
228
  // `docs/guides/appsync-iam-system-caller.md`.
211
229
 
212
- let _permissionToCognitoGroups = (permission: Reventless.Authorization.permission): option<
213
- array<string>,
214
- > =>
215
- switch permission {
216
- | AllowGroups([]) => Some(["__deny_all__"])
217
- | AllowGroups(groups) => Some(groups)
218
- | DenyAll => Some(["__deny_all__"])
219
- | AllowAuthenticated | AllowAnonymous => None
220
- }
221
-
222
- // Both directive forms are defined once in the runtime-pure AppSync_SdlDecorate
223
- // so this deploy path and the bundled AdminEventCollector Lambda's reactive push
224
- // cannot drift. The Cognito-only arm carries the rationale for why it is
225
- // `@aws_cognito_user_pools(...)` and never `@aws_auth(...)`.
226
- let _formatGroupsDirective = AppSync_SdlDecorate.formatCognitoGroupsDirective
227
-
228
- // Multi-auth directive for a field that must accept BOTH Cognito and IAM.
229
- // `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps the
230
- // field open to any authenticated Cognito user. `@aws_iam` admits the
231
- // deploy-time SigV4 system caller. See the dual-auth note above.
232
- let _formatDualAuthDirective = AppSync_SdlDecorate.formatDualAuthDirective
233
-
234
- // ── Type-level dual-auth ─────────────────────────────────────────────────────
235
- // On a multi-auth API, object TYPES without auth directives are accessible only
236
- // via the default auth mode — a field-level `@aws_iam` admits the system caller
237
- // to the top-level field, but response shaping then walks the return types
238
- // (`…Connection` → `…Edge` → node type → nested state types) and dies with
239
- // "Not Authorized to access <field> on type <T>" one level in. Types reachable
240
- // from a systemCallable field therefore carry the bare multi-auth pair: the
241
- // group-less Cognito arm matches the pre-existing accessibility of an
242
- // undirectived type (any authenticated user; entry gating stays on the fields),
243
- // the IAM arm admits the traversal. Only `type` declarations take auth
244
- // directives (inputs/enums/interfaces do not).
245
-
246
- // The declared name of a `type …` SDL declaration; None for input/enum/union/etc.
247
- let _typeDeclName = (decl: string): option<string> =>
248
- if decl->String.startsWith("type ") {
249
- let rest = decl->String.slice(~start=5, ~end=decl->String.length)
250
- let end = switch rest->String.search(/[\s{]/) {
251
- | -1 => rest->String.length
252
- | i => i
253
- }
254
- Some(rest->String.slice(~start=0, ~end))
255
- } else {
256
- None
257
- }
258
-
259
- // Insert the multi-auth pair into a type declaration header (before its `{`).
260
- let _stampTypeDualAuth = (decl: string): string =>
261
- switch decl->String.indexOfOpt("{") {
262
- | Some(i) =>
263
- decl->String.slice(~start=0, ~end=i) ++
264
- "@aws_cognito_user_pools @aws_iam " ++
265
- decl->String.slice(~start=i, ~end=decl->String.length)
266
- | None => decl
267
- }
230
+ // Everything this file needs to decide what a directive says — the two directive
231
+ // formats, the permission → gate mapping, the `AllowAnonymous` refusal, the type
232
+ // header stampers and `typeDeclNameOf` — lives in the runtime-pure
233
+ // AppSync_SdlDecorate, so this deploy path and the bundled AdminEventCollector
234
+ // Lambda's reactive push cannot drift. The Cognito-only arm carries the rationale
235
+ // for why it is `@aws_cognito_user_pools(...)` and never `@aws_auth(...)`.
236
+ // This file is left with resource creation, which is what it is for.
268
237
 
269
238
  // Shared traversal types every callable surface reaches — `PageInfo` (relay
270
239
  // connections, injected by the stitcher) and the `CommandResult` members
@@ -299,6 +268,10 @@ let injectAwsAuth = (
299
268
  let iamQueryFieldPrefixes: array<string> = []
300
269
  let iamTypePrefixes: array<string> = []
301
270
 
271
+ // Collected across both entry kinds and refused once, so a deploy reports
272
+ // every offending field rather than the first one found.
273
+ let anonymousFields: array<string> = []
274
+
302
275
  let mutationAuthMap: Dict.t<array<string>> = Dict.make()
303
276
  mutationEntries->Array.forEach(entry => {
304
277
  if entry.systemCallable->Option.getOr(false) {
@@ -314,9 +287,10 @@ let injectAwsAuth = (
314
287
  fp
315
288
  ->Dict.toArray
316
289
  ->Array.forEach(((fieldName, permission)) => {
317
- switch _permissionToCognitoGroups(permission) {
318
- | Some(groups) => mutationAuthMap->Dict.set(fieldName, groups)
319
- | None => mutationAuthMap->Dict.delete(fieldName)
290
+ switch AppSync_SdlDecorate.permissionToGate(permission) {
291
+ | Groups(groups) => mutationAuthMap->Dict.set(fieldName, groups)
292
+ | AnyAuthenticated => mutationAuthMap->Dict.delete(fieldName)
293
+ | Anonymous => anonymousFields->Array.push(fieldName)
320
294
  }
321
295
  })
322
296
  | None => ()
@@ -342,18 +316,28 @@ let injectAwsAuth = (
342
316
  }
343
317
  switch entry.permission {
344
318
  | Some(permission) =>
345
- switch _permissionToCognitoGroups(permission) {
346
- | Some(groups) =>
319
+ switch AppSync_SdlDecorate.permissionToGate(permission) {
320
+ | Groups(groups) =>
347
321
  queryAuthMap->Dict.set(entry.singleFieldName, groups)
348
322
  queryAuthMap->Dict.set(entry.listFieldName, groups)
349
- | None =>
323
+ | AnyAuthenticated =>
350
324
  queryAuthMap->Dict.delete(entry.singleFieldName)
351
325
  queryAuthMap->Dict.delete(entry.listFieldName)
326
+ | Anonymous =>
327
+ // Both derived field names, because the spec declares one permission and
328
+ // the generator emits two fields from it; naming only one would send the
329
+ // author looking for a second declaration that does not exist.
330
+ anonymousFields->Array.push(entry.singleFieldName)
331
+ anonymousFields->Array.push(entry.listFieldName)
352
332
  }
353
333
  | None => ()
354
334
  }
355
335
  })
356
336
 
337
+ if anonymousFields->Array.length > 0 {
338
+ AppSync_SdlDecorate.refuseAnonymousFields(anonymousFields)
339
+ }
340
+
357
341
  // A field with no group restriction gets the group-less Cognito directive
358
342
  // rather than no directive at all. Same reachability under `defaultAction:
359
343
  // ALLOW`; the difference only shows once the default is DENY, where an
@@ -364,10 +348,10 @@ let injectAwsAuth = (
364
348
  let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
365
349
  let groups = mutationAuthMap->Dict.get(fieldName)
366
350
  if iamFields->Dict.get(fieldName)->Option.getOr(false) {
367
- `${field}\n ${_formatDualAuthDirective(groups)}`
351
+ `${field}\n ${AppSync_SdlDecorate.formatDualAuthDirective(groups)}`
368
352
  } else {
369
353
  switch groups {
370
- | Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
354
+ | Some(groups) => `${field}\n ${AppSync_SdlDecorate.formatCognitoGroupsDirective(groups)}`
371
355
  | None => `${field}\n ${openDirective}`
372
356
  }
373
357
  }
@@ -380,10 +364,10 @@ let injectAwsAuth = (
380
364
  iamFields->Dict.get(fieldName)->Option.getOr(false) ||
381
365
  iamQueryFieldPrefixes->Array.some(p => fieldName->String.startsWith(p))
382
366
  if isIam {
383
- `${field} ${_formatDualAuthDirective(groups)}`
367
+ `${field} ${AppSync_SdlDecorate.formatDualAuthDirective(groups)}`
384
368
  } else {
385
369
  switch groups {
386
- | Some(groups) => `${field} ${_formatGroupsDirective(groups)}`
370
+ | Some(groups) => `${field} ${AppSync_SdlDecorate.formatCognitoGroupsDirective(groups)}`
387
371
  | None => `${field} ${openDirective}`
388
372
  }
389
373
  }
@@ -396,7 +380,7 @@ let injectAwsAuth = (
396
380
  let augmentedSubscriptions = parts.subscriptions->Array.map(field => {
397
381
  let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
398
382
  switch mutationAuthMap->Dict.get(fieldName) {
399
- | Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
383
+ | Some(groups) => `${field}\n ${AppSync_SdlDecorate.formatCognitoGroupsDirective(groups)}`
400
384
  | None => `${field}\n ${openDirective}`
401
385
  }
402
386
  })
@@ -405,9 +389,9 @@ let injectAwsAuth = (
405
389
  // the node type, its Connection/Edge wrappers, and nested state types all
406
390
  // share the entry's returnTypeName prefix.
407
391
  let augmentedTypes = parts.types->Array.map(decl =>
408
- switch _typeDeclName(decl) {
392
+ switch AppSync_SdlDecorate.typeDeclNameOf(decl) {
409
393
  | Some(name) if iamTypePrefixes->Array.some(p => name->String.startsWith(p)) =>
410
- _stampTypeDualAuth(decl)
394
+ AppSync_SdlDecorate.stampTypeDualAuth(decl)
411
395
  | _ => decl
412
396
  }
413
397
  )
@@ -6,7 +6,6 @@ import * as Aws from "@pulumi/aws";
6
6
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
7
7
  import * as Nodecrypto from "node:crypto";
8
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
- import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
10
9
  import * as Effect$1 from "effect/Effect";
11
10
  import * as Pulumi from "@pulumi/pulumi";
12
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
@@ -107,49 +106,12 @@ async function waitForMergeSuccess(client, associationId, mergedApiIdentifier, m
107
106
  return Stdlib_JsError.throwWithMessage(`Source API association ` + associationId + ` on ` + mergedApiIdentifier + ` failed to merge (` + status + `): ` + detail);
108
107
  }
109
108
 
110
- function _permissionToCognitoGroups(permission) {
111
- if (typeof permission !== "object") {
112
- switch (permission) {
113
- case "AllowAuthenticated" :
114
- case "AllowAnonymous" :
115
- return;
116
- case "DenyAll" :
117
- return ["__deny_all__"];
118
- }
119
- } else {
120
- let groups = permission._0;
121
- if (groups.length !== 0) {
122
- return groups;
123
- } else {
124
- return ["__deny_all__"];
125
- }
126
- }
127
- }
128
-
129
- function _typeDeclName(decl) {
130
- if (!decl.startsWith("type ")) {
131
- return;
132
- }
133
- let rest = decl.slice(5, decl.length);
134
- let i = rest.search(/[\s{]/);
135
- let end = i !== -1 ? i : rest.length;
136
- return rest.slice(0, end);
137
- }
138
-
139
- function _stampTypeDualAuth(decl) {
140
- let i = Stdlib_String.indexOfOpt(decl, "{");
141
- if (i !== undefined) {
142
- return decl.slice(0, i) + "@aws_cognito_user_pools @aws_iam " + decl.slice(i, decl.length);
143
- } else {
144
- return decl;
145
- }
146
- }
147
-
148
109
  function injectAwsAuth(fragment, mutationEntries, queryEntries) {
149
110
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
150
111
  let iamFields = {};
151
112
  let iamQueryFieldPrefixes = [];
152
113
  let iamTypePrefixes = [];
114
+ let anonymousFields = [];
153
115
  let mutationAuthMap = {};
154
116
  mutationEntries.forEach(entry => {
155
117
  if (Stdlib_Option.getOr(entry.systemCallable, false)) {
@@ -168,12 +130,16 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
168
130
  if (fp !== undefined) {
169
131
  Object.entries(fp).forEach(param => {
170
132
  let fieldName = param[0];
171
- let groups = _permissionToCognitoGroups(param[1]);
172
- if (groups !== undefined) {
173
- mutationAuthMap[fieldName] = groups;
133
+ let groups = AppSync_SdlDecorate$ReventlessAws.permissionToGate(param[1]);
134
+ if (typeof groups !== "object") {
135
+ if (groups === "AnyAuthenticated") {
136
+ return Stdlib_Dict.$$delete(mutationAuthMap, fieldName);
137
+ }
138
+ anonymousFields.push(fieldName);
174
139
  return;
175
140
  } else {
176
- return Stdlib_Dict.$$delete(mutationAuthMap, fieldName);
141
+ mutationAuthMap[fieldName] = groups._0;
142
+ return;
177
143
  }
178
144
  });
179
145
  return;
@@ -198,16 +164,25 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
198
164
  if (permission === undefined) {
199
165
  return;
200
166
  }
201
- let groups = _permissionToCognitoGroups(permission);
202
- if (groups !== undefined) {
203
- queryAuthMap[entry.singleFieldName] = groups;
204
- queryAuthMap[entry.listFieldName] = groups;
167
+ let groups = AppSync_SdlDecorate$ReventlessAws.permissionToGate(permission);
168
+ if (typeof groups !== "object") {
169
+ if (groups === "AnyAuthenticated") {
170
+ Stdlib_Dict.$$delete(queryAuthMap, entry.singleFieldName);
171
+ return Stdlib_Dict.$$delete(queryAuthMap, entry.listFieldName);
172
+ }
173
+ anonymousFields.push(entry.singleFieldName);
174
+ anonymousFields.push(entry.listFieldName);
205
175
  return;
206
176
  } else {
207
- Stdlib_Dict.$$delete(queryAuthMap, entry.singleFieldName);
208
- return Stdlib_Dict.$$delete(queryAuthMap, entry.listFieldName);
177
+ let groups$1 = groups._0;
178
+ queryAuthMap[entry.singleFieldName] = groups$1;
179
+ queryAuthMap[entry.listFieldName] = groups$1;
180
+ return;
209
181
  }
210
182
  });
183
+ if (anonymousFields.length !== 0) {
184
+ AppSync_SdlDecorate$ReventlessAws.refuseAnonymousFields(anonymousFields);
185
+ }
211
186
  let augmentedMutations = parts.mutations.map(field => {
212
187
  let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
213
188
  let groups = mutationAuthMap[fieldName];
@@ -241,9 +216,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
241
216
  }
242
217
  });
243
218
  let augmentedTypes = parts.types.map(decl => {
244
- let name = _typeDeclName(decl);
219
+ let name = AppSync_SdlDecorate$ReventlessAws.typeDeclNameOf(decl);
245
220
  if (name !== undefined && iamTypePrefixes.some(p => name.startsWith(p))) {
246
- return _stampTypeDualAuth(decl);
221
+ return AppSync_SdlDecorate$ReventlessAws.stampTypeDualAuth(decl);
247
222
  } else {
248
223
  return decl;
249
224
  }
@@ -340,10 +315,6 @@ function generateFragment(mutationEntries, queryEntries) {
340
315
  return injectAwsAuth(fragment, mutationEntries, queryEntries);
341
316
  }
342
317
 
343
- let _formatGroupsDirective = AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective;
344
-
345
- let _formatDualAuthDirective = AppSync_SdlDecorate$ReventlessAws.formatDualAuthDirective;
346
-
347
318
  let stampSharedIamTypes = AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes;
348
319
 
349
320
  let primaryAuthenticationType = "AMAZON_COGNITO_USER_POOLS";
@@ -357,11 +328,6 @@ export {
357
328
  _client,
358
329
  getClient,
359
330
  waitForMergeSuccess,
360
- _permissionToCognitoGroups,
361
- _formatGroupsDirective,
362
- _formatDualAuthDirective,
363
- _typeDeclName,
364
- _stampTypeDualAuth,
365
331
  stampSharedIamTypes,
366
332
  injectAwsAuth,
367
333
  injectAwsAuthAll,
@@ -90,7 +90,11 @@ let formatCognitoGroupsDirective = (groups: array<string>): string => {
90
90
 
91
91
  // The group-less Cognito directive: reachable by any authenticated Cognito
92
92
  // caller. Emitted where a field or type is deliberately open — `AllowAuthenticated`,
93
- // `AllowAnonymous`, a field carrying no permission at all, and every object type.
93
+ // a field carrying no permission at all, and every object type.
94
+ //
95
+ // NOT for `AllowAnonymous`: "any authenticated caller" is the inverse of what
96
+ // that declares, and emitting this for it is the defect `permissionToGate` and
97
+ // `refuseAnonymousFields` above exist to prevent.
94
98
  //
95
99
  // Under `userPoolConfig.defaultAction: ALLOW` this is a no-op: an undirectived
96
100
  // field is already reachable by any authenticated caller, so stamping it changes
@@ -112,6 +116,58 @@ let formatDualAuthDirective = (groups: option<array<string>>): string => {
112
116
  `${cognito} @aws_iam`
113
117
  }
114
118
 
119
+ // ── What a declared permission asks this API to enforce ─────────────────────
120
+ //
121
+ // Lives here rather than beside the Pulumi resources because it is the same
122
+ // concern as the directive vocabulary above: reading a spec-level rule and
123
+ // deciding what the schema says. Keeping the mapping next to the strings it
124
+ // maps onto is what stops a fourth spelling of a directive appearing.
125
+
126
+ /** Three arms rather than an `option<array<string>>`, which is the shape this
127
+ had while it was wrong: the option form could not hold `AllowAuthenticated`
128
+ and `AllowAnonymous` apart, so both became `None` and emitted one directive.
129
+ AppSync can enforce the first and cannot express the second at all. */
130
+ type fieldGate =
131
+ | Groups(array<string>)
132
+ | AnyAuthenticated
133
+ /** No directive can express this; the deploy is refused rather than gated
134
+ into its opposite. See `refuseAnonymousFields`. */
135
+ | Anonymous
136
+
137
+ let permissionToGate = (permission: Reventless.Authorization.permission): fieldGate =>
138
+ switch permission {
139
+ | AllowGroups([]) => Groups(["__deny_all__"])
140
+ | AllowGroups(groups) => Groups(groups)
141
+ | DenyAll => Groups(["__deny_all__"])
142
+ | AllowAuthenticated => AnyAuthenticated
143
+ | AllowAnonymous => Anonymous
144
+ }
145
+
146
+ /** The deploy-time refusal for fields declared `AllowAnonymous`, and the
147
+ sibling of `assertGateable` below: both refuse a schema that would claim a
148
+ gate it does not have. This one fires earlier, while the permission is still
149
+ a value — by SDL time the two permissions are indistinguishable. */
150
+ let refuseAnonymousFields = (fieldNames: array<string>): 'a =>
151
+ JsError.throwWithMessage(
152
+ `Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` ++
153
+ ` ${fieldNames->Array.length->Int.toString} field(s) declare AllowAnonymous:\n` ++
154
+ ` ${fieldNames->Array.join(", ")}\n\n` ++
155
+ `AppSync has no anonymous authorization mode. This API is provisioned with\n` ++
156
+ `AMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\n` ++
157
+ `directive available is @aws_cognito_user_pools — which means ANY\n` ++
158
+ `AUTHENTICATED caller, the opposite of what the spec declares. Mutations have\n` ++
159
+ `no runtime authorization check to correct it: on this platform the directive\n` ++
160
+ `is the whole enforcement.\n\n` ++
161
+ `The local platform DOES honour AllowAnonymous (its resolvers call\n` ++
162
+ `Authorization.isAllowed), so a spec that passes locally can still not be\n` ++
163
+ `deployable here. That divergence is the reason this refuses instead of\n` ++
164
+ `emitting a directive that reads as gated and is not.\n\n` ++
165
+ `Anonymous fields on AWS are deferred. When they land it will be as a separate\n` ++
166
+ `API carrying only the unauthenticated fields. Do not add API_KEY to this one:\n` ++
167
+ `every field here would then have to say whether the key reaches it, and one\n` ++
168
+ `that forgot would be open. Until then, declare the field AllowAuthenticated\n` ++ `(or AllowGroups) and mean it.`,
169
+ )
170
+
115
171
  // Injects the Cognito group gate on ALL mutation, query, and subscription fields
116
172
  // in a fragment. `~iamFieldNames` opts the named mutation/query fields into
117
173
  // deploy-time IAM as well, appending the `@aws_iam` arm. Subscriptions are never
@@ -226,6 +282,31 @@ let stampAllTypesCognito = (sdl: string): string =>
226
282
  )
227
283
  ->Array.join("\n")
228
284
 
285
+ // Insert the multi-auth pair into ONE type declaration's header, before its `{`.
286
+ //
287
+ // The sibling of `stampAllTypesCognito` and deliberately not folded into it:
288
+ // that one sweeps a whole assembled SDL and fills in the Cognito-only arm for
289
+ // anything undirectived, this one is applied to a declaration already chosen by
290
+ // its caller and adds the IAM arm the traversal needs. Different input, different
291
+ // directive, same header surgery.
292
+ //
293
+ // On a multi-auth API, object TYPES without auth directives are reachable only
294
+ // via the default auth mode: a field-level `@aws_iam` admits the system caller to
295
+ // the top-level field, but response shaping then walks the return types
296
+ // (`…Connection` → `…Edge` → node → nested state types) and dies with "Not
297
+ // Authorized to access <field> on type <T>" one level in. The group-less Cognito
298
+ // arm matches the accessibility an undirectived type already has (entry gating
299
+ // stays on the fields); the IAM arm admits the traversal.
300
+ let stampTypeDualAuth = (decl: string): string =>
301
+ switch decl->String.indexOfOpt("{") {
302
+ | Some(i) =>
303
+ decl->String.slice(~start=0, ~end=i) ++
304
+ formatDualAuthDirective(None) ++
305
+ " " ++
306
+ decl->String.slice(~start=i, ~end=decl->String.length)
307
+ | None => decl
308
+ }
309
+
229
310
  // ── Deploy-time gate invariant ──────────────────────────────────────────────
230
311
  //
231
312
  // Refuse to push a schema that cannot be gated. This is the fail-closed
@@ -51,6 +51,39 @@ function formatDualAuthDirective(groups) {
51
51
  return cognito + ` @aws_iam`;
52
52
  }
53
53
 
54
+ function permissionToGate(permission) {
55
+ if (typeof permission !== "object") {
56
+ switch (permission) {
57
+ case "AllowAuthenticated" :
58
+ return "AnyAuthenticated";
59
+ case "AllowAnonymous" :
60
+ return "Anonymous";
61
+ case "DenyAll" :
62
+ return {
63
+ TAG: "Groups",
64
+ _0: ["__deny_all__"]
65
+ };
66
+ }
67
+ } else {
68
+ let groups = permission._0;
69
+ if (groups.length !== 0) {
70
+ return {
71
+ TAG: "Groups",
72
+ _0: groups
73
+ };
74
+ } else {
75
+ return {
76
+ TAG: "Groups",
77
+ _0: ["__deny_all__"]
78
+ };
79
+ }
80
+ }
81
+ }
82
+
83
+ function refuseAnonymousFields(fieldNames) {
84
+ return Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` + (` ` + fieldNames.length.toString() + ` field(s) declare AllowAnonymous:\n`) + (` ` + fieldNames.join(", ") + `\n\n`) + `AppSync has no anonymous authorization mode. This API is provisioned with\nAMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\ndirective available is @aws_cognito_user_pools — which means ANY\nAUTHENTICATED caller, the opposite of what the spec declares. Mutations have\nno runtime authorization check to correct it: on this platform the directive\nis the whole enforcement.\n\nThe local platform DOES honour AllowAnonymous (its resolvers call\nAuthorization.isAllowed), so a spec that passes locally can still not be\ndeployable here. That divergence is the reason this refuses instead of\nemitting a directive that reads as gated and is not.\n\nAnonymous fields on AWS are deferred. When they land it will be as a separate\nAPI carrying only the unauthenticated fields. Do not add API_KEY to this one:\nevery field here would then have to say whether the key reaches it, and one\nthat forgot would be open. Until then, declare the field AllowAuthenticated\n(or AllowGroups) and mean it.`);
85
+ }
86
+
54
87
  function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
55
88
  let iamFieldNames = iamFieldNamesOpt !== undefined ? iamFieldNamesOpt : [];
56
89
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
@@ -122,6 +155,15 @@ function stampAllTypesCognito(sdl) {
122
155
  }).join("\n");
123
156
  }
124
157
 
158
+ function stampTypeDualAuth(decl) {
159
+ let i = Stdlib_String.indexOfOpt(decl, "{");
160
+ if (i !== undefined) {
161
+ return decl.slice(0, i) + formatDualAuthDirective(undefined) + " " + decl.slice(i, decl.length);
162
+ } else {
163
+ return decl;
164
+ }
165
+ }
166
+
125
167
  let enforcedDirectives = [
126
168
  "@aws_cognito_user_pools",
127
169
  "@aws_iam"
@@ -328,11 +370,14 @@ export {
328
370
  formatCognitoGroupsDirective,
329
371
  cognitoOpenDirective,
330
372
  formatDualAuthDirective,
373
+ permissionToGate,
374
+ refuseAnonymousFields,
331
375
  injectAwsAuthAll,
332
376
  sharedIamTypeNames,
333
377
  stampSharedIamTypes,
334
378
  stampUndirectivedFields,
335
379
  stampAllTypesCognito,
380
+ stampTypeDualAuth,
336
381
  enforcedDirectives,
337
382
  hasEnforcedDirective,
338
383
  rootOperationTypes,
@@ -141,11 +141,7 @@ export const handler = async (event) => {
141
141
  let code = Pulumi.Archive.assetArchive(clonerArchiveContents)
142
142
  let sourceCodeHash = Util_Bundle.hashString(entryPointCode ++ "\n---\n" ++ loaderHash)
143
143
 
144
- let layers =
145
- Lambda.reventlessLayerArn
146
- ->Option.map(arn => [arn->Pulumi.Input.make])
147
- ->Option.getOr([])
148
- ->Pulumi.Input.make
144
+ let layers = Lambda.reventlessLayers()
149
145
 
150
146
  let vpcStackName = Pulumi.Config.make(Some("vpc"))->Pulumi.Config.get("stack")->Option.getOrThrow
151
147
  let vpcConfig = Util_Vpc.getVpcConfig(~stackName=vpcStackName, ~outputName="vpc")
@@ -87,7 +87,7 @@ export const handler = async (event) => {
87
87
  let loaderHash = Util_Bundle$ReventlessAws.addEsmLoaderAssets(clonerArchiveContents);
88
88
  let code = new (Pulumi.asset.AssetArchive)(clonerArchiveContents);
89
89
  let sourceCodeHash = Util_Bundle$ReventlessAws.hashString(entryPointCode + "\n---\n" + loaderHash);
90
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
90
+ let layers = Lambda$PulumiAws.reventlessLayers();
91
91
  let vpcStackName = Stdlib_Option.getOrThrow(new Pulumi.Config("vpc").get("stack"), undefined);
92
92
  let vpcConfig = Util_Vpc$ReventlessAws.getVpcConfig(vpcStackName, "vpc");
93
93
  let secrets = Pulumi.all(secretUrns.map(urn => GetSecretVersion$PulumiAws.getSecretNames(urn).apply(names => names.map(name => ({
@@ -157,9 +157,13 @@ let makeUiBundleDistribution = (
157
157
  // shell, where blue/green between versions is wanted.
158
158
  let name = stableName ? pluginId : pluginId ++ "-" ++ bundleVersion
159
159
 
160
+ // Emptied on removal from a disposable stack: the manifest bake writes files
161
+ // here outside Pulumi, which would otherwise fail the destroy.
160
162
  let bucket = PulumiAws.S3.Bucket.make(
161
163
  ~name=name ++ "-bundle",
162
164
  ~args={
165
+ forceDestroy: (Util_StoreLayout.protectionOfDeployedStack() == Unprotected)
166
+ ->Pulumi.Input.make,
163
167
  tags: AWS.Tags.make(
164
168
  ~name=name ++ "-bundle",
165
169
  ~kind=ReventlessCore.ComponentType.Plugin,
@@ -11,6 +11,7 @@ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_excep
11
11
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
12
12
  import * as AWS_Tags$ReventlessAws from "../../adapter/AWS_Tags.res.mjs";
13
13
  import * as ClientCloudfront from "@aws-sdk/client-cloudfront";
14
+ import * as Util_StoreLayout$ReventlessAws from "../../util/Util_StoreLayout.res.mjs";
14
15
  import * as Util_StaticBundle$ReventlessAws from "../../util/Util_StaticBundle.res.mjs";
15
16
 
16
17
  let log = Logger$ReventlessCore.fromEnv();
@@ -76,6 +77,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
76
77
  let servedBuckets = servedBucketsOpt !== undefined ? servedBucketsOpt : [];
77
78
  let name = stableName ? pluginId : pluginId + "-" + bundleVersion;
78
79
  let bucket = new (Aws.s3.Bucket)(name + "-bundle", {
80
+ forceDestroy: Util_StoreLayout$ReventlessAws.protectionOfDeployedStack() === "Unprotected",
79
81
  tags: AWS_Tags$ReventlessAws.make(name + "-bundle", "Plugin", "Hosting", "Plugin", undefined, undefined, pluginId, undefined)
80
82
  });
81
83
  new (Aws.s3.BucketPublicAccessBlock)(name + "-bundle-pab", {
@@ -123,11 +123,7 @@ archiveContents->Dict.set(
123
123
  let code = Pulumi.Archive.assetArchive(archiveContents)
124
124
  let sourceCodeHash = Util_Bundle.hashString(entryPointCode)
125
125
 
126
- let layers =
127
- Lambda.reventlessLayerArn
128
- ->Option.map(arn => [arn->Pulumi.Input.make])
129
- ->Option.getOr([])
130
- ->Pulumi.Input.make
126
+ let layers = Lambda.reventlessLayers()
131
127
 
132
128
  // This is the one Lambda in the framework built by hand rather than through
133
129
  // `RuntimeEnvironment_Lambda`, and it was the one Lambda whose logs Lambda
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
4
  import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
5
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
5
  import * as Pulumi from "@pulumi/pulumi";
7
6
  import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
8
7
  import * as AWS$ReventlessAws from "../adapter/AWS.res.mjs";
@@ -76,7 +75,7 @@ let code = new (Pulumi.asset.AssetArchive)(archiveContents);
76
75
 
77
76
  let sourceCodeHash = Util_Bundle$ReventlessAws.hashString(entryPointCode);
78
77
 
79
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
78
+ let layers = Lambda$PulumiAws.reventlessLayers();
80
79
 
81
80
  let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, undefined, AWS_Tags$ReventlessAws.make(name + `LogGroup`, "Plugin", "Logs", "Plugin", undefined, undefined, undefined, undefined), opts, undefined);
82
81
 
@@ -86,13 +86,26 @@ Destroy semantics follow disposability, **not** layout.
86
86
  `reventless:wipeable`, but that authorises the reset tool to empty stores
87
87
  *deliberately*, after a scope prompt and a typed confirmation. It does not say a
88
88
  field rename may destroy a bucket by accident. Only stacks that are routinely
89
- torn down are unprotected.
89
+ torn down are unprotected: a `pr-*` stack, or one that declares
90
+ `reventless:disposable: "true"` — a try-out meant to be removed whole.
90
91
  */
91
92
  let protectionFor = (
92
93
  ~stack: string,
94
+ ~disposable: bool=false,
93
95
  ~ephemeralPrefixes: array<string>=defaultEphemeralPrefixes,
94
96
  ): protection =>
95
- ephemeralPrefixes->Array.some(p => stack->String.startsWith(p)) ? Unprotected : Protected
97
+ disposable || ephemeralPrefixes->Array.some(p => stack->String.startsWith(p))
98
+ ? Unprotected
99
+ : Protected
100
+
101
+ /** `protectionFor` the stack being deployed, reading its `reventless:disposable`
102
+ setting. The one function here that touches Pulumi. */
103
+ let protectionOfDeployedStack = (): protection =>
104
+ protectionFor(
105
+ ~stack=Pulumi.Pulumi.getStackName(),
106
+ ~disposable=Pulumi.Config.make(Some("reventless"))->Pulumi.Config.get("disposable") ==
107
+ Some("true"),
108
+ )
96
109
 
97
110
  /**
98
111
  The bucket a store's objects live in.