@reventlessdev/reventless-aws 3.0.0-alpha.231 → 3.0.0-alpha.233

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.
Files changed (104) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +7 -7
  3. package/src/adapter/Api/AppSyncEventsSigner_Ops.res +137 -0
  4. package/src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs +121 -0
  5. package/src/adapter/Api/Platform_AdminScan_Ops.res +136 -0
  6. package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +98 -0
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +12 -90
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -84
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +86 -0
  10. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +79 -0
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res +9 -79
  12. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +7 -143
  13. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +48 -0
  14. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs +63 -0
  15. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +21 -80
  16. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +23 -68
  17. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +70 -0
  18. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res.mjs +68 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -1
  20. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +1 -1
  21. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  22. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
  23. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +42 -126
  24. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +246 -0
  25. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +145 -0
  26. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +27 -2
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +10 -3
  28. package/src/adapter/Runtime/CounterEntryPoint.mjs +19 -73
  29. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +145 -0
  30. package/src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs +129 -0
  31. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +16 -3
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +11 -6
  33. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
  34. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
  35. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
  36. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
  37. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
  38. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
  39. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
  40. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
  41. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
  42. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
  43. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
  44. package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
  45. package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
  46. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
  48. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
  49. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
  50. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
  51. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
  52. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
  53. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
  54. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
  55. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
  56. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
  57. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
  58. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
  59. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
  60. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
  61. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
  62. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
  63. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
  64. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
  65. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
  66. package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
  67. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
  68. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
  69. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
  70. package/src/components/AutomationSlice_Builder.res +2 -0
  71. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  72. package/src/components/OutboundTranslationSlice_Builder.res +1 -0
  73. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  74. package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
  75. package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
  76. package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
  77. package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
  78. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
  79. package/tests/CounterEntryPoint_OpsTest.res +115 -0
  80. package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
  81. package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
  82. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
  83. package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
  84. package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
  85. package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
  86. package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
  87. package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
  88. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
  89. package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
  90. package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
  91. package/tests/Platform_AdminScan_OpsTest.res +80 -0
  92. package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
  93. package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
  94. package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
  95. package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
  96. package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
  97. package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
  98. package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
  99. package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
  100. package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
  101. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
  102. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
  103. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
  104. package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +0 -40
package/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
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.233 (2026-07-27)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** grant DCB command Lambda write access to InboundTranslationSlice audit tables ([2c445fa](https://github.com/ReventlessDev/reventless-core/commit/2c445faf9692573d0eab2f0d4a456e8c5d723b41))
11
+ * refactor(aws)!: convert remaining Group-1 entry points to typed-core/thin-shell; fix Counter publish + EP stub wiring ([d7f1aeb](https://github.com/ReventlessDev/reventless-core/commit/d7f1aeb90cfc122106ccc3ffa522911fba5db2de))
12
+
13
+ ### BREAKING CHANGES
14
+
15
+ * reventless-postgres QueryEnginePostgres.Make no longer
16
+ exposes the dead deploy-time `make` (queryEngineMaker); its Pulumi.Output
17
+ wrap was the last @pulumi/pulumi import in the PgQueryResolver Lambda's
18
+ cold-start graph. Pure helpers now alias QueryDbStorage_Postgres_Ops.
19
+
20
+
21
+
22
+ # 3.0.0-alpha.232 (2026-07-27)
23
+
24
+ ### Bug Fixes
25
+
26
+ * **aws:** keep InboundTranslationSlice runtime path out of the Pulumi graph ([9166cff](https://github.com/ReventlessDev/reventless-core/commit/9166cff85939012d12e44835e2eb79a88fd1b66a))
27
+
28
+
6
29
  # 3.0.0-alpha.231 (2026-07-27)
7
30
 
8
31
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.231",
3
+ "version": "3.0.0-alpha.233",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -11,17 +11,17 @@
11
11
  "@aws-sdk/s3-request-presigner": "3.970.0",
12
12
  "sury": "11.0.0-alpha.4",
13
13
  "uuid": "^13.0.0",
14
- "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.24",
14
+ "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.25",
15
15
  "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
16
16
  "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
17
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.60",
18
17
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
19
- "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
20
- "@reventlessdev/reventless-infra": "3.0.0-alpha.105",
18
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.61",
19
+ "@reventlessdev/reventless-core": "3.0.0-alpha.184",
21
20
  "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.105",
22
+ "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
22
23
  "@reventlessdev/reventless-spec": "3.0.0-alpha.81",
23
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.46",
24
- "@reventlessdev/reventless-core": "3.0.0-alpha.182"
24
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.48"
25
25
  },
26
26
  "devDependencies": {
27
27
  "rescript": "12.3.0",
@@ -0,0 +1,137 @@
1
+ // Shared SigV4 signer + AppSync Events publisher for the two stream-relay
2
+ // Lambdas (EventLogSubscription, StateTopic). Both sign a `POST /event` to the
3
+ // AppSync Events HTTP API with the ambient Lambda execution-role credentials and
4
+ // publish onto a channel.
5
+ //
6
+ // Runtime-pure and dependency-free by design: the signing is hand-rolled on
7
+ // `node:crypto` (always present, no SDK) rather than @smithy/signature-v4 — so
8
+ // the Lambda cold-start graph carries NO AWS SDK and cannot hit the SDK-version
9
+ // skew ("Frankenstein SDK") class of failure. This is a faithful port of the
10
+ // former inline JS `signedHeaders` the two handler strings duplicated verbatim;
11
+ // AppSyncEventsSigner_ParityFixture.mjs pins that original as a golden reference
12
+ // and AppSyncEventsSigner_OpsTest asserts byte-for-byte equality.
13
+
14
+ // ── node:crypto (string- and buffer-keyed HMAC; the signing key chain feeds a
15
+ // Buffer digest back in as the next key) ──────────────────────────────────
16
+
17
+ type buffer
18
+
19
+ type hmac
20
+ @module("node:crypto") external createHmacStr: (string, string) => hmac = "createHmac"
21
+ @module("node:crypto") external createHmacBuf: (string, buffer) => hmac = "createHmac"
22
+ @send external hmacUpdate: (hmac, string) => hmac = "update"
23
+ @send external hmacDigestBuf: hmac => buffer = "digest"
24
+ @send external hmacDigestHex: (hmac, string) => string = "digest"
25
+
26
+ type hash
27
+ @module("node:crypto") external createHash: string => hash = "createHash"
28
+ @send external hashUpdate: (hash, string) => hash = "update"
29
+ @send external hashDigestHex: (hash, string) => string = "digest"
30
+
31
+ let sha256hex = (data: string): string => createHash("sha256")->hashUpdate(data)->hashDigestHex("hex")
32
+
33
+ let hmacFromStr = (key: string, data: string): buffer =>
34
+ createHmacStr("sha256", key)->hmacUpdate(data)->hmacDigestBuf
35
+ let hmacFromBuf = (key: buffer, data: string): buffer =>
36
+ createHmacBuf("sha256", key)->hmacUpdate(data)->hmacDigestBuf
37
+
38
+ // ── SigV4 ───────────────────────────────────────────────────────────────────
39
+
40
+ type creds = {accessKeyId: string, secretAccessKey: string, sessionToken: option<string>}
41
+
42
+ @val @scope("process") external processEnv: dict<string> = "env"
43
+
44
+ // Ambient Lambda execution-role credentials (the managed runtime injects these).
45
+ let envCreds = (): creds => {
46
+ accessKeyId: processEnv->Dict.get("AWS_ACCESS_KEY_ID")->Option.getOr(""),
47
+ secretAccessKey: processEnv->Dict.get("AWS_SECRET_ACCESS_KEY")->Option.getOr(""),
48
+ sessionToken: processEnv->Dict.get("AWS_SESSION_TOKEN"),
49
+ }
50
+
51
+ let region = (): string => processEnv->Dict.get("AWS_REGION")->Option.getOr("eu-west-1")
52
+
53
+ // Sign a `POST <path>` for service `appsync`. `isoNow` is a Date().toISOString()
54
+ // string, taken as an argument so the signer is pure and parity-testable. The
55
+ // amzDate / dateStamp are derived by fixed slicing of the ISO-8601 layout
56
+ // ("YYYY-MM-DDTHH:MM:SS.sssZ") — equivalent to the former regex strip, without
57
+ // the regex. Returns the headers (host, x-amz-date, [security-token],
58
+ // Authorization) to merge into the request.
59
+ let signedHeaders = (
60
+ ~host: string,
61
+ ~path: string,
62
+ ~body: string,
63
+ ~region: string,
64
+ ~isoNow: string,
65
+ ~creds: creds,
66
+ ): dict<string> => {
67
+ let sl = (a, b) => isoNow->String.slice(~start=a, ~end=b)
68
+ let dateStamp = sl(0, 4) ++ sl(5, 7) ++ sl(8, 10)
69
+ let amzDate = dateStamp ++ "T" ++ sl(11, 13) ++ sl(14, 16) ++ sl(17, 19) ++ "Z"
70
+
71
+ let baseHeaders = [("host", host), ("x-amz-date", amzDate)]
72
+ let headers = switch creds.sessionToken {
73
+ | Some(token) if token != "" => baseHeaders->Array.concat([("x-amz-security-token", token)])
74
+ | _ => baseHeaders
75
+ }
76
+ // Codepoint sort matches both the JS `localeCompare` (canonical headers) and
77
+ // the default `.sort()` (signed-headers list) for these lowercase-ASCII keys.
78
+ let sorted = headers->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
79
+ let canonicalHeaders = sorted->Array.map(((k, v)) => `${k}:${v}\n`)->Array.join("")
80
+ let signedHeaderList = sorted->Array.map(((k, _)) => k)->Array.join(";")
81
+
82
+ let canonicalRequest =
83
+ ["POST", path, "", canonicalHeaders, signedHeaderList, sha256hex(body)]->Array.join("\n")
84
+ let scope = `${dateStamp}/${region}/appsync/aws4_request`
85
+ let stringToSign =
86
+ ["AWS4-HMAC-SHA256", amzDate, scope, sha256hex(canonicalRequest)]->Array.join("\n")
87
+
88
+ let kDate = hmacFromStr("AWS4" ++ creds.secretAccessKey, dateStamp)
89
+ let kRegion = hmacFromBuf(kDate, region)
90
+ let kService = hmacFromBuf(kRegion, "appsync")
91
+ let kSigning = hmacFromBuf(kService, "aws4_request")
92
+ let signature = createHmacBuf("sha256", kSigning)->hmacUpdate(stringToSign)->hmacDigestHex("hex")
93
+
94
+ let authorization = `AWS4-HMAC-SHA256 Credential=${creds.accessKeyId}/${scope}, SignedHeaders=${signedHeaderList}, Signature=${signature}`
95
+ let out = Dict.fromArray(sorted)
96
+ out->Dict.set("Authorization", authorization)
97
+ out
98
+ }
99
+
100
+ // ── AppSync Events publish (Node 18+ global fetch) ──────────────────────────
101
+
102
+ type url
103
+ @new external parseUrl: string => url = "URL"
104
+ @get external urlHostname: url => string = "hostname"
105
+
106
+ type fetchInit = {method: string, headers: dict<string>, body: string}
107
+ type fetchResponse
108
+ @val external fetch: (string, fetchInit) => promise<fetchResponse> = "fetch"
109
+ @get external responseOk: fetchResponse => bool = "ok"
110
+ @get external responseStatus: fetchResponse => int = "status"
111
+ @send external responseText: fetchResponse => promise<string> = "text"
112
+
113
+ // Sign and POST one AppSync Events request body to `<endpoint>/event`. The
114
+ // caller builds `body` (channel + events) and inspects the response; error
115
+ // handling differs per handler (log-and-continue vs. transient/permanent split).
116
+ let postEvent = async (
117
+ ~endpoint: string,
118
+ ~region: string,
119
+ ~isoNow: string,
120
+ ~creds: creds,
121
+ ~body: string,
122
+ ): fetchResponse => {
123
+ let host = parseUrl(endpoint)->urlHostname
124
+ let auth = signedHeaders(~host, ~path="/event", ~body, ~region, ~isoNow, ~creds)
125
+ let headers = Dict.fromArray([
126
+ ("accept", "application/json, text/javascript"),
127
+ ("content-encoding", "amz-1.0"),
128
+ ("content-type", "application/json; charset=UTF-8"),
129
+ ])
130
+ auth->Dict.forEachWithKey((v, k) => headers->Dict.set(k, v))
131
+ await fetch(`${endpoint}/event`, {method: "POST", headers, body})
132
+ }
133
+
134
+ // AppSync Events channel segments allow only [A-Za-z0-9-]; anything else
135
+ // collapses to `-`. Mirrors the UI's AutoLive.normalizeSegment.
136
+ let pathSegment = (value: string): string =>
137
+ value->String.replaceRegExp(%re("/[^A-Za-z0-9-]/g"), "-")
@@ -0,0 +1,121 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Nodecrypto from "node:crypto";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
+
8
+ function sha256hex(data) {
9
+ return Nodecrypto.createHash("sha256").update(data).digest("hex");
10
+ }
11
+
12
+ function hmacFromStr(key, data) {
13
+ return Nodecrypto.createHmac("sha256", key).update(data).digest();
14
+ }
15
+
16
+ function hmacFromBuf(key, data) {
17
+ return Nodecrypto.createHmac("sha256", key).update(data).digest();
18
+ }
19
+
20
+ function envCreds() {
21
+ return {
22
+ accessKeyId: Stdlib_Option.getOr(process.env["AWS_ACCESS_KEY_ID"], ""),
23
+ secretAccessKey: Stdlib_Option.getOr(process.env["AWS_SECRET_ACCESS_KEY"], ""),
24
+ sessionToken: process.env["AWS_SESSION_TOKEN"]
25
+ };
26
+ }
27
+
28
+ function region() {
29
+ return Stdlib_Option.getOr(process.env["AWS_REGION"], "eu-west-1");
30
+ }
31
+
32
+ function signedHeaders(host, path, body, region, isoNow, creds) {
33
+ let sl = (a, b) => isoNow.slice(a, b);
34
+ let dateStamp = sl(0, 4) + sl(5, 7) + sl(8, 10);
35
+ let amzDate = dateStamp + "T" + sl(11, 13) + sl(14, 16) + sl(17, 19) + "Z";
36
+ let baseHeaders = [
37
+ [
38
+ "host",
39
+ host
40
+ ],
41
+ [
42
+ "x-amz-date",
43
+ amzDate
44
+ ]
45
+ ];
46
+ let token = creds.sessionToken;
47
+ let headers = token !== undefined && token !== "" ? baseHeaders.concat([[
48
+ "x-amz-security-token",
49
+ token
50
+ ]]) : baseHeaders;
51
+ let sorted = headers.toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
52
+ let canonicalHeaders = sorted.map(param => param[0] + `:` + param[1] + `\n`).join("");
53
+ let signedHeaderList = sorted.map(param => param[0]).join(";");
54
+ let canonicalRequest = [
55
+ "POST",
56
+ path,
57
+ "",
58
+ canonicalHeaders,
59
+ signedHeaderList,
60
+ sha256hex(body)
61
+ ].join("\n");
62
+ let scope = dateStamp + `/` + region + `/appsync/aws4_request`;
63
+ let stringToSign = [
64
+ "AWS4-HMAC-SHA256",
65
+ amzDate,
66
+ scope,
67
+ sha256hex(canonicalRequest)
68
+ ].join("\n");
69
+ let kDate = hmacFromStr("AWS4" + creds.secretAccessKey, dateStamp);
70
+ let kRegion = hmacFromBuf(kDate, region);
71
+ let kService = hmacFromBuf(kRegion, "appsync");
72
+ let kSigning = hmacFromBuf(kService, "aws4_request");
73
+ let signature = Nodecrypto.createHmac("sha256", kSigning).update(stringToSign).digest("hex");
74
+ let authorization = `AWS4-HMAC-SHA256 Credential=` + creds.accessKeyId + `/` + scope + `, SignedHeaders=` + signedHeaderList + `, Signature=` + signature;
75
+ let out = Object.fromEntries(sorted);
76
+ out["Authorization"] = authorization;
77
+ return out;
78
+ }
79
+
80
+ async function postEvent(endpoint, region, isoNow, creds, body) {
81
+ let host = new URL(endpoint).hostname;
82
+ let auth = signedHeaders(host, "/event", body, region, isoNow, creds);
83
+ let headers = Object.fromEntries([
84
+ [
85
+ "accept",
86
+ "application/json, text/javascript"
87
+ ],
88
+ [
89
+ "content-encoding",
90
+ "amz-1.0"
91
+ ],
92
+ [
93
+ "content-type",
94
+ "application/json; charset=UTF-8"
95
+ ]
96
+ ]);
97
+ Stdlib_Dict.forEachWithKey(auth, (v, k) => {
98
+ headers[k] = v;
99
+ });
100
+ return await fetch(endpoint + `/event`, {
101
+ method: "POST",
102
+ headers: headers,
103
+ body: body
104
+ });
105
+ }
106
+
107
+ function pathSegment(value) {
108
+ return value.replace(/[^A-Za-z0-9-]/g, "-");
109
+ }
110
+
111
+ export {
112
+ sha256hex,
113
+ hmacFromStr,
114
+ hmacFromBuf,
115
+ envCreds,
116
+ region,
117
+ signedHeaders,
118
+ postEvent,
119
+ pathSegment,
120
+ }
121
+ /* node:crypto Not a pure module */
@@ -0,0 +1,136 @@
1
+ // Shared runtime helpers for the two admin-query resolver Lambdas
2
+ // (Platform_UIFragments, Platform_ComponentDefinitions). Both scan a read-model
3
+ // table and collapse the rows to the highest version per bare plugin name.
4
+ //
5
+ // Runtime-pure and self-contained: bare `@aws-sdk/*` specifiers (resolved from
6
+ // the managed runtime via the ESM resolve-hook), no `PulumiAws` — so it ships
7
+ // inside an EntryPoint code archive with no serialized closure and no Pulumi in
8
+ // the cold-start graph. Extracted from the former inline JS handler strings,
9
+ // which duplicated the version comparator verbatim across both files.
10
+
11
+ // ── @aws-sdk DynamoDB DocumentClient (bare specifiers) ──────────────────────
12
+
13
+ type ddbClient
14
+ type docClient
15
+ @module("@aws-sdk/client-dynamodb") @new external makeDdbClient: unit => ddbClient = "DynamoDBClient"
16
+ @module("@aws-sdk/lib-dynamodb") @scope("DynamoDBDocumentClient")
17
+ external docFrom: ddbClient => docClient = "from"
18
+
19
+ type scanInput = {
20
+ @as("TableName") tableName: string,
21
+ @as("Limit") limit?: int,
22
+ @as("ExclusiveStartKey") exclusiveStartKey?: JSON.t,
23
+ @as("FilterExpression") filterExpression?: string,
24
+ @as("ExpressionAttributeNames") expressionAttributeNames?: dict<string>,
25
+ @as("ExpressionAttributeValues") expressionAttributeValues?: dict<JSON.t>,
26
+ }
27
+ type scanCommand
28
+ @module("@aws-sdk/lib-dynamodb") @new external makeScanCommand: scanInput => scanCommand = "ScanCommand"
29
+ type scanOutput = {
30
+ @as("Items") items?: array<dict<JSON.t>>,
31
+ @as("LastEvaluatedKey") lastEvaluatedKey?: JSON.t,
32
+ }
33
+ @send external send: (docClient, scanCommand) => promise<scanOutput> = "send"
34
+
35
+ // One document client per container (cold start).
36
+ let doc = makeDdbClient()->docFrom
37
+
38
+ // Drain every page of a table scan (1000/page), following LastEvaluatedKey.
39
+ let scanAll = async (
40
+ ~tableName: string,
41
+ ~filterExpression: option<string>=?,
42
+ ~expressionAttributeNames: option<dict<string>>=?,
43
+ ~expressionAttributeValues: option<dict<JSON.t>>=?,
44
+ ): array<dict<JSON.t>> => {
45
+ let items = []
46
+ let startKey = ref(None)
47
+ let more = ref(true)
48
+ while more.contents {
49
+ let out =
50
+ await doc->send(
51
+ makeScanCommand({
52
+ tableName,
53
+ limit: 1000,
54
+ exclusiveStartKey: ?startKey.contents,
55
+ filterExpression: ?filterExpression,
56
+ expressionAttributeNames: ?expressionAttributeNames,
57
+ expressionAttributeValues: ?expressionAttributeValues,
58
+ }),
59
+ )
60
+ out.items->Option.forEach(is => is->Array.forEach(i => items->Array.push(i)))
61
+ switch out.lastEvaluatedKey {
62
+ | Some(k) => startKey := Some(k)
63
+ | None => more := false
64
+ }
65
+ }
66
+ items
67
+ }
68
+
69
+ // ── Version comparison ──────────────────────────────────────────────────────
70
+
71
+ // `Number(...)` binding so the numeric-segment comparison is byte-identical to
72
+ // the former JS `cmpVer` (empty string is guarded separately, exactly as before).
73
+ @val external jsNumber: string => float = "Number"
74
+
75
+ // Compare two version strings (`-`/`+` normalised to `.`, then segment-wise:
76
+ // numeric segments compared as numbers, others lexically). Mirrors the platform
77
+ // invariant "one version per plugin at a time" ordering. Returns 1/-1/0.
78
+ let compareVersions = (a: string, b: string): int => {
79
+ let norm = s => s->String.replaceRegExp(%re("/[-+]/g"), ".")->String.split(".")
80
+ let pa = norm(a)
81
+ let pb = norm(b)
82
+ let len = Math.Int.max(pa->Array.length, pb->Array.length)
83
+ let res = ref(0)
84
+ let i = ref(0)
85
+ while res.contents == 0 && i.contents < len {
86
+ let sa = pa->Array.get(i.contents)->Option.getOr("")
87
+ let sb = pb->Array.get(i.contents)->Option.getOr("")
88
+ let na = jsNumber(sa)
89
+ let nb = jsNumber(sb)
90
+ let bothNum = sa != "" && sb != "" && !Float.isNaN(na) && !Float.isNaN(nb)
91
+ if bothNum {
92
+ if na > nb {
93
+ res := 1
94
+ } else if na < nb {
95
+ res := -1
96
+ }
97
+ } else if sa > sb {
98
+ res := 1
99
+ } else if sa < sb {
100
+ res := -1
101
+ }
102
+ i := i.contents + 1
103
+ }
104
+ res.contents
105
+ }
106
+
107
+ // Collapse scanned rows to the highest-version entry per bare plugin name.
108
+ // `nameVersionOf` reads the row's `name@version` string; `toEntry` builds the
109
+ // output entry for a surviving row (returning None drops a malformed row). The
110
+ // name/version split mirrors ReventlessCore.Plugin.name / compareVersions.
111
+ let latestByName = (
112
+ items: array<dict<JSON.t>>,
113
+ ~nameVersionOf: dict<JSON.t> => option<string>,
114
+ ~toEntry: (dict<JSON.t>, ~name: string) => option<JSON.t>,
115
+ ): array<JSON.t> => {
116
+ // bare plugin name -> (version, entry); highest version wins.
117
+ let latest: dict<(string, JSON.t)> = Dict.make()
118
+ items->Array.forEach(item =>
119
+ switch item->nameVersionOf {
120
+ | None => ()
121
+ | Some(nameVersion) =>
122
+ let segments = nameVersion->String.split("@")
123
+ let name = segments->Array.get(0)->Option.getOr("")
124
+ let version = segments->Array.get(1)->Option.getOr("")
125
+ switch item->toEntry(~name) {
126
+ | None => ()
127
+ | Some(entry) =>
128
+ switch latest->Dict.get(name) {
129
+ | Some((prevVersion, _)) if compareVersions(version, prevVersion) <= 0 => ()
130
+ | _ => latest->Dict.set(name, (version, entry))
131
+ }
132
+ }
133
+ }
134
+ )
135
+ latest->Dict.valuesToArray->Array.map(((_, entry)) => entry)
136
+ }
@@ -0,0 +1,98 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
5
+ import * as ClientDynamodb from "@aws-sdk/client-dynamodb";
6
+
7
+ let doc = LibDynamodb.DynamoDBDocumentClient.from(new ClientDynamodb.DynamoDBClient());
8
+
9
+ async function scanAll(tableName, filterExpression, expressionAttributeNames, expressionAttributeValues) {
10
+ let items = [];
11
+ let startKey;
12
+ let more = true;
13
+ while (more) {
14
+ let out = await doc.send(new LibDynamodb.ScanCommand({
15
+ TableName: tableName,
16
+ Limit: 1000,
17
+ ExclusiveStartKey: startKey,
18
+ FilterExpression: filterExpression,
19
+ ExpressionAttributeNames: expressionAttributeNames,
20
+ ExpressionAttributeValues: expressionAttributeValues
21
+ }));
22
+ Stdlib_Option.forEach(out.Items, is => {
23
+ is.forEach(i => {
24
+ items.push(i);
25
+ });
26
+ });
27
+ let k = out.LastEvaluatedKey;
28
+ if (k !== undefined) {
29
+ startKey = k;
30
+ } else {
31
+ more = false;
32
+ }
33
+ };
34
+ return items;
35
+ }
36
+
37
+ function compareVersions(a, b) {
38
+ let norm = s => s.replace(/[-+]/g, ".").split(".");
39
+ let pa = norm(a);
40
+ let pb = norm(b);
41
+ let len = Math.max(pa.length, pb.length);
42
+ let res = 0;
43
+ let i = 0;
44
+ while (res === 0 && i < len) {
45
+ let sa = Stdlib_Option.getOr(pa[i], "");
46
+ let sb = Stdlib_Option.getOr(pb[i], "");
47
+ let na = Number(sa);
48
+ let nb = Number(sb);
49
+ let bothNum = sa !== "" && sb !== "" && !Number.isNaN(na) && !Number.isNaN(nb);
50
+ if (bothNum) {
51
+ if (na > nb) {
52
+ res = 1;
53
+ } else if (na < nb) {
54
+ res = -1;
55
+ }
56
+ } else if (sa > sb) {
57
+ res = 1;
58
+ } else if (sa < sb) {
59
+ res = -1;
60
+ }
61
+ i = i + 1 | 0;
62
+ };
63
+ return res;
64
+ }
65
+
66
+ function latestByName(items, nameVersionOf, toEntry) {
67
+ let latest = {};
68
+ items.forEach(item => {
69
+ let nameVersion = nameVersionOf(item);
70
+ if (nameVersion === undefined) {
71
+ return;
72
+ }
73
+ let segments = nameVersion.split("@");
74
+ let name = Stdlib_Option.getOr(segments[0], "");
75
+ let version = Stdlib_Option.getOr(segments[1], "");
76
+ let entry = toEntry(item, name);
77
+ if (entry === undefined) {
78
+ return;
79
+ }
80
+ let match = latest[name];
81
+ if (match !== undefined && compareVersions(version, match[0]) <= 0) {
82
+ return;
83
+ }
84
+ latest[name] = [
85
+ version,
86
+ entry
87
+ ];
88
+ });
89
+ return Object.values(latest).map(param => param[1]);
90
+ }
91
+
92
+ export {
93
+ doc,
94
+ scanAll,
95
+ compareVersions,
96
+ latestByName,
97
+ }
98
+ /* doc Not a pure module */
@@ -13,85 +13,6 @@
13
13
 
14
14
  open PulumiAws
15
15
 
16
- // `adminEntryJson` is the JSON-stringified entry for the built-in Platform_Admin
17
- // plugin (Plugin aggregate with Activate/Deactivate, Plugin read model).
18
- // The admin never `Connect`s to itself, so its structure never
19
- // enters the Plugin read model — we inject it at deploy time so the host shell
20
- // renders Auto UI for it alongside user plugins.
21
- let makeHandlerCode = (~tableName as _: string, ~adminEntryJson: string): string =>
22
- `
23
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
24
- import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
25
-
26
- const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
27
- const TABLE = process.env.PLUGIN_RM_TABLE;
28
- const ADMIN_ENTRY = ${adminEntryJson};
29
-
30
- export async function handler() {
31
- if (!TABLE) {
32
- console.error("Platform_ComponentDefinitions: PLUGIN_RM_TABLE env var not set");
33
- return [ADMIN_ENTRY];
34
- }
35
- const items = [];
36
- let exclusiveStartKey;
37
- do {
38
- const out = await client.send(new ScanCommand({
39
- TableName: TABLE,
40
- FilterExpression: "contains(#status, :connected)",
41
- ExpressionAttributeNames: { "#status": "status" },
42
- ExpressionAttributeValues: { ":connected": "Connected" },
43
- Limit: 1000,
44
- ExclusiveStartKey: exclusiveStartKey,
45
- }));
46
- if (out.Items) items.push(...out.Items);
47
- exclusiveStartKey = out.LastEvaluatedKey;
48
- } while (exclusiveStartKey);
49
-
50
- // Platform invariant: one version per plugin at a time, so the UI sees just
51
- // the bare plugin name (mirrors ReventlessCore.Plugin.name). The Connected
52
- // filter above does not enforce single-version: during a rolling deploy or a
53
- // missed retire, several versions of the same plugin sit in Connected at once
54
- // and each would otherwise emit a full duplicate set of AutoUI menu entries.
55
- // Collapse to the highest version per plugin name (mirrors
56
- // ReventlessCore.Plugin.compareVersions).
57
- const cmpVer = (a, b) => {
58
- const pa = String(a).replace(/[-+]/g, ".").split(".");
59
- const pb = String(b).replace(/[-+]/g, ".").split(".");
60
- const len = Math.max(pa.length, pb.length);
61
- for (let i = 0; i < len; i++) {
62
- const sa = pa[i] ?? "", sb = pb[i] ?? "";
63
- const na = Number(sa), nb = Number(sb);
64
- const bothNum = sa !== "" && sb !== "" && !Number.isNaN(na) && !Number.isNaN(nb);
65
- if (bothNum) { if (na !== nb) return na > nb ? 1 : -1; }
66
- else if (sa !== sb) return sa > sb ? 1 : -1;
67
- }
68
- return 0;
69
- };
70
- // The persisted structure carries Internal ReadModels / StateViewSlices for
71
- // developer tooling; they must stay out of the deployed AutoUI. Mirror
72
- // ReventlessCore.Platform_ComponentDefinitionsApi.isPublicQueryable here on the
73
- // read path (the persisted structure is pre-filter — it is NOT re-encoded).
74
- const isPublicQueryable = (q) => q?.visibility !== "Internal";
75
- const filterStructure = (s) => ({
76
- ...s,
77
- readModels: (s.readModels ?? []).filter(isPublicQueryable),
78
- stateViewSlices: (s.stateViewSlices ?? []).filter(isPublicQueryable),
79
- });
80
- const latestByName = new Map();
81
- for (const item of items) {
82
- if (!item || !item.structure) continue;
83
- const name = String(item.name).split("@")[0];
84
- const version = String(item.name).split("@")[1] ?? "";
85
- const prev = latestByName.get(name);
86
- if (!prev || cmpVer(version, prev.version) > 0) {
87
- latestByName.set(name, { version, entry: { pluginId: name, ...filterStructure(item.structure) } });
88
- }
89
- }
90
- const userEntries = [...latestByName.values()].map(v => v.entry);
91
- return [ADMIN_ENTRY, ...userEntries];
92
- }
93
- `
94
-
95
16
  let resolverCode = `
96
17
  import { util } from '@aws-appsync/utils';
97
18
  export function request(ctx) {
@@ -160,18 +81,18 @@ let make = (
160
81
  ReventlessCore.Platform_Admin_Structure.structure,
161
82
  )->JSON.stringify
162
83
 
163
- let archiveContents: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
164
- let handlerCodeStub = makeHandlerCode(~tableName="", ~adminEntryJson)
165
- archiveContents->Dict.set(
166
- "index.mjs",
167
- Pulumi.Asset.stringAsset(handlerCodeStub)->Pulumi.Archive.assetToAssetOrArchive,
84
+ // Bundle reventless-aws (the compiled `_Ops` handler lives inside it) and
85
+ // re-export its `handler`; buildCodeArchive also ships the ESM resolve-hook so
86
+ // the handler's bare @aws-sdk/* specifiers resolve from the managed runtime.
87
+ // The admin entry is passed as an env var (ADMIN_ENTRY_JSON) rather than
88
+ // interpolated into the source, so the archive hash is stable across deploys.
89
+ let packageDirs = Dict.fromArray([
90
+ ("@reventlessdev/reventless-aws", Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws")),
91
+ ])
92
+ let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
93
+ ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs",
94
+ ~packageDirs,
168
95
  )
169
- // ESM self-containment: the handler imports @aws-sdk/* bare specifiers the
170
- // nodejs22.x runtime provides only under /var/runtime — unreachable from
171
- // /var/task ESM without the resolver hook. Ship the loader + set its env vars.
172
- let loaderHash = Util_Bundle.addEsmLoaderAssets(archiveContents)
173
- let code = Pulumi.Archive.assetArchive(archiveContents)
174
- let sourceCodeHash = Util_Bundle.hashString(handlerCodeStub ++ "\n---\n" ++ loaderHash)
175
96
 
176
97
  let layers =
177
98
  Lambda.reventlessLayerArn
@@ -196,6 +117,7 @@ let make = (
196
117
  Lambda.Function.variables: Dict.fromArray([
197
118
  ("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
198
119
  ("PLUGIN_RM_TABLE", pluginReadModelTableName->Pulumi.Output.asInput),
120
+ ("ADMIN_ENTRY_JSON", adminEntryJson->Pulumi.Input.make),
199
121
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
200
122
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
201
123
  ]),