@reventlessdev/reventless-aws 3.0.0-alpha.291 โ 3.0.0-alpha.293
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 +15 -0
- package/package.json +9 -9
- package/src/Platform.res +85 -5
- package/src/Platform.res.mjs +62 -6
- package/src/Platform_Stack.res +78 -0
- package/src/Platform_Stack.res.mjs +17 -2
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +10 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +8 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +89 -12
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +55 -4
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +309 -0
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +230 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore.res +280 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +163 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +148 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +128 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +180 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +105 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +201 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +126 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +16 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +6 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res +211 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +232 -0
- package/tests/Auth_ActiveRoleStoreTest.res +42 -0
- package/tests/Auth_ActiveRoleStoreTest.res.mjs +41 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res +169 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +189 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +110 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +87 -0
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.293 (2026-08-13)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **aws:** narrow the token to the role a caller chose to act as ([194332f](https://github.com/ReventlessDev/reventless-core/commit/194332fa0af8e13417f72d57904e2ed469747dde))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.292 (2026-08-13)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** re-detect once the Lambda that answers the handshake is up ([597e174](https://github.com/ReventlessDev/reventless-core/commit/597e174059e25461d66db35118c933d2ffce7e1f))
|
|
18
|
+
* **aws:** retain offloaded plugin structures so refs cannot dangle ([120a1a7](https://github.com/ReventlessDev/reventless-core/commit/120a1a7228087dd79f8bcfdd2fddc7b6dbdaf296))
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
# 3.0.0-alpha.291 (2026-08-13)
|
|
7
22
|
|
|
8
23
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.293",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,17 +12,17 @@
|
|
|
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-aws-sdk": "3.0.0-alpha.
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.9",
|
|
16
16
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.5",
|
|
18
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.72",
|
|
19
17
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
20
|
-
"@reventlessdev/rescript-
|
|
18
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.5",
|
|
19
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.74",
|
|
20
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
21
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
-
"@reventlessdev/reventless-
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.
|
|
25
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.232",
|
|
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.96",
|
|
26
26
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.112"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
package/src/Platform.res
CHANGED
|
@@ -245,11 +245,12 @@ module MakeWithConfig = (
|
|
|
245
245
|
// unified mode carries the admin base here instead; it declares no stores in practice,
|
|
246
246
|
// so the upload resolvers below are never created there.)
|
|
247
247
|
let domainBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
|
|
248
|
-
types:
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
types: ReventlessCore.Platform_AdminApi.uploadTypes
|
|
249
|
+
->Array.concat(ReventlessCore.Platform_AdminApi.geocodeTypes)
|
|
250
|
+
->Array.concat(ReventlessCore.Platform_AdminApi.activeRoleTypes),
|
|
251
|
+
mutations: ReventlessCore.Platform_AdminApi.uploadMutationFields->Array.concat(
|
|
252
|
+
ReventlessCore.Platform_AdminApi.activeRoleMutationFields,
|
|
251
253
|
),
|
|
252
|
-
mutations: ReventlessCore.Platform_AdminApi.uploadMutationFields,
|
|
253
254
|
queries: Array.concat(
|
|
254
255
|
[" Platform_ping: String"],
|
|
255
256
|
ReventlessCore.Platform_AdminApi.geocodeQueryFields,
|
|
@@ -2106,6 +2107,31 @@ module MakeWithConfig = (
|
|
|
2106
2107
|
| _ => ()
|
|
2107
2108
|
}
|
|
2108
2109
|
|
|
2110
|
+
// Where a caller records the role they want to act as
|
|
2111
|
+
// (docs/plans/active-role-narrows-the-token.md ยง6). The mutation only writes
|
|
2112
|
+
// a preference; the pre-token-generation trigger reads the row and re-checks
|
|
2113
|
+
// it against real membership before narrowing anything, so this door grants
|
|
2114
|
+
// nobody anything on its own.
|
|
2115
|
+
//
|
|
2116
|
+
// The table and the trigger were provisioned with the pool โ they have to
|
|
2117
|
+
// be, since the pool is declared carrying the trigger's ARN. Only the write
|
|
2118
|
+
// door waits for the API.
|
|
2119
|
+
//
|
|
2120
|
+
// Split-API only, like the upload and geocode resolvers above:
|
|
2121
|
+
// `Platform_SetActiveRole` lives on the domain base document, which a
|
|
2122
|
+
// unified-mode deployment's single API does not carry. The table and
|
|
2123
|
+
// trigger are provisioned either way โ see `Platform_Stack` for why that
|
|
2124
|
+
// is not gated on this flag.
|
|
2125
|
+
if Config.splitApi {
|
|
2126
|
+
Auth_ActiveRoleStore.makeWriteDoor(
|
|
2127
|
+
~api=domainApi,
|
|
2128
|
+
~table=cognitoPool.activeRoleTable,
|
|
2129
|
+
~cognitoUserPoolId=cognitoPool.poolId->Pulumi.Output.asInput,
|
|
2130
|
+
~cognitoUserPoolArn=cognitoPool.poolArn->Pulumi.Output.asInput,
|
|
2131
|
+
~opts={},
|
|
2132
|
+
)
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2109
2135
|
// The place index itself, exported so the unattended slice path can reach the
|
|
2110
2136
|
// capability: a slice's Lambda signs an SDK call directly and needs the index
|
|
2111
2137
|
// name, not an endpoint (no proxy hop, no dependence on a public URL). The
|
|
@@ -2296,11 +2322,34 @@ module MakeWithConfig = (
|
|
|
2296
2322
|
->Pulumi.Output.apply(o => o->Option.getOr("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY"))
|
|
2297
2323
|
| None => Pulumi.Output.make("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY")
|
|
2298
2324
|
}
|
|
2325
|
+
// The key this deploy wrote for the plugin's structure, exported below. The
|
|
2326
|
+
// Plugin read model is what the manifest bake reads and it is updated
|
|
2327
|
+
// asynchronously, so the bake needs to know which structure "current" means;
|
|
2328
|
+
// an equality check against the key the stack just wrote says that without
|
|
2329
|
+
// timestamps, and passes immediately for a plugin whose structure did not
|
|
2330
|
+
// change.
|
|
2331
|
+
let structureOffloadKey = ref(None)
|
|
2332
|
+
|
|
2299
2333
|
ReventlessCore.Plugin_Helpers.registerOffload((~store, ~bytes) => {
|
|
2300
2334
|
let hash = NodeCrypto.sha256Hex(bytes)
|
|
2301
2335
|
let key = "sha256/" ++ hash
|
|
2336
|
+
if store == "pluginStructures" {
|
|
2337
|
+
structureOffloadKey := Some(key)
|
|
2338
|
+
}
|
|
2302
2339
|
// Content-addressed: the name and key are the hash, so re-deploying an
|
|
2303
2340
|
// unchanged field writes the same object (idempotent, deduplicating).
|
|
2341
|
+
//
|
|
2342
|
+
// `retainOnDelete`: the only readers of these objects are the persisted
|
|
2343
|
+
// refs on the Plugin read model, and that row is updated asynchronously โ
|
|
2344
|
+
// the deploy publishes a re-detect, the Plugin aggregate handles it, the
|
|
2345
|
+
// projection lands, minutes later. A changed field means a new hash, so
|
|
2346
|
+
// without this Pulumi DELETES the previous object as part of the same
|
|
2347
|
+
// update, and every reference still on the read model dangles until
|
|
2348
|
+
// registration catches up. S3 answers a GET for a deleted key with
|
|
2349
|
+
// AccessDenied (it masks the 404 unless the caller may ListBucket), so the
|
|
2350
|
+
// window shows up as an authorization failure in the manifest bake and in
|
|
2351
|
+
// the AutoUI definitions query. Content-addressed objects are immutable
|
|
2352
|
+
// and cost kilobytes; keeping them closes the window outright.
|
|
2304
2353
|
let _ = PulumiAws.S3.BucketObject.make(
|
|
2305
2354
|
~name="offload-" ++ hash,
|
|
2306
2355
|
~args={
|
|
@@ -2309,6 +2358,7 @@ module MakeWithConfig = (
|
|
|
2309
2358
|
content: Pulumi.Input.make(bytes),
|
|
2310
2359
|
contentType: Pulumi.Input.make("application/json"),
|
|
2311
2360
|
},
|
|
2361
|
+
~opts={retainOnDelete: true},
|
|
2312
2362
|
)
|
|
2313
2363
|
{Reventless.Offload.store, key, hash, bytes: bytes->String.length}
|
|
2314
2364
|
})
|
|
@@ -2361,6 +2411,23 @@ module MakeWithConfig = (
|
|
|
2361
2411
|
let pluginOutputs = pluginComponent->ReventlessCore.Component.outputs
|
|
2362
2412
|
ReventlessCore.Plugin_Helpers.exportPluginOutputs(pluginOutputs)
|
|
2363
2413
|
|
|
2414
|
+
// What the manifest bake compares the read model against. Keyed by the bare
|
|
2415
|
+
// plugin name because that is how the Plugin read model keys its rows โ the
|
|
2416
|
+
// stack's own id carries the version, which the bake has no opinion about.
|
|
2417
|
+
switch structureOffloadKey.contents {
|
|
2418
|
+
| Some(key) =>
|
|
2419
|
+
Pulumi.Pulumi.export(
|
|
2420
|
+
"pluginStructureRef",
|
|
2421
|
+
pluginOutputs.id->Pulumi.Output.apply(id =>
|
|
2422
|
+
Dict.fromArray([
|
|
2423
|
+
("plugin", JSON.Encode.string(ReventlessCore.Plugin.name(id))),
|
|
2424
|
+
("key", JSON.Encode.string(key)),
|
|
2425
|
+
])->JSON.Encode.object
|
|
2426
|
+
),
|
|
2427
|
+
)
|
|
2428
|
+
| None => ()
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2364
2431
|
// โโ Merged-API association (merged-api plan, Phase 4) โโโโโโโโโโโโโโโโโโโ
|
|
2365
2432
|
// Associate this plugin's source API with the platform's merged API โ
|
|
2366
2433
|
// this replaces the SigV4 RegisterApiFragment handshake + reactive push +
|
|
@@ -2539,7 +2606,20 @@ module MakeWithConfig = (
|
|
|
2539
2606
|
// synchronously always saw the initial empty config, so the publish was skipped
|
|
2540
2607
|
// on every deploy. `pluginOutputs.heartbeat` derives from that same callback's
|
|
2541
2608
|
// output, so applying to it is the earliest point the config is populated.
|
|
2542
|
-
|
|
2609
|
+
//
|
|
2610
|
+
// The EventCollector Lambda is the other half of the gate because it is what
|
|
2611
|
+
// ANSWERS the handshake: the re-detect only asks the platform to re-run it,
|
|
2612
|
+
// and the definition that comes back is the one compiled into that Lambda
|
|
2613
|
+
// (PluginConnectExtension_Mapping's UnknownPluginDetected branch). Published
|
|
2614
|
+
// before it carries the new code, the plugin answers with the PREVIOUS
|
|
2615
|
+
// deploy's definition, `Connect` sees a definition it already holds and emits
|
|
2616
|
+
// nothing, and the row keeps the old structure โ silently, and with no second
|
|
2617
|
+
// re-detect coming to correct it. Waiting on the heartbeat alone gated on a
|
|
2618
|
+
// Lambda that has no part in the answer.
|
|
2619
|
+
let redetectReady =
|
|
2620
|
+
(pluginOutputs.heartbeat, PluginRuntime_Builder.eventCollectorReadyRef.contents)
|
|
2621
|
+
->Pulumi.Output.all2
|
|
2622
|
+
let _ = redetectReady->Pulumi.Output.apply(_ => {
|
|
2543
2623
|
let hbConfig = PluginRuntime_Builder.heartbeatConfigRef.contents
|
|
2544
2624
|
switch (Pulumi.Pulumi.isDryRun(), hbConfig.epQueueUrl) {
|
|
2545
2625
|
| (true, _) => () // `pulumi preview` โ no deploy-time side effects
|
package/src/Platform.res.mjs
CHANGED
|
@@ -16,6 +16,7 @@ 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";
|
|
@@ -59,6 +60,7 @@ import * as StateTopic_AppSync$ReventlessAws from "./adapter/StateTopic/StateTop
|
|
|
59
60
|
import * as AppSync_SdlDecorate$ReventlessAws from "./components/Api/AppSync_SdlDecorate.res.mjs";
|
|
60
61
|
import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs";
|
|
61
62
|
import * as Util_ResourceNaming$ReventlessAws from "./util/Util_ResourceNaming.res.mjs";
|
|
63
|
+
import * as Auth_ActiveRoleStore$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleStore.res.mjs";
|
|
62
64
|
import * as ClonerRunner_Fargate$ReventlessAws from "./plugin/cloner/ClonerRunner_Fargate.res.mjs";
|
|
63
65
|
import * as QueryEngine_DynamoDb$ReventlessAws from "./adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs";
|
|
64
66
|
import * as PluginRuntime_Builder$ReventlessAws from "./plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
@@ -191,8 +193,8 @@ function MakeWithConfig(Config) {
|
|
|
191
193
|
}
|
|
192
194
|
};
|
|
193
195
|
let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
194
|
-
types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes),
|
|
195
|
-
mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
|
|
196
|
+
types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes).concat(Platform_AdminApi$ReventlessCore.activeRoleTypes),
|
|
197
|
+
mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields.concat(Platform_AdminApi$ReventlessCore.activeRoleMutationFields),
|
|
196
198
|
queries: [" Platform_ping: String"].concat(Platform_AdminApi$ReventlessCore.geocodeQueryFields),
|
|
197
199
|
subscriptions: [],
|
|
198
200
|
subscriptionSources: []
|
|
@@ -1129,6 +1131,9 @@ function MakeWithConfig(Config) {
|
|
|
1129
1131
|
if (index !== undefined && Config.splitApi) {
|
|
1130
1132
|
Geocoder_AwsLocation_Resolver$ReventlessAws.make(domainApi, index.indexName, undefined, {});
|
|
1131
1133
|
}
|
|
1134
|
+
if (Config.splitApi) {
|
|
1135
|
+
Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, cognitoPool.activeRoleTable, cognitoPool.poolId, cognitoPool.poolArn, undefined, {});
|
|
1136
|
+
}
|
|
1132
1137
|
let index$1 = hostUiBundle.geocoderPlaceIndex;
|
|
1133
1138
|
let geocoderPlaceIndexFlat = index$1 !== undefined ? index$1.indexName : Pulumi.output("");
|
|
1134
1139
|
Pulumi$Pulumi.$$export("geocoderPlaceIndex", geocoderPlaceIndexFlat);
|
|
@@ -1248,14 +1253,22 @@ function MakeWithConfig(Config) {
|
|
|
1248
1253
|
hooksApiRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApi);
|
|
1249
1254
|
hooksApiRoleRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApiRole);
|
|
1250
1255
|
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");
|
|
1256
|
+
let structureOffloadKey = {
|
|
1257
|
+
contents: undefined
|
|
1258
|
+
};
|
|
1251
1259
|
Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
|
|
1252
1260
|
let hash = NodeCrypto.sha256Hex(bytes);
|
|
1253
1261
|
let key = "sha256/" + hash;
|
|
1262
|
+
if (store === "pluginStructures") {
|
|
1263
|
+
structureOffloadKey.contents = key;
|
|
1264
|
+
}
|
|
1254
1265
|
new (Aws.s3.BucketObject)("offload-" + hash, {
|
|
1255
1266
|
bucket: offloadBucketName,
|
|
1256
1267
|
key: key,
|
|
1257
1268
|
content: bytes,
|
|
1258
1269
|
contentType: "application/json"
|
|
1270
|
+
}, {
|
|
1271
|
+
retainOnDelete: true
|
|
1259
1272
|
});
|
|
1260
1273
|
return {
|
|
1261
1274
|
store: store,
|
|
@@ -1273,6 +1286,19 @@ function MakeWithConfig(Config) {
|
|
|
1273
1286
|
Pulumi$Pulumi.$$export("_interopMeta", Plugin_Helpers$ReventlessCore.getInteropMeta());
|
|
1274
1287
|
let pluginOutputs = Component$ReventlessCore.outputs(pluginComponent);
|
|
1275
1288
|
Plugin_Helpers$ReventlessCore.exportPluginOutputs(pluginOutputs);
|
|
1289
|
+
let key = structureOffloadKey.contents;
|
|
1290
|
+
if (key !== undefined) {
|
|
1291
|
+
Pulumi$Pulumi.$$export("pluginStructureRef", pluginOutputs.id.apply(id => Object.fromEntries([
|
|
1292
|
+
[
|
|
1293
|
+
"plugin",
|
|
1294
|
+
Plugin$ReventlessCore.name(id)
|
|
1295
|
+
],
|
|
1296
|
+
[
|
|
1297
|
+
"key",
|
|
1298
|
+
key
|
|
1299
|
+
]
|
|
1300
|
+
])));
|
|
1301
|
+
}
|
|
1276
1302
|
if (platformStackRef !== undefined) {
|
|
1277
1303
|
let stackRef = Primitive_option.valFromOption(platformStackRef);
|
|
1278
1304
|
let defaultOutput = stackRef.getOutput("default");
|
|
@@ -1335,7 +1361,11 @@ function MakeWithConfig(Config) {
|
|
|
1335
1361
|
if (sel !== undefined) {
|
|
1336
1362
|
PgQueryResolver_Builder$ReventlessAws.provision(domainApi, sel, {});
|
|
1337
1363
|
}
|
|
1338
|
-
|
|
1364
|
+
let redetectReady = Pulumi.all([
|
|
1365
|
+
pluginOutputs.heartbeat,
|
|
1366
|
+
PluginRuntime_Builder$ReventlessAws.eventCollectorReadyRef.contents
|
|
1367
|
+
]);
|
|
1368
|
+
redetectReady.apply(param => {
|
|
1339
1369
|
let hbConfig = PluginRuntime_Builder$ReventlessAws.heartbeatConfigRef.contents;
|
|
1340
1370
|
let match = IndexJs.isDryRun();
|
|
1341
1371
|
let match$1 = hbConfig.epQueueUrl;
|
|
@@ -1465,8 +1495,8 @@ function Make($star) {
|
|
|
1465
1495
|
}
|
|
1466
1496
|
};
|
|
1467
1497
|
let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
1468
|
-
types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes),
|
|
1469
|
-
mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
|
|
1498
|
+
types: Platform_AdminApi$ReventlessCore.uploadTypes.concat(Platform_AdminApi$ReventlessCore.geocodeTypes).concat(Platform_AdminApi$ReventlessCore.activeRoleTypes),
|
|
1499
|
+
mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields.concat(Platform_AdminApi$ReventlessCore.activeRoleMutationFields),
|
|
1470
1500
|
queries: [" Platform_ping: String"].concat(Platform_AdminApi$ReventlessCore.geocodeQueryFields),
|
|
1471
1501
|
subscriptions: [],
|
|
1472
1502
|
subscriptionSources: []
|
|
@@ -2380,6 +2410,7 @@ function Make($star) {
|
|
|
2380
2410
|
if (index !== undefined) {
|
|
2381
2411
|
Geocoder_AwsLocation_Resolver$ReventlessAws.make(domainApi, index.indexName, undefined, {});
|
|
2382
2412
|
}
|
|
2413
|
+
Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, cognitoPool.activeRoleTable, cognitoPool.poolId, cognitoPool.poolArn, undefined, {});
|
|
2383
2414
|
let index$1 = hostUiBundle.geocoderPlaceIndex;
|
|
2384
2415
|
let geocoderPlaceIndexFlat = index$1 !== undefined ? index$1.indexName : Pulumi.output("");
|
|
2385
2416
|
Pulumi$Pulumi.$$export("geocoderPlaceIndex", geocoderPlaceIndexFlat);
|
|
@@ -2499,14 +2530,22 @@ function Make($star) {
|
|
|
2499
2530
|
hooksApiRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApi);
|
|
2500
2531
|
hooksApiRoleRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApiRole);
|
|
2501
2532
|
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");
|
|
2533
|
+
let structureOffloadKey = {
|
|
2534
|
+
contents: undefined
|
|
2535
|
+
};
|
|
2502
2536
|
Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
|
|
2503
2537
|
let hash = NodeCrypto.sha256Hex(bytes);
|
|
2504
2538
|
let key = "sha256/" + hash;
|
|
2539
|
+
if (store === "pluginStructures") {
|
|
2540
|
+
structureOffloadKey.contents = key;
|
|
2541
|
+
}
|
|
2505
2542
|
new (Aws.s3.BucketObject)("offload-" + hash, {
|
|
2506
2543
|
bucket: offloadBucketName,
|
|
2507
2544
|
key: key,
|
|
2508
2545
|
content: bytes,
|
|
2509
2546
|
contentType: "application/json"
|
|
2547
|
+
}, {
|
|
2548
|
+
retainOnDelete: true
|
|
2510
2549
|
});
|
|
2511
2550
|
return {
|
|
2512
2551
|
store: store,
|
|
@@ -2524,6 +2563,19 @@ function Make($star) {
|
|
|
2524
2563
|
Pulumi$Pulumi.$$export("_interopMeta", Plugin_Helpers$ReventlessCore.getInteropMeta());
|
|
2525
2564
|
let pluginOutputs = Component$ReventlessCore.outputs(pluginComponent);
|
|
2526
2565
|
Plugin_Helpers$ReventlessCore.exportPluginOutputs(pluginOutputs);
|
|
2566
|
+
let key = structureOffloadKey.contents;
|
|
2567
|
+
if (key !== undefined) {
|
|
2568
|
+
Pulumi$Pulumi.$$export("pluginStructureRef", pluginOutputs.id.apply(id => Object.fromEntries([
|
|
2569
|
+
[
|
|
2570
|
+
"plugin",
|
|
2571
|
+
Plugin$ReventlessCore.name(id)
|
|
2572
|
+
],
|
|
2573
|
+
[
|
|
2574
|
+
"key",
|
|
2575
|
+
key
|
|
2576
|
+
]
|
|
2577
|
+
])));
|
|
2578
|
+
}
|
|
2527
2579
|
if (platformStackRef !== undefined) {
|
|
2528
2580
|
let stackRef = Primitive_option.valFromOption(platformStackRef);
|
|
2529
2581
|
let defaultOutput = stackRef.getOutput("default");
|
|
@@ -2586,7 +2638,11 @@ function Make($star) {
|
|
|
2586
2638
|
if (sel !== undefined) {
|
|
2587
2639
|
PgQueryResolver_Builder$ReventlessAws.provision(domainApi, sel, {});
|
|
2588
2640
|
}
|
|
2589
|
-
|
|
2641
|
+
let redetectReady = Pulumi.all([
|
|
2642
|
+
pluginOutputs.heartbeat,
|
|
2643
|
+
PluginRuntime_Builder$ReventlessAws.eventCollectorReadyRef.contents
|
|
2644
|
+
]);
|
|
2645
|
+
redetectReady.apply(param => {
|
|
2590
2646
|
let hbConfig = PluginRuntime_Builder$ReventlessAws.heartbeatConfigRef.contents;
|
|
2591
2647
|
let match = IndexJs.isDryRun();
|
|
2592
2648
|
let match$1 = hbConfig.epQueueUrl;
|
package/src/Platform_Stack.res
CHANGED
|
@@ -6,6 +6,12 @@ type cognitoUserPool = {
|
|
|
6
6
|
clientId: Pulumi.Output.t<string>,
|
|
7
7
|
poolArn: Pulumi.Output.t<string>,
|
|
8
8
|
managed: bool,
|
|
9
|
+
/** Rows holding the role each caller chose to act as, read by the pool's
|
|
10
|
+
pre-token-generation trigger. Carried on the resolved pool because the table
|
|
11
|
+
has to be provisioned *before* the pool (the trigger reads it, and the pool
|
|
12
|
+
is declared with the trigger's ARN), while the mutation that writes it is
|
|
13
|
+
provisioned after the API โ see [Auth_ActiveRoleStore.res]. */
|
|
14
|
+
activeRoleTable: Auth_ActiveRoleStore.storeTable,
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
/**
|
|
@@ -32,6 +38,11 @@ type cognitoUserPool = {
|
|
|
32
38
|
* `cognitoUserPoolArn`, `cognitoRegion`, and `cognitoUserPoolManaged` as
|
|
33
39
|
* stack outputs so downstream stacks (and Stage D AppSync wiring) can read
|
|
34
40
|
* them via `StackReference`.
|
|
41
|
+
*
|
|
42
|
+
* Also provisions the role-state table and the pre-token-generation trigger that
|
|
43
|
+
* narrows `cognito:groups` to a caller's chosen role. They belong here rather
|
|
44
|
+
* than beside the mutation that writes the table because the pool must be
|
|
45
|
+
* declared carrying the trigger's ARN โ see the ordering note below.
|
|
35
46
|
*/
|
|
36
47
|
let _resolveUncached = (): cognitoUserPool => {
|
|
37
48
|
let cfg = Pulumi.Config.make(Some("platform"))
|
|
@@ -40,6 +51,33 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
40
51
|
| None => cfg->Pulumi.Config.get("cognitoUserPoolId")
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
// Provisioned ahead of the pool in both pool modes: the pre-token-generation
|
|
55
|
+
// trigger reads these rows, and in auto mode the pool is declared carrying the
|
|
56
|
+
// trigger's ARN โ so table โ trigger โ pool is a forced order, not a choice.
|
|
57
|
+
// See [docs/plans/active-role-narrows-the-token.md] ยง6.
|
|
58
|
+
//
|
|
59
|
+
// Unconditional, including for a unified-API platform that mounts no
|
|
60
|
+
// `Platform_SetActiveRole` and so can never write a row.
|
|
61
|
+
//
|
|
62
|
+
// This was briefly gated on whether a write door would exist, to save a
|
|
63
|
+
// DynamoDB read per sign-in in that mode. The gate was a parameter on this
|
|
64
|
+
// function โ and because the result is process-cached, it made the feature's
|
|
65
|
+
// existence depend on *which caller resolved the pool first*. `Auth_Cognito.make`
|
|
66
|
+
// reaches this through an adapter record and gets there before the platform
|
|
67
|
+
// does, so the whole feature silently vanished from the deploy: no error, no
|
|
68
|
+
// resource, nothing to notice. A local `pulumi preview` was the only thing that
|
|
69
|
+
// caught it.
|
|
70
|
+
//
|
|
71
|
+
// Saving one GetItem in a legacy mode is not worth a switch whose failure mode
|
|
72
|
+
// is a feature that quietly is not there. If the cost ever matters, the fix is
|
|
73
|
+
// a design that does not hinge on call order โ not this parameter again.
|
|
74
|
+
let activeRoleTable = Auth_ActiveRoleStore.makeTable(~opts={})
|
|
75
|
+
let activeRoleTrigger = Auth_ActiveRoleTrigger.make(
|
|
76
|
+
~activeRoleTableName=activeRoleTable.name->Pulumi.Output.asInput,
|
|
77
|
+
~activeRoleTableArn=activeRoleTable.arn->Pulumi.Output.asInput,
|
|
78
|
+
~opts={},
|
|
79
|
+
)
|
|
80
|
+
|
|
43
81
|
let result = switch existingPoolId {
|
|
44
82
|
| Some(poolId) =>
|
|
45
83
|
let tokenUnits: PulumiAws.Cognito.UserPoolClient.tokenValidityUnits = {
|
|
@@ -66,11 +104,25 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
66
104
|
|
|
67
105
|
let lookup = PulumiAws.Cognito.UserPool.getUserPoolOutput(~args={userPoolId: poolId})
|
|
68
106
|
|
|
107
|
+
// BYO: we hold no handle to set `lambdaConfig` on, and Cognito offers no
|
|
108
|
+
// separate attachment resource โ so the trigger is attached by a declared
|
|
109
|
+
// resource that describes the pool, merges into what it finds, and sends the
|
|
110
|
+
// result back whole. Never a hand-run command: that would leave the
|
|
111
|
+
// deployment's behaviour depending on an act no source describes.
|
|
112
|
+
let _attachment = Auth_ActiveRolePoolAttachment.make(
|
|
113
|
+
~props={
|
|
114
|
+
userPoolId: Pulumi.Input.make(poolId),
|
|
115
|
+
preTokenGenerationArn: activeRoleTrigger.functionArn->Pulumi.Output.asInput,
|
|
116
|
+
},
|
|
117
|
+
~opts={},
|
|
118
|
+
)
|
|
119
|
+
|
|
69
120
|
{
|
|
70
121
|
poolId: Pulumi.Output.make(poolId),
|
|
71
122
|
clientId: client.id,
|
|
72
123
|
poolArn: lookup->Pulumi.Output.apply(r => r.arn),
|
|
73
124
|
managed: false,
|
|
125
|
+
activeRoleTable,
|
|
74
126
|
}
|
|
75
127
|
|
|
76
128
|
| None =>
|
|
@@ -83,6 +135,11 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
83
135
|
requireUppercase: Pulumi.Input.make(true),
|
|
84
136
|
requireNumbers: Pulumi.Input.make(true),
|
|
85
137
|
}
|
|
138
|
+
// Auto: the pool is ours, so the trigger is an ordinary declared property and
|
|
139
|
+
// no out-of-band `UpdateUserPool` is involved. Declaring it here rather than
|
|
140
|
+
// attaching it afterwards also keeps Pulumi's own state the authority โ an
|
|
141
|
+
// attachment made behind the resource's back would read as drift on the next
|
|
142
|
+
// refresh and be reverted.
|
|
86
143
|
let pool = PulumiAws.Cognito.UserPool.make(
|
|
87
144
|
~name="HostUiPool",
|
|
88
145
|
~args={
|
|
@@ -90,6 +147,9 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
90
147
|
usernameAttributes: Pulumi.Input.make([Pulumi.Input.make("email")]),
|
|
91
148
|
passwordPolicy: Pulumi.Input.make(pwdPolicy),
|
|
92
149
|
mfaConfiguration: Pulumi.Input.make("OFF"),
|
|
150
|
+
lambdaConfig: Pulumi.Input.make({
|
|
151
|
+
PulumiAws.Cognito.UserPool.preTokenGeneration: activeRoleTrigger.functionArn->Pulumi.Output.asInput,
|
|
152
|
+
}),
|
|
93
153
|
tags: AWS.Tags.make(
|
|
94
154
|
~name="HostUiPool",
|
|
95
155
|
~kind=ReventlessCore.ComponentType.Platform,
|
|
@@ -126,9 +186,18 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
126
186
|
clientId: client.id,
|
|
127
187
|
poolArn: pool.arn,
|
|
128
188
|
managed: true,
|
|
189
|
+
activeRoleTable,
|
|
129
190
|
}
|
|
130
191
|
}
|
|
131
192
|
|
|
193
|
+
// After the branch, because the permission is scoped to the pool's ARN and the
|
|
194
|
+
// pool is what the branch produces.
|
|
195
|
+
Auth_ActiveRoleTrigger.grantInvoke(
|
|
196
|
+
~trigger=activeRoleTrigger,
|
|
197
|
+
~cognitoUserPoolArn=result.poolArn->Pulumi.Output.asInput,
|
|
198
|
+
~opts={},
|
|
199
|
+
)
|
|
200
|
+
|
|
132
201
|
let regionStr =
|
|
133
202
|
Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->Option.getOr("unknown")
|
|
134
203
|
|
|
@@ -149,6 +218,15 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
149
218
|
// stack-export calls happen exactly once per Pulumi run.
|
|
150
219
|
let _cached: ref<option<cognitoUserPool>> = ref(None)
|
|
151
220
|
|
|
221
|
+
/**
|
|
222
|
+
๐จ **Takes no arguments, and must not grow any.** Everything here is provisioned
|
|
223
|
+
on the *first* call and every later caller gets that same result โ so a
|
|
224
|
+
parameter would not configure the pool, it would configure whichever caller
|
|
225
|
+
happened to arrive first. `Auth_Cognito.make` reaches this through an adapter
|
|
226
|
+
record and gets here before the platform's own call does, which is not obvious
|
|
227
|
+
from any call site. Anything that needs to vary belongs in [_resolveUncached],
|
|
228
|
+
read from config or environment where every caller sees the same answer.
|
|
229
|
+
*/
|
|
152
230
|
let resolveCognitoUserPool = (): cognitoUserPool =>
|
|
153
231
|
switch _cached.contents {
|
|
154
232
|
| Some(p) => p
|
|
@@ -6,11 +6,16 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
7
|
import * as AWS_Tags$ReventlessAws from "./adapter/AWS_Tags.res.mjs";
|
|
8
8
|
import * as Util_LocalConfig$ReventlessAws from "./util/Util_LocalConfig.res.mjs";
|
|
9
|
+
import * as Auth_ActiveRoleStore$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleStore.res.mjs";
|
|
10
|
+
import * as Auth_ActiveRoleTrigger$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleTrigger.res.mjs";
|
|
11
|
+
import * as Auth_ActiveRolePoolAttachment$ReventlessAws from "./adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs";
|
|
9
12
|
|
|
10
13
|
function _resolveUncached() {
|
|
11
14
|
let cfg = new Pulumi.Config("platform");
|
|
12
15
|
let v = Util_LocalConfig$ReventlessAws.get("cognitoUserPoolId");
|
|
13
16
|
let existingPoolId = v !== undefined ? v : cfg.get("cognitoUserPoolId");
|
|
17
|
+
let activeRoleTable = Auth_ActiveRoleStore$ReventlessAws.makeTable(undefined, {});
|
|
18
|
+
let activeRoleTrigger = Auth_ActiveRoleTrigger$ReventlessAws.make(activeRoleTable.name, activeRoleTable.arn, undefined, {});
|
|
14
19
|
let result;
|
|
15
20
|
if (existingPoolId !== undefined) {
|
|
16
21
|
let tokenUnits_accessToken = "minutes";
|
|
@@ -37,11 +42,16 @@ function _resolveUncached() {
|
|
|
37
42
|
let lookup = Aws.cognito.getUserPoolOutput({
|
|
38
43
|
userPoolId: existingPoolId
|
|
39
44
|
});
|
|
45
|
+
Auth_ActiveRolePoolAttachment$ReventlessAws.make(undefined, {
|
|
46
|
+
userPoolId: existingPoolId,
|
|
47
|
+
preTokenGenerationArn: activeRoleTrigger.functionArn
|
|
48
|
+
}, {});
|
|
40
49
|
result = {
|
|
41
50
|
poolId: Pulumi.output(existingPoolId),
|
|
42
51
|
clientId: client.id,
|
|
43
52
|
poolArn: lookup.apply(r => r.arn),
|
|
44
|
-
managed: false
|
|
53
|
+
managed: false,
|
|
54
|
+
activeRoleTable: activeRoleTable
|
|
45
55
|
};
|
|
46
56
|
} else {
|
|
47
57
|
let adminConfig = {
|
|
@@ -58,6 +68,9 @@ function _resolveUncached() {
|
|
|
58
68
|
requireUppercase: pwdPolicy_requireUppercase
|
|
59
69
|
};
|
|
60
70
|
let pool = new (Aws.cognito.UserPool)("HostUiPool", {
|
|
71
|
+
lambdaConfig: {
|
|
72
|
+
preTokenGeneration: activeRoleTrigger.functionArn
|
|
73
|
+
},
|
|
61
74
|
adminCreateUserConfig: adminConfig,
|
|
62
75
|
usernameAttributes: ["email"],
|
|
63
76
|
passwordPolicy: pwdPolicy,
|
|
@@ -89,9 +102,11 @@ function _resolveUncached() {
|
|
|
89
102
|
poolId: pool.id,
|
|
90
103
|
clientId: client$1.id,
|
|
91
104
|
poolArn: pool.arn,
|
|
92
|
-
managed: true
|
|
105
|
+
managed: true,
|
|
106
|
+
activeRoleTable: activeRoleTable
|
|
93
107
|
};
|
|
94
108
|
}
|
|
109
|
+
Auth_ActiveRoleTrigger$ReventlessAws.grantInvoke(activeRoleTrigger, result.poolArn, undefined, {});
|
|
95
110
|
let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
|
|
96
111
|
Pulumi$Pulumi.$$export("cognitoUserPoolId", result.poolId);
|
|
97
112
|
Pulumi$Pulumi.$$export("cognitoUserPoolClientId", result.clientId);
|
|
@@ -117,6 +117,16 @@ let make = (
|
|
|
117
117
|
actions: Actions(["s3:GetObject"]),
|
|
118
118
|
resources: Resource("arn:aws:s3:::" ++ offloadBucket ++ "/*"),
|
|
119
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
|
+
},
|
|
120
130
|
],
|
|
121
131
|
)
|
|
122
132
|
->PolicyDocument.toJsonString
|
|
@@ -46,6 +46,7 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
|
|
|
46
46
|
pluginReadModelTableName,
|
|
47
47
|
offloadBucketName
|
|
48
48
|
]).apply(param => {
|
|
49
|
+
let offloadBucket = param[1];
|
|
49
50
|
new (Aws.iam.RolePolicy)(name + "LambdaPolicy", {
|
|
50
51
|
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "LambdaPolicy", [
|
|
51
52
|
{
|
|
@@ -64,7 +65,13 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
|
|
|
64
65
|
Sid: "AllowOffloadGet",
|
|
65
66
|
Effect: "Allow",
|
|
66
67
|
Action: ["s3:GetObject"],
|
|
67
|
-
Resource: "arn:aws:s3:::" +
|
|
68
|
+
Resource: "arn:aws:s3:::" + offloadBucket + "/*"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
Sid: "AllowOffloadList",
|
|
72
|
+
Effect: "Allow",
|
|
73
|
+
Action: ["s3:ListBucket"],
|
|
74
|
+
Resource: "arn:aws:s3:::" + offloadBucket
|
|
68
75
|
}
|
|
69
76
|
])),
|
|
70
77
|
role: lambdaRole.id
|