@reventlessdev/reventless-local 3.0.0-alpha.302 → 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.
Files changed (26) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/package.json +9 -9
  3. package/src/Platform.res +1 -1
  4. package/src/Platform.res.mjs +4 -6
  5. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +2 -2
  6. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +0 -2
  7. package/src/adapter/LocalCapabilities.res +1 -1
  8. package/src/adapter/LocalCapabilities.res.mjs +2 -1
  9. package/src/adapter/LocalFingerprintKey.res +66 -0
  10. package/src/adapter/LocalFingerprintKey.res.mjs +75 -0
  11. package/src/adapter/LocalUploadResolvers.res +7 -7
  12. package/src/adapter/LocalUploadResolvers.res.mjs +5 -4
  13. package/src/adapter/Scheduler/LocalScheduledPublisher.res +0 -1
  14. package/src/adapter/Scheduler/LocalScheduledPublisher.res.mjs +0 -1
  15. package/tests/PluginEventDecodeTest.res.mjs +2 -2
  16. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +1 -1
  17. package/tests/adapter/DcbEventLogStorageTest.res.mjs +1 -1
  18. package/tests/adapter/ObjectStorePersistenceTest.res +6 -3
  19. package/tests/adapter/ObjectStorePersistenceTest.res.mjs +3 -3
  20. package/tests/adapter/ServedBucketHttpTest.res +3 -1
  21. package/tests/adapter/ServedBucketHttpTest.res.mjs +2 -1
  22. package/tests/components/dcb/DcbCrossPartitionFixtures.res.mjs +1 -1
  23. package/tests/components/dcb/DcbEventLogAppendStreamTest.res.mjs +1 -1
  24. package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res.mjs +1 -1
  25. package/tests/components/stateviewslice/StateViewSliceFixtures.res.mjs +1 -1
  26. package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
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
+
6
30
  # 3.0.0-alpha.302 (2026-10-29)
7
31
 
8
32
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-local",
3
- "version": "3.0.0-alpha.302",
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",
38
+ "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
39
+ "@reventlessdev/rescript-effect": "1.0.0-alpha.1",
39
40
  "@reventlessdev/rescript-jest": "1.0.0-alpha.13",
40
41
  "@reventlessdev/rescript-node": "2.0.0-alpha.23",
41
- "@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
42
+ "@reventlessdev/reventless-core": "3.0.0-alpha.312",
42
43
  "@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
43
- "@reventlessdev/reventless-core": "3.0.0-alpha.311",
44
- "@reventlessdev/reventless-gwt": "1.0.0-alpha.259",
45
- "@reventlessdev/reventless-infra": "3.0.0-alpha.208",
46
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.175",
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-spec": "3.0.0-alpha.179",
49
- "@reventlessdev/reventless-graphql-server": "1.0.0-alpha.159"
49
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.180"
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: {ip: [], user: identity.userId, info: `mcp/tools/${toolName}`},
354
+ meta: {user: identity.userId, info: `mcp/tools/${toolName}`},
355
355
  identity,
356
356
  }
357
357
  let outcome = await generateCommand(payload)->Effect.runPromise
@@ -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, 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, 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, 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, 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: {ip: [], user: identity.userId, info: `Mutation.${field}`},
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: {ip: [], user: identity.userId, info: `Mutation.${fieldName}`},
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.provider,
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.provider,
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}/{fileName}` ref: both the PUT target and the
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, ~fileName: string): 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()}/${fileName}`
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 fileName =
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, ~fileName)
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, fileName) {
10
+ function mintRef(store, contentType) {
10
11
  let prefix = Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.storePrefix(store), LocalObjectStore$ReventlessLocal.defaultUploadPrefix);
11
- return `/` + prefix + `/` + Nodecrypto.randomUUID() + `/` + fileName;
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 fileName = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["fileName"], Stdlib_JSON.Decode.string), "upload");
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, fileName);
37
+ let ref = mintRef(store, contentType);
37
38
  return Object.fromEntries([
38
39
  [
39
40
  "uploadUrl",
@@ -14,7 +14,6 @@ module Make = (Bus: LocalBus.T) => {
14
14
  let scheduleMeta: ReventlessCore.Message.meta = {
15
15
  service: "Scheduler",
16
16
  time: "",
17
- ip: "",
18
17
  user: "Scheduler",
19
18
  msgId: "",
20
19
  correlationId: "",
@@ -9,7 +9,6 @@ function Make(Bus) {
9
9
  let scheduleMeta = {
10
10
  service: "Scheduler",
11
11
  time: "",
12
- ip: "",
13
12
  user: "Scheduler",
14
13
  msgId: "",
15
14
  correlationId: ""
@@ -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, 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, 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, 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, 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(~store="Catalog.productImages", ~fileName="logo.svg")
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("/logo.svg"))->toEqual(true)
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", ~fileName="logo.svg")
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", "logo.svg");
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("/logo.svg")).toEqual(true);
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", "logo.svg");
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
- expect(storageRef->String.endsWith("/logo.svg"))->toEqual(true)
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("/logo.svg")).toEqual(true);
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, 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, 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, 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, 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, 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],