@reventlessdev/reventless-aws 3.0.0-alpha.229 → 3.0.0-alpha.231
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 +5 -5
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +60 -141
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +52 -118
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +137 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +124 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +60 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +29 -1
- package/src/adapter/Runtime/HeartbeatEntryPoint.res +44 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +47 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +234 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs +226 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
- package/src/adapter/Upload/Upload_Presign_S3.res +65 -164
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +56 -119
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res +157 -0
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +120 -0
- package/src/components/InboundTranslationSlice_Builder.res +27 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -1
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +59 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +49 -2
- package/tests/DcbInboundTranslationRoutingTest.res +73 -0
- package/tests/DcbInboundTranslationRoutingTest.res.mjs +71 -0
- package/tests/EpInboundTestSlice.res +19 -0
- package/tests/EpInboundTestSlice.res.mjs +31 -0
- package/tests/EpInboundTestSliceTranslation.res +13 -0
- package/tests/EpInboundTestSliceTranslation.res.mjs +33 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +0 -38
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +0 -127
|
@@ -1,113 +1,16 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Aws from "@pulumi/aws";
|
|
4
|
-
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
4
|
import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
|
|
6
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
8
7
|
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
9
8
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
9
|
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
11
|
-
import * as ClientS3 from "@aws-sdk/client-s3";
|
|
12
10
|
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
13
11
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
|
+
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
14
13
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
15
|
-
import * as S3RequestPresigner from "@aws-sdk/s3-request-presigner";
|
|
16
|
-
|
|
17
|
-
let getEnv = (function(k) { var v = process.env[k]; return (v === undefined || v === null || v === "") ? undefined : v; });
|
|
18
|
-
|
|
19
|
-
let genUuid = (function() {
|
|
20
|
-
return (globalThis.crypto && globalThis.crypto.randomUUID)
|
|
21
|
-
? globalThis.crypto.randomUUID()
|
|
22
|
-
: require("crypto").randomUUID();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
let decodeJwtSub = (function(header) {
|
|
26
|
-
try {
|
|
27
|
-
var token = header.indexOf("Bearer ") === 0 ? header.slice(7) : header;
|
|
28
|
-
var parts = token.split(".");
|
|
29
|
-
if (parts.length < 2) return undefined;
|
|
30
|
-
var base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
31
|
-
var json = Buffer.from(base64, "base64").toString("utf8");
|
|
32
|
-
var claims = JSON.parse(json);
|
|
33
|
-
return (claims && claims.sub) ? String(claims.sub) : undefined;
|
|
34
|
-
} catch (_) { return undefined; }
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
function corsHeaders() {
|
|
38
|
-
return Object.fromEntries([
|
|
39
|
-
[
|
|
40
|
-
"content-type",
|
|
41
|
-
"application/json"
|
|
42
|
-
],
|
|
43
|
-
[
|
|
44
|
-
"access-control-allow-origin",
|
|
45
|
-
"*"
|
|
46
|
-
],
|
|
47
|
-
[
|
|
48
|
-
"access-control-allow-methods",
|
|
49
|
-
"POST,OPTIONS"
|
|
50
|
-
],
|
|
51
|
-
[
|
|
52
|
-
"access-control-allow-headers",
|
|
53
|
-
"*"
|
|
54
|
-
]
|
|
55
|
-
]);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function identityPrefix(event) {
|
|
59
|
-
let authHeader = Stdlib_Option.flatMap(event.headers, h => Stdlib_Option.orElse(h["authorization"], h["Authorization"]));
|
|
60
|
-
let sub = Stdlib_Option.flatMap(authHeader, decodeJwtSub);
|
|
61
|
-
if (sub !== undefined) {
|
|
62
|
-
return sub + `/`;
|
|
63
|
-
} else {
|
|
64
|
-
return "";
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function handlePresign(event, _context) {
|
|
69
|
-
try {
|
|
70
|
-
let bucket = Stdlib_Option.getOr(getEnv("UPLOAD_BUCKET"), "");
|
|
71
|
-
let parsed = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(Stdlib_Option.getOr(event.body, "{}"))), {});
|
|
72
|
-
let fileName = Stdlib_Option.getOr(Stdlib_Option.flatMap(parsed["fileName"], Stdlib_JSON.Decode.string), "upload");
|
|
73
|
-
let contentType = Stdlib_Option.flatMap(parsed["contentType"], Stdlib_JSON.Decode.string);
|
|
74
|
-
let servedPrefix = Stdlib_Option.getOr(getEnv("SERVED_PREFIX"), "uploads");
|
|
75
|
-
let key = servedPrefix + `/` + identityPrefix(event) + genUuid() + `/` + fileName;
|
|
76
|
-
let client = new ClientS3.S3Client();
|
|
77
|
-
let command = new ClientS3.PutObjectCommand({
|
|
78
|
-
Bucket: bucket,
|
|
79
|
-
Key: key,
|
|
80
|
-
ContentType: contentType
|
|
81
|
-
});
|
|
82
|
-
let uploadUrl = await S3RequestPresigner.getSignedUrl(client, command, {
|
|
83
|
-
expiresIn: 300
|
|
84
|
-
});
|
|
85
|
-
let storageRef = `/` + key;
|
|
86
|
-
return {
|
|
87
|
-
statusCode: 200,
|
|
88
|
-
headers: corsHeaders(),
|
|
89
|
-
body: JSON.stringify(Object.fromEntries([
|
|
90
|
-
[
|
|
91
|
-
"uploadUrl",
|
|
92
|
-
uploadUrl
|
|
93
|
-
],
|
|
94
|
-
[
|
|
95
|
-
"storageRef",
|
|
96
|
-
storageRef
|
|
97
|
-
]
|
|
98
|
-
]))
|
|
99
|
-
};
|
|
100
|
-
} catch (exn) {
|
|
101
|
-
return {
|
|
102
|
-
statusCode: 400,
|
|
103
|
-
headers: corsHeaders(),
|
|
104
|
-
body: JSON.stringify(Object.fromEntries([[
|
|
105
|
-
"error",
|
|
106
|
-
"presign_failed"
|
|
107
|
-
]]))
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
14
|
|
|
112
15
|
function make(bucketName, corsOriginsOpt, servedPrefixOpt, opts) {
|
|
113
16
|
let corsOrigins = corsOriginsOpt !== undefined ? corsOriginsOpt : ["*"];
|
|
@@ -118,28 +21,68 @@ function make(bucketName, corsOriginsOpt, servedPrefixOpt, opts) {
|
|
|
118
21
|
bucketName.apply(b => {
|
|
119
22
|
let arn = `arn:aws:s3:::` + b + `/*`;
|
|
120
23
|
new (Aws.iam.RolePolicy)(serviceName + `Policy`, {
|
|
121
|
-
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, serviceName + `Policy`, [
|
|
24
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, serviceName + `Policy`, [
|
|
25
|
+
{
|
|
26
|
+
Sid: "AllowLambdaLogging",
|
|
27
|
+
Effect: "Allow",
|
|
28
|
+
Action: [
|
|
29
|
+
"logs:CreateLogGroup",
|
|
30
|
+
"logs:CreateLogStream",
|
|
31
|
+
"logs:PutLogEvents"
|
|
32
|
+
],
|
|
33
|
+
Resource: "arn:aws:logs:*:*:*"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
122
36
|
Sid: "AllowUploadPut",
|
|
123
37
|
Effect: "Allow",
|
|
124
38
|
Action: "s3:PutObject",
|
|
125
39
|
Resource: arn
|
|
126
|
-
}
|
|
40
|
+
}
|
|
41
|
+
])),
|
|
127
42
|
role: lambdaRole.id
|
|
128
43
|
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
129
44
|
});
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
45
|
+
let packageDirs = Object.fromEntries([[
|
|
46
|
+
"@reventlessdev/reventless-aws",
|
|
47
|
+
Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws")
|
|
48
|
+
]]);
|
|
49
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs", packageDirs, undefined);
|
|
50
|
+
let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
|
|
51
|
+
let lambda = new (Aws.lambda.Function)(serviceName, {
|
|
52
|
+
handler: "index.handler",
|
|
53
|
+
runtime: "nodejs22.x",
|
|
54
|
+
code: match.code,
|
|
55
|
+
role: lambdaRole.arn,
|
|
56
|
+
memorySize: 256,
|
|
57
|
+
timeout: 30,
|
|
58
|
+
layers: layers,
|
|
59
|
+
tags: AWS_Tags$ReventlessAws.make(serviceName, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined),
|
|
60
|
+
environment: {
|
|
61
|
+
variables: Object.fromEntries([
|
|
62
|
+
[
|
|
63
|
+
"Environment",
|
|
64
|
+
Pulumi.getStack()
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
"UPLOAD_BUCKET",
|
|
68
|
+
bucketName
|
|
69
|
+
],
|
|
70
|
+
[
|
|
71
|
+
"SERVED_PREFIX",
|
|
72
|
+
servedPrefix
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"NODE_OPTIONS",
|
|
76
|
+
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
"ESM_FALLBACK_DIRS",
|
|
80
|
+
Util_Bundle$ReventlessAws.esmFallbackDirs
|
|
81
|
+
]
|
|
82
|
+
])
|
|
83
|
+
},
|
|
84
|
+
sourceCodeHash: match.sourceCodeHash
|
|
85
|
+
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
143
86
|
let functionUrl = new (Aws.lambda.FunctionUrl)(serviceName + `Url`, {
|
|
144
87
|
authorizationType: "NONE",
|
|
145
88
|
functionName: lambda.name,
|
|
@@ -159,12 +102,6 @@ function make(bucketName, corsOriginsOpt, servedPrefixOpt, opts) {
|
|
|
159
102
|
}
|
|
160
103
|
|
|
161
104
|
export {
|
|
162
|
-
getEnv,
|
|
163
|
-
genUuid,
|
|
164
|
-
decodeJwtSub,
|
|
165
|
-
corsHeaders,
|
|
166
|
-
identityPrefix,
|
|
167
|
-
handlePresign,
|
|
168
105
|
make,
|
|
169
106
|
}
|
|
170
107
|
/* @pulumi/aws Not a pure module */
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Runtime handler for the upload presign service — compiled, type-checked, and
|
|
2
|
+
// Pulumi-free so it can be shipped as an EntryPoint module (`Upload_Presign_S3`
|
|
3
|
+
// bundles it and re-exports `handler` from the code archive). Keeping it out of
|
|
4
|
+
// the deploy-time module is what avoids both the serialized-closure SDK skew and
|
|
5
|
+
// the deploy-time Pulumi import leaking into the Lambda's cold-start graph.
|
|
6
|
+
//
|
|
7
|
+
// Reads a JSON body `{fileName, contentType}`, derives a target key
|
|
8
|
+
// `uploads/<identity?>/<uuid>/<fileName>`, presigns a PUT to `UPLOAD_BUCKET`
|
|
9
|
+
// (expires in 300s), and returns `{uploadUrl, storageRef}`. A Bearer token, when
|
|
10
|
+
// present, is decoded (no signature verification here — the auth layer verifies;
|
|
11
|
+
// here we only namespace the storage key) and its `sub` namespaces the key.
|
|
12
|
+
|
|
13
|
+
// ── AWS SDK v3 bindings ─────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
type s3Client
|
|
16
|
+
|
|
17
|
+
type putObjectInput = {
|
|
18
|
+
@as("Bucket") bucket: string,
|
|
19
|
+
@as("Key") key: string,
|
|
20
|
+
@as("ContentType") contentType?: string,
|
|
21
|
+
}
|
|
22
|
+
type putObjectCommand
|
|
23
|
+
|
|
24
|
+
@module("@aws-sdk/client-s3") @new external makeS3Client: unit => s3Client = "S3Client"
|
|
25
|
+
|
|
26
|
+
@module("@aws-sdk/client-s3") @new
|
|
27
|
+
external makePutObjectCommand: putObjectInput => putObjectCommand = "PutObjectCommand"
|
|
28
|
+
|
|
29
|
+
type presignOptions = {expiresIn: int}
|
|
30
|
+
|
|
31
|
+
@module("@aws-sdk/s3-request-presigner")
|
|
32
|
+
external getSignedUrl: (s3Client, putObjectCommand, presignOptions) => promise<string> =
|
|
33
|
+
"getSignedUrl"
|
|
34
|
+
|
|
35
|
+
// ── Node bindings (replacing the former `%raw` helpers with typed externals) ──
|
|
36
|
+
|
|
37
|
+
@val @scope("process") external processEnv: dict<string> = "env"
|
|
38
|
+
|
|
39
|
+
@module("node:crypto") external randomUUID: unit => string = "randomUUID"
|
|
40
|
+
|
|
41
|
+
type buffer
|
|
42
|
+
@val @scope("Buffer") external bufferFromBase64: (string, string) => buffer = "from"
|
|
43
|
+
@send external bufferToString: (buffer, string) => string = "toString"
|
|
44
|
+
|
|
45
|
+
// Read an env var, mapping "" / unset to None.
|
|
46
|
+
let getEnv = (k: string): option<string> =>
|
|
47
|
+
switch processEnv->Dict.get(k) {
|
|
48
|
+
| Some("") | None => None
|
|
49
|
+
| Some(v) => Some(v)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Function URL event / response shapes (payload format 2.0) ────────────────
|
|
53
|
+
|
|
54
|
+
type functionUrlEvent = {
|
|
55
|
+
body?: string,
|
|
56
|
+
headers?: dict<string>,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type response = {
|
|
60
|
+
statusCode: int,
|
|
61
|
+
headers?: dict<string>,
|
|
62
|
+
body: string,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let corsHeaders = () =>
|
|
66
|
+
Dict.fromArray([
|
|
67
|
+
("content-type", "application/json"),
|
|
68
|
+
("access-control-allow-origin", "*"),
|
|
69
|
+
("access-control-allow-methods", "POST,OPTIONS"),
|
|
70
|
+
("access-control-allow-headers", "*"),
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
// Decode a JWT payload's `sub` claim without verifying the signature.
|
|
74
|
+
let decodeJwtSub = (header: string): option<string> =>
|
|
75
|
+
try {
|
|
76
|
+
let token =
|
|
77
|
+
header->String.startsWith("Bearer ")
|
|
78
|
+
? header->String.slice(~start=7, ~end=header->String.length)
|
|
79
|
+
: header
|
|
80
|
+
switch token->String.split(".")->Array.get(1) {
|
|
81
|
+
| Some(payload) =>
|
|
82
|
+
let base64 = payload->String.replaceAll("-", "+")->String.replaceAll("_", "/")
|
|
83
|
+
switch JSON.parseOrThrow(bufferFromBase64(base64, "base64")->bufferToString("utf8")) {
|
|
84
|
+
| Object(obj) => obj->Dict.get("sub")->Option.flatMap(JSON.Decode.string)
|
|
85
|
+
| _ => None
|
|
86
|
+
}
|
|
87
|
+
| None => None
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
| _ => None
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Key prefix from the caller identity when a Bearer token is present.
|
|
94
|
+
let identityPrefix = (event: functionUrlEvent): string => {
|
|
95
|
+
let authHeader =
|
|
96
|
+
event.headers->Option.flatMap(h =>
|
|
97
|
+
h->Dict.get("authorization")->Option.orElse(h->Dict.get("Authorization"))
|
|
98
|
+
)
|
|
99
|
+
switch authHeader->Option.flatMap(decodeJwtSub) {
|
|
100
|
+
| Some(sub) => `${sub}/`
|
|
101
|
+
| None => ""
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Runtime handler ─────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
let handler = async (event: functionUrlEvent): response => {
|
|
108
|
+
try {
|
|
109
|
+
let bucket = getEnv("UPLOAD_BUCKET")->Option.getOr("")
|
|
110
|
+
let parsed =
|
|
111
|
+
event.body
|
|
112
|
+
->Option.getOr("{}")
|
|
113
|
+
->JSON.parseOrThrow
|
|
114
|
+
->JSON.Decode.object
|
|
115
|
+
->Option.getOr(Dict.make())
|
|
116
|
+
let fileName =
|
|
117
|
+
parsed->Dict.get("fileName")->Option.flatMap(JSON.Decode.string)->Option.getOr("upload")
|
|
118
|
+
let contentType = parsed->Dict.get("contentType")->Option.flatMap(JSON.Decode.string)
|
|
119
|
+
|
|
120
|
+
// The object key doubles as the served path segment: it is rooted at the
|
|
121
|
+
// served prefix (`SERVED_PREFIX`, e.g. `uploads`) so the CloudFront
|
|
122
|
+
// `{prefix}/*` behavior fronts it. Callers PUT to this exact key.
|
|
123
|
+
let servedPrefix = getEnv("SERVED_PREFIX")->Option.getOr("uploads")
|
|
124
|
+
let key = `${servedPrefix}/${identityPrefix(event)}${randomUUID()}/${fileName}`
|
|
125
|
+
let client = makeS3Client()
|
|
126
|
+
let command = makePutObjectCommand({bucket, key, contentType: ?contentType})
|
|
127
|
+
let uploadUrl = await getSignedUrl(client, command, {expiresIn: 300})
|
|
128
|
+
|
|
129
|
+
// The stored ref is a same-origin relative URL `/{key}`: the served bucket is
|
|
130
|
+
// fronted read-only by the UI's own CloudFront distribution under
|
|
131
|
+
// `{prefix}/*`, so a command stores this directly-renderable value and an
|
|
132
|
+
// `image`-semantic field thumbnails it with no post-processing and no public
|
|
133
|
+
// bucket. See [docs/plans/done/ui-served-buckets.md].
|
|
134
|
+
let storageRef = `/${key}`
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
statusCode: 200,
|
|
138
|
+
headers: corsHeaders(),
|
|
139
|
+
body: Dict.fromArray([
|
|
140
|
+
("uploadUrl", JSON.Encode.string(uploadUrl)),
|
|
141
|
+
("storageRef", JSON.Encode.string(storageRef)),
|
|
142
|
+
])
|
|
143
|
+
->JSON.Encode.object
|
|
144
|
+
->JSON.stringify,
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
| exn =>
|
|
148
|
+
Console.error2("UploadPresign: presign failed", exn)
|
|
149
|
+
{
|
|
150
|
+
statusCode: 400,
|
|
151
|
+
headers: corsHeaders(),
|
|
152
|
+
body: Dict.fromArray([("error", JSON.Encode.string("presign_failed"))])
|
|
153
|
+
->JSON.Encode.object
|
|
154
|
+
->JSON.stringify,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Nodecrypto from "node:crypto";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as ClientS3 from "@aws-sdk/client-s3";
|
|
7
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
8
|
+
import * as S3RequestPresigner from "@aws-sdk/s3-request-presigner";
|
|
9
|
+
|
|
10
|
+
function getEnv(k) {
|
|
11
|
+
let v = process.env[k];
|
|
12
|
+
if (v !== undefined && v !== "") {
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function corsHeaders() {
|
|
18
|
+
return Object.fromEntries([
|
|
19
|
+
[
|
|
20
|
+
"content-type",
|
|
21
|
+
"application/json"
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"access-control-allow-origin",
|
|
25
|
+
"*"
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"access-control-allow-methods",
|
|
29
|
+
"POST,OPTIONS"
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"access-control-allow-headers",
|
|
33
|
+
"*"
|
|
34
|
+
]
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeJwtSub(header) {
|
|
39
|
+
try {
|
|
40
|
+
let token = header.startsWith("Bearer ") ? header.slice(7, header.length) : header;
|
|
41
|
+
let payload = token.split(".")[1];
|
|
42
|
+
if (payload === undefined) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let base64 = payload.replaceAll("-", "+").replaceAll("_", "/");
|
|
46
|
+
let obj = JSON.parse(Buffer.from(base64, "base64").toString("utf8"));
|
|
47
|
+
if (typeof obj === "object" && obj !== null && !Array.isArray(obj)) {
|
|
48
|
+
return Stdlib_Option.flatMap(obj["sub"], Stdlib_JSON.Decode.string);
|
|
49
|
+
} else {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
} catch (exn) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function identityPrefix(event) {
|
|
58
|
+
let authHeader = Stdlib_Option.flatMap(event.headers, h => Stdlib_Option.orElse(h["authorization"], h["Authorization"]));
|
|
59
|
+
let sub = Stdlib_Option.flatMap(authHeader, decodeJwtSub);
|
|
60
|
+
if (sub !== undefined) {
|
|
61
|
+
return sub + `/`;
|
|
62
|
+
} else {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function handler(event) {
|
|
68
|
+
try {
|
|
69
|
+
let bucket = Stdlib_Option.getOr(getEnv("UPLOAD_BUCKET"), "");
|
|
70
|
+
let parsed = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(Stdlib_Option.getOr(event.body, "{}"))), {});
|
|
71
|
+
let fileName = Stdlib_Option.getOr(Stdlib_Option.flatMap(parsed["fileName"], Stdlib_JSON.Decode.string), "upload");
|
|
72
|
+
let contentType = Stdlib_Option.flatMap(parsed["contentType"], Stdlib_JSON.Decode.string);
|
|
73
|
+
let servedPrefix = Stdlib_Option.getOr(getEnv("SERVED_PREFIX"), "uploads");
|
|
74
|
+
let key = servedPrefix + `/` + identityPrefix(event) + Nodecrypto.randomUUID() + `/` + fileName;
|
|
75
|
+
let client = new ClientS3.S3Client();
|
|
76
|
+
let command = new ClientS3.PutObjectCommand({
|
|
77
|
+
Bucket: bucket,
|
|
78
|
+
Key: key,
|
|
79
|
+
ContentType: contentType
|
|
80
|
+
});
|
|
81
|
+
let uploadUrl = await S3RequestPresigner.getSignedUrl(client, command, {
|
|
82
|
+
expiresIn: 300
|
|
83
|
+
});
|
|
84
|
+
let storageRef = `/` + key;
|
|
85
|
+
return {
|
|
86
|
+
statusCode: 200,
|
|
87
|
+
headers: corsHeaders(),
|
|
88
|
+
body: JSON.stringify(Object.fromEntries([
|
|
89
|
+
[
|
|
90
|
+
"uploadUrl",
|
|
91
|
+
uploadUrl
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"storageRef",
|
|
95
|
+
storageRef
|
|
96
|
+
]
|
|
97
|
+
]))
|
|
98
|
+
};
|
|
99
|
+
} catch (raw_exn) {
|
|
100
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
101
|
+
console.error("UploadPresign: presign failed", exn);
|
|
102
|
+
return {
|
|
103
|
+
statusCode: 400,
|
|
104
|
+
headers: corsHeaders(),
|
|
105
|
+
body: JSON.stringify(Object.fromEntries([[
|
|
106
|
+
"error",
|
|
107
|
+
"presign_failed"
|
|
108
|
+
]]))
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export {
|
|
114
|
+
getEnv,
|
|
115
|
+
corsHeaders,
|
|
116
|
+
decodeJwtSub,
|
|
117
|
+
identityPrefix,
|
|
118
|
+
handler,
|
|
119
|
+
}
|
|
120
|
+
/* node:crypto Not a pure module */
|
|
@@ -15,11 +15,37 @@ module Make = (Api: {
|
|
|
15
15
|
Spec: Reventless.InboundTranslationSlice.Spec,
|
|
16
16
|
Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec,
|
|
17
17
|
): (ReventlessCore.InboundTranslationSlice.T with module Spec = Spec) => {
|
|
18
|
+
// Register spec + translation module paths so the shared DCB command Lambda's
|
|
19
|
+
// entry point can import them and route `__inboundTranslation` payloads to this
|
|
20
|
+
// slice's `receive` (mirrors StateChangeSlice_Builder's path registration).
|
|
21
|
+
PluginRuntime_Builder.registerInboundTranslationSliceSpec(
|
|
22
|
+
~specName=Spec.name,
|
|
23
|
+
~specPath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
|
|
24
|
+
~translationPath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
|
|
25
|
+
)
|
|
18
26
|
module InnerMake = Inner.Make(Spec, Translation)
|
|
19
27
|
module Spec = Spec
|
|
20
28
|
module Translation = Translation
|
|
21
29
|
type component = InnerMake.component
|
|
22
30
|
let queryDbName = InnerMake.queryDbName
|
|
23
|
-
|
|
31
|
+
|
|
32
|
+
let make = (~publishJsons, ~runtime=?, ~opts=?) => {
|
|
33
|
+
let component = InnerMake.make(~publishJsons, ~runtime?, ~opts?)
|
|
34
|
+
// The audit QueryDb's physical table name is Pulumi-generated, so it can only
|
|
35
|
+
// be read off the constructed component's outputs (resolved synchronously here,
|
|
36
|
+
// before forDcbCommandTopic builds HANDLER_CONFIG). Thread it so the entry
|
|
37
|
+
// point's Route 0 can persist the audit rows the in-process path writes inline.
|
|
38
|
+
let outputs: ReventlessInfra.InboundTranslationSlice.outputs =
|
|
39
|
+
component->ReventlessCore.Component.outputs
|
|
40
|
+
switch outputs.queryDb.resources->Array.get(0) {
|
|
41
|
+
| Some(tableResource) =>
|
|
42
|
+
PluginRuntime_Builder.registerInboundAuditTableName(
|
|
43
|
+
~specName=Spec.name,
|
|
44
|
+
tableResource.name,
|
|
45
|
+
)
|
|
46
|
+
| None => ()
|
|
47
|
+
}
|
|
48
|
+
component
|
|
49
|
+
}
|
|
24
50
|
}
|
|
25
51
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
4
|
+
import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
|
|
5
|
+
import * as PluginRuntime_Builder$ReventlessAws from "../plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
3
6
|
import * as QueryDbStorage_DynamoDb$ReventlessAws from "../adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs";
|
|
4
7
|
import * as QueryDbResolvers_AppSync$ReventlessAws from "../adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs";
|
|
5
8
|
import * as InboundTranslationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs";
|
|
@@ -11,12 +14,22 @@ function Make(Api) {
|
|
|
11
14
|
make: QueryDbResolvers_AppSync$ReventlessAws.make
|
|
12
15
|
})(Api);
|
|
13
16
|
let Make$1 = Spec => (Translation => {
|
|
17
|
+
PluginRuntime_Builder$ReventlessAws.registerInboundTranslationSliceSpec(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl));
|
|
14
18
|
let InnerMake = Inner.Make(Spec)(Translation);
|
|
19
|
+
let make = (publishJsons, runtime, opts) => {
|
|
20
|
+
let component = InnerMake.make(publishJsons, runtime, opts);
|
|
21
|
+
let outputs = Component$ReventlessCore.outputs(component);
|
|
22
|
+
let tableResource = outputs.queryDb.resources[0];
|
|
23
|
+
if (tableResource !== undefined) {
|
|
24
|
+
PluginRuntime_Builder$ReventlessAws.registerInboundAuditTableName(Spec.name, tableResource.name);
|
|
25
|
+
}
|
|
26
|
+
return component;
|
|
27
|
+
};
|
|
15
28
|
return {
|
|
16
29
|
Spec: Spec,
|
|
17
30
|
Translation: Translation,
|
|
18
31
|
queryDbName: InnerMake.queryDbName,
|
|
19
|
-
make:
|
|
32
|
+
make: make
|
|
20
33
|
};
|
|
21
34
|
});
|
|
22
35
|
return {
|
|
@@ -28,4 +41,4 @@ function Make(Api) {
|
|
|
28
41
|
export {
|
|
29
42
|
Make,
|
|
30
43
|
}
|
|
31
|
-
/*
|
|
44
|
+
/* Component-ReventlessCore Not a pure module */
|
|
@@ -96,7 +96,7 @@ let forCommandTopic = (
|
|
|
96
96
|
|
|
97
97
|
// No user packages — all framework imports are in the Layer
|
|
98
98
|
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
99
|
-
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs",
|
|
99
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs",
|
|
100
100
|
~packageDirs=Dict.make(),
|
|
101
101
|
)
|
|
102
102
|
|
|
@@ -65,7 +65,7 @@ function forCommandTopic(param, connect, memorySizeOpt, timeoutOpt, param$1, par
|
|
|
65
65
|
return `{"queueUrl":"` + queueUrl + `","pluginReadModelTableName":"` + rmTable + `","schedulerRoleArn":"` + schedRoleArn + `","schedulerQueueArn":"` + schedQueueArn + `","schedulerQueueName":"` + schedQueueName + `","publishToAggregates":{` + publishToAggregatesJson + `}}`;
|
|
66
66
|
});
|
|
67
67
|
envVars["HANDLER_CONFIG"] = handlerConfigJson;
|
|
68
|
-
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs", {}, undefined);
|
|
68
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs", {}, undefined);
|
|
69
69
|
return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, "CommandHandler", "ExtensionPoint", match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -43,6 +43,44 @@ let registerStateChangeSliceSpec = (~specPath: string, ~behaviorPath: string) =>
|
|
|
43
43
|
let _ = registeredSliceModulePaths->Array.push({specPath, behaviorPath})
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// InboundTranslationSlice registration for the shared DCB command Lambda. Unlike
|
|
47
|
+
// StateChangeSlices (routed by command TAG), inbound slices are invoked directly
|
|
48
|
+
// by their AppSync mutation resolver with an `__inboundTranslation` payload — the
|
|
49
|
+
// entry point routes those to a `receive` handler built from the spec + translation
|
|
50
|
+
// modules. Registration is two-phase: the AWS `InboundTranslationSlice_Builder`
|
|
51
|
+
// functor registers the module paths at instantiation (spec/translation `moduleUrl`,
|
|
52
|
+
// like StateChangeSlice), and its wrapped `make` registers the audit QueryDb's
|
|
53
|
+
// resolved table-name Output once the component is constructed (the physical name
|
|
54
|
+
// is Pulumi-generated, so it can't be reconstructed at runtime). Both fire before
|
|
55
|
+
// `forDcbCommandTopic` reads them.
|
|
56
|
+
type inboundSliceReg = {
|
|
57
|
+
specPath: string,
|
|
58
|
+
translationPath: string,
|
|
59
|
+
mutable auditTableName: option<Pulumi.Output.t<string>>,
|
|
60
|
+
}
|
|
61
|
+
let registeredInboundSlices: dict<inboundSliceReg> = Dict.make()
|
|
62
|
+
|
|
63
|
+
let registerInboundTranslationSliceSpec = (
|
|
64
|
+
~specName: string,
|
|
65
|
+
~specPath: string,
|
|
66
|
+
~translationPath: string,
|
|
67
|
+
) =>
|
|
68
|
+
switch registeredInboundSlices->Dict.get(specName) {
|
|
69
|
+
| Some(reg) => registeredInboundSlices->Dict.set(specName, {...reg, specPath, translationPath})
|
|
70
|
+
| None =>
|
|
71
|
+
registeredInboundSlices->Dict.set(specName, {specPath, translationPath, auditTableName: None})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let registerInboundAuditTableName = (~specName: string, tableName: Pulumi.Output.t<string>) =>
|
|
75
|
+
switch registeredInboundSlices->Dict.get(specName) {
|
|
76
|
+
| Some(reg) => reg.auditTableName = Some(tableName)
|
|
77
|
+
| None =>
|
|
78
|
+
registeredInboundSlices->Dict.set(
|
|
79
|
+
specName,
|
|
80
|
+
{specPath: "", translationPath: "", auditTableName: Some(tableName)},
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
/**
|
|
47
85
|
Redundant with the seams that populate `dcbConfigRef` automatically:
|
|
48
86
|
`pluginName` is set by `registerPluginName` (called from `Plugin_Builder.make`
|
|
@@ -731,7 +769,7 @@ module Make = (
|
|
|
731
769
|
|
|
732
770
|
// Static re-export from Layer entry point — no esbuild, no user packages
|
|
733
771
|
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
734
|
-
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/HeartbeatEntryPoint.mjs",
|
|
772
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs",
|
|
735
773
|
~packageDirs=Dict.make(),
|
|
736
774
|
)
|
|
737
775
|
|
|
@@ -768,8 +806,28 @@ module Make = (
|
|
|
768
806
|
registeredSliceModulePaths
|
|
769
807
|
->Array.concat(dcbConfig.stateChangeSliceModulePaths)
|
|
770
808
|
->Array.map(({specPath, behaviorPath}) => (specPath, behaviorPath))
|
|
809
|
+
// Drop registrations that never received their module paths (a bare
|
|
810
|
+
// audit-table registration with no matching spec functor — shouldn't happen,
|
|
811
|
+
// but keeps a half-registered slice out of HANDLER_CONFIG).
|
|
812
|
+
let inboundSlices =
|
|
813
|
+
registeredInboundSlices
|
|
814
|
+
->Dict.valuesToArray
|
|
815
|
+
->Array.filterMap(reg =>
|
|
816
|
+
reg.specPath == ""
|
|
817
|
+
? None
|
|
818
|
+
: Some(
|
|
819
|
+
(
|
|
820
|
+
{
|
|
821
|
+
StateChangeSliceRuntime_Builder_Single.specPath: reg.specPath,
|
|
822
|
+
translationPath: reg.translationPath,
|
|
823
|
+
auditTableName: reg.auditTableName,
|
|
824
|
+
}: StateChangeSliceRuntime_Builder_Single.inboundSlicePaths
|
|
825
|
+
),
|
|
826
|
+
)
|
|
827
|
+
)
|
|
771
828
|
StateChangeSliceRuntime_Builder_Single.forDcbCommandTopic(
|
|
772
829
|
~slicePaths,
|
|
830
|
+
~inboundSlices,
|
|
773
831
|
~dcbTableName=dcbConfig.dcbTableName,
|
|
774
832
|
~pluginName=dcbConfig.pluginName,
|
|
775
833
|
~syncConfig=syncStateChangesConfigRef.contents,
|