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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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.292 (2026-08-13)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** re-detect once the Lambda that answers the handshake is up ([597e174](https://github.com/ReventlessDev/reventless-core/commit/597e174059e25461d66db35118c933d2ffce7e1f))
11
+ * **aws:** retain offloaded plugin structures so refs cannot dangle ([120a1a7](https://github.com/ReventlessDev/reventless-core/commit/120a1a7228087dd79f8bcfdd2fddc7b6dbdaf296))
12
+
13
+
14
+ # 3.0.0-alpha.291 (2026-08-13)
15
+
16
+ ### Features
17
+
18
+ * **aws:** bake the component manifest after every plugin stack is up ([2fff09a](https://github.com/ReventlessDev/reventless-core/commit/2fff09aa32b77cc8e33a65414c58c12026ca009e))
19
+
20
+
6
21
  # 3.0.0-alpha.290 (2026-08-13)
7
22
 
8
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.290",
3
+ "version": "3.0.0-alpha.292",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -12,18 +12,18 @@
12
12
  "@aws-sdk/s3-request-presigner": "3.970.0",
13
13
  "sury": "11.0.0-alpha.4",
14
14
  "uuid": "^13.0.0",
15
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
15
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.8",
16
16
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
17
- "@reventlessdev/rescript-node": "2.0.0-alpha.4",
18
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.7",
19
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.71",
20
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
17
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.73",
18
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
19
+ "@reventlessdev/rescript-node": "2.0.0-alpha.5",
20
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.229",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.137",
24
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.111",
26
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.93"
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.231",
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.139",
24
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
25
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.95",
26
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.112"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -1454,17 +1454,24 @@ module MakeWithConfig = (
1454
1454
  // Also register the Plugin RM table with the AllAggregates Lambda runtime
1455
1455
  // so its in-Lambda plugin status gate can read plugin status at command
1456
1456
  // dispatch time (Part 2.3 of the resolver plan).
1457
- switch pluginReadModelTableName {
1457
+ // Handed back so the host-UI half of this deploy can grant the bake its write
1458
+ // into the bucket it creates, and so the function name can be exported for
1459
+ // the post-deploy step. `None` when there is no Plugin RM to scan — there is
1460
+ // then no function, and nothing downstream to grant.
1461
+ let componentDefinitions = switch pluginReadModelTableName {
1458
1462
  | Some(tableName) =>
1459
1463
  AggregateRuntime_Builder_Single.setPluginReadModelTable(~name=tableName)
1460
- Platform_ComponentDefinitions_Lambda.make(
1461
- ~api=platformApi,
1462
- ~pluginReadModelTableName=tableName,
1463
- ~offloadBucketName,
1464
- ~schemaReady=admin.adminSchemaPushed,
1465
- ~opts={},
1464
+ Some(
1465
+ Platform_ComponentDefinitions_Lambda.make(
1466
+ ~api=platformApi,
1467
+ ~pluginReadModelTableName=tableName,
1468
+ ~offloadBucketName,
1469
+ ~schemaReady=admin.adminSchemaPushed,
1470
+ ~bakedManifest=?hostUiBundle->Option.flatMap(cfg => cfg.bakedManifest),
1471
+ ~opts={},
1472
+ ),
1466
1473
  )
1467
- | None => ()
1474
+ | None => None
1468
1475
  }
1469
1476
 
1470
1477
  // Mount the Platform_UIFragments Lambda resolver — scans the UiFragments
@@ -2163,6 +2170,7 @@ module MakeWithConfig = (
2163
2170
  Util_ShellConfig.fields(
2164
2171
  ~computed=withEvents,
2165
2172
  ~viewModes=?cfg.viewModes,
2173
+ ~bakedManifest=?cfg.bakedManifest,
2166
2174
  ~shellConfig=?cfg.shellConfig,
2167
2175
  )
2168
2176
  ->JSON.Encode.object
@@ -2202,6 +2210,50 @@ module MakeWithConfig = (
2202
2210
  )
2203
2211
  }
2204
2212
 
2213
+ // The bake's write, granted here because here is where both facts are
2214
+ // known: which bucket serves the shell, and that this deployment declared a
2215
+ // manifest at all. Scoped to the one key the deploy told the shell to fetch
2216
+ // — a function that may rewrite the bundle would be a much larger grant
2217
+ // than "keep one generated file current".
2218
+ //
2219
+ // Exported alongside so the post-deploy step has the function and the
2220
+ // target without reading either out of the deploy program.
2221
+ switch (componentDefinitions, cfg.bakedManifest) {
2222
+ | (Some(fn), Some(bake)) =>
2223
+ let manifestKey = bake.key->Option.getOr(
2224
+ ReventlessCore.Platform_BakedManifest.defaultKey,
2225
+ )
2226
+ let _ =
2227
+ (fn.roleId, bucketName)
2228
+ ->Pulumi.Output.all2
2229
+ ->Pulumi.Output.apply(((roleId, bucket)) => {
2230
+ open PulumiAws.PolicyDocument
2231
+ let _ = PulumiAws.IAM.RolePolicy.make(
2232
+ ~name="PlatformUIDefinitionsBake",
2233
+ ~args={
2234
+ PulumiAws.IAM.RolePolicy.policy: PulumiAws.PolicyDocument.make(
2235
+ ~id="PlatformUIDefinitionsBakePolicy",
2236
+ ~statements=[
2237
+ {
2238
+ sid: "AllowWriteBakedManifest",
2239
+ effect: Allow,
2240
+ actions: Actions(["s3:PutObject"]),
2241
+ resources: Resource(`arn:aws:s3:::${bucket}/${manifestKey}`),
2242
+ },
2243
+ ],
2244
+ )
2245
+ ->PulumiAws.PolicyDocument.toJsonString
2246
+ ->Pulumi.Input.make,
2247
+ role: roleId->Pulumi.Input.make,
2248
+ },
2249
+ )
2250
+ })
2251
+ Pulumi.Pulumi.export("bakedManifestFunction", fn.functionName)
2252
+ Pulumi.Pulumi.export("bakedManifestBucket", bucketName)
2253
+ Pulumi.Pulumi.export("bakedManifestKey", Pulumi.Output.make(manifestKey))
2254
+ | _ => ()
2255
+ }
2256
+
2205
2257
  Pulumi.Pulumi.export("hostShellUrl", distributionUrl)
2206
2258
  }
2207
2259
 
@@ -2244,11 +2296,34 @@ module MakeWithConfig = (
2244
2296
  ->Pulumi.Output.apply(o => o->Option.getOr("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY"))
2245
2297
  | None => Pulumi.Output.make("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY")
2246
2298
  }
2299
+ // The key this deploy wrote for the plugin's structure, exported below. The
2300
+ // Plugin read model is what the manifest bake reads and it is updated
2301
+ // asynchronously, so the bake needs to know which structure "current" means;
2302
+ // an equality check against the key the stack just wrote says that without
2303
+ // timestamps, and passes immediately for a plugin whose structure did not
2304
+ // change.
2305
+ let structureOffloadKey = ref(None)
2306
+
2247
2307
  ReventlessCore.Plugin_Helpers.registerOffload((~store, ~bytes) => {
2248
2308
  let hash = NodeCrypto.sha256Hex(bytes)
2249
2309
  let key = "sha256/" ++ hash
2310
+ if store == "pluginStructures" {
2311
+ structureOffloadKey := Some(key)
2312
+ }
2250
2313
  // Content-addressed: the name and key are the hash, so re-deploying an
2251
2314
  // unchanged field writes the same object (idempotent, deduplicating).
2315
+ //
2316
+ // `retainOnDelete`: the only readers of these objects are the persisted
2317
+ // refs on the Plugin read model, and that row is updated asynchronously —
2318
+ // the deploy publishes a re-detect, the Plugin aggregate handles it, the
2319
+ // projection lands, minutes later. A changed field means a new hash, so
2320
+ // without this Pulumi DELETES the previous object as part of the same
2321
+ // update, and every reference still on the read model dangles until
2322
+ // registration catches up. S3 answers a GET for a deleted key with
2323
+ // AccessDenied (it masks the 404 unless the caller may ListBucket), so the
2324
+ // window shows up as an authorization failure in the manifest bake and in
2325
+ // the AutoUI definitions query. Content-addressed objects are immutable
2326
+ // and cost kilobytes; keeping them closes the window outright.
2252
2327
  let _ = PulumiAws.S3.BucketObject.make(
2253
2328
  ~name="offload-" ++ hash,
2254
2329
  ~args={
@@ -2257,6 +2332,7 @@ module MakeWithConfig = (
2257
2332
  content: Pulumi.Input.make(bytes),
2258
2333
  contentType: Pulumi.Input.make("application/json"),
2259
2334
  },
2335
+ ~opts={retainOnDelete: true},
2260
2336
  )
2261
2337
  {Reventless.Offload.store, key, hash, bytes: bytes->String.length}
2262
2338
  })
@@ -2309,6 +2385,23 @@ module MakeWithConfig = (
2309
2385
  let pluginOutputs = pluginComponent->ReventlessCore.Component.outputs
2310
2386
  ReventlessCore.Plugin_Helpers.exportPluginOutputs(pluginOutputs)
2311
2387
 
2388
+ // What the manifest bake compares the read model against. Keyed by the bare
2389
+ // plugin name because that is how the Plugin read model keys its rows — the
2390
+ // stack's own id carries the version, which the bake has no opinion about.
2391
+ switch structureOffloadKey.contents {
2392
+ | Some(key) =>
2393
+ Pulumi.Pulumi.export(
2394
+ "pluginStructureRef",
2395
+ pluginOutputs.id->Pulumi.Output.apply(id =>
2396
+ Dict.fromArray([
2397
+ ("plugin", JSON.Encode.string(ReventlessCore.Plugin.name(id))),
2398
+ ("key", JSON.Encode.string(key)),
2399
+ ])->JSON.Encode.object
2400
+ ),
2401
+ )
2402
+ | None => ()
2403
+ }
2404
+
2312
2405
  // ── Merged-API association (merged-api plan, Phase 4) ───────────────────
2313
2406
  // Associate this plugin's source API with the platform's merged API —
2314
2407
  // this replaces the SigV4 RegisterApiFragment handshake + reactive push +
@@ -2487,7 +2580,20 @@ module MakeWithConfig = (
2487
2580
  // synchronously always saw the initial empty config, so the publish was skipped
2488
2581
  // on every deploy. `pluginOutputs.heartbeat` derives from that same callback's
2489
2582
  // output, so applying to it is the earliest point the config is populated.
2490
- let _ = pluginOutputs.heartbeat->Pulumi.Output.apply(_ => {
2583
+ //
2584
+ // The EventCollector Lambda is the other half of the gate because it is what
2585
+ // ANSWERS the handshake: the re-detect only asks the platform to re-run it,
2586
+ // and the definition that comes back is the one compiled into that Lambda
2587
+ // (PluginConnectExtension_Mapping's UnknownPluginDetected branch). Published
2588
+ // before it carries the new code, the plugin answers with the PREVIOUS
2589
+ // deploy's definition, `Connect` sees a definition it already holds and emits
2590
+ // nothing, and the row keeps the old structure — silently, and with no second
2591
+ // re-detect coming to correct it. Waiting on the heartbeat alone gated on a
2592
+ // Lambda that has no part in the answer.
2593
+ let redetectReady =
2594
+ (pluginOutputs.heartbeat, PluginRuntime_Builder.eventCollectorReadyRef.contents)
2595
+ ->Pulumi.Output.all2
2596
+ let _ = redetectReady->Pulumi.Output.apply(_ => {
2491
2597
  let hbConfig = PluginRuntime_Builder.heartbeatConfigRef.contents
2492
2598
  switch (Pulumi.Pulumi.isDryRun(), hbConfig.epQueueUrl) {
2493
2599
  | (true, _) => () // `pulumi preview` — no deploy-time side effects
@@ -16,12 +16,14 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
16
16
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
17
17
  import * as Plugin$ReventlessAws from "./components/Plugin.res.mjs";
18
18
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
19
+ import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
19
20
  import * as AWS_Tags$ReventlessAws from "./adapter/AWS_Tags.res.mjs";
20
21
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
21
22
  import * as Scheduler$ReventlessAws from "./components/Scheduler.res.mjs";
22
23
  import * as Aggregate$ReventlessCore from "@reventlessdev/reventless-core/src/components/Aggregate/Aggregate.res.mjs";
23
24
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
24
25
  import * as DcbBackend$ReventlessAws from "./adapter/DcbEventLog/DcbBackend.res.mjs";
26
+ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
25
27
  import * as ReadModel$ReventlessCore from "@reventlessdev/reventless-core/src/components/ReadModel/ReadModel.res.mjs";
26
28
  import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
27
29
  import * as Util_Bundle$ReventlessAws from "./util/Util_Bundle.res.mjs";
@@ -70,6 +72,7 @@ import * as AutomationSlice_Builder$ReventlessAws from "./components/AutomationS
70
72
  import * as EventTopicPublisher_SNS$ReventlessAws from "./adapter/EventTopic/EventTopicPublisher_SNS.res.mjs";
71
73
  import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
72
74
  import * as PgQueryResolver_Builder$ReventlessAws from "./adapter/QueryDb/PgQueryResolver_Builder.res.mjs";
75
+ import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
73
76
  import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs";
74
77
  import * as Aggregate_Builder_Single$ReventlessAws from "./components/Aggregate_Builder_Single.res.mjs";
75
78
  import * as ReadModel_Builder_Single$ReventlessAws from "./components/ReadModel_Builder_Single.res.mjs";
@@ -859,10 +862,7 @@ function MakeWithConfig(Config) {
859
862
  restrictPublicBuckets: true
860
863
  });
861
864
  Pulumi$Pulumi.$$export("offloadBucket", offloadBucketName);
862
- if (pluginReadModelTableName !== undefined) {
863
- AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName);
864
- Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, {});
865
- }
865
+ let componentDefinitions = pluginReadModelTableName !== undefined ? (AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName), Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), {})) : undefined;
866
866
  let rm = admin.stateViewSlicesOutputs["UiFragments"];
867
867
  if (rm !== undefined) {
868
868
  let r$1 = rm.queryDb.resources[0];
@@ -1189,7 +1189,7 @@ function MakeWithConfig(Config) {
1189
1189
  AppSync_EventsApi$ReventlessAws.clientNamespaceName
1190
1190
  ]
1191
1191
  ]) : computed;
1192
- return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.shellConfig));
1192
+ return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
1193
1193
  });
1194
1194
  new (Aws.s3.BucketObject)("host-ui-config-json", {
1195
1195
  bucket: bucketName,
@@ -1207,6 +1207,27 @@ function MakeWithConfig(Config) {
1207
1207
  contentType: "application/json"
1208
1208
  });
1209
1209
  }
1210
+ let match$5 = hostUiBundle.bakedManifest;
1211
+ if (componentDefinitions !== undefined && match$5 !== undefined) {
1212
+ let manifestKey = Stdlib_Option.getOr(match$5.key, Platform_BakedManifest$ReventlessCore.defaultKey);
1213
+ Pulumi.all([
1214
+ componentDefinitions.roleId,
1215
+ bucketName
1216
+ ]).apply(param => {
1217
+ new (Aws.iam.RolePolicy)("PlatformUIDefinitionsBake", {
1218
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "PlatformUIDefinitionsBakePolicy", [{
1219
+ Sid: "AllowWriteBakedManifest",
1220
+ Effect: "Allow",
1221
+ Action: ["s3:PutObject"],
1222
+ Resource: `arn:aws:s3:::` + param[1] + `/` + manifestKey
1223
+ }])),
1224
+ role: param[0]
1225
+ });
1226
+ });
1227
+ Pulumi$Pulumi.$$export("bakedManifestFunction", componentDefinitions.functionName);
1228
+ Pulumi$Pulumi.$$export("bakedManifestBucket", bucketName);
1229
+ Pulumi$Pulumi.$$export("bakedManifestKey", Pulumi.output(manifestKey));
1230
+ }
1210
1231
  Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
1211
1232
  }
1212
1233
  return Pulumi$Pulumi.getOutputs();
@@ -1228,14 +1249,22 @@ function MakeWithConfig(Config) {
1228
1249
  hooksApiRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApi);
1229
1250
  hooksApiRoleRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApiRole);
1230
1251
  let offloadBucketName = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("offloadBucket").apply(o => Stdlib_Option.getOr(o, "OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY")) : Pulumi.output("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY");
1252
+ let structureOffloadKey = {
1253
+ contents: undefined
1254
+ };
1231
1255
  Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
1232
1256
  let hash = NodeCrypto.sha256Hex(bytes);
1233
1257
  let key = "sha256/" + hash;
1258
+ if (store === "pluginStructures") {
1259
+ structureOffloadKey.contents = key;
1260
+ }
1234
1261
  new (Aws.s3.BucketObject)("offload-" + hash, {
1235
1262
  bucket: offloadBucketName,
1236
1263
  key: key,
1237
1264
  content: bytes,
1238
1265
  contentType: "application/json"
1266
+ }, {
1267
+ retainOnDelete: true
1239
1268
  });
1240
1269
  return {
1241
1270
  store: store,
@@ -1253,6 +1282,19 @@ function MakeWithConfig(Config) {
1253
1282
  Pulumi$Pulumi.$$export("_interopMeta", Plugin_Helpers$ReventlessCore.getInteropMeta());
1254
1283
  let pluginOutputs = Component$ReventlessCore.outputs(pluginComponent);
1255
1284
  Plugin_Helpers$ReventlessCore.exportPluginOutputs(pluginOutputs);
1285
+ let key = structureOffloadKey.contents;
1286
+ if (key !== undefined) {
1287
+ Pulumi$Pulumi.$$export("pluginStructureRef", pluginOutputs.id.apply(id => Object.fromEntries([
1288
+ [
1289
+ "plugin",
1290
+ Plugin$ReventlessCore.name(id)
1291
+ ],
1292
+ [
1293
+ "key",
1294
+ key
1295
+ ]
1296
+ ])));
1297
+ }
1256
1298
  if (platformStackRef !== undefined) {
1257
1299
  let stackRef = Primitive_option.valFromOption(platformStackRef);
1258
1300
  let defaultOutput = stackRef.getOutput("default");
@@ -1315,7 +1357,11 @@ function MakeWithConfig(Config) {
1315
1357
  if (sel !== undefined) {
1316
1358
  PgQueryResolver_Builder$ReventlessAws.provision(domainApi, sel, {});
1317
1359
  }
1318
- pluginOutputs.heartbeat.apply(param => {
1360
+ let redetectReady = Pulumi.all([
1361
+ pluginOutputs.heartbeat,
1362
+ PluginRuntime_Builder$ReventlessAws.eventCollectorReadyRef.contents
1363
+ ]);
1364
+ redetectReady.apply(param => {
1319
1365
  let hbConfig = PluginRuntime_Builder$ReventlessAws.heartbeatConfigRef.contents;
1320
1366
  let match = IndexJs.isDryRun();
1321
1367
  let match$1 = hbConfig.epQueueUrl;
@@ -2098,10 +2144,7 @@ function Make($star) {
2098
2144
  restrictPublicBuckets: true
2099
2145
  });
2100
2146
  Pulumi$Pulumi.$$export("offloadBucket", offloadBucketName);
2101
- if (pluginReadModelTableName !== undefined) {
2102
- AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName);
2103
- Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, {});
2104
- }
2147
+ let componentDefinitions = pluginReadModelTableName !== undefined ? (AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName), Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), {})) : undefined;
2105
2148
  let rm = admin.stateViewSlicesOutputs["UiFragments"];
2106
2149
  if (rm !== undefined) {
2107
2150
  let r$1 = rm.queryDb.resources[0];
@@ -2422,7 +2465,7 @@ function Make($star) {
2422
2465
  AppSync_EventsApi$ReventlessAws.clientNamespaceName
2423
2466
  ]
2424
2467
  ]) : computed;
2425
- return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.shellConfig));
2468
+ return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
2426
2469
  });
2427
2470
  new (Aws.s3.BucketObject)("host-ui-config-json", {
2428
2471
  bucket: bucketName,
@@ -2440,6 +2483,27 @@ function Make($star) {
2440
2483
  contentType: "application/json"
2441
2484
  });
2442
2485
  }
2486
+ let match$5 = hostUiBundle.bakedManifest;
2487
+ if (componentDefinitions !== undefined && match$5 !== undefined) {
2488
+ let manifestKey = Stdlib_Option.getOr(match$5.key, Platform_BakedManifest$ReventlessCore.defaultKey);
2489
+ Pulumi.all([
2490
+ componentDefinitions.roleId,
2491
+ bucketName
2492
+ ]).apply(param => {
2493
+ new (Aws.iam.RolePolicy)("PlatformUIDefinitionsBake", {
2494
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "PlatformUIDefinitionsBakePolicy", [{
2495
+ Sid: "AllowWriteBakedManifest",
2496
+ Effect: "Allow",
2497
+ Action: ["s3:PutObject"],
2498
+ Resource: `arn:aws:s3:::` + param[1] + `/` + manifestKey
2499
+ }])),
2500
+ role: param[0]
2501
+ });
2502
+ });
2503
+ Pulumi$Pulumi.$$export("bakedManifestFunction", componentDefinitions.functionName);
2504
+ Pulumi$Pulumi.$$export("bakedManifestBucket", bucketName);
2505
+ Pulumi$Pulumi.$$export("bakedManifestKey", Pulumi.output(manifestKey));
2506
+ }
2443
2507
  Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
2444
2508
  }
2445
2509
  return Pulumi$Pulumi.getOutputs();
@@ -2461,14 +2525,22 @@ function Make($star) {
2461
2525
  hooksApiRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApi);
2462
2526
  hooksApiRoleRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApiRole);
2463
2527
  let offloadBucketName = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("offloadBucket").apply(o => Stdlib_Option.getOr(o, "OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY")) : Pulumi.output("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY");
2528
+ let structureOffloadKey = {
2529
+ contents: undefined
2530
+ };
2464
2531
  Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
2465
2532
  let hash = NodeCrypto.sha256Hex(bytes);
2466
2533
  let key = "sha256/" + hash;
2534
+ if (store === "pluginStructures") {
2535
+ structureOffloadKey.contents = key;
2536
+ }
2467
2537
  new (Aws.s3.BucketObject)("offload-" + hash, {
2468
2538
  bucket: offloadBucketName,
2469
2539
  key: key,
2470
2540
  content: bytes,
2471
2541
  contentType: "application/json"
2542
+ }, {
2543
+ retainOnDelete: true
2472
2544
  });
2473
2545
  return {
2474
2546
  store: store,
@@ -2486,6 +2558,19 @@ function Make($star) {
2486
2558
  Pulumi$Pulumi.$$export("_interopMeta", Plugin_Helpers$ReventlessCore.getInteropMeta());
2487
2559
  let pluginOutputs = Component$ReventlessCore.outputs(pluginComponent);
2488
2560
  Plugin_Helpers$ReventlessCore.exportPluginOutputs(pluginOutputs);
2561
+ let key = structureOffloadKey.contents;
2562
+ if (key !== undefined) {
2563
+ Pulumi$Pulumi.$$export("pluginStructureRef", pluginOutputs.id.apply(id => Object.fromEntries([
2564
+ [
2565
+ "plugin",
2566
+ Plugin$ReventlessCore.name(id)
2567
+ ],
2568
+ [
2569
+ "key",
2570
+ key
2571
+ ]
2572
+ ])));
2573
+ }
2489
2574
  if (platformStackRef !== undefined) {
2490
2575
  let stackRef = Primitive_option.valFromOption(platformStackRef);
2491
2576
  let defaultOutput = stackRef.getOutput("default");
@@ -2548,7 +2633,11 @@ function Make($star) {
2548
2633
  if (sel !== undefined) {
2549
2634
  PgQueryResolver_Builder$ReventlessAws.provision(domainApi, sel, {});
2550
2635
  }
2551
- pluginOutputs.heartbeat.apply(param => {
2636
+ let redetectReady = Pulumi.all([
2637
+ pluginOutputs.heartbeat,
2638
+ PluginRuntime_Builder$ReventlessAws.eventCollectorReadyRef.contents
2639
+ ]);
2640
+ redetectReady.apply(param => {
2552
2641
  let hbConfig = PluginRuntime_Builder$ReventlessAws.heartbeatConfigRef.contents;
2553
2642
  let match = IndexJs.isDryRun();
2554
2643
  let match$1 = hbConfig.epQueueUrl;
@@ -108,13 +108,17 @@ let compareVersions = (a: string, b: string): int => {
108
108
  // `nameVersionOf` reads the row's `name@version` string; `toEntry` builds the
109
109
  // output entry for a surviving row (returning None drops a malformed row). The
110
110
  // name/version split mirrors ReventlessCore.Plugin.name / compareVersions.
111
+ // Generic in what an entry is: the GraphQL fields collapse to encoded JSON, the
112
+ // bake collapses to the decoded structure it curates. One version-collapse rule
113
+ // either way — a second copy would be a second chance to disagree about which
114
+ // version of a plugin is the deployed one.
111
115
  let latestByName = (
112
116
  items: array<dict<JSON.t>>,
113
117
  ~nameVersionOf: dict<JSON.t> => option<string>,
114
- ~toEntry: (dict<JSON.t>, ~name: string) => option<JSON.t>,
115
- ): array<JSON.t> => {
118
+ ~toEntry: (dict<JSON.t>, ~name: string) => option<'entry>,
119
+ ): array<'entry> => {
116
120
  // bare plugin name -> (version, entry); highest version wins.
117
- let latest: dict<(string, JSON.t)> = Dict.make()
121
+ let latest: dict<(string, 'entry)> = Dict.make()
118
122
  items->Array.forEach(item =>
119
123
  switch item->nameVersionOf {
120
124
  | None => ()
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
4
5
  import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
5
6
  import * as ClientDynamodb from "@aws-sdk/client-dynamodb";
6
7
 
@@ -83,7 +84,7 @@ function latestByName(items, nameVersionOf, toEntry) {
83
84
  }
84
85
  latest[name] = [
85
86
  version,
86
- entry
87
+ Primitive_option.valFromOption(entry)
87
88
  ];
88
89
  });
89
90
  return Object.values(latest).map(param => param[1]);
@@ -37,6 +37,22 @@ export function response(ctx) {
37
37
  }
38
38
  `->Pulumi.Input.make
39
39
 
40
+ /**
41
+ What the platform needs back to finish wiring the bake.
42
+
43
+ The manifest is written into the host-UI bucket, and that bucket is created in a
44
+ later half of the same deploy — so the grant cannot be attached here. Handing the
45
+ role back is what lets the caller attach it at the point both facts are known,
46
+ rather than widening this function's write scope to "some bucket, later".
47
+
48
+ `functionName` is exported so the post-deploy step has something to invoke
49
+ without guessing at a generated name.
50
+ */
51
+ type t = {
52
+ roleId: Pulumi.Output.t<string>,
53
+ functionName: Pulumi.Output.t<string>,
54
+ }
55
+
40
56
  let make = (
41
57
  ~api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
42
58
  ~pluginReadModelTableName: Pulumi.Output.t<string>,
@@ -47,8 +63,13 @@ let make = (
47
63
  // first ships it. `Platform_ComponentDefinitions` predates the gate and is left
48
64
  // ungated so this change adds no dependency edge to an already-deployed resource.
49
65
  ~schemaReady: Pulumi.Output.t<unit>,
66
+ // The deployment's include-list, when it declares one. Carried into the
67
+ // function's environment so the bake curates what the platform program
68
+ // declared and not what its caller asked for — the invocation supplies only
69
+ // where to write.
70
+ ~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>=?,
50
71
  ~opts: Pulumi.ComponentResource.options,
51
- ) => {
72
+ ): t => {
52
73
  let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
53
74
  let name = "PlatformUIDefinitions"
54
75
 
@@ -96,6 +117,16 @@ let make = (
96
117
  actions: Actions(["s3:GetObject"]),
97
118
  resources: Resource("arn:aws:s3:::" ++ offloadBucket ++ "/*"),
98
119
  },
120
+ // Read-only on the bucket itself, purely so a key that is not there
121
+ // says so: S3 answers GET for a missing key with AccessDenied unless
122
+ // the caller may list the bucket, which reports a ref the handler
123
+ // cannot resolve as an IAM problem it does not have.
124
+ {
125
+ sid: "AllowOffloadList",
126
+ effect: Allow,
127
+ actions: Actions(["s3:ListBucket"]),
128
+ resources: Resource("arn:aws:s3:::" ++ offloadBucket),
129
+ },
99
130
  ],
100
131
  )
101
132
  ->PolicyDocument.toJsonString
@@ -106,6 +137,28 @@ let make = (
106
137
  )
107
138
  })
108
139
 
140
+ // The include-list as the handler reads it. Encoded here rather than passed
141
+ // as a record so the deploy input and the runtime input are the same shape —
142
+ // `views`/`commands` absent means "every public component", which a record
143
+ // with explicit nulls would not say.
144
+ let bakeSelectionsJson = switch bakedManifest {
145
+ | None => ""
146
+ | Some(bake) =>
147
+ bake.components
148
+ ->Array.map(sel => {
149
+ let entry = Dict.fromArray([("plugin", JSON.Encode.string(sel.plugin))])
150
+ let strings = (key, value) =>
151
+ value->Option.forEach(names =>
152
+ entry->Dict.set(key, names->Array.map(JSON.Encode.string)->JSON.Encode.array)
153
+ )
154
+ strings("views", sel.views)
155
+ strings("commands", sel.commands)
156
+ entry->JSON.Encode.object
157
+ })
158
+ ->JSON.Encode.array
159
+ ->JSON.stringify
160
+ }
161
+
109
162
  let adminEntryJson =
110
163
  ReventlessCore.Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
111
164
  ~pluginId=ReventlessCore.Platform_Admin_Structure.pluginId,
@@ -167,6 +220,11 @@ let make = (
167
220
  ("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
168
221
  ("PLUGIN_RM_TABLE", pluginReadModelTableName->Pulumi.Output.asInput),
169
222
  ("OFFLOAD_BUCKET", offloadBucketName->Pulumi.Output.asInput),
223
+ // Absent declaration ⇒ empty string ⇒ a bake invocation fails saying
224
+ // this platform declares none, rather than writing an empty shop.
225
+ // An include-list is names only, so it stays far from the 4096-byte
226
+ // total the admin entry already pushed out of this dict.
227
+ ("BAKE_SELECTIONS", bakeSelectionsJson->Pulumi.Input.make),
170
228
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
171
229
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
172
230
  Util_LambdaLogging.logLevelEntry(),
@@ -256,4 +314,6 @@ let make = (
256
314
  ~opts,
257
315
  )
258
316
  )
317
+
318
+ {roleId: lambdaRole.id, functionName: lambda.name}
259
319
  }