@reventlessdev/reventless-infra 3.0.0-alpha.152 → 3.0.0-alpha.153

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 CHANGED
@@ -3,6 +3,19 @@
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.153 (2026-09-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** [@no](https://github.com/no)Api was lost when a command variant was spliced into another type ([00403fb](https://github.com/ReventlessDev/reventless-core/commit/00403fb4b8873a68a69558829e598339a5012833))
11
+ * **capabilities:** a plugin's geocoding need was declared nowhere and failed silently ([67917dd](https://github.com/ReventlessDev/reventless-core/commit/67917dd504b43fa78b7c6a51644c9eae656b7f6b))
12
+ ### Features
13
+
14
+ * **capabilities:** a plugin can send a message without naming a provider ([b001a1e](https://github.com/ReventlessDev/reventless-core/commit/b001a1e9361a0f4d0affe228cb4c08c38a5a995e))
15
+ * **spec:** a command declares its lifecycle edge as a value ([40eee9f](https://github.com/ReventlessDev/reventless-core/commit/40eee9f7723dc05e418be680528f01967d074da4))
16
+ * **spec:** a graft leaves a trace the deployed plugin can read ([c08ff6c](https://github.com/ReventlessDev/reventless-core/commit/c08ff6c0f6177d58603e7ae1e5cec392d9bac16a))
17
+
18
+
6
19
  # 3.0.0-alpha.152 (2026-08-27)
7
20
 
8
21
  * feat(spec)!: reflect the command direction across a port, both halves ([f2fe258](https://github.com/ReventlessDev/reventless-core/commit/f2fe258d195b74f4a61488edee305665341020ea))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-infra",
3
- "version": "3.0.0-alpha.152",
3
+ "version": "3.0.0-alpha.153",
4
4
  "description": "Infrastructure types for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -17,9 +17,9 @@
17
17
  "sury-ppx": "11.0.0-rc.2",
18
18
  "uuid": "^13.0.0",
19
19
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
20
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
20
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.125",
21
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.124"
22
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.0",
@@ -7,6 +7,18 @@ let noApiId: S.Metadata.Id.t<bool> = S.Metadata.Id.make(~namespace="api", ~name=
7
7
  let noApiVariantsId: S.Metadata.Id.t<Set.t<string>> =
8
8
  S.Metadata.Id.make(~namespace="api", ~name="noApiVariants")
9
9
 
10
+ /** The same exclusion, carried by the excluded *member* rather than by the union
11
+ it sits in — so it survives a variant spread.
12
+
13
+ A spread splices members: the generated code concatenates the source union's
14
+ `anyOf` into the target's. Metadata on the source *union* is not part of that,
15
+ so an exclusion recorded only there vanishes on the way into the host, and the
16
+ host publishes a command its author marked internal. A mark on the member
17
+ travels, because the member is what is spliced — the host's union holds the
18
+ same schema object. */
19
+ let noApiVariantId: S.Metadata.Id.t<bool> =
20
+ S.Metadata.Id.make(~namespace="api", ~name="noApiVariant")
21
+
10
22
  /** Internal sury metadata ID storing per-variant allowed-state lists for AutoUI command filtering.
11
23
  Maps variant name → set of status values under which the command is meaningful. */
12
24
  let allowedStatesId: S.Metadata.Id.t<dict<array<string>>> =
@@ -21,9 +33,60 @@ let targetStateId: S.Metadata.Id.t<dict<string>> =
21
33
  let markNoApi = (schema: S.t<'a>): S.t<'a> =>
22
34
  schema->S.Metadata.set(~id=noApiId, true)
23
35
 
24
- /** PPX helper: attaches variant-level exclusions to a command schema. Called by generated code. */
25
- let markNoApiVariants = (schema: S.t<'a>, variants: array<string>): S.t<'a> =>
26
- schema->S.Metadata.set(~id=noApiVariantsId, Set.fromArray(variants))
36
+ /** The constructor a union member stands for: the `TAG` literal of a
37
+ payload-bearing variant, or the bare literal a payload-less one compiles to. */
38
+ let variantNameOf = (member: S.t<unknown>): option<string> =>
39
+ switch member {
40
+ | Object({properties}) =>
41
+ properties
42
+ ->Dict.get("TAG")
43
+ ->Option.flatMap(tag =>
44
+ switch tag {
45
+ | String({const: ?Some(name)}) => Some(name)
46
+ | _ => None
47
+ }
48
+ )
49
+ | String({const: ?Some(name)}) => Some(name)
50
+ | _ => None
51
+ }
52
+
53
+ /**
54
+ PPX helper: attaches variant-level exclusions to a command schema. Called by
55
+ generated code.
56
+
57
+ Recorded twice, on purpose. The set on the union is what every reader has always
58
+ consulted; the per-member mark is what survives being spliced into another type.
59
+ Writing only the second would break any reader holding a union whose members it
60
+ did not build, and writing only the first is the defect this exists to close.
61
+
62
+ The union is rebuilt from the marked members because sury metadata is immutable —
63
+ setting it returns a new schema, so the members the original union holds would
64
+ otherwise still be the unmarked ones. Sound here because the PPX calls this
65
+ *first*, directly on the freshly built union, before any other metadata is
66
+ attached: there is nothing on the parent yet to lose. A schema that is not a
67
+ union, or that names no excluded variant, is left structurally untouched.
68
+ */
69
+ let markNoApiVariants = (schema: S.t<'a>, variants: array<string>): S.t<'a> => {
70
+ let excluded = Set.fromArray(variants)
71
+ let marked = switch (variants, schema->S.castToUnknown) {
72
+ | ([], _) => schema
73
+ | (_, AnyOf({anyOf})) =>
74
+ anyOf
75
+ ->Array.map(member =>
76
+ switch variantNameOf(member) {
77
+ | Some(name) if excluded->Set.has(name) => member->S.Metadata.set(~id=noApiVariantId, true)
78
+ | _ => member
79
+ }
80
+ )
81
+ ->S.union
82
+ // The rebuilt union describes the same values as the one handed in — same
83
+ // members, same order, only metadata added. `S.union` cannot express that in
84
+ // the type, so the equivalence is carried here rather than by the compiler.
85
+ ->(Obj.magic: S.t<unknown> => S.t<'a>)
86
+ | (_, _) => schema
87
+ }
88
+ marked->S.Metadata.set(~id=noApiVariantsId, excluded)
89
+ }
27
90
 
28
91
  /** PPX helper: attaches per-variant allowedStates to a command schema. Called by generated code
29
92
  emitted from the from-set of an @transition attribute. Each entry is (variantName, allowedStateNames). */
@@ -1,11 +1,14 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
5
 
5
6
  let noApiId = Sury.$Metadata_Id_make("api", "noApi");
6
7
 
7
8
  let noApiVariantsId = Sury.$Metadata_Id_make("api", "noApiVariants");
8
9
 
10
+ let noApiVariantId = Sury.$Metadata_Id_make("api", "noApiVariant");
11
+
9
12
  let allowedStatesId = Sury.$Metadata_Id_make("api", "allowedStates");
10
13
 
11
14
  let targetStateId = Sury.$Metadata_Id_make("api", "targetState");
@@ -14,8 +17,41 @@ function markNoApi(schema) {
14
17
  return Sury.$Metadata_set(schema, noApiId, true);
15
18
  }
16
19
 
20
+ function variantNameOf(member) {
21
+ switch (member.type) {
22
+ case "string" :
23
+ let name = member.const;
24
+ if (name !== undefined) {
25
+ return name;
26
+ } else {
27
+ return;
28
+ }
29
+ case "object" :
30
+ return Stdlib_Option.flatMap(member.properties["TAG"], tag => {
31
+ if (tag.type !== "string") {
32
+ return;
33
+ }
34
+ let name = tag.const;
35
+ if (name !== undefined) {
36
+ return name;
37
+ }
38
+ });
39
+ default:
40
+ return;
41
+ }
42
+ }
43
+
17
44
  function markNoApiVariants(schema, variants) {
18
- return Sury.$Metadata_set(schema, noApiVariantsId, new Set(variants));
45
+ let excluded = new Set(variants);
46
+ let marked = variants.length !== 0 && schema.type === "anyOf" ? Sury.union(schema.anyOf.map(member => {
47
+ let name = variantNameOf(member);
48
+ if (name !== undefined && excluded.has(name)) {
49
+ return Sury.$Metadata_set(member, noApiVariantId, true);
50
+ } else {
51
+ return member;
52
+ }
53
+ })) : schema;
54
+ return Sury.$Metadata_set(marked, noApiVariantsId, excluded);
19
55
  }
20
56
 
21
57
  function markAllowedStates(schema, entries) {
@@ -29,9 +65,11 @@ function markTargetState(schema, entries) {
29
65
  export {
30
66
  noApiId,
31
67
  noApiVariantsId,
68
+ noApiVariantId,
32
69
  allowedStatesId,
33
70
  targetStateId,
34
71
  markNoApi,
72
+ variantNameOf,
35
73
  markNoApiVariants,
36
74
  markAllowedStates,
37
75
  markTargetState,
@@ -147,6 +147,9 @@ module NoDelegate = {
147
147
  let moduleUrl: string = %raw(`import.meta.url`)
148
148
  let commandAuthorization = (_: command): Reventless.Authorization.permission =>
149
149
  AllowAuthenticated
150
+ type lifecycleState = unit
151
+ let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
152
+ let traits: array<Reventless.Trait.t> = []
150
153
  }
151
154
 
152
155
 
@@ -32,6 +32,12 @@ function commandAuthorization() {
32
32
  return "AllowAuthenticated";
33
33
  }
34
34
 
35
+ function commandTransition() {
36
+ return "Unrestricted";
37
+ }
38
+
39
+ let traits = [];
40
+
35
41
  let NoDelegate = {
36
42
  name: "NoDelegate",
37
43
  Id: Id,
@@ -39,7 +45,9 @@ let NoDelegate = {
39
45
  errorSchema: errorSchema,
40
46
  commandSchema: commandSchema,
41
47
  moduleUrl: moduleUrl,
42
- commandAuthorization: commandAuthorization
48
+ commandAuthorization: commandAuthorization,
49
+ commandTransition: commandTransition,
50
+ traits: traits
43
51
  };
44
52
 
45
53
  function Make(MappingImpl) {
@@ -109,6 +109,7 @@ store, and reaching another plugin's store is the qualified form.
109
109
  type capability =
110
110
  | ObjectStore({plugin: string, store: string})
111
111
  | Geocoding
112
+ | Messaging
112
113
 
113
114
  // A provisioned geocoding place index, as returned by the framework's geocoding
114
115
  // capability helper. A record rather than a bare name so the handle can grow
@@ -181,6 +182,20 @@ type bakedManifest = {
181
182
 
182
183
  type geocoderIndex = {indexName: Pulumi.Input.t<string>}
183
184
 
185
+ /**
186
+ A provisioned messaging sender, as returned by the framework's messaging
187
+ capability helper.
188
+
189
+ `emailSender` is the verified address messages are sent *from* — a provider-side
190
+ identity, not something an application picks per message, which is why it is a
191
+ deploy-time handle rather than a field on `Messaging.message`. Absent when the
192
+ deployment provisions no email channel.
193
+
194
+ The channel set is derived from which senders are present rather than declared
195
+ beside them, so a handle carrying no sender at all cannot claim a channel.
196
+ */
197
+ type messagingSender = {emailSender?: Pulumi.Input.t<string>}
198
+
184
199
  // Options for the shell's map view mode. Written to config.json flat, beside
185
200
  // `viewModes`, because that is where the shell reads them; carried on the arm
186
201
  // here so a style set with the map off is unrepresentable.
@@ -426,6 +441,12 @@ module type T = {
426
441
  // on the platform API — and exports the index name for the unattended slice
427
442
  // path. Unset ⇒ no resolver, no export. In-memory platforms ignore this.
428
443
  geocoderPlaceIndex?: geocoderIndex,
444
+ // Optional messaging sender. When set, the deploy exports the verified
445
+ // sender address for the unattended slice path and grants those Lambdas the
446
+ // right to send from it. No client door — a browser has no business sending
447
+ // from the deployment's identity. Unset ⇒ no export, no grant. In-memory
448
+ // platforms ignore this.
449
+ messagingSender?: messagingSender,
429
450
  // Optional object store for direct-to-store uploads. When set, the deploy
430
451
  // provisions a presign service against it, threads that service's URL into
431
452
  // config.json as `uploadEndpoint`, and serves the store read-only from the
@@ -175,6 +175,14 @@ let requiredStoreDeclarationSchema = Plugin$Reventless.requiredStoreDeclarationS
175
175
 
176
176
  let requiredStoreDeclarationArrayOptionSchema = Plugin$Reventless.requiredStoreDeclarationArrayOptionSchema;
177
177
 
178
+ let requiredCapabilityDeclarationSchema = Plugin$Reventless.requiredCapabilityDeclarationSchema;
179
+
180
+ let requiredCapabilityDeclarationArrayOptionSchema = Plugin$Reventless.requiredCapabilityDeclarationArrayOptionSchema;
181
+
182
+ let traitDeclarationSchema = Plugin$Reventless.traitDeclarationSchema;
183
+
184
+ let traitDeclarationArrayOptionSchema = Plugin$Reventless.traitDeclarationArrayOptionSchema;
185
+
178
186
  let pluginStructureSchema = Plugin$Reventless.pluginStructureSchema;
179
187
 
180
188
  let pluginStructureOffloadSchema = Plugin$Reventless.pluginStructureOffloadSchema;
@@ -225,6 +233,10 @@ export {
225
233
  extensionPointDefArrayOptionSchema,
226
234
  requiredStoreDeclarationSchema,
227
235
  requiredStoreDeclarationArrayOptionSchema,
236
+ requiredCapabilityDeclarationSchema,
237
+ requiredCapabilityDeclarationArrayOptionSchema,
238
+ traitDeclarationSchema,
239
+ traitDeclarationArrayOptionSchema,
228
240
  pluginStructureSchema,
229
241
  pluginStructureOffloadSchema,
230
242
  pluginDefinitionSchema,