@reventlessdev/reventless-aws 3.0.0-alpha.227 → 3.0.0-alpha.228
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 +11 -0
- package/package.json +11 -8
- package/src/Platform.res +98 -9
- package/src/Platform.res.mjs +73 -8
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +233 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +168 -0
- package/src/adapter/Mcp/MCP_Lambda.res +8 -7
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +7 -1
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +4 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +7 -1
- package/src/adapter/Task/TaskBucket_S3.res +2 -2
- package/src/adapter/Task/TaskBucket_S3.res.mjs +4 -2
- package/src/adapter/Upload/Upload_Presign_S3.res +258 -0
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +170 -0
- package/src/plugin/stack/Plugin_Stack.res +71 -8
- package/src/plugin/stack/Plugin_Stack.res.mjs +57 -8
- package/src/util/Util_DynamoDbStream_Runtime.res +6 -0
- package/src/util/Util_DynamoDbStream_Runtime.res.mjs +5 -0
- package/src/util/Util_StaticBundle.res +21 -0
- package/src/util/Util_StaticBundle.res.mjs +14 -0
- package/tests/Util_StaticBundleTest.res +40 -0
- package/tests/Util_StaticBundleTest.res.mjs +44 -0
- package/tests/integration/SvsTestSlice_Projection.res +1 -1
- package/tests/integration/SvsTestSlice_Projection.res.mjs +2 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
8
|
+
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
9
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
|
+
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
11
|
+
import * as ClientS3 from "@aws-sdk/client-s3";
|
|
12
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
13
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
14
|
+
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
|
+
|
|
112
|
+
function make(bucketName, corsOriginsOpt, servedPrefixOpt, opts) {
|
|
113
|
+
let corsOrigins = corsOriginsOpt !== undefined ? corsOriginsOpt : ["*"];
|
|
114
|
+
let servedPrefix = servedPrefixOpt !== undefined ? servedPrefixOpt : "uploads";
|
|
115
|
+
let serviceName = "UploadPresignService";
|
|
116
|
+
let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
|
|
117
|
+
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(serviceName, Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(serviceName, "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
118
|
+
bucketName.apply(b => {
|
|
119
|
+
let arn = `arn:aws:s3:::` + b + `/*`;
|
|
120
|
+
new (Aws.iam.RolePolicy)(serviceName + `Policy`, {
|
|
121
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, serviceName + `Policy`, [{
|
|
122
|
+
Sid: "AllowUploadPut",
|
|
123
|
+
Effect: "Allow",
|
|
124
|
+
Action: "s3:PutObject",
|
|
125
|
+
Resource: arn
|
|
126
|
+
}])),
|
|
127
|
+
role: lambdaRole.id
|
|
128
|
+
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
129
|
+
});
|
|
130
|
+
let environment = {
|
|
131
|
+
variables: Object.fromEntries([
|
|
132
|
+
[
|
|
133
|
+
"UPLOAD_BUCKET",
|
|
134
|
+
bucketName
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"SERVED_PREFIX",
|
|
138
|
+
servedPrefix
|
|
139
|
+
]
|
|
140
|
+
])
|
|
141
|
+
};
|
|
142
|
+
let lambda = new (Aws.lambda.CallbackFunction)(serviceName, Lambda$PulumiAws.CallbackFunction.Args.make(handlePresign, lambdaRole, undefined, undefined, undefined, undefined, 30, undefined, undefined, undefined, AWS_Tags$ReventlessAws.make(serviceName, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined), environment), opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
143
|
+
let functionUrl = new (Aws.lambda.FunctionUrl)(serviceName + `Url`, {
|
|
144
|
+
authorizationType: "NONE",
|
|
145
|
+
functionName: lambda.name,
|
|
146
|
+
cors: {
|
|
147
|
+
allowHeaders: ["*"].map(prim => prim),
|
|
148
|
+
allowMethods: ["POST"].map(prim => prim),
|
|
149
|
+
allowOrigins: corsOrigins.map(prim => prim)
|
|
150
|
+
}
|
|
151
|
+
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
152
|
+
return {
|
|
153
|
+
url: functionUrl.functionUrl,
|
|
154
|
+
resources: [
|
|
155
|
+
lambda.arn,
|
|
156
|
+
functionUrl.functionArn
|
|
157
|
+
]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
getEnv,
|
|
163
|
+
genUuid,
|
|
164
|
+
decodeJwtSub,
|
|
165
|
+
corsHeaders,
|
|
166
|
+
identityPrefix,
|
|
167
|
+
handlePresign,
|
|
168
|
+
make,
|
|
169
|
+
}
|
|
170
|
+
/* @pulumi/aws Not a pure module */
|
|
@@ -142,6 +142,7 @@ let makeUiBundleDistribution = (
|
|
|
142
142
|
~stableName: bool=false,
|
|
143
143
|
~excludeFiles: array<string>=[],
|
|
144
144
|
~customDomain: option<customDomain>=?,
|
|
145
|
+
~servedBuckets: array<ReventlessInfra.Platform.servedBucket>=[],
|
|
145
146
|
): bundleDistribution => {
|
|
146
147
|
// When stableName=true, Pulumi resource names omit bundleVersion so every
|
|
147
148
|
// deploy updates the same bucket + CloudFront distribution in place. The
|
|
@@ -219,9 +220,26 @@ let makeUiBundleDistribution = (
|
|
|
219
220
|
cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
220
221
|
cachePolicyId: Pulumi.Input.make(cachingDisabledPolicyId),
|
|
221
222
|
}
|
|
223
|
+
// Ordered cache behavior routing a served bucket's `{prefix}/*` path to its own
|
|
224
|
+
// origin. Served objects have immutable uuid keys ⇒ the long-TTL
|
|
225
|
+
// CachingOptimized policy is safe. Prepended to `orderedCacheBehaviors` so a
|
|
226
|
+
// served path is matched before the SPA-serving default behavior and never
|
|
227
|
+
// routed to the bundle bucket.
|
|
228
|
+
let servedOriginId = (prefix: string): string => "served-" ++ prefix
|
|
229
|
+
let servedCacheBehavior = (prefix: string): PulumiAws.CloudFront.Distribution.orderedCacheBehavior => {
|
|
230
|
+
pathPattern: Pulumi.Input.make(prefix ++ "/*"),
|
|
231
|
+
targetOriginId: Pulumi.Input.make(servedOriginId(prefix)),
|
|
232
|
+
viewerProtocolPolicy: Pulumi.Input.make("redirect-to-https"),
|
|
233
|
+
allowedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
234
|
+
cachedMethods: Pulumi.Input.make(["GET", "HEAD"]),
|
|
235
|
+
cachePolicyId: Pulumi.Input.make(cachingOptimizedPolicyId),
|
|
236
|
+
}
|
|
222
237
|
let orderedCacheBehaviors = Array.concat(
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
servedBuckets->Array.map(sb => servedCacheBehavior(sb.prefix)),
|
|
239
|
+
Array.concat(
|
|
240
|
+
[noCacheBehavior("/remoteEntry.js")],
|
|
241
|
+
spaFallback ? [noCacheBehavior("/" ++ indexDocument), noCacheBehavior("/config.json")] : [],
|
|
242
|
+
),
|
|
225
243
|
)
|
|
226
244
|
|
|
227
245
|
// Custom-domain provisioning. When `customDomain` is supplied the bundle is
|
|
@@ -305,13 +323,23 @@ let makeUiBundleDistribution = (
|
|
|
305
323
|
origins: (bucket.bucketRegionalDomainName, oac.id)
|
|
306
324
|
->Pulumi.Output.all2
|
|
307
325
|
->Pulumi.Output.apply(((domainName, oacId)) =>
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
326
|
+
Array.concat(
|
|
327
|
+
[
|
|
328
|
+
{
|
|
329
|
+
PulumiAws.CloudFront.Distribution.domainName: Pulumi.Input.make(domainName),
|
|
330
|
+
originId: Pulumi.Input.make(originId),
|
|
331
|
+
originAccessControlId: Pulumi.Input.make(oacId),
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
// One S3 origin per served bucket, sharing the same OAC as the bundle
|
|
335
|
+
// origin (the OAC only authorizes CloudFront→S3 sigv4 signing; the
|
|
336
|
+
// per-bucket read grant is the served bucket's own BucketPolicy below).
|
|
337
|
+
servedBuckets->Array.map(sb => {
|
|
338
|
+
PulumiAws.CloudFront.Distribution.domainName: sb.bucketRegionalDomainName,
|
|
339
|
+
originId: Pulumi.Input.make(servedOriginId(sb.prefix)),
|
|
312
340
|
originAccessControlId: Pulumi.Input.make(oacId),
|
|
313
|
-
},
|
|
314
|
-
|
|
341
|
+
}),
|
|
342
|
+
)
|
|
315
343
|
)
|
|
316
344
|
->Pulumi.Output.asInput,
|
|
317
345
|
defaultCacheBehavior: Pulumi.Input.make(
|
|
@@ -400,6 +428,41 @@ let makeUiBundleDistribution = (
|
|
|
400
428
|
},
|
|
401
429
|
)
|
|
402
430
|
|
|
431
|
+
// Per served bucket: grant CloudFront read scoped to THIS distribution, so the
|
|
432
|
+
// object is fetchable at `https://<ui-domain>/{prefix}/<key>` while the bucket
|
|
433
|
+
// stays private (direct S3 GET is 403). Mirrors the bundle bucket policy above;
|
|
434
|
+
// the served bucket keeps its own all-true BucketPublicAccessBlock (app-owned).
|
|
435
|
+
servedBuckets->Array.forEach(sb => {
|
|
436
|
+
let _ = PulumiAws.S3.BucketPolicy.make(
|
|
437
|
+
~name=name ++ "-served-" ++ sb.prefix ++ "-policy",
|
|
438
|
+
~args={
|
|
439
|
+
bucket: sb.bucketId,
|
|
440
|
+
policy: (sb.bucketArn->Pulumi.Output.fromInput, distribution.arn)
|
|
441
|
+
->Pulumi.Output.all2
|
|
442
|
+
->Pulumi.Output.apply(((bucketArn, distributionArn)) =>
|
|
443
|
+
{
|
|
444
|
+
"Version": "2012-10-17",
|
|
445
|
+
"Statement": [
|
|
446
|
+
{
|
|
447
|
+
"Sid": "AllowCloudFrontServicePrincipal",
|
|
448
|
+
"Effect": "Allow",
|
|
449
|
+
"Principal": {"Service": "cloudfront.amazonaws.com"},
|
|
450
|
+
"Action": "s3:GetObject",
|
|
451
|
+
"Resource": bucketArn ++ "/*",
|
|
452
|
+
"Condition": {
|
|
453
|
+
"StringEquals": {"AWS:SourceArn": distributionArn},
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
}
|
|
458
|
+
->JSON.stringifyAny
|
|
459
|
+
->Option.getUnsafe
|
|
460
|
+
)
|
|
461
|
+
->Pulumi.Output.asInput,
|
|
462
|
+
},
|
|
463
|
+
)
|
|
464
|
+
})
|
|
465
|
+
|
|
403
466
|
switch assetsDir {
|
|
404
467
|
| None => ()
|
|
405
468
|
| Some(dir) =>
|
|
@@ -68,11 +68,12 @@ async function invalidateDistribution(distributionId, paths) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbackOpt, indexDocumentOpt, stableNameOpt, excludeFilesOpt, customDomain) {
|
|
71
|
+
function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbackOpt, indexDocumentOpt, stableNameOpt, excludeFilesOpt, customDomain, servedBucketsOpt) {
|
|
72
72
|
let spaFallback = spaFallbackOpt !== undefined ? spaFallbackOpt : false;
|
|
73
73
|
let indexDocument = indexDocumentOpt !== undefined ? indexDocumentOpt : "index.html";
|
|
74
74
|
let stableName = stableNameOpt !== undefined ? stableNameOpt : false;
|
|
75
75
|
let excludeFiles = excludeFilesOpt !== undefined ? excludeFilesOpt : [];
|
|
76
|
+
let servedBuckets = servedBucketsOpt !== undefined ? servedBucketsOpt : [];
|
|
76
77
|
let name = stableName ? pluginId : pluginId + "-" + bundleVersion;
|
|
77
78
|
let bucket = new (Aws.s3.Bucket)(name + "-bundle", {
|
|
78
79
|
tags: AWS_Tags$ReventlessAws.make(name + "-bundle", "Plugin", "Hosting", "Plugin", undefined, undefined, pluginId, undefined)
|
|
@@ -118,10 +119,26 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
118
119
|
],
|
|
119
120
|
cachePolicyId: cachingDisabledPolicyId
|
|
120
121
|
});
|
|
121
|
-
let orderedCacheBehaviors =
|
|
122
|
+
let orderedCacheBehaviors = servedBuckets.map(sb => {
|
|
123
|
+
let prefix = sb.prefix;
|
|
124
|
+
return {
|
|
125
|
+
pathPattern: prefix + "/*",
|
|
126
|
+
targetOriginId: "served-" + prefix,
|
|
127
|
+
viewerProtocolPolicy: "redirect-to-https",
|
|
128
|
+
allowedMethods: [
|
|
129
|
+
"GET",
|
|
130
|
+
"HEAD"
|
|
131
|
+
],
|
|
132
|
+
cachedMethods: [
|
|
133
|
+
"GET",
|
|
134
|
+
"HEAD"
|
|
135
|
+
],
|
|
136
|
+
cachePolicyId: cachingOptimizedPolicyId
|
|
137
|
+
};
|
|
138
|
+
}).concat([noCacheBehavior("/remoteEntry.js")].concat(spaFallback ? [
|
|
122
139
|
noCacheBehavior("/" + indexDocument),
|
|
123
140
|
noCacheBehavior("/config.json")
|
|
124
|
-
] : []);
|
|
141
|
+
] : []));
|
|
125
142
|
let viewerCertificate;
|
|
126
143
|
if (customDomain !== undefined) {
|
|
127
144
|
let usEast1 = _getUsEast1Provider();
|
|
@@ -163,11 +180,18 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
163
180
|
origins: Pulumi.all([
|
|
164
181
|
bucket.bucketRegionalDomainName,
|
|
165
182
|
oac.id
|
|
166
|
-
]).apply(param =>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
]).apply(param => {
|
|
184
|
+
let oacId = param[1];
|
|
185
|
+
return [{
|
|
186
|
+
domainName: param[0],
|
|
187
|
+
originId: originId,
|
|
188
|
+
originAccessControlId: oacId
|
|
189
|
+
}].concat(servedBuckets.map(sb => ({
|
|
190
|
+
domainName: sb.bucketRegionalDomainName,
|
|
191
|
+
originId: "served-" + sb.prefix,
|
|
192
|
+
originAccessControlId: oacId
|
|
193
|
+
})));
|
|
194
|
+
}),
|
|
171
195
|
defaultCacheBehavior: {
|
|
172
196
|
targetOriginId: originId,
|
|
173
197
|
viewerProtocolPolicy: "redirect-to-https",
|
|
@@ -228,6 +252,31 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
|
|
|
228
252
|
}]
|
|
229
253
|
}))
|
|
230
254
|
});
|
|
255
|
+
servedBuckets.forEach(sb => {
|
|
256
|
+
new (Aws.s3.BucketPolicy)(name + "-served-" + sb.prefix + "-policy", {
|
|
257
|
+
bucket: sb.bucketId,
|
|
258
|
+
policy: Pulumi.all([
|
|
259
|
+
sb.bucketArn,
|
|
260
|
+
distribution.arn
|
|
261
|
+
]).apply(param => JSON.stringify({
|
|
262
|
+
Version: "2012-10-17",
|
|
263
|
+
Statement: [{
|
|
264
|
+
Sid: "AllowCloudFrontServicePrincipal",
|
|
265
|
+
Effect: "Allow",
|
|
266
|
+
Principal: {
|
|
267
|
+
Service: "cloudfront.amazonaws.com"
|
|
268
|
+
},
|
|
269
|
+
Action: "s3:GetObject",
|
|
270
|
+
Resource: param[0] + "/*",
|
|
271
|
+
Condition: {
|
|
272
|
+
StringEquals: {
|
|
273
|
+
"AWS:SourceArn": param[1]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}]
|
|
277
|
+
}))
|
|
278
|
+
});
|
|
279
|
+
});
|
|
231
280
|
if (assetsDir !== undefined) {
|
|
232
281
|
let allEntries = Util_StaticBundle$ReventlessAws.walk(assetsDir);
|
|
233
282
|
if (allEntries.length === 0) {
|
|
@@ -34,10 +34,16 @@ let buildJsonEvent' = dict => {
|
|
|
34
34
|
->Dict.fromArray
|
|
35
35
|
->JSON.Encode.object
|
|
36
36
|
}
|
|
37
|
+
// Carry the stored `recorded_at` column into the envelope so StateViewSlice
|
|
38
|
+
// projections receive it as `consumed.recordedAt` (the authoritative storage
|
|
39
|
+
// time on the AWS path). Absent on non-event rows → empty string.
|
|
40
|
+
let recordedAt =
|
|
41
|
+
dict->Dict.get("recordedAt")->Option.getOr(""->JSON.Encode.string)
|
|
37
42
|
Some(
|
|
38
43
|
[
|
|
39
44
|
("id", dict->Dict.get("id")->Option.getOrThrow),
|
|
40
45
|
("meta", meta),
|
|
46
|
+
("recordedAt", recordedAt),
|
|
41
47
|
("event", ReventlessCore.Message.combineMessage(eventType, payload)),
|
|
42
48
|
]
|
|
43
49
|
->Dict.fromArray
|
|
@@ -53,6 +53,7 @@ function buildJsonEvent$p(dict) {
|
|
|
53
53
|
]
|
|
54
54
|
]);
|
|
55
55
|
}
|
|
56
|
+
let recordedAt = Stdlib_Option.getOr(dict["recordedAt"], "");
|
|
56
57
|
return Object.fromEntries([
|
|
57
58
|
[
|
|
58
59
|
"id",
|
|
@@ -62,6 +63,10 @@ function buildJsonEvent$p(dict) {
|
|
|
62
63
|
"meta",
|
|
63
64
|
meta
|
|
64
65
|
],
|
|
66
|
+
[
|
|
67
|
+
"recordedAt",
|
|
68
|
+
recordedAt
|
|
69
|
+
],
|
|
65
70
|
[
|
|
66
71
|
"event",
|
|
67
72
|
Message$ReventlessCore.combineMessage(match, payload)
|
|
@@ -5,6 +5,7 @@ let log = ReventlessCore.Logger.fromEnv()
|
|
|
5
5
|
|
|
6
6
|
@module("fs") external existsSync: string => bool = "existsSync"
|
|
7
7
|
@module("fs") external readFileSync: string => Js.TypedArray2.Uint8Array.t = "readFileSync"
|
|
8
|
+
@module("fs") external readFileSyncUtf8: (string, string) => string = "readFileSync"
|
|
8
9
|
type dirent
|
|
9
10
|
@module("fs")
|
|
10
11
|
external readdirSync: (string, {"withFileTypes": bool}) => array<dirent> = "readdirSync"
|
|
@@ -69,6 +70,26 @@ let walk = (assetsDir: string): array<fileEntry> => {
|
|
|
69
70
|
acc
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Read a JSON file at deploy time and return its exact bytes as a string, after
|
|
75
|
+
* validating that it parses. A missing or malformed file throws with an
|
|
76
|
+
* actionable message so a broken file fails the deploy rather than shipping a
|
|
77
|
+
* file a consumer will fetch-and-ignore. The verbatim bytes are returned (not
|
|
78
|
+
* re-serialised) so formatting/key order the author chose is preserved.
|
|
79
|
+
*/
|
|
80
|
+
let readJsonFileVerbatim = (~path: string, ~label: string): string => {
|
|
81
|
+
if !existsSync(path) {
|
|
82
|
+
JsError.throwWithMessage(`${label}: file does not exist: ${path}`)
|
|
83
|
+
}
|
|
84
|
+
let content = readFileSyncUtf8(path, "utf8")
|
|
85
|
+
try {
|
|
86
|
+
let _ = JSON.parseOrThrow(content)
|
|
87
|
+
content
|
|
88
|
+
} catch {
|
|
89
|
+
| _ => JsError.throwWithMessage(`${label}: file is not valid JSON: ${path}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
/**
|
|
73
94
|
* Replace `/` and `.` so a path can be used as a Pulumi resource URN segment.
|
|
74
95
|
*/
|
|
@@ -52,6 +52,19 @@ function walk(assetsDir) {
|
|
|
52
52
|
return acc;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function readJsonFileVerbatim(path, label) {
|
|
56
|
+
if (!Fs.existsSync(path)) {
|
|
57
|
+
Stdlib_JsError.throwWithMessage(label + `: file does not exist: ` + path);
|
|
58
|
+
}
|
|
59
|
+
let content = Fs.readFileSync(path, "utf8");
|
|
60
|
+
try {
|
|
61
|
+
JSON.parse(content);
|
|
62
|
+
return content;
|
|
63
|
+
} catch (exn) {
|
|
64
|
+
return Stdlib_JsError.throwWithMessage(label + `: file is not valid JSON: ` + path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
function sanitizeName(relativePath) {
|
|
56
69
|
return relativePath.replaceAll("/", "-").replaceAll(".", "-");
|
|
57
70
|
}
|
|
@@ -125,6 +138,7 @@ export {
|
|
|
125
138
|
isHidden,
|
|
126
139
|
walkInto,
|
|
127
140
|
walk,
|
|
141
|
+
readJsonFileVerbatim,
|
|
128
142
|
sanitizeName,
|
|
129
143
|
extensionOf,
|
|
130
144
|
contentTypeFor,
|
|
@@ -134,3 +134,43 @@ describe("Util_StaticBundle.walk", () => {
|
|
|
134
134
|
expect(threw)->toBe(true)
|
|
135
135
|
})
|
|
136
136
|
})
|
|
137
|
+
|
|
138
|
+
describe("Util_StaticBundle.readJsonFileVerbatim", () => {
|
|
139
|
+
let writeTmp = (contents: string): string => {
|
|
140
|
+
let dir = mkdtempSync(join2(tmpdir(), "ui-hints-"))
|
|
141
|
+
let path = join2(dir, "ui-hints.json")
|
|
142
|
+
writeFileSync(path, contents)
|
|
143
|
+
path
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
testSync("returns the file's exact bytes for valid JSON (verbatim, not re-serialised)", () => {
|
|
147
|
+
// Deliberately non-canonical formatting: extra spaces + trailing newline.
|
|
148
|
+
let raw = "{\n \"dashboards\": [ ] \n}\n"
|
|
149
|
+
let path = writeTmp(raw)
|
|
150
|
+
expect(Util_StaticBundle.readJsonFileVerbatim(~path, ~label="test"))->toBe(raw)
|
|
151
|
+
rmSync(path, {"recursive": true, "force": true})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
testSync("throws on malformed JSON", () => {
|
|
155
|
+
let path = writeTmp("{ not valid json ")
|
|
156
|
+
let threw = try {
|
|
157
|
+
let _ = Util_StaticBundle.readJsonFileVerbatim(~path, ~label="test")
|
|
158
|
+
false
|
|
159
|
+
} catch {
|
|
160
|
+
| _ => true
|
|
161
|
+
}
|
|
162
|
+
expect(threw)->toBe(true)
|
|
163
|
+
rmSync(path, {"recursive": true, "force": true})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
testSync("throws when the file does not exist", () => {
|
|
167
|
+
let missing = join2(tmpdir(), "ui-hints-missing-xyz123.json")
|
|
168
|
+
let threw = try {
|
|
169
|
+
let _ = Util_StaticBundle.readJsonFileVerbatim(~path=missing, ~label="test")
|
|
170
|
+
false
|
|
171
|
+
} catch {
|
|
172
|
+
| _ => true
|
|
173
|
+
}
|
|
174
|
+
expect(threw)->toBe(true)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -143,6 +143,50 @@ globalThis.describe("Util_StaticBundle.walk", () => {
|
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
+
globalThis.describe("Util_StaticBundle.readJsonFileVerbatim", () => {
|
|
147
|
+
let writeTmp = contents => {
|
|
148
|
+
let dir = Fs.mkdtempSync(Path.join(Os.tmpdir(), "ui-hints-"));
|
|
149
|
+
let path = Path.join(dir, "ui-hints.json");
|
|
150
|
+
Fs.writeFileSync(path, contents);
|
|
151
|
+
return path;
|
|
152
|
+
};
|
|
153
|
+
globalThis.test("returns the file's exact bytes for valid JSON (verbatim, not re-serialised)", () => {
|
|
154
|
+
let raw = "{\n \"dashboards\": [ ] \n}\n";
|
|
155
|
+
let path = writeTmp(raw);
|
|
156
|
+
globalThis.expect(Util_StaticBundle$ReventlessAws.readJsonFileVerbatim(path, "test")).toBe(raw);
|
|
157
|
+
Fs.rmSync(path, {
|
|
158
|
+
recursive: true,
|
|
159
|
+
force: true
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
globalThis.test("throws on malformed JSON", () => {
|
|
163
|
+
let path = writeTmp("{ not valid json ");
|
|
164
|
+
let threw;
|
|
165
|
+
try {
|
|
166
|
+
Util_StaticBundle$ReventlessAws.readJsonFileVerbatim(path, "test");
|
|
167
|
+
threw = false;
|
|
168
|
+
} catch (exn) {
|
|
169
|
+
threw = true;
|
|
170
|
+
}
|
|
171
|
+
globalThis.expect(threw).toBe(true);
|
|
172
|
+
Fs.rmSync(path, {
|
|
173
|
+
recursive: true,
|
|
174
|
+
force: true
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
globalThis.test("throws when the file does not exist", () => {
|
|
178
|
+
let missing = Path.join(Os.tmpdir(), "ui-hints-missing-xyz123.json");
|
|
179
|
+
let threw;
|
|
180
|
+
try {
|
|
181
|
+
Util_StaticBundle$ReventlessAws.readJsonFileVerbatim(missing, "test");
|
|
182
|
+
threw = false;
|
|
183
|
+
} catch (exn) {
|
|
184
|
+
threw = true;
|
|
185
|
+
}
|
|
186
|
+
globalThis.expect(threw).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
146
190
|
export {
|
|
147
191
|
makeFixture,
|
|
148
192
|
}
|