@reventlessdev/reventless-local 3.0.0-alpha.302 → 3.0.0-alpha.304
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 +32 -0
- package/package.json +10 -10
- package/src/Platform.res +1 -1
- package/src/Platform.res.mjs +4 -6
- 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/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/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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
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.304 (2026-10-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.303 (2026-10-29)
|
|
15
|
+
|
|
16
|
+
* feat(registration)!: the ledger tags an address by its keyed fingerprint ([ccafddf](https://github.com/ReventlessDev/reventless-core/commit/ccafddfc7eb8039687afc62a483ce34b8e7081dd))
|
|
17
|
+
* feat(core)!: the caller's IP address leaves the messages a request causes ([aa9e8c7](https://github.com/ReventlessDev/reventless-core/commit/aa9e8c79c0360528a5cfeda2bb88d1da52f28674))
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **upload:** an upload's path carries nothing a person typed ([456c88c](https://github.com/ReventlessDev/reventless-core/commit/456c88c4ccee6a50943e3a71011a2e78aef0d770))
|
|
21
|
+
|
|
22
|
+
### BREAKING CHANGES
|
|
23
|
+
|
|
24
|
+
* `Secrets.t` has a third field, `fingerprint`; a hand-built provider
|
|
25
|
+
adds it. `Register` and `Invite` take `address`, and their `email` is the
|
|
26
|
+
fingerprint from `ContactAddressId.fingerprint`, not `makeFromString(address)`.
|
|
27
|
+
|
|
28
|
+
Step PD5 of docs/plans/a-personal-field-is-marked-redacted-and-encrypted-at-the-seam.md.
|
|
29
|
+
Claude-Session: https://claude.ai/code/session_01HiQfdxVEsHwLHRoHSUcus1
|
|
30
|
+
* `Message.generateMeta` no longer takes `~ip`. Drop the argument; a
|
|
31
|
+
message that must carry the address declares it as a field of its own payload.
|
|
32
|
+
|
|
33
|
+
Step PD3 of docs/plans/a-personal-field-is-marked-redacted-and-encrypted-at-the-seam.md.
|
|
34
|
+
Claude-Session: https://claude.ai/code/session_01HiQfdxVEsHwLHRoHSUcus1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
6
38
|
# 3.0.0-alpha.302 (2026-10-29)
|
|
7
39
|
|
|
8
40
|
### Bug Fixes
|
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.304",
|
|
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
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
41
38
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
39
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
40
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
42
41
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/
|
|
46
|
-
"@reventlessdev/reventless-
|
|
42
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.161",
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.313",
|
|
44
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.261",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.210",
|
|
47
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.177",
|
|
47
48
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.38",
|
|
48
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
49
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.159"
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.181"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
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
|
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,
|
|
@@ -2050,7 +2049,6 @@ function Make($star) {
|
|
|
2050
2049
|
return `{"error":"no handler for ` + toolName + `"}`;
|
|
2051
2050
|
}
|
|
2052
2051
|
let payload_meta = {
|
|
2053
|
-
ip: [],
|
|
2054
2052
|
user: identity.userId,
|
|
2055
2053
|
info: `mcp/tools/` + toolName
|
|
2056
2054
|
};
|
|
@@ -2731,7 +2729,7 @@ function Make($star) {
|
|
|
2731
2729
|
let pluginCmdTopicKey = PluginSpec$ReventlessCore.name + "AggrCmdTopic";
|
|
2732
2730
|
let pluginEventTopicKey = PluginSpec$ReventlessCore.name + "AggrEventTopic";
|
|
2733
2731
|
let dispatchPluginCommand = (pluginName, command) => {
|
|
2734
|
-
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);
|
|
2735
2733
|
let cmdJson_commandJson = Util_Sury$Reventless.toJson(command, PluginSpec$ReventlessCore.commandSchema);
|
|
2736
2734
|
let cmdJson = {
|
|
2737
2735
|
id: pluginName,
|
|
@@ -2742,7 +2740,7 @@ function Make($star) {
|
|
|
2742
2740
|
};
|
|
2743
2741
|
let adminDcbCmdTopicKey = ComponentType$ReventlessCore.name(Platform_Admin_Structure$ReventlessCore.pluginId + "Dcb", "CommandTopic");
|
|
2744
2742
|
let dispatchUiFragmentCommand = (pluginName, command) => {
|
|
2745
|
-
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);
|
|
2746
2744
|
let cmdJson_commandJson = Util_Sury$Reventless.toJson(command, UiFragmentRegistry$ReventlessCore.commandSchema);
|
|
2747
2745
|
let cmdJson = {
|
|
2748
2746
|
id: pluginName,
|
|
@@ -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",
|
|
@@ -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", () => {
|
|
@@ -175,7 +175,9 @@ testPromise("Upload_Presign returns matching uploadUrl + storageRef under the pr
|
|
|
175
175
|
// Same same-origin ref serves as both the PUT target and the stored value.
|
|
176
176
|
expect(uploadUrl)->toEqual(storageRef)
|
|
177
177
|
expect(storageRef->String.startsWith("/uploads/"))->toEqual(true)
|
|
178
|
-
|
|
178
|
+
// The name is opaque; the extension is the declared content type's.
|
|
179
|
+
expect(storageRef->String.endsWith(".svg"))->toEqual(true)
|
|
180
|
+
expect(storageRef->String.includes("logo"))->toEqual(false)
|
|
179
181
|
})
|
|
180
182
|
|
|
181
183
|
testPromise("presign → PUT → GET → release → GET(404) is the full loop", async () => {
|
|
@@ -158,7 +158,8 @@ globalThis.test("Upload_Presign returns matching uploadUrl + storageRef under th
|
|
|
158
158
|
let storageRef = getString(r, "storageRef");
|
|
159
159
|
globalThis.expect(uploadUrl).toEqual(storageRef);
|
|
160
160
|
globalThis.expect(storageRef.startsWith("/uploads/")).toEqual(true);
|
|
161
|
-
globalThis.expect(storageRef.endsWith("
|
|
161
|
+
globalThis.expect(storageRef.endsWith(".svg")).toEqual(true);
|
|
162
|
+
globalThis.expect(storageRef.includes("logo")).toEqual(false);
|
|
162
163
|
});
|
|
163
164
|
|
|
164
165
|
globalThis.test("presign → PUT → GET → release → GET(404) is the full loop", async () => {
|
|
@@ -248,7 +248,7 @@ function encodeEvent(event) {
|
|
|
248
248
|
let json = Util_Sury$Reventless.toJson(event, eventSchema);
|
|
249
249
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
250
250
|
let tags = DcbTag$Reventless.extractTags(eventSchema, event);
|
|
251
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
251
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
252
252
|
return {
|
|
253
253
|
eventType: match[0],
|
|
254
254
|
data: match[1],
|
|
@@ -13,7 +13,7 @@ function encodeEvent(event) {
|
|
|
13
13
|
let json = Util_Sury$Reventless.toJson(event, DcbFixtures$ReventlessLocal.ItemEventLog.eventSchema);
|
|
14
14
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
15
15
|
let tags = DcbTag$Reventless.extractTags(DcbFixtures$ReventlessLocal.ItemEventLog.eventSchema, event);
|
|
16
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
16
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
17
17
|
return {
|
|
18
18
|
eventType: match[0],
|
|
19
19
|
data: match[1],
|
|
@@ -155,7 +155,7 @@ function encodeEvent(event) {
|
|
|
155
155
|
let json = Util_Sury$Reventless.toJson(event, eventSchema);
|
|
156
156
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
157
157
|
let tags = DcbTag$Reventless.extractTags(eventSchema, event);
|
|
158
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
158
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
159
159
|
return {
|
|
160
160
|
eventType: match[0],
|
|
161
161
|
data: match[1],
|
|
@@ -158,7 +158,7 @@ function encodeEvent(event) {
|
|
|
158
158
|
let json = Util_Sury$Reventless.toJson(event, eventSchema);
|
|
159
159
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
160
160
|
let tags = DcbTag$Reventless.extractTags(eventSchema, event);
|
|
161
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
161
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
162
162
|
return {
|
|
163
163
|
eventType: match[0],
|
|
164
164
|
data: match[1],
|
|
@@ -153,7 +153,7 @@ function encodeEvent(event) {
|
|
|
153
153
|
let json = Util_Sury$Reventless.toJson(event, eventSchema);
|
|
154
154
|
let match = Message$ReventlessCore.splitMessage(json);
|
|
155
155
|
let tags = DcbTag$Reventless.extractTags(eventSchema, event);
|
|
156
|
-
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined
|
|
156
|
+
let meta = Message$ReventlessCore.generateMeta("test", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
157
157
|
return {
|
|
158
158
|
eventType: match[0],
|
|
159
159
|
data: match[1],
|