@reventlessdev/reventless-local 3.0.0-alpha.301 → 3.0.0-alpha.303
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 +46 -0
- package/package.json +9 -9
- package/src/EmitCapabilities.res +12 -5
- package/src/EmitCapabilities.res.mjs +13 -1
- package/src/Platform.res +1 -2
- package/src/Platform.res.mjs +6 -10
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +2 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +0 -2
- package/src/adapter/LocalCapabilities.res +1 -1
- package/src/adapter/LocalCapabilities.res.mjs +2 -1
- package/src/adapter/LocalFingerprintKey.res +66 -0
- package/src/adapter/LocalFingerprintKey.res.mjs +75 -0
- package/src/adapter/LocalUploadResolvers.res +7 -7
- package/src/adapter/LocalUploadResolvers.res.mjs +5 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +0 -1
- package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +0 -1
- package/tests/EmitCapabilitiesTest.res +6 -0
- package/tests/EmitCapabilitiesTest.res.mjs +6 -0
- package/tests/PluginEventDecodeTest.res.mjs +2 -2
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +1 -1
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +1 -1
- package/tests/adapter/ObjectStorePersistenceTest.res +6 -3
- package/tests/adapter/ObjectStorePersistenceTest.res.mjs +3 -3
- package/tests/adapter/ServedBucketHttpTest.res +3 -1
- package/tests/adapter/ServedBucketHttpTest.res.mjs +2 -1
- package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +1 -1
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +1 -1
- package/tests/components/dcb/DcbRepeatTest.res +291 -0
- package/tests/components/dcb/DcbRepeatTest.res.mjs +420 -0
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res +71 -15
- package/tests/components/readmodel/DcbReadModelTypoFailFastTest.res.mjs +119 -14
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res.mjs +1 -1
- package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +1 -1
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +1 -1
- package/tests/plugin/Plugin_Fixtures.res +1 -3
- package/tests/plugin/Plugin_Fixtures.res.mjs +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
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.303 (2026-10-29)
|
|
7
|
+
|
|
8
|
+
* feat(registration)!: the ledger tags an address by its keyed fingerprint ([ccafddf](https://github.com/ReventlessDev/reventless-core/commit/ccafddfc7eb8039687afc62a483ce34b8e7081dd))
|
|
9
|
+
* feat(core)!: the caller's IP address leaves the messages a request causes ([aa9e8c7](https://github.com/ReventlessDev/reventless-core/commit/aa9e8c79c0360528a5cfeda2bb88d1da52f28674))
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **upload:** an upload's path carries nothing a person typed ([456c88c](https://github.com/ReventlessDev/reventless-core/commit/456c88c4ccee6a50943e3a71011a2e78aef0d770))
|
|
13
|
+
|
|
14
|
+
### BREAKING CHANGES
|
|
15
|
+
|
|
16
|
+
* `Secrets.t` has a third field, `fingerprint`; a hand-built provider
|
|
17
|
+
adds it. `Register` and `Invite` take `address`, and their `email` is the
|
|
18
|
+
fingerprint from `ContactAddressId.fingerprint`, not `makeFromString(address)`.
|
|
19
|
+
|
|
20
|
+
Step PD5 of docs/plans/a-personal-field-is-marked-redacted-and-encrypted-at-the-seam.md.
|
|
21
|
+
Claude-Session: https://claude.ai/code/session_01HiQfdxVEsHwLHRoHSUcus1
|
|
22
|
+
* `Message.generateMeta` no longer takes `~ip`. Drop the argument; a
|
|
23
|
+
message that must carry the address declares it as a field of its own payload.
|
|
24
|
+
|
|
25
|
+
Step PD3 of docs/plans/a-personal-field-is-marked-redacted-and-encrypted-at-the-seam.md.
|
|
26
|
+
Claude-Session: https://claude.ai/code/session_01HiQfdxVEsHwLHRoHSUcus1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# 3.0.0-alpha.302 (2026-10-29)
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
* **local:** emit-capabilities writes an empty manifest for a plugin with no parts yet ([56586a6](https://github.com/ReventlessDev/reventless-core/commit/56586a687aa4658e2357c836878b44050b424d41))
|
|
35
|
+
* refactor!: drop `dcbSources`, the cross-plugin DCB subscription nobody declared ([cf3b66d](https://github.com/ReventlessDev/reventless-core/commit/cf3b66d8c1b5d87673197b64eaee174598bb8253))
|
|
36
|
+
* refactor!: a read model hears another plugin only through an Extension and a slice ([15c7cae](https://github.com/ReventlessDev/reventless-core/commit/15c7cae214d5aa04404779fcd87a8429aad1965c))
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
* **core:** a handler drops a command it has already handled ([605862b](https://github.com/ReventlessDev/reventless-core/commit/605862bba765b1501eb19d08f3924bf619159e21))
|
|
40
|
+
|
|
41
|
+
### BREAKING CHANGES
|
|
42
|
+
|
|
43
|
+
* `Reventless.Plugin.extensionDefinition` has no `dcbSources`
|
|
44
|
+
field, and `Platform_PluginExtensions.dcbSources` is gone from the GraphQL
|
|
45
|
+
schema. A query that selects it fails as a whole; drop the selection.
|
|
46
|
+
* `ReventlessInfra.Extension.T.make` and the core
|
|
47
|
+
`Extension_Builder.make` no longer take `~readModelNamesForSourceName` and
|
|
48
|
+
`~publishToReadModels`. Both arguments did nothing; drop them at the call site.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
6
52
|
# 3.0.0-alpha.301 (2026-10-25)
|
|
7
53
|
|
|
8
54
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.303",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -34,19 +34,19 @@
|
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
|
-
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
38
37
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
39
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
40
38
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
39
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
40
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
41
41
|
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
42
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.312",
|
|
42
43
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
44
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.160",
|
|
45
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.209",
|
|
46
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.260",
|
|
47
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.176",
|
|
47
48
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.38",
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.178"
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.180"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
package/src/EmitCapabilities.res
CHANGED
|
@@ -25,9 +25,19 @@
|
|
|
25
25
|
|
|
26
26
|
type platform
|
|
27
27
|
type localPlatformExports = {"Make": unit => platform}
|
|
28
|
-
|
|
28
|
+
// A plugin with no parts yet has no structure: `generate-plugin` leaves `pluginStructure` out of
|
|
29
|
+
// its composition root.
|
|
30
|
+
type builtPlugin = {"pluginStructure": Nullable.t<Reventless.Plugin.pluginStructure>}
|
|
29
31
|
type compositionExports = {"Make": platform => builtPlugin}
|
|
30
32
|
|
|
33
|
+
/** The manifest's text for a plugin's structure. A plugin with no parts yet has none and
|
|
34
|
+
declares nothing: its manifest says so, as one for a structure with no declarations does. */
|
|
35
|
+
let manifestText = (structure: option<Reventless.Plugin.pluginStructure>): string =>
|
|
36
|
+
switch structure {
|
|
37
|
+
| Some(structure) => Reventless.CapabilityManifest.renderForStructure(structure)
|
|
38
|
+
| None => Reventless.CapabilityManifest.render({capabilities: []})
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
// ── Which module holds the composition root ──────────────────────────────────
|
|
32
42
|
|
|
33
43
|
/** `Plugin.res.mjs` is what `generate-plugin` emits, so it is the default and
|
|
@@ -99,10 +109,7 @@ let main = () =>
|
|
|
99
109
|
let built = composition["Make"](platform)
|
|
100
110
|
|
|
101
111
|
let manifestPath = NodePath.join([srcDir, "capabilities.json"])
|
|
102
|
-
NodeFs.writeFileSync(
|
|
103
|
-
manifestPath,
|
|
104
|
-
Reventless.CapabilityManifest.renderForStructure(built["pluginStructure"]),
|
|
105
|
-
)
|
|
112
|
+
NodeFs.writeFileSync(manifestPath, manifestText(built["pluginStructure"]->Nullable.toOption))
|
|
106
113
|
Console.log("Generated: " ++ manifestPath)
|
|
107
114
|
|
|
108
115
|
// Applying the platform functor wires in-process infrastructure; exit
|
|
@@ -4,9 +4,20 @@ import * as Nodefs from "node:fs";
|
|
|
4
4
|
import * as Nodeurl from "node:url";
|
|
5
5
|
import * as Nodepath from "node:path";
|
|
6
6
|
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
7
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
8
|
import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
|
|
8
9
|
import * as CapabilityManifest$Reventless from "@reventlessdev/reventless-spec/src/components/CapabilityManifest.res.mjs";
|
|
9
10
|
|
|
11
|
+
function manifestText(structure) {
|
|
12
|
+
if (structure !== undefined) {
|
|
13
|
+
return CapabilityManifest$Reventless.renderForStructure(structure);
|
|
14
|
+
} else {
|
|
15
|
+
return CapabilityManifest$Reventless.render({
|
|
16
|
+
capabilities: []
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
function compositionModulePath(srcDir, moduleArg) {
|
|
11
22
|
let withExtension = name => {
|
|
12
23
|
if (name.endsWith(".mjs")) {
|
|
@@ -84,13 +95,14 @@ function main() {
|
|
|
84
95
|
let composition = await import(Nodeurl.pathToFileURL(modulePath).href);
|
|
85
96
|
let built = composition.Make(platform);
|
|
86
97
|
let manifestPath = Nodepath.join(srcDir, "capabilities.json");
|
|
87
|
-
Nodefs.writeFileSync(manifestPath,
|
|
98
|
+
Nodefs.writeFileSync(manifestPath, manifestText(Primitive_option.fromNullable(built.pluginStructure)), "utf8");
|
|
88
99
|
console.log("Generated: " + manifestPath);
|
|
89
100
|
process.exit(0);
|
|
90
101
|
});
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
export {
|
|
105
|
+
manifestText,
|
|
94
106
|
compositionModulePath,
|
|
95
107
|
fail,
|
|
96
108
|
usage,
|
package/src/Platform.res
CHANGED
|
@@ -351,7 +351,7 @@ module MakeWithConfig = (
|
|
|
351
351
|
let payload: ReventlessCore.CommandGenerator.payload = {
|
|
352
352
|
command: toolName,
|
|
353
353
|
arguments: args->Obj.magic,
|
|
354
|
-
meta: {
|
|
354
|
+
meta: {user: identity.userId, info: `mcp/tools/${toolName}`},
|
|
355
355
|
identity,
|
|
356
356
|
}
|
|
357
357
|
let outcome = await generateCommand(payload)->Effect.runPromise
|
|
@@ -1255,7 +1255,6 @@ module MakeWithConfig = (
|
|
|
1255
1255
|
((_, ext: ReventlessInfra.Extension.outputs)) => {
|
|
1256
1256
|
Reventless.Plugin.name: ext.name,
|
|
1257
1257
|
extensionPointName: ext.extensionPointName,
|
|
1258
|
-
dcbSources: [],
|
|
1259
1258
|
},
|
|
1260
1259
|
),
|
|
1261
1260
|
extensionProtocols: [],
|
package/src/Platform.res.mjs
CHANGED
|
@@ -256,7 +256,6 @@ function MakeWithConfig(Config) {
|
|
|
256
256
|
return `{"error":"no handler for ` + toolName + `"}`;
|
|
257
257
|
}
|
|
258
258
|
let payload_meta = {
|
|
259
|
-
ip: [],
|
|
260
259
|
user: identity.userId,
|
|
261
260
|
info: `mcp/tools/` + toolName
|
|
262
261
|
};
|
|
@@ -937,7 +936,7 @@ function MakeWithConfig(Config) {
|
|
|
937
936
|
let pluginCmdTopicKey = PluginSpec$ReventlessCore.name + "AggrCmdTopic";
|
|
938
937
|
let pluginEventTopicKey = PluginSpec$ReventlessCore.name + "AggrEventTopic";
|
|
939
938
|
let dispatchPluginCommand = (pluginName, command) => {
|
|
940
|
-
let cmdJson_meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined
|
|
939
|
+
let cmdJson_meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
941
940
|
let cmdJson_commandJson = Util_Sury$Reventless.toJson(command, PluginSpec$ReventlessCore.commandSchema);
|
|
942
941
|
let cmdJson = {
|
|
943
942
|
id: pluginName,
|
|
@@ -948,7 +947,7 @@ function MakeWithConfig(Config) {
|
|
|
948
947
|
};
|
|
949
948
|
let adminDcbCmdTopicKey = ComponentType$ReventlessCore.name(Platform_Admin_Structure$ReventlessCore.pluginId + "Dcb", "CommandTopic");
|
|
950
949
|
let dispatchUiFragmentCommand = (pluginName, command) => {
|
|
951
|
-
let cmdJson_meta = Message$ReventlessCore.generateMeta(UiFragmentRegistry$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined
|
|
950
|
+
let cmdJson_meta = Message$ReventlessCore.generateMeta(UiFragmentRegistry$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
952
951
|
let cmdJson_commandJson = Util_Sury$Reventless.toJson(command, UiFragmentRegistry$ReventlessCore.commandSchema);
|
|
953
952
|
let cmdJson = {
|
|
954
953
|
id: pluginName,
|
|
@@ -1063,8 +1062,7 @@ function MakeWithConfig(Config) {
|
|
|
1063
1062
|
let ext = param[1];
|
|
1064
1063
|
return {
|
|
1065
1064
|
name: ext.name,
|
|
1066
|
-
extensionPointName: ext.extensionPointName
|
|
1067
|
-
dcbSources: []
|
|
1065
|
+
extensionPointName: ext.extensionPointName
|
|
1068
1066
|
};
|
|
1069
1067
|
}),
|
|
1070
1068
|
eventCollector: param[2].name,
|
|
@@ -2051,7 +2049,6 @@ function Make($star) {
|
|
|
2051
2049
|
return `{"error":"no handler for ` + toolName + `"}`;
|
|
2052
2050
|
}
|
|
2053
2051
|
let payload_meta = {
|
|
2054
|
-
ip: [],
|
|
2055
2052
|
user: identity.userId,
|
|
2056
2053
|
info: `mcp/tools/` + toolName
|
|
2057
2054
|
};
|
|
@@ -2732,7 +2729,7 @@ function Make($star) {
|
|
|
2732
2729
|
let pluginCmdTopicKey = PluginSpec$ReventlessCore.name + "AggrCmdTopic";
|
|
2733
2730
|
let pluginEventTopicKey = PluginSpec$ReventlessCore.name + "AggrEventTopic";
|
|
2734
2731
|
let dispatchPluginCommand = (pluginName, command) => {
|
|
2735
|
-
let cmdJson_meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined
|
|
2732
|
+
let cmdJson_meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2736
2733
|
let cmdJson_commandJson = Util_Sury$Reventless.toJson(command, PluginSpec$ReventlessCore.commandSchema);
|
|
2737
2734
|
let cmdJson = {
|
|
2738
2735
|
id: pluginName,
|
|
@@ -2743,7 +2740,7 @@ function Make($star) {
|
|
|
2743
2740
|
};
|
|
2744
2741
|
let adminDcbCmdTopicKey = ComponentType$ReventlessCore.name(Platform_Admin_Structure$ReventlessCore.pluginId + "Dcb", "CommandTopic");
|
|
2745
2742
|
let dispatchUiFragmentCommand = (pluginName, command) => {
|
|
2746
|
-
let cmdJson_meta = Message$ReventlessCore.generateMeta(UiFragmentRegistry$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined
|
|
2743
|
+
let cmdJson_meta = Message$ReventlessCore.generateMeta(UiFragmentRegistry$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
2747
2744
|
let cmdJson_commandJson = Util_Sury$Reventless.toJson(command, UiFragmentRegistry$ReventlessCore.commandSchema);
|
|
2748
2745
|
let cmdJson = {
|
|
2749
2746
|
id: pluginName,
|
|
@@ -2858,8 +2855,7 @@ function Make($star) {
|
|
|
2858
2855
|
let ext = param[1];
|
|
2859
2856
|
return {
|
|
2860
2857
|
name: ext.name,
|
|
2861
|
-
extensionPointName: ext.extensionPointName
|
|
2862
|
-
dcbSources: []
|
|
2858
|
+
extensionPointName: ext.extensionPointName
|
|
2863
2859
|
};
|
|
2864
2860
|
}),
|
|
2865
2861
|
eventCollector: param[2].name,
|
|
@@ -248,7 +248,7 @@ let register = (
|
|
|
248
248
|
let payload: CommandGenerator.payload = {
|
|
249
249
|
command: commandName,
|
|
250
250
|
arguments: args->Obj.magic,
|
|
251
|
-
meta: {
|
|
251
|
+
meta: {user: identity.userId, info: `Mutation.${field}`},
|
|
252
252
|
identity,
|
|
253
253
|
}
|
|
254
254
|
let outcome = await runCommand(generateCommand, payload)
|
|
@@ -320,7 +320,7 @@ let registerDcb = (
|
|
|
320
320
|
let payload: CommandGenerator.payload = {
|
|
321
321
|
command: tag,
|
|
322
322
|
arguments: args->Obj.magic,
|
|
323
|
-
meta: {
|
|
323
|
+
meta: {user: identity.userId, info: `Mutation.${fieldName}`},
|
|
324
324
|
identity,
|
|
325
325
|
}
|
|
326
326
|
let outcome = await runCommand(generateCommand, payload)
|
|
@@ -179,7 +179,6 @@ function register(fields, commandSchema, authorizationOf, server) {
|
|
|
179
179
|
return rejectThrottled(field);
|
|
180
180
|
}
|
|
181
181
|
let payload_meta = {
|
|
182
|
-
ip: [],
|
|
183
182
|
user: identity.userId,
|
|
184
183
|
info: `Mutation.` + field
|
|
185
184
|
};
|
|
@@ -225,7 +224,6 @@ function registerDcb(fieldName, commandSchema, authorizationOf, server) {
|
|
|
225
224
|
return rejectThrottled(fieldName);
|
|
226
225
|
}
|
|
227
226
|
let payload_meta = {
|
|
228
|
-
ip: [],
|
|
229
227
|
user: identity.userId,
|
|
230
228
|
info: `Mutation.` + fieldName
|
|
231
229
|
};
|
|
@@ -92,6 +92,6 @@ let capabilities = (): Reventless.Capabilities.t => {
|
|
|
92
92
|
geocode: Reventless.Capabilities.none.geocode,
|
|
93
93
|
messaging: ReventlessCore.Messaging_Log_Backend.provider(~sender=messagingSenderRef.contents),
|
|
94
94
|
identityProvider: identityProviderRef.contents,
|
|
95
|
-
secrets: ReventlessCore.Secrets_Node_Backend.
|
|
95
|
+
secrets: ReventlessCore.Secrets_Node_Backend.make(~fingerprintKey=LocalFingerprintKey.get),
|
|
96
96
|
stash: stash.contents,
|
|
97
97
|
}
|
|
@@ -4,6 +4,7 @@ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
|
4
4
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
5
5
|
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
6
6
|
import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/semantic/CapabilityNeed.res.mjs";
|
|
7
|
+
import * as LocalFingerprintKey$ReventlessLocal from "./LocalFingerprintKey.res.mjs";
|
|
7
8
|
import * as Secrets_Node_Backend$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Secrets/Secrets_Node_Backend.res.mjs";
|
|
8
9
|
import * as Stash_Memory_Backend$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Stash/Stash_Memory_Backend.res.mjs";
|
|
9
10
|
import * as Messaging_Log_Backend$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Messaging/Messaging_Log_Backend.res.mjs";
|
|
@@ -64,7 +65,7 @@ function capabilities() {
|
|
|
64
65
|
geocode: Capabilities$Reventless.none.geocode,
|
|
65
66
|
messaging: Messaging_Log_Backend$ReventlessCore.provider(messagingSenderRef.contents),
|
|
66
67
|
identityProvider: identityProviderRef.contents,
|
|
67
|
-
secrets: Secrets_Node_Backend$ReventlessCore.
|
|
68
|
+
secrets: Secrets_Node_Backend$ReventlessCore.make(LocalFingerprintKey$ReventlessLocal.get),
|
|
68
69
|
stash: stash.contents
|
|
69
70
|
};
|
|
70
71
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The local platform's fingerprint key, the one `secrets.fingerprint` is keyed by.
|
|
2
|
+
//
|
|
3
|
+
// Resolved once, on first use, from the same ladder as the local token secret:
|
|
4
|
+
//
|
|
5
|
+
// 1. `REVENTLESS_FINGERPRINT_KEY` — an explicit pin, for a test or a developer
|
|
6
|
+
// who wants to state it.
|
|
7
|
+
// 2. `.reventless/fingerprint-key` beside the platform's other local state,
|
|
8
|
+
// minted on the first run that finds the directory and reused after. A
|
|
9
|
+
// fingerprint is a tag in the event log, so a key that changed on restart
|
|
10
|
+
// would lose every registration a SQLite store kept.
|
|
11
|
+
// 3. A random 32 bytes, per process, for a run with no `.reventless/`.
|
|
12
|
+
//
|
|
13
|
+
// The directory is used, never created, for the reason `LocalAuth` gives: its
|
|
14
|
+
// presence is what tells a platform someone develops against from a unit test.
|
|
15
|
+
|
|
16
|
+
let fileName = "fingerprint-key"
|
|
17
|
+
|
|
18
|
+
let mint = (): string => NodeCrypto.randomBytes(32)->NodeCrypto.bufferToString("hex")
|
|
19
|
+
|
|
20
|
+
let readAt = (path: string): option<string> =>
|
|
21
|
+
switch NodeFs.readFileSync(path) {
|
|
22
|
+
| contents
|
|
23
|
+
if String.length(String.trim(contents)) >=
|
|
24
|
+
ReventlessCore.Secrets_Node_Backend.minimumFingerprintKeyLength =>
|
|
25
|
+
Some(String.trim(contents))
|
|
26
|
+
| _ => None
|
|
27
|
+
| exception _ => None
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The ladder, against a stated directory, so a test can walk it without a
|
|
31
|
+
`.reventless/` in its own working directory. */
|
|
32
|
+
let resolveIn = (~dir: string): string =>
|
|
33
|
+
switch NodeProcess.env->Dict.get(ReventlessCore.Secrets_Node_Backend.fingerprintKeyVariable) {
|
|
34
|
+
| Some(key) if key != "" => key
|
|
35
|
+
| _ =>
|
|
36
|
+
if NodeFs.existsSync(dir) {
|
|
37
|
+
let path = NodePath.join([dir, fileName])
|
|
38
|
+
switch readAt(path) {
|
|
39
|
+
| Some(persisted) => persisted
|
|
40
|
+
| None =>
|
|
41
|
+
let minted = mint()
|
|
42
|
+
switch NodeFs.writeFileSync(path, minted) {
|
|
43
|
+
| () => ()
|
|
44
|
+
| exception _ => ()
|
|
45
|
+
}
|
|
46
|
+
minted
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
mint()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let resolved: ref<option<string>> = ref(None)
|
|
54
|
+
|
|
55
|
+
/** The key, resolved on first use and kept for the process. */
|
|
56
|
+
let get = (): option<string> =>
|
|
57
|
+
switch resolved.contents {
|
|
58
|
+
| Some(key) => Some(key)
|
|
59
|
+
| None =>
|
|
60
|
+
let key = resolveIn(~dir=NodePath.join([NodeProcess.cwd(), ".reventless"]))
|
|
61
|
+
resolved := Some(key)
|
|
62
|
+
Some(key)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Forget the resolved key, for a test that pins another. */
|
|
66
|
+
let reset = () => resolved := None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Nodecrypto from "node:crypto";
|
|
6
|
+
import * as Secrets_Node_Backend$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Secrets/Secrets_Node_Backend.res.mjs";
|
|
7
|
+
|
|
8
|
+
let fileName = "fingerprint-key";
|
|
9
|
+
|
|
10
|
+
function mint() {
|
|
11
|
+
return Nodecrypto.randomBytes(32).toString("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readAt(path) {
|
|
15
|
+
let contents;
|
|
16
|
+
try {
|
|
17
|
+
contents = Nodefs.readFileSync(path, "utf8");
|
|
18
|
+
} catch (exn) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (contents.trim().length >= Secrets_Node_Backend$ReventlessCore.minimumFingerprintKeyLength) {
|
|
22
|
+
return contents.trim();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveIn(dir) {
|
|
27
|
+
let key = process.env[Secrets_Node_Backend$ReventlessCore.fingerprintKeyVariable];
|
|
28
|
+
if (key !== undefined && key !== "") {
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
if (!Nodefs.existsSync(dir)) {
|
|
32
|
+
return Nodecrypto.randomBytes(32).toString("hex");
|
|
33
|
+
}
|
|
34
|
+
let path = Nodepath.join(dir, fileName);
|
|
35
|
+
let persisted = readAt(path);
|
|
36
|
+
if (persisted !== undefined) {
|
|
37
|
+
return persisted;
|
|
38
|
+
}
|
|
39
|
+
let minted = Nodecrypto.randomBytes(32).toString("hex");
|
|
40
|
+
try {
|
|
41
|
+
Nodefs.writeFileSync(path, minted, "utf8");
|
|
42
|
+
} catch (exn) {
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
return minted;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let resolved = {
|
|
49
|
+
contents: undefined
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function get() {
|
|
53
|
+
let key = resolved.contents;
|
|
54
|
+
if (key !== undefined) {
|
|
55
|
+
return key;
|
|
56
|
+
}
|
|
57
|
+
let key$1 = resolveIn(Nodepath.join(process.cwd(), ".reventless"));
|
|
58
|
+
resolved.contents = key$1;
|
|
59
|
+
return key$1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function reset() {
|
|
63
|
+
resolved.contents = undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
fileName,
|
|
68
|
+
mint,
|
|
69
|
+
readAt,
|
|
70
|
+
resolveIn,
|
|
71
|
+
resolved,
|
|
72
|
+
get,
|
|
73
|
+
reset,
|
|
74
|
+
}
|
|
75
|
+
/* node:fs Not a pure module */
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// — dev parity is about the client seeing the same contract, not reproducing AWS's
|
|
8
8
|
// guarantees. See [docs/plans/done/upload-release-path.md] § Step 3.
|
|
9
9
|
|
|
10
|
-
// Mint a same-origin `/{prefix}/{uuid}
|
|
11
|
-
// stored value, mirroring the AWS presign ticket's shape.
|
|
10
|
+
// Mint a same-origin `/{prefix}/{uuid}.{ext}` ref: both the PUT target and the
|
|
11
|
+
// stored value, mirroring the AWS presign ticket's shape. The name is opaque and the
|
|
12
|
+
// extension comes from the declared content type, never from the file name.
|
|
12
13
|
//
|
|
13
14
|
// The prefix is the declaring store's (`{plugin}/{store}`), so an object carries
|
|
14
15
|
// the plugin that owns it in its own key — what lets a scoped reset attribute it,
|
|
@@ -16,12 +17,12 @@
|
|
|
16
17
|
// back to the shared `uploads/` space rather than being refused: local plugins
|
|
17
18
|
// need not declare a store to upload during development, and refusing here would
|
|
18
19
|
// make dev stricter than deploy about something dev cannot check.
|
|
19
|
-
let mintRef = (~store: string, ~
|
|
20
|
+
let mintRef = (~store: string, ~contentType: option<string>=?): string => {
|
|
20
21
|
let prefix =
|
|
21
22
|
LocalObjectStore.storePrefix(~qualified=store)->Option.getOr(
|
|
22
23
|
LocalObjectStore.defaultUploadPrefix,
|
|
23
24
|
)
|
|
24
|
-
`/${prefix}/${NodeCrypto.randomUUID()}
|
|
25
|
+
`/${prefix}/${Reventless.StorageRef.objectName(~uuid=NodeCrypto.randomUUID(), ~contentType)}`
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// Release a ref: delete iff it sits under a served prefix; idempotent (deleting an
|
|
@@ -40,10 +41,9 @@ let register = (server: ReventlessGraphqlServer.GraphQL_ServerInstance.t): unit
|
|
|
40
41
|
let resolvers = Dict.make()
|
|
41
42
|
resolvers->Dict.set("Upload_Presign", async (_root, args, _ctx): JSON.t => {
|
|
42
43
|
let obj = args->JSON.Decode.object->Option.getOr(Dict.make())
|
|
43
|
-
let
|
|
44
|
-
obj->Dict.get("fileName")->Option.flatMap(JSON.Decode.string)->Option.getOr("upload")
|
|
44
|
+
let contentType = obj->Dict.get("contentType")->Option.flatMap(JSON.Decode.string)
|
|
45
45
|
let store = obj->Dict.get("store")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
46
|
-
let ref = mintRef(~store, ~
|
|
46
|
+
let ref = mintRef(~store, ~contentType?)
|
|
47
47
|
Dict.fromArray([
|
|
48
48
|
("uploadUrl", JSON.Encode.string(ref)),
|
|
49
49
|
("storageRef", JSON.Encode.string(ref)),
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Nodecrypto from "node:crypto";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
6
7
|
import * as LocalObjectStore$ReventlessLocal from "./ObjectStore/LocalObjectStore.res.mjs";
|
|
7
8
|
import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
|
|
8
9
|
|
|
9
|
-
function mintRef(store,
|
|
10
|
+
function mintRef(store, contentType) {
|
|
10
11
|
let prefix = Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.storePrefix(store), LocalObjectStore$ReventlessLocal.defaultUploadPrefix);
|
|
11
|
-
return `/` + prefix + `/` + Nodecrypto.randomUUID()
|
|
12
|
+
return `/` + prefix + `/` + StorageRef$Reventless.objectName(Nodecrypto.randomUUID(), contentType);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function release(storageRef) {
|
|
@@ -31,9 +32,9 @@ function register(server) {
|
|
|
31
32
|
let resolvers = {};
|
|
32
33
|
resolvers["Upload_Presign"] = async (_root, args, _ctx) => {
|
|
33
34
|
let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(args), {});
|
|
34
|
-
let
|
|
35
|
+
let contentType = Stdlib_Option.flatMap(obj["contentType"], Stdlib_JSON.Decode.string);
|
|
35
36
|
let store = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["store"], Stdlib_JSON.Decode.string), "");
|
|
36
|
-
let ref = mintRef(store,
|
|
37
|
+
let ref = mintRef(store, contentType);
|
|
37
38
|
return Object.fromEntries([
|
|
38
39
|
[
|
|
39
40
|
"uploadUrl",
|
|
@@ -19,6 +19,12 @@ describe("EmitCapabilities.parseArgs", () => {
|
|
|
19
19
|
)
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
+
describe("EmitCapabilities.manifestText", () => {
|
|
23
|
+
testSync("a plugin with no parts yet declares nothing", () =>
|
|
24
|
+
expect(EmitCapabilities.manifestText(None))->toBe(`{\n "capabilities": []\n}\n`)
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
22
28
|
describe("emit-capabilities on the command line", () => {
|
|
23
29
|
module CliArgs = Reventless.CliArgs
|
|
24
30
|
let usage = EmitCapabilities.usage->String.trim
|
|
@@ -30,6 +30,12 @@ globalThis.describe("EmitCapabilities.parseArgs", () => {
|
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
globalThis.describe("EmitCapabilities.manifestText", () => {
|
|
34
|
+
globalThis.test("a plugin with no parts yet declares nothing", () => {
|
|
35
|
+
globalThis.expect(EmitCapabilities$ReventlessLocal.manifestText(undefined)).toBe(`{\n "capabilities": []\n}\n`);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
33
39
|
globalThis.describe("emit-capabilities on the command line", () => {
|
|
34
40
|
let usage = EmitCapabilities$ReventlessLocal.usage.trim();
|
|
35
41
|
globalThis.test("--help anywhere prints the usage to stdout and exits 0", async () => {
|
|
@@ -12,7 +12,7 @@ import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventl
|
|
|
12
12
|
|
|
13
13
|
TestRunner$ReventlessLocal.setup();
|
|
14
14
|
|
|
15
|
-
let meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined
|
|
15
|
+
let meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
16
16
|
|
|
17
17
|
function envelopeOf(event) {
|
|
18
18
|
return Message$ReventlessCore.composeEventJson$p("Catalog", meta, "2024-01-01T00:00:00Z", Util_Sury$Reventless.toJson(event, PluginSpec$ReventlessCore.eventSchema));
|
|
@@ -60,7 +60,7 @@ globalThis.describe("Platform.decodePluginEventEnvelope", () => {
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
globalThis.describe("Platform.decodeUiFragmentRegistryEventEnvelope", () => {
|
|
63
|
-
let dcbMeta = Message$ReventlessCore.generateMeta(ComponentType$ReventlessCore.name(Platform_Admin_Structure$ReventlessCore.pluginId, "DcbEventLog"), undefined, undefined, undefined, undefined, undefined, undefined
|
|
63
|
+
let dcbMeta = Message$ReventlessCore.generateMeta(ComponentType$ReventlessCore.name(Platform_Admin_Structure$ReventlessCore.pluginId, "DcbEventLog"), undefined, undefined, undefined, undefined, undefined, undefined);
|
|
64
64
|
let dcbEnvelopeOf = event => {
|
|
65
65
|
let json = Util_Sury$Reventless.toJson(event, UiFragmentRegistry$ReventlessCore.eventSchema);
|
|
66
66
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
@@ -24,7 +24,7 @@ function stored(eventType, tags, data) {
|
|
|
24
24
|
eventType: eventType,
|
|
25
25
|
data: data,
|
|
26
26
|
tags: tags,
|
|
27
|
-
meta: Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
27
|
+
meta: Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined)
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -23,7 +23,7 @@ function makeEvent(eventType, data, tagsOpt) {
|
|
|
23
23
|
eventType: eventType,
|
|
24
24
|
data: data,
|
|
25
25
|
tags: tags,
|
|
26
|
-
meta: Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
26
|
+
meta: Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined)
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -200,13 +200,16 @@ describe("declared store prefixes", () => {
|
|
|
200
200
|
~qualified="Catalog.productImages",
|
|
201
201
|
~prefix=LocalObjectStore.localPrefixFor(~qualified="Catalog.productImages"),
|
|
202
202
|
)
|
|
203
|
-
let ref = LocalUploadResolvers.mintRef(
|
|
203
|
+
let ref = LocalUploadResolvers.mintRef(
|
|
204
|
+
~store="Catalog.productImages",
|
|
205
|
+
~contentType="image/svg+xml",
|
|
206
|
+
)
|
|
204
207
|
expect(ref->String.startsWith("/uploads/Catalog/productImages/"))->toEqual(true)
|
|
205
|
-
expect(ref->String.endsWith("
|
|
208
|
+
expect(ref->String.endsWith(".svg"))->toEqual(true)
|
|
206
209
|
})
|
|
207
210
|
|
|
208
211
|
testSync("an undeclared store falls back to the shared uploads prefix", () => {
|
|
209
|
-
let ref = LocalUploadResolvers.mintRef(~store="Nobody.knows", ~
|
|
212
|
+
let ref = LocalUploadResolvers.mintRef(~store="Nobody.knows", ~contentType="image/svg+xml")
|
|
210
213
|
expect(ref->String.startsWith("/uploads/"))->toEqual(true)
|
|
211
214
|
})
|
|
212
215
|
|
|
@@ -128,12 +128,12 @@ globalThis.describe("declared store prefixes", () => {
|
|
|
128
128
|
globalThis.afterEach(() => LocalObjectStore$ReventlessLocal.reset());
|
|
129
129
|
globalThis.test("a declared store mints under uploads/{plugin}/{store}", () => {
|
|
130
130
|
LocalObjectStore$ReventlessLocal.registerStore("Catalog.productImages", LocalObjectStore$ReventlessLocal.localPrefixFor("Catalog.productImages"));
|
|
131
|
-
let ref = LocalUploadResolvers$ReventlessLocal.mintRef("Catalog.productImages", "
|
|
131
|
+
let ref = LocalUploadResolvers$ReventlessLocal.mintRef("Catalog.productImages", "image/svg+xml");
|
|
132
132
|
globalThis.expect(ref.startsWith("/uploads/Catalog/productImages/")).toEqual(true);
|
|
133
|
-
globalThis.expect(ref.endsWith("
|
|
133
|
+
globalThis.expect(ref.endsWith(".svg")).toEqual(true);
|
|
134
134
|
});
|
|
135
135
|
globalThis.test("an undeclared store falls back to the shared uploads prefix", () => {
|
|
136
|
-
let ref = LocalUploadResolvers$ReventlessLocal.mintRef("Nobody.knows", "
|
|
136
|
+
let ref = LocalUploadResolvers$ReventlessLocal.mintRef("Nobody.knows", "image/svg+xml");
|
|
137
137
|
globalThis.expect(ref.startsWith("/uploads/")).toEqual(true);
|
|
138
138
|
});
|
|
139
139
|
globalThis.test("a store's objects stay under the served uploads prefix", () => {
|