@reventlessdev/reventless-aws 3.0.0-alpha.293 → 3.0.0-alpha.294
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 +10 -0
- package/package.json +3 -3
- package/src/Platform.res +38 -25
- package/src/Platform.res.mjs +6 -4
- package/src/Platform_Stack.res +4 -0
- package/src/Platform_Stack.res.mjs +2 -1
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +211 -3
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +115 -4
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +6 -1
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +4 -2
- package/tests/Auth_ActiveRolePoolAttachmentTest.res +74 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.294 (2026-08-14)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** provision the active-role write door outside the host-UI branch ([2ffc121](https://github.com/ReventlessDev/reventless-core/commit/2ffc12198dfedd151c298add42fbb9bb0dc95ca6))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **aws:** prove the pre-token-generation trigger before attaching it to a pool ([6e981a4](https://github.com/ReventlessDev/reventless-core/commit/6e981a4f6b4089a140cd7dda7df00177b98d4a65))
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
# 3.0.0-alpha.293 (2026-08-13)
|
|
7
17
|
|
|
8
18
|
### 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.294",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
15
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.9",
|
|
16
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
16
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
17
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
18
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.5",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.74",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
21
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
22
|
"@reventlessdev/reventless-core": "3.0.0-alpha.232",
|
|
23
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.96",
|
|
23
24
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.139",
|
|
24
25
|
"@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
|
@@ -2107,31 +2107,6 @@ module MakeWithConfig = (
|
|
|
2107
2107
|
| _ => ()
|
|
2108
2108
|
}
|
|
2109
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
|
-
|
|
2135
2110
|
// The place index itself, exported so the unattended slice path can reach the
|
|
2136
2111
|
// capability: a slice's Lambda signs an SDK call directly and needs the index
|
|
2137
2112
|
// name, not an endpoint (no proxy hop, no dependence on a public URL). The
|
|
@@ -2283,6 +2258,44 @@ module MakeWithConfig = (
|
|
|
2283
2258
|
Pulumi.Pulumi.export("hostShellUrl", distributionUrl)
|
|
2284
2259
|
}
|
|
2285
2260
|
|
|
2261
|
+
// Where a caller records the role they want to act as
|
|
2262
|
+
// (docs/plans/active-role-narrows-the-token.md §6). The mutation only writes
|
|
2263
|
+
// a preference; the pre-token-generation trigger reads the row and re-checks
|
|
2264
|
+
// it against real membership before narrowing anything, so this door grants
|
|
2265
|
+
// nobody anything on its own.
|
|
2266
|
+
//
|
|
2267
|
+
// The table and the trigger were provisioned with the pool — they have to be,
|
|
2268
|
+
// since the pool is declared carrying the trigger's ARN. Only the write door
|
|
2269
|
+
// waits for the API, which is why it is here and not there.
|
|
2270
|
+
//
|
|
2271
|
+
// 🚨 **Outside `switch hostUiBundle`, and that is the whole point.** It was
|
|
2272
|
+
// written inside it, where the pool happens to be resolved — and a platform
|
|
2273
|
+
// whose shell ships from its own stack passes no bundle, so the door silently
|
|
2274
|
+
// did not exist there. The table and trigger still appeared, because
|
|
2275
|
+
// `resolveCognitoUserPool` is process-cached and `Auth_Cognito.make` reaches
|
|
2276
|
+
// it first: the feature deployed looking complete, with the trigger attached
|
|
2277
|
+
// to the pool and nothing able to write a row for it to read. Same shape as
|
|
2278
|
+
// the object-store endpoints at the top of this file, which sat in this branch
|
|
2279
|
+
// for the same reason and were moved out for the same one. Nothing that is not
|
|
2280
|
+
// *about* the host UI belongs inside this switch.
|
|
2281
|
+
//
|
|
2282
|
+
// Split-API only, like the upload and geocode resolvers: `Platform_SetActiveRole`
|
|
2283
|
+
// lives on the domain base document, which a unified-mode deployment's single
|
|
2284
|
+
// API does not carry. The table and trigger are provisioned either way — see
|
|
2285
|
+
// `Platform_Stack` for why that is not gated on this flag.
|
|
2286
|
+
if Config.splitApi {
|
|
2287
|
+
// Resolved here rather than reused from the branch above, which may not have
|
|
2288
|
+
// run. The call is process-cached, so this is the same pool either way.
|
|
2289
|
+
let pool = Platform_Stack.resolveCognitoUserPool()
|
|
2290
|
+
Auth_ActiveRoleStore.makeWriteDoor(
|
|
2291
|
+
~api=domainApi,
|
|
2292
|
+
~table=pool.activeRoleTable,
|
|
2293
|
+
~cognitoUserPoolId=pool.poolId->Pulumi.Output.asInput,
|
|
2294
|
+
~cognitoUserPoolArn=pool.poolArn->Pulumi.Output.asInput,
|
|
2295
|
+
~opts={},
|
|
2296
|
+
)
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2286
2299
|
Pulumi.Pulumi.getOutputs()
|
|
2287
2300
|
}
|
|
2288
2301
|
|
package/src/Platform.res.mjs
CHANGED
|
@@ -1131,9 +1131,6 @@ function MakeWithConfig(Config) {
|
|
|
1131
1131
|
if (index !== undefined && Config.splitApi) {
|
|
1132
1132
|
Geocoder_AwsLocation_Resolver$ReventlessAws.make(domainApi, index.indexName, undefined, {});
|
|
1133
1133
|
}
|
|
1134
|
-
if (Config.splitApi) {
|
|
1135
|
-
Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, cognitoPool.activeRoleTable, cognitoPool.poolId, cognitoPool.poolArn, undefined, {});
|
|
1136
|
-
}
|
|
1137
1134
|
let index$1 = hostUiBundle.geocoderPlaceIndex;
|
|
1138
1135
|
let geocoderPlaceIndexFlat = index$1 !== undefined ? index$1.indexName : Pulumi.output("");
|
|
1139
1136
|
Pulumi$Pulumi.$$export("geocoderPlaceIndex", geocoderPlaceIndexFlat);
|
|
@@ -1234,6 +1231,10 @@ function MakeWithConfig(Config) {
|
|
|
1234
1231
|
}
|
|
1235
1232
|
Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
|
|
1236
1233
|
}
|
|
1234
|
+
if (Config.splitApi) {
|
|
1235
|
+
let pool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
|
|
1236
|
+
Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, pool.activeRoleTable, pool.poolId, pool.poolArn, undefined, {});
|
|
1237
|
+
}
|
|
1237
1238
|
return Pulumi$Pulumi.getOutputs();
|
|
1238
1239
|
};
|
|
1239
1240
|
let startServers = () => {};
|
|
@@ -2410,7 +2411,6 @@ function Make($star) {
|
|
|
2410
2411
|
if (index !== undefined) {
|
|
2411
2412
|
Geocoder_AwsLocation_Resolver$ReventlessAws.make(domainApi, index.indexName, undefined, {});
|
|
2412
2413
|
}
|
|
2413
|
-
Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, cognitoPool.activeRoleTable, cognitoPool.poolId, cognitoPool.poolArn, undefined, {});
|
|
2414
2414
|
let index$1 = hostUiBundle.geocoderPlaceIndex;
|
|
2415
2415
|
let geocoderPlaceIndexFlat = index$1 !== undefined ? index$1.indexName : Pulumi.output("");
|
|
2416
2416
|
Pulumi$Pulumi.$$export("geocoderPlaceIndex", geocoderPlaceIndexFlat);
|
|
@@ -2511,6 +2511,8 @@ function Make($star) {
|
|
|
2511
2511
|
}
|
|
2512
2512
|
Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
|
|
2513
2513
|
}
|
|
2514
|
+
let pool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
|
|
2515
|
+
Auth_ActiveRoleStore$ReventlessAws.makeWriteDoor(domainApi, pool.activeRoleTable, pool.poolId, pool.poolArn, undefined, {});
|
|
2514
2516
|
return Pulumi$Pulumi.getOutputs();
|
|
2515
2517
|
};
|
|
2516
2518
|
let startServers = () => {};
|
package/src/Platform_Stack.res
CHANGED
|
@@ -109,10 +109,14 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
109
109
|
// resource that describes the pool, merges into what it finds, and sends the
|
|
110
110
|
// result back whole. Never a hand-run command: that would leave the
|
|
111
111
|
// deployment's behaviour depending on an act no source describes.
|
|
112
|
+
// The code hash is an input so a new build of the trigger reaches this
|
|
113
|
+
// resource: the function ARN is stable, so it alone would let a broken
|
|
114
|
+
// version onto the pool unchecked after the first deploy.
|
|
112
115
|
let _attachment = Auth_ActiveRolePoolAttachment.make(
|
|
113
116
|
~props={
|
|
114
117
|
userPoolId: Pulumi.Input.make(poolId),
|
|
115
118
|
preTokenGenerationArn: activeRoleTrigger.functionArn->Pulumi.Output.asInput,
|
|
119
|
+
codeHash: Pulumi.Input.make(activeRoleTrigger.sourceCodeHash),
|
|
116
120
|
},
|
|
117
121
|
~opts={},
|
|
118
122
|
)
|
|
@@ -44,7 +44,8 @@ function _resolveUncached() {
|
|
|
44
44
|
});
|
|
45
45
|
Auth_ActiveRolePoolAttachment$ReventlessAws.make(undefined, {
|
|
46
46
|
userPoolId: existingPoolId,
|
|
47
|
-
preTokenGenerationArn: activeRoleTrigger.functionArn
|
|
47
|
+
preTokenGenerationArn: activeRoleTrigger.functionArn,
|
|
48
|
+
codeHash: activeRoleTrigger.sourceCodeHash
|
|
48
49
|
}, {});
|
|
49
50
|
result = {
|
|
50
51
|
poolId: Pulumi.output(existingPoolId),
|
|
@@ -159,6 +159,54 @@ let getClient = async (): cognitoClient =>
|
|
|
159
159
|
c
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// ── Lambda bindings, for proving the trigger before attaching it ─────────────
|
|
163
|
+
|
|
164
|
+
type lambdaClient
|
|
165
|
+
type invokeCmd
|
|
166
|
+
|
|
167
|
+
type lambdaSdkModule = {
|
|
168
|
+
@as("LambdaClient") clientCtor: unit => lambdaClient,
|
|
169
|
+
@as("InvokeCommand") invokeCtor: dict<JSON.t> => invokeCmd,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type invokeResult = {
|
|
173
|
+
@as("FunctionError") functionError?: string,
|
|
174
|
+
@as("Payload") payload?: JSON.t,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@send external sendInvoke: (lambdaClient, invokeCmd) => promise<invokeResult> = "send"
|
|
178
|
+
@val external dynImportLambda: string => promise<lambdaSdkModule> = "import"
|
|
179
|
+
|
|
180
|
+
let _lambdaSdk: ref<option<lambdaSdkModule>> = ref(None)
|
|
181
|
+
let _lambdaClient: ref<option<lambdaClient>> = ref(None)
|
|
182
|
+
|
|
183
|
+
let getLambdaSdk = async (): lambdaSdkModule =>
|
|
184
|
+
switch _lambdaSdk.contents {
|
|
185
|
+
| Some(m) => m
|
|
186
|
+
| None =>
|
|
187
|
+
let m = await dynImportLambda("@aws-sdk/client-lambda")
|
|
188
|
+
_lambdaSdk.contents = Some(m)
|
|
189
|
+
m
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let getLambdaClient = async (): lambdaClient =>
|
|
193
|
+
switch _lambdaClient.contents {
|
|
194
|
+
| Some(c) => c
|
|
195
|
+
| None =>
|
|
196
|
+
let sdk = await getLambdaSdk()
|
|
197
|
+
let c = newOf0(sdk.clientCtor)
|
|
198
|
+
_lambdaClient.contents = Some(c)
|
|
199
|
+
c
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Decodes the SDK's `Payload`, which arrives as a byte array rather than a
|
|
203
|
+
string. */
|
|
204
|
+
let decodePayload: JSON.t => string = %raw(`(p) => {
|
|
205
|
+
if (p == null) return "";
|
|
206
|
+
if (typeof p === "string") return p;
|
|
207
|
+
try { return new TextDecoder().decode(p); } catch { return String(p); }
|
|
208
|
+
}`)
|
|
209
|
+
|
|
162
210
|
@get @return(nullable) external exnName: JsExn.t => option<string> = "name"
|
|
163
211
|
|
|
164
212
|
let isPoolGoneError = (jsErr: JsExn.t): bool =>
|
|
@@ -168,6 +216,129 @@ let isPoolGoneError = (jsErr: JsExn.t): bool =>
|
|
|
168
216
|
| _ => false
|
|
169
217
|
}
|
|
170
218
|
|
|
219
|
+
// ── Prove the trigger before attaching it ────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/** The subject the probe presents. It is not a real user and must never match
|
|
222
|
+
one: the handler looks the id up in the role table, and a probe that
|
|
223
|
+
collided with a real row would exercise a different branch than the one it
|
|
224
|
+
is here to check. */
|
|
225
|
+
let probeSubject = "reventless-attachment-probe"
|
|
226
|
+
|
|
227
|
+
/** A minimal `V1_0` pre-token-generation event.
|
|
228
|
+
|
|
229
|
+
Empty membership on purpose — with no groups and no stored role there is
|
|
230
|
+
nothing to narrow, so a healthy handler returns the event unchanged. The
|
|
231
|
+
probe is checking that the function *runs*, not what it decides; what it
|
|
232
|
+
decides has unit tests, and they do not need a deployed pool. */
|
|
233
|
+
let probeEvent = (~userPoolId: string): JSON.t => {
|
|
234
|
+
let groupConfiguration = Dict.make()
|
|
235
|
+
groupConfiguration->Dict.set("groupsToOverride", JSON.Encode.array([]))
|
|
236
|
+
groupConfiguration->Dict.set("iamRolesToOverride", JSON.Encode.array([]))
|
|
237
|
+
|
|
238
|
+
let userAttributes = Dict.make()
|
|
239
|
+
userAttributes->Dict.set("sub", JSON.Encode.string(probeSubject))
|
|
240
|
+
|
|
241
|
+
let request = Dict.make()
|
|
242
|
+
request->Dict.set("userAttributes", JSON.Encode.object(userAttributes))
|
|
243
|
+
request->Dict.set("groupConfiguration", JSON.Encode.object(groupConfiguration))
|
|
244
|
+
|
|
245
|
+
let event = Dict.make()
|
|
246
|
+
event->Dict.set("version", JSON.Encode.string("1"))
|
|
247
|
+
event->Dict.set("triggerSource", JSON.Encode.string("TokenGeneration_Authentication"))
|
|
248
|
+
event->Dict.set("userPoolId", JSON.Encode.string(userPoolId))
|
|
249
|
+
event->Dict.set("userName", JSON.Encode.string(probeSubject))
|
|
250
|
+
event->Dict.set("request", JSON.Encode.object(request))
|
|
251
|
+
event->Dict.set("response", JSON.Encode.object(Dict.make()))
|
|
252
|
+
JSON.Encode.object(event)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** What one probe invocation proved.
|
|
256
|
+
|
|
257
|
+
A variant rather than a bool so the two failures stay distinguishable: they
|
|
258
|
+
have different causes and want different sentences. */
|
|
259
|
+
type probeVerdict =
|
|
260
|
+
| Healthy
|
|
261
|
+
| Crashed(string)
|
|
262
|
+
| NotAnEvent
|
|
263
|
+
|
|
264
|
+
/** The verdict for a given invoke result.
|
|
265
|
+
|
|
266
|
+
Pure and total, so every branch is checkable without a deployed function —
|
|
267
|
+
the same reason [mergedUpdateInput] is. A payload that is not JSON at all
|
|
268
|
+
counts as not an event rather than throwing: the point here is to produce a
|
|
269
|
+
verdict, and a parse error escaping would fail the deploy with a message
|
|
270
|
+
about JSON instead of about the trigger. */
|
|
271
|
+
let probeVerdict = (~functionError: option<string>, ~payload: string): probeVerdict =>
|
|
272
|
+
switch functionError {
|
|
273
|
+
| Some(kind) => Crashed(kind)
|
|
274
|
+
| None =>
|
|
275
|
+
// A trigger must hand the event back for Cognito to mint anything from. A
|
|
276
|
+
// function that returns 200 with something else shaped is as fatal as one
|
|
277
|
+
// that throws, and rather harder to notice.
|
|
278
|
+
let hasRequest = try {
|
|
279
|
+
payload
|
|
280
|
+
->JSON.parseOrThrow
|
|
281
|
+
->JSON.Decode.object
|
|
282
|
+
->Option.flatMap(o => o->Dict.get("request"))
|
|
283
|
+
->Option.isSome
|
|
284
|
+
} catch {
|
|
285
|
+
| _ => false
|
|
286
|
+
}
|
|
287
|
+
hasRequest ? Healthy : NotAnEvent
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
Invoke the trigger once and refuse to attach it if it cannot answer.
|
|
292
|
+
|
|
293
|
+
🚨 **This is the check that keeps a broken trigger off a live pool.** Cognito
|
|
294
|
+
runs this function on every token it mints, and a function that throws fails the
|
|
295
|
+
sign-in — so a trigger that cannot start does not degrade the feature, it takes
|
|
296
|
+
authentication away from every user of the pool. Nothing downstream notices: the
|
|
297
|
+
deploy reports success, and the first report is a person unable to log in.
|
|
298
|
+
|
|
299
|
+
A trigger cannot make itself fail open. It dies at module load, before any
|
|
300
|
+
handler code runs, so no `try` inside the handler can catch it. The only place
|
|
301
|
+
that can refuse a broken function is the thing about to point a pool at it, and
|
|
302
|
+
that is here.
|
|
303
|
+
|
|
304
|
+
So: broken function → this throws → the deploy fails with the function's own
|
|
305
|
+
error → **the pool is never touched**. That is the same trade [readOnlyKeys]
|
|
306
|
+
already makes deliberately, where a loud failure beats a quiet reset.
|
|
307
|
+
|
|
308
|
+
Needs `lambda:InvokeFunction` on the deploying principal.
|
|
309
|
+
*/
|
|
310
|
+
let verifyTrigger = async (~userPoolId: string, ~preTokenGenerationArn: string): unit => {
|
|
311
|
+
let sdk = await getLambdaSdk()
|
|
312
|
+
let client = await getLambdaClient()
|
|
313
|
+
|
|
314
|
+
let input = Dict.make()
|
|
315
|
+
input->Dict.set("FunctionName", JSON.Encode.string(preTokenGenerationArn))
|
|
316
|
+
input->Dict.set("InvocationType", JSON.Encode.string("RequestResponse"))
|
|
317
|
+
input->Dict.set("Payload", JSON.Encode.string(probeEvent(~userPoolId)->JSON.stringify))
|
|
318
|
+
|
|
319
|
+
let result = await client->sendInvoke(newOf1(sdk.invokeCtor, input))
|
|
320
|
+
let payload = result.payload->Option.mapOr("", decodePayload)
|
|
321
|
+
|
|
322
|
+
// The payload carries the runtime's own error — module-resolution failures, a
|
|
323
|
+
// missing environment variable, an unhandled throw. Surfaced verbatim because
|
|
324
|
+
// it is the most useful sentence anyone debugging this will read.
|
|
325
|
+
switch probeVerdict(~functionError=result.functionError, ~payload) {
|
|
326
|
+
| Crashed(kind) =>
|
|
327
|
+
JsError.throwWithMessage(
|
|
328
|
+
`active-role trigger ${preTokenGenerationArn} failed its pre-attach check (${kind}) and was NOT attached to user pool ${userPoolId}. Attaching it would have failed every sign-in on that pool. The function reported: ${payload}`,
|
|
329
|
+
)
|
|
330
|
+
| NotAnEvent =>
|
|
331
|
+
JsError.throwWithMessage(
|
|
332
|
+
`active-role trigger ${preTokenGenerationArn} answered its pre-attach check with something that is not a pre-token-generation event, so it was NOT attached to user pool ${userPoolId}. Cognito would have failed every sign-in. It returned: ${payload}`,
|
|
333
|
+
)
|
|
334
|
+
| Healthy =>
|
|
335
|
+
log.info(
|
|
336
|
+
~comp="Auth_ActiveRolePoolAttachment",
|
|
337
|
+
`trigger ${preTokenGenerationArn} answered the pre-attach check; attaching to ${userPoolId}`,
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
171
342
|
// ── Describe → merge → update ────────────────────────────────────────────────
|
|
172
343
|
|
|
173
344
|
let describePool = async (~userPoolId: string): option<dict<JSON.t>> => {
|
|
@@ -200,11 +371,20 @@ let applyTrigger = async (~userPoolId: string, ~preTokenGenerationArn: option<st
|
|
|
200
371
|
type providerInputs = {
|
|
201
372
|
userPoolId: string,
|
|
202
373
|
preTokenGenerationArn: string,
|
|
374
|
+
/** The deployed function's code hash.
|
|
375
|
+
|
|
376
|
+
Carried purely so a code change reaches this resource. The function ARN is
|
|
377
|
+
stable across deploys, so without this the attachment sees identical inputs
|
|
378
|
+
every time and never runs again — meaning [verifyTrigger] would prove the
|
|
379
|
+
function once, on the deploy that created it, and never for any version
|
|
380
|
+
shipped afterwards. A check that only ever runs once is not a check. */
|
|
381
|
+
codeHash: string,
|
|
203
382
|
}
|
|
204
383
|
|
|
205
384
|
type createOuts = {
|
|
206
385
|
userPoolId: string,
|
|
207
386
|
preTokenGenerationArn: string,
|
|
387
|
+
codeHash: string,
|
|
208
388
|
}
|
|
209
389
|
|
|
210
390
|
type createResult = {id: string, outs: createOuts}
|
|
@@ -213,22 +393,43 @@ type diffResult = {changes: bool, replaces: array<string>, deleteBeforeReplace:
|
|
|
213
393
|
type readResult = {id?: string, props?: createOuts}
|
|
214
394
|
|
|
215
395
|
let create = async (inputs: providerInputs): createResult => {
|
|
396
|
+
await verifyTrigger(
|
|
397
|
+
~userPoolId=inputs.userPoolId,
|
|
398
|
+
~preTokenGenerationArn=inputs.preTokenGenerationArn,
|
|
399
|
+
)
|
|
216
400
|
await applyTrigger(
|
|
217
401
|
~userPoolId=inputs.userPoolId,
|
|
218
402
|
~preTokenGenerationArn=Some(inputs.preTokenGenerationArn),
|
|
219
403
|
)
|
|
220
404
|
{
|
|
221
405
|
id: inputs.userPoolId,
|
|
222
|
-
outs: {
|
|
406
|
+
outs: {
|
|
407
|
+
userPoolId: inputs.userPoolId,
|
|
408
|
+
preTokenGenerationArn: inputs.preTokenGenerationArn,
|
|
409
|
+
codeHash: inputs.codeHash,
|
|
410
|
+
},
|
|
223
411
|
}
|
|
224
412
|
}
|
|
225
413
|
|
|
226
414
|
let update = async (_id: string, _olds: createOuts, news: providerInputs): updateResult => {
|
|
415
|
+
// Re-proved on every update, not only on create: the usual reason this runs is
|
|
416
|
+
// that the function's code changed, which is exactly when it might have stopped
|
|
417
|
+
// working. The pool keeps the trigger it already has until this succeeds.
|
|
418
|
+
await verifyTrigger(
|
|
419
|
+
~userPoolId=news.userPoolId,
|
|
420
|
+
~preTokenGenerationArn=news.preTokenGenerationArn,
|
|
421
|
+
)
|
|
227
422
|
await applyTrigger(
|
|
228
423
|
~userPoolId=news.userPoolId,
|
|
229
424
|
~preTokenGenerationArn=Some(news.preTokenGenerationArn),
|
|
230
425
|
)
|
|
231
|
-
{
|
|
426
|
+
{
|
|
427
|
+
outs: {
|
|
428
|
+
userPoolId: news.userPoolId,
|
|
429
|
+
preTokenGenerationArn: news.preTokenGenerationArn,
|
|
430
|
+
codeHash: news.codeHash,
|
|
431
|
+
},
|
|
432
|
+
}
|
|
232
433
|
}
|
|
233
434
|
|
|
234
435
|
/** Detaching on destroy is not optional. The Lambda is torn down with the rest
|
|
@@ -253,7 +454,9 @@ let delete_ = async (_id: string, props: createOuts): unit =>
|
|
|
253
454
|
let diff_ = (_id: string, olds: createOuts, news: providerInputs): diffResult => {
|
|
254
455
|
let poolChanged = olds.userPoolId != news.userPoolId
|
|
255
456
|
{
|
|
256
|
-
changes: poolChanged ||
|
|
457
|
+
changes: poolChanged ||
|
|
458
|
+
olds.preTokenGenerationArn != news.preTokenGenerationArn ||
|
|
459
|
+
olds.codeHash != news.codeHash,
|
|
257
460
|
replaces: poolChanged ? ["userPoolId"] : [],
|
|
258
461
|
deleteBeforeReplace: true,
|
|
259
462
|
}
|
|
@@ -271,6 +474,10 @@ let read_ = async (id: string, props: createOuts): readResult =>
|
|
|
271
474
|
props: {
|
|
272
475
|
userPoolId: props.userPoolId,
|
|
273
476
|
preTokenGenerationArn: attachedTrigger(~described)->Option.getOr(""),
|
|
477
|
+
// The pool cannot report what code the function is running, so the
|
|
478
|
+
// recorded hash is carried through unchanged. Refresh answers "is the
|
|
479
|
+
// right function attached", which is what the pool actually knows.
|
|
480
|
+
codeHash: props.codeHash,
|
|
274
481
|
},
|
|
275
482
|
}
|
|
276
483
|
}
|
|
@@ -295,6 +502,7 @@ type t = {id: Pulumi.Output.t<string>}
|
|
|
295
502
|
type constructorProps = {
|
|
296
503
|
userPoolId: Pulumi.Input.t<string>,
|
|
297
504
|
preTokenGenerationArn: Pulumi.Input.t<string>,
|
|
505
|
+
codeHash: Pulumi.Input.t<string>,
|
|
298
506
|
}
|
|
299
507
|
|
|
300
508
|
// Explicit /index.js path because @pulumi/pulumi/dynamic is a directory import
|
|
@@ -89,6 +89,41 @@ async function getClient() {
|
|
|
89
89
|
return c$1;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
let _lambdaSdk = {
|
|
93
|
+
contents: undefined
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let _lambdaClient = {
|
|
97
|
+
contents: undefined
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
async function getLambdaSdk() {
|
|
101
|
+
let m = _lambdaSdk.contents;
|
|
102
|
+
if (m !== undefined) {
|
|
103
|
+
return m;
|
|
104
|
+
}
|
|
105
|
+
let m$1 = await import("@aws-sdk/client-lambda");
|
|
106
|
+
_lambdaSdk.contents = m$1;
|
|
107
|
+
return m$1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function getLambdaClient() {
|
|
111
|
+
let c = _lambdaClient.contents;
|
|
112
|
+
if (c !== undefined) {
|
|
113
|
+
return Primitive_option.valFromOption(c);
|
|
114
|
+
}
|
|
115
|
+
let sdk = await getLambdaSdk();
|
|
116
|
+
let c$1 = newOf0(sdk.LambdaClient);
|
|
117
|
+
_lambdaClient.contents = Primitive_option.some(c$1);
|
|
118
|
+
return c$1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let decodePayload = ((p) => {
|
|
122
|
+
if (p == null) return "";
|
|
123
|
+
if (typeof p === "string") return p;
|
|
124
|
+
try { return new TextDecoder().decode(p); } catch { return String(p); }
|
|
125
|
+
});
|
|
126
|
+
|
|
92
127
|
function isPoolGoneError(jsErr) {
|
|
93
128
|
let match = jsErr.name;
|
|
94
129
|
let match$1 = Stdlib_JsExn.message(jsErr);
|
|
@@ -107,6 +142,68 @@ function isPoolGoneError(jsErr) {
|
|
|
107
142
|
}
|
|
108
143
|
}
|
|
109
144
|
|
|
145
|
+
let probeSubject = "reventless-attachment-probe";
|
|
146
|
+
|
|
147
|
+
function probeEvent(userPoolId) {
|
|
148
|
+
let groupConfiguration = {};
|
|
149
|
+
groupConfiguration["groupsToOverride"] = [];
|
|
150
|
+
groupConfiguration["iamRolesToOverride"] = [];
|
|
151
|
+
let userAttributes = {};
|
|
152
|
+
userAttributes["sub"] = probeSubject;
|
|
153
|
+
let request = {};
|
|
154
|
+
request["userAttributes"] = userAttributes;
|
|
155
|
+
request["groupConfiguration"] = groupConfiguration;
|
|
156
|
+
let event = {};
|
|
157
|
+
event["version"] = "1";
|
|
158
|
+
event["triggerSource"] = "TokenGeneration_Authentication";
|
|
159
|
+
event["userPoolId"] = userPoolId;
|
|
160
|
+
event["userName"] = probeSubject;
|
|
161
|
+
event["request"] = request;
|
|
162
|
+
event["response"] = {};
|
|
163
|
+
return event;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function probeVerdict(functionError, payload) {
|
|
167
|
+
if (functionError !== undefined) {
|
|
168
|
+
return {
|
|
169
|
+
TAG: "Crashed",
|
|
170
|
+
_0: functionError
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
let hasRequest;
|
|
174
|
+
try {
|
|
175
|
+
hasRequest = Stdlib_Option.isSome(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(payload)), o => o["request"]));
|
|
176
|
+
} catch (exn) {
|
|
177
|
+
hasRequest = false;
|
|
178
|
+
}
|
|
179
|
+
if (hasRequest) {
|
|
180
|
+
return "Healthy";
|
|
181
|
+
} else {
|
|
182
|
+
return "NotAnEvent";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function verifyTrigger(userPoolId, preTokenGenerationArn) {
|
|
187
|
+
let sdk = await getLambdaSdk();
|
|
188
|
+
let client = await getLambdaClient();
|
|
189
|
+
let input = {};
|
|
190
|
+
input["FunctionName"] = preTokenGenerationArn;
|
|
191
|
+
input["InvocationType"] = "RequestResponse";
|
|
192
|
+
input["Payload"] = JSON.stringify(probeEvent(userPoolId));
|
|
193
|
+
let result = await client.send(newOf1(sdk.InvokeCommand, input));
|
|
194
|
+
let payload = Stdlib_Option.mapOr(result.Payload, "", decodePayload);
|
|
195
|
+
let kind = probeVerdict(result.FunctionError, payload);
|
|
196
|
+
if (typeof kind !== "object") {
|
|
197
|
+
if (kind === "Healthy") {
|
|
198
|
+
return log.info("Auth_ActiveRolePoolAttachment", undefined, `trigger ` + preTokenGenerationArn + ` answered the pre-attach check; attaching to ` + userPoolId);
|
|
199
|
+
} else {
|
|
200
|
+
return Stdlib_JsError.throwWithMessage(`active-role trigger ` + preTokenGenerationArn + ` answered its pre-attach check with something that is not a pre-token-generation event, so it was NOT attached to user pool ` + userPoolId + `. Cognito would have failed every sign-in. It returned: ` + payload);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
return Stdlib_JsError.throwWithMessage(`active-role trigger ` + preTokenGenerationArn + ` failed its pre-attach check (` + kind._0 + `) and was NOT attached to user pool ` + userPoolId + `. Attaching it would have failed every sign-in on that pool. The function reported: ` + payload);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
110
207
|
async function describePool(userPoolId) {
|
|
111
208
|
let sdk = await getSdk();
|
|
112
209
|
let client = await getClient();
|
|
@@ -128,22 +225,26 @@ async function applyTrigger(userPoolId, preTokenGenerationArn) {
|
|
|
128
225
|
}
|
|
129
226
|
|
|
130
227
|
async function create(inputs) {
|
|
228
|
+
await verifyTrigger(inputs.userPoolId, inputs.preTokenGenerationArn);
|
|
131
229
|
await applyTrigger(inputs.userPoolId, inputs.preTokenGenerationArn);
|
|
132
230
|
return {
|
|
133
231
|
id: inputs.userPoolId,
|
|
134
232
|
outs: {
|
|
135
233
|
userPoolId: inputs.userPoolId,
|
|
136
|
-
preTokenGenerationArn: inputs.preTokenGenerationArn
|
|
234
|
+
preTokenGenerationArn: inputs.preTokenGenerationArn,
|
|
235
|
+
codeHash: inputs.codeHash
|
|
137
236
|
}
|
|
138
237
|
};
|
|
139
238
|
}
|
|
140
239
|
|
|
141
240
|
async function update(_id, _olds, news) {
|
|
241
|
+
await verifyTrigger(news.userPoolId, news.preTokenGenerationArn);
|
|
142
242
|
await applyTrigger(news.userPoolId, news.preTokenGenerationArn);
|
|
143
243
|
return {
|
|
144
244
|
outs: {
|
|
145
245
|
userPoolId: news.userPoolId,
|
|
146
|
-
preTokenGenerationArn: news.preTokenGenerationArn
|
|
246
|
+
preTokenGenerationArn: news.preTokenGenerationArn,
|
|
247
|
+
codeHash: news.codeHash
|
|
147
248
|
}
|
|
148
249
|
};
|
|
149
250
|
}
|
|
@@ -163,7 +264,7 @@ async function delete_(_id, props) {
|
|
|
163
264
|
function diff_(_id, olds, news) {
|
|
164
265
|
let poolChanged = olds.userPoolId !== news.userPoolId;
|
|
165
266
|
return {
|
|
166
|
-
changes: poolChanged || olds.preTokenGenerationArn !== news.preTokenGenerationArn,
|
|
267
|
+
changes: poolChanged || olds.preTokenGenerationArn !== news.preTokenGenerationArn || olds.codeHash !== news.codeHash,
|
|
167
268
|
replaces: poolChanged ? ["userPoolId"] : [],
|
|
168
269
|
deleteBeforeReplace: true
|
|
169
270
|
};
|
|
@@ -177,7 +278,8 @@ async function read_(id, props) {
|
|
|
177
278
|
id: id,
|
|
178
279
|
props: {
|
|
179
280
|
userPoolId: props.userPoolId,
|
|
180
|
-
preTokenGenerationArn: Stdlib_Option.getOr(attachedTrigger(described), "")
|
|
281
|
+
preTokenGenerationArn: Stdlib_Option.getOr(attachedTrigger(described), ""),
|
|
282
|
+
codeHash: props.codeHash
|
|
181
283
|
}
|
|
182
284
|
};
|
|
183
285
|
} else {
|
|
@@ -216,7 +318,16 @@ export {
|
|
|
216
318
|
_client,
|
|
217
319
|
getSdk,
|
|
218
320
|
getClient,
|
|
321
|
+
_lambdaSdk,
|
|
322
|
+
_lambdaClient,
|
|
323
|
+
getLambdaSdk,
|
|
324
|
+
getLambdaClient,
|
|
325
|
+
decodePayload,
|
|
219
326
|
isPoolGoneError,
|
|
327
|
+
probeSubject,
|
|
328
|
+
probeEvent,
|
|
329
|
+
probeVerdict,
|
|
330
|
+
verifyTrigger,
|
|
220
331
|
describePool,
|
|
221
332
|
applyTrigger,
|
|
222
333
|
create,
|
|
@@ -20,6 +20,11 @@ open PulumiAws
|
|
|
20
20
|
type triggerOutputs = {
|
|
21
21
|
functionArn: Pulumi.Output.t<string>,
|
|
22
22
|
functionName: Pulumi.Output.t<string>,
|
|
23
|
+
/** The archive's hash, exposed so whoever attaches this trigger to a pool can
|
|
24
|
+
tell that the code changed. The function ARN does not move when the code
|
|
25
|
+
does, so without this an attachment sees identical inputs across deploys
|
|
26
|
+
and never re-checks a function it proved once. */
|
|
27
|
+
sourceCodeHash: string,
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
let make = (
|
|
@@ -150,7 +155,7 @@ let make = (
|
|
|
150
155
|
~opts,
|
|
151
156
|
)
|
|
152
157
|
|
|
153
|
-
{functionArn: lambda.arn, functionName: lambda.name}
|
|
158
|
+
{functionArn: lambda.arn, functionName: lambda.name, sourceCodeHash}
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
/** Let Cognito invoke the trigger — scoped to one pool, so a function attached
|
|
@@ -44,6 +44,7 @@ function make(activeRoleTableName, activeRoleTableArn, nameOpt, opts) {
|
|
|
44
44
|
Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
|
|
45
45
|
]]);
|
|
46
46
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs", packageDirs, undefined, false);
|
|
47
|
+
let sourceCodeHash = match.sourceCodeHash;
|
|
47
48
|
let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
|
|
48
49
|
let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, undefined, AWS_Tags$ReventlessAws.make(name + "LogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
|
|
49
50
|
let lambda = new (Aws.lambda.Function)(name, {
|
|
@@ -76,12 +77,13 @@ function make(activeRoleTableName, activeRoleTableArn, nameOpt, opts) {
|
|
|
76
77
|
Util_LambdaLogging$ReventlessAws.logLevelEntry()
|
|
77
78
|
])
|
|
78
79
|
},
|
|
79
|
-
sourceCodeHash:
|
|
80
|
+
sourceCodeHash: sourceCodeHash,
|
|
80
81
|
loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
|
|
81
82
|
}, opts$1);
|
|
82
83
|
return {
|
|
83
84
|
functionArn: lambda.arn,
|
|
84
|
-
functionName: lambda.name
|
|
85
|
+
functionName: lambda.name,
|
|
86
|
+
sourceCodeHash: sourceCodeHash
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -209,3 +209,77 @@ describe("Auth_ActiveRolePoolAttachment.attachedTrigger", () => {
|
|
|
209
209
|
)
|
|
210
210
|
)
|
|
211
211
|
})
|
|
212
|
+
|
|
213
|
+
// The pre-attach check. Its whole job is to keep a trigger that cannot run off a
|
|
214
|
+
// live pool, because a pre-token-generation trigger that throws does not degrade
|
|
215
|
+
// the feature — it fails every sign-in for every user of that pool.
|
|
216
|
+
describe("Auth_ActiveRolePoolAttachment.probeVerdict", () => {
|
|
217
|
+
// The real payload from the outage this check exists to prevent: the function
|
|
218
|
+
// could not resolve a package at module load, so it died during init and never
|
|
219
|
+
// reached its handler. Nothing inside the handler could have caught this.
|
|
220
|
+
let initFailurePayload = `{"errorType":"Error","errorMessage":"Cannot find package '@reventlessdev/reventless-core' imported from /var/task/node_modules/@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs","code":"ERR_MODULE_NOT_FOUND"}`
|
|
221
|
+
|
|
222
|
+
testSync("refuses a function that died before reaching its handler", () =>
|
|
223
|
+
expect(
|
|
224
|
+
Attachment.probeVerdict(~functionError=Some("Unhandled"), ~payload=initFailurePayload),
|
|
225
|
+
)->toEqual(Attachment.Crashed("Unhandled"))
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
testSync("accepts a function that hands the event back", () =>
|
|
229
|
+
expect(
|
|
230
|
+
Attachment.probeVerdict(
|
|
231
|
+
~functionError=None,
|
|
232
|
+
~payload=`{"request":{"groupConfiguration":{"groupsToOverride":[]}},"response":{}}`,
|
|
233
|
+
),
|
|
234
|
+
)->toEqual(Attachment.Healthy)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// A 200 carrying the wrong shape is as fatal to sign-in as a throw, and far
|
|
238
|
+
// easier to mistake for success.
|
|
239
|
+
testSync("refuses a successful call that returns something else", () =>
|
|
240
|
+
expect(Attachment.probeVerdict(~functionError=None, ~payload=`{"ok":true}`))->toEqual(
|
|
241
|
+
Attachment.NotAnEvent,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
// Produces a verdict rather than escaping: a parse error here would fail the
|
|
246
|
+
// deploy with a message about JSON instead of about the trigger.
|
|
247
|
+
testSync("treats an unparseable payload as not an event", () =>
|
|
248
|
+
expect(Attachment.probeVerdict(~functionError=None, ~payload="<html>502</html>"))->toEqual(
|
|
249
|
+
Attachment.NotAnEvent,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
testSync("treats an empty payload as not an event", () =>
|
|
254
|
+
expect(Attachment.probeVerdict(~functionError=None, ~payload=""))->toEqual(
|
|
255
|
+
Attachment.NotAnEvent,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe("Auth_ActiveRolePoolAttachment.probeEvent", () => {
|
|
261
|
+
let event = Attachment.probeEvent(~userPoolId="eu-west-1_Example")->JSON.Decode.object
|
|
262
|
+
|
|
263
|
+
testSync("is shaped like the V1_0 event Cognito sends", () =>
|
|
264
|
+
expect(
|
|
265
|
+
event
|
|
266
|
+
->Option.flatMap(o => o->Dict.get("request"))
|
|
267
|
+
->Option.flatMap(JSON.Decode.object)
|
|
268
|
+
->Option.isSome,
|
|
269
|
+
)->toBe(true)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Empty membership and a subject that cannot exist: the probe checks that the
|
|
273
|
+
// function runs, not what it decides, and must not collide with a real row.
|
|
274
|
+
testSync("presents no groups to narrow", () =>
|
|
275
|
+
expect(
|
|
276
|
+
event
|
|
277
|
+
->Option.flatMap(o => o->Dict.get("request"))
|
|
278
|
+
->Option.flatMap(JSON.Decode.object)
|
|
279
|
+
->Option.flatMap(r => r->Dict.get("groupConfiguration"))
|
|
280
|
+
->Option.flatMap(JSON.Decode.object)
|
|
281
|
+
->Option.flatMap(g => g->Dict.get("groupsToOverride"))
|
|
282
|
+
->Option.flatMap(JSON.Decode.array),
|
|
283
|
+
)->toEqual(Some([]))
|
|
284
|
+
)
|
|
285
|
+
})
|
|
@@ -219,6 +219,37 @@ globalThis.describe("Auth_ActiveRolePoolAttachment.attachedTrigger", () => {
|
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.probeVerdict", () => {
|
|
223
|
+
globalThis.test("refuses a function that died before reaching its handler", () => {
|
|
224
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict("Unhandled", `{"errorType":"Error","errorMessage":"Cannot find package '@reventlessdev/reventless-core' imported from /var/task/node_modules/@reventlessdev/reventless-aws/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs","code":"ERR_MODULE_NOT_FOUND"}`)).toEqual({
|
|
225
|
+
TAG: "Crashed",
|
|
226
|
+
_0: "Unhandled"
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
globalThis.test("accepts a function that hands the event back", () => {
|
|
230
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, `{"request":{"groupConfiguration":{"groupsToOverride":[]}},"response":{}}`)).toEqual("Healthy");
|
|
231
|
+
});
|
|
232
|
+
globalThis.test("refuses a successful call that returns something else", () => {
|
|
233
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, `{"ok":true}`)).toEqual("NotAnEvent");
|
|
234
|
+
});
|
|
235
|
+
globalThis.test("treats an unparseable payload as not an event", () => {
|
|
236
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, "<html>502</html>")).toEqual("NotAnEvent");
|
|
237
|
+
});
|
|
238
|
+
globalThis.test("treats an empty payload as not an event", () => {
|
|
239
|
+
globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.probeVerdict(undefined, "")).toEqual("NotAnEvent");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
globalThis.describe("Auth_ActiveRolePoolAttachment.probeEvent", () => {
|
|
244
|
+
let event = Stdlib_JSON.Decode.object(Auth_ActiveRolePoolAttachment$ReventlessAws.probeEvent("eu-west-1_Example"));
|
|
245
|
+
globalThis.test("is shaped like the V1_0 event Cognito sends", () => {
|
|
246
|
+
globalThis.expect(Stdlib_Option.isSome(Stdlib_Option.flatMap(Stdlib_Option.flatMap(event, o => o["request"]), Stdlib_JSON.Decode.object))).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
globalThis.test("presents no groups to narrow", () => {
|
|
249
|
+
globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(event, o => o["request"]), Stdlib_JSON.Decode.object), r => r["groupConfiguration"]), Stdlib_JSON.Decode.object), g => g["groupsToOverride"]), Stdlib_JSON.Decode.array)).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
222
253
|
let Attachment;
|
|
223
254
|
|
|
224
255
|
export {
|