@reventlessdev/reventless-aws 3.0.0-alpha.368 → 3.0.0-alpha.370

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 (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +9 -8
  3. package/run-resolve-environment.mjs +3 -0
  4. package/scripts/ProvisionAdmin.res +2 -2
  5. package/scripts/ProvisionAdmin.res.mjs +7 -6
  6. package/scripts/ResolveEnvironment.res +135 -0
  7. package/scripts/ResolveEnvironment.res.mjs +148 -0
  8. package/src/Platform.res +32 -3
  9. package/src/Platform.res.mjs +22 -5
  10. package/src/Platform_Stack.res +6 -3
  11. package/src/Platform_Stack.res.mjs +3 -3
  12. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -1
  13. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  14. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +1 -1
  15. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +1 -1
  16. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
  17. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -0
  18. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -0
  19. package/src/components/Api/AppSync_Adapter.res +4 -3
  20. package/src/components/Api/AppSync_SdlDecorate.res +7 -3
  21. package/src/components/Api/AppSync_SdlDecorate.res.mjs +4 -3
  22. package/src/util/Util_OwnerScopeEnv.res +9 -0
  23. package/src/util/Util_OwnerScopeEnv.res.mjs +14 -1
  24. package/src/util/Util_ShellConfig.res +4 -0
  25. package/src/util/Util_ShellConfig.res.mjs +5 -1
  26. package/tests/AppSync_AdapterTest.res +46 -23
  27. package/tests/AppSync_AdapterTest.res.mjs +35 -27
  28. package/tests/PgPipeline_IntegrationTest.res +2 -1
  29. package/tests/PgQueryDbUnion_IntegrationTest.res +2 -1
  30. package/tests/ResolveEnvironmentTest.res +91 -0
  31. package/tests/ResolveEnvironmentTest.res.mjs +128 -0
  32. package/tests/Util_OwnerScopeEnvTest.res +48 -2
  33. package/tests/Util_OwnerScopeEnvTest.res.mjs +49 -3
  34. package/tests/Util_ShellConfigTest.res +17 -0
  35. package/tests/Util_ShellConfigTest.res.mjs +18 -0
  36. package/tests/integration/AggTestAggregate.res +2 -1
  37. package/tests/integration/EpCompositeSlice.res +2 -1
  38. package/tests/integration/EpTestSlice.res +2 -1
  39. package/tests/integration/QueryDbUnion_DynamoDb_IntegrationTest.res +2 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
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.370 (2026-10-01)
7
+
8
+ * feat!: roles a plugin declares, groups a platform provides ([43daace](https://github.com/ReventlessDev/reventless-core/commit/43daace6ca588afe22562d079af1e6f1da81942e))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * AllowGroups and AdminGroup are removed — write AllowRoles over
13
+ the plugin's Roles.t, and use Role.admin / Platform.adminGroup(). Spec module
14
+ types declare `type role`, and their rules are Authorization.rule<role>; a spec
15
+ that writes its own rule declares `type role` (Reventless.Role.name for a rule
16
+ over Authorization.permission). Role.name is a private string: make one from a
17
+ string with Role.make.
18
+
19
+
20
+
21
+ # 3.0.0-alpha.369 (2026-10-01)
22
+
23
+ ### Features
24
+
25
+ * **spec, aws, core, local:** an app declares its environments, and every deployment records its commit ([c7cc925](https://github.com/ReventlessDev/reventless-core/commit/c7cc92573e97742b681a9000cea0bdfd6e9a33fc))
26
+
27
+
6
28
  # 3.0.0-alpha.368 (2026-09-29)
7
29
 
8
30
  **Note:** Version bump only for package @reventlessdev/reventless-aws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.368",
3
+ "version": "3.0.0-alpha.370",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  "provision-admin": "./run-provision-admin.mjs",
9
9
  "provision-accounts": "./run-provision-accounts.mjs",
10
10
  "bake-manifest": "./run-bake-manifest.mjs",
11
- "deploy-app": "./run-deploy-app.mjs"
11
+ "deploy-app": "./run-deploy-app.mjs",
12
+ "resolve-environment": "./run-resolve-environment.mjs"
12
13
  },
13
14
  "dependencies": {
14
15
  "@aws-sdk/client-appsync": "3.970.0",
@@ -22,17 +23,17 @@
22
23
  "yaml": "^2.8.3",
23
24
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.27",
24
25
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
26
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
25
27
  "@reventlessdev/rescript-node": "2.0.0-alpha.20",
26
28
  "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.22",
27
- "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
28
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
29
29
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
30
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
31
+ "@reventlessdev/reventless-core": "3.0.0-alpha.286",
30
32
  "@reventlessdev/rescript-web": "1.0.0-alpha.7",
31
33
  "@reventlessdev/reventless-interop": "3.0.0-alpha.38",
32
- "@reventlessdev/reventless-core": "3.0.0-alpha.284",
33
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.148",
34
- "@reventlessdev/reventless-spec": "3.0.0-alpha.154",
35
- "@reventlessdev/reventless-infra": "3.0.0-alpha.182"
34
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.150",
35
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.156",
36
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.184"
36
37
  },
37
38
  "devDependencies": {
38
39
  "rescript": "12.3.1",
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./scripts/ResolveEnvironment.res.mjs"
3
+ await main()
@@ -76,7 +76,7 @@ Make the first administrator of a Reventless deployment.
76
76
  one. The run always names the stack it used.
77
77
  --email <address> The address they sign in with.
78
78
 
79
- Creates the "${Reventless.AdminGroup.name}" group if it is missing, the account if
79
+ Creates the "${Reventless.Role.adminGroup()}" group if it is missing, the account if
80
80
  it is missing, a working password, and the membership between them. Provisions
81
81
  nothing that a platform stack owns. Region and credentials come from the
82
82
  environment, as for any AWS SDK call.
@@ -222,7 +222,7 @@ let run = async ({email, providerId, stack}: request): result<unit, string> =>
222
222
  switch await checkPoolAcceptsEmail(~providerId) {
223
223
  | Error(_) as e => e
224
224
  | Ok() =>
225
- let group = Reventless.AdminGroup.name
225
+ let group = Reventless.Role.adminGroup()
226
226
  let password = Reventless.Util_Password.generate()
227
227
  await ensureGroup(~providerId, ~group)
228
228
  await ensureUser(~providerId, ~email)
@@ -1,9 +1,9 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
4
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
4
5
  import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
5
6
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
6
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
7
7
  import * as Util_Password$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Password.res.mjs";
8
8
  import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
9
9
  import * as ProvisionCognito$ReventlessAws from "./ProvisionCognito.res.mjs";
@@ -36,7 +36,7 @@ Make the first administrator of a Reventless deployment.
36
36
  one. The run always names the stack it used.
37
37
  --email <address> The address they sign in with.
38
38
 
39
- Creates the "` + AdminGroup$Reventless.name + `" group if it is missing, the account if
39
+ Creates the "` + Role$Reventless.adminGroup() + `" group if it is missing, the account if
40
40
  it is missing, a working password, and the membership between them. Provisions
41
41
  nothing that a platform stack owns. Region and credentials come from the
42
42
  environment, as for any AWS SDK call.
@@ -156,12 +156,13 @@ async function run(param) {
156
156
  if (e$1.TAG !== "Ok") {
157
157
  return e$1;
158
158
  }
159
+ let group = Role$Reventless.adminGroup();
159
160
  let password = Util_Password$Reventless.generate();
160
- await ensureGroup(providerId, AdminGroup$Reventless.name);
161
+ await ensureGroup(providerId, group);
161
162
  await ensureUser(providerId, email);
162
163
  await setPassword(providerId, email, password);
163
- await addToGroup(providerId, email, AdminGroup$Reventless.name);
164
- console.log(signInDetails(providerId, email, password, AdminGroup$Reventless.name));
164
+ await addToGroup(providerId, email, group);
165
+ console.log(signInDetails(providerId, email, password, group));
165
166
  return {
166
167
  TAG: "Ok",
167
168
  _0: undefined
@@ -212,4 +213,4 @@ export {
212
213
  cli,
213
214
  main,
214
215
  }
215
- /* CliArgs-Reventless Not a pure module */
216
+ /* usage Not a pure module */
@@ -0,0 +1,135 @@
1
+ /***
2
+ Which environment a push, a tag or a pull request deploys to, from the app's
3
+ `environments.yaml`.
4
+
5
+ ```
6
+ pnpm exec resolve-environment --ref refs/heads/test
7
+ pnpm exec resolve-environment --pull-request 7 --base dev
8
+ ```
9
+
10
+ Prints `key=value` lines, so the deploy workflow appends them to
11
+ `$GITHUB_OUTPUT` as they are:
12
+
13
+ ```
14
+ matched=true
15
+ environment=test
16
+ stack=test
17
+ purpose=test
18
+ ```
19
+
20
+ Nothing matched is an answer, not a failure: `matched=false` and exit 0, so a push
21
+ to a branch no environment names deploys nowhere and says so. The workflow asks
22
+ only when the file exists; without it the stack is the branch, as it always was.
23
+ */
24
+
25
+ type request =
26
+ | Push(string)
27
+ | PullRequest({number: int, base: string, draft: bool})
28
+
29
+ type args = {
30
+ file: option<string>,
31
+ manifest: option<string>,
32
+ request: request,
33
+ }
34
+
35
+ let parseArgs = (argv: array<string>): result<args, string> =>
36
+ Reventless.CliArgs.parse(
37
+ ~strings=["file", "manifest", "ref", "pull-request", "base"],
38
+ ~bools=["draft"],
39
+ argv,
40
+ )
41
+ ->Result.flatMap(Reventless.CliArgs.noPositionals)
42
+ ->Result.flatMap(a => {
43
+ let string = name => a->Reventless.CliArgs.string(name)
44
+ let request = switch (string("ref"), string("pull-request"), string("base")) {
45
+ | (Some(ref), None, None) => Ok(Push(ref))
46
+ | (None, Some(number), Some(base)) =>
47
+ switch Int.fromString(number) {
48
+ | Some(number) => Ok(PullRequest({number, base, draft: a->Reventless.CliArgs.bool("draft")}))
49
+ | None => Error(`--pull-request takes a number, not "${number}"`)
50
+ }
51
+ | (None, Some(_), None) => Error("--pull-request needs --base, the branch it targets")
52
+ | (None, None, None) => Error("say what deploys: --ref, or --pull-request with --base")
53
+ | _ => Error("--ref and --pull-request are two different questions; ask one")
54
+ }
55
+ request->Result.map(request => {file: string("file"), manifest: string("manifest"), request})
56
+ })
57
+
58
+ let usage = `
59
+ Usage: resolve-environment (--ref <ref> | --pull-request <n> --base <branch> [--draft])
60
+ [--file <path>] [--manifest <path>]
61
+
62
+ Which environment of the app's environments.yaml a deploy goes to.
63
+
64
+ --ref <ref> A push or a tag: refs/heads/<branch> or refs/tags/<tag>.
65
+ --pull-request <n> A pull request, by number ...
66
+ --base <branch> ... and the branch it targets.
67
+ --draft The pull request is a draft.
68
+ --file <path> The environments file. Defaults to ${Reventless.Environments.fileName}
69
+ beside the deploy manifest.
70
+ --manifest <path> The deploy manifest. Defaults to ${DeployManifest.defaultFile}
71
+ in the working directory.
72
+
73
+ Prints matched, environment, stack and purpose as key=value lines; a deploy that
74
+ matches nothing prints matched=false and exits 0.
75
+ `
76
+
77
+ /** The lines printed for a request, from the environments already read. */
78
+ let answer = (environments: Reventless.Environments.t, request: request): array<string> => {
79
+ let resolved = switch request {
80
+ | Push(ref) =>
81
+ ref
82
+ ->Reventless.Environments.refOf
83
+ ->Option.flatMap(ref => environments->Reventless.Environments.resolve(ref))
84
+ | PullRequest({number, base, draft}) =>
85
+ environments->Reventless.Environments.resolvePullRequest(~base, ~number, ~draft)
86
+ }
87
+ switch resolved {
88
+ | Some({name, stack, purpose}) => [
89
+ "matched=true",
90
+ `environment=${name}`,
91
+ `stack=${stack}`,
92
+ `purpose=${purpose->Reventless.Environments.purposeName}`,
93
+ ]
94
+ | None => ["matched=false"]
95
+ }
96
+ }
97
+
98
+ let run = (args: args): result<array<string>, string> => {
99
+ let file = switch args.file {
100
+ | Some(file) => file
101
+ | None =>
102
+ Reventless.Environments.pathBeside(
103
+ ~manifest=args.manifest->Option.getOr(
104
+ NodePath.join([NodeProcess.cwd(), DeployManifest.defaultFile]),
105
+ ),
106
+ )
107
+ }
108
+ if !NodeFs.existsSync(file) {
109
+ Error(
110
+ `no environments file at ${file}. Without one every branch deploys to the stack of its own name, and there is nothing to resolve; --file names another path.`,
111
+ )
112
+ } else {
113
+ file
114
+ ->Reventless.Environments.parseFile
115
+ ->Result.map(environments => answer(environments, args.request))
116
+ ->Result.mapError(message =>
117
+ `${file}: ${message}. The deploy reads any file of this name beside the deploy manifest as its environments; a file kept there for something else needs another name, or the workflow's environments input pointed elsewhere.`
118
+ )
119
+ }
120
+ }
121
+
122
+ let cli: Reventless.CliArgs.cli<args> = {bin: "resolve-environment", usage, parse: parseArgs}
123
+
124
+ let main = () =>
125
+ Reventless.CliArgs.run(cli, async args =>
126
+ switch run(args) {
127
+ | Ok(lines) => lines->Array.forEach(line => Console.log(line))
128
+ | Error(message) =>
129
+ Console.error(`resolve-environment: ${message}`)
130
+ NodeProcess.exit(1)
131
+ }
132
+ )
133
+
134
+ // No top-level call: `../run-resolve-environment.mjs` invokes [main], so a test can
135
+ // import this module.
@@ -0,0 +1,148 @@
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 Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
8
+ import * as CliArgs$Reventless from "@reventlessdev/reventless-spec/src/CliArgs.res.mjs";
9
+ import * as Environments$Reventless from "@reventlessdev/reventless-spec/src/types/Environments.res.mjs";
10
+ import * as DeployManifest$ReventlessAws from "./DeployManifest.res.mjs";
11
+
12
+ function parseArgs(argv) {
13
+ return Stdlib_Result.flatMap(Stdlib_Result.flatMap(CliArgs$Reventless.parse([
14
+ "file",
15
+ "manifest",
16
+ "ref",
17
+ "pull-request",
18
+ "base"
19
+ ], ["draft"], undefined, undefined, argv), CliArgs$Reventless.noPositionals), a => {
20
+ let match = CliArgs$Reventless.string(a, "ref");
21
+ let match$1 = CliArgs$Reventless.string(a, "pull-request");
22
+ let match$2 = CliArgs$Reventless.string(a, "base");
23
+ let request;
24
+ if (match !== undefined) {
25
+ request = match$1 !== undefined || match$2 !== undefined ? ({
26
+ TAG: "Error",
27
+ _0: "--ref and --pull-request are two different questions; ask one"
28
+ }) : ({
29
+ TAG: "Ok",
30
+ _0: {
31
+ TAG: "Push",
32
+ _0: match
33
+ }
34
+ });
35
+ } else if (match$1 !== undefined) {
36
+ if (match$2 !== undefined) {
37
+ let number = Stdlib_Int.fromString(match$1, undefined);
38
+ request = number !== undefined ? ({
39
+ TAG: "Ok",
40
+ _0: {
41
+ TAG: "PullRequest",
42
+ number: number,
43
+ base: match$2,
44
+ draft: CliArgs$Reventless.bool(a, "draft")
45
+ }
46
+ }) : ({
47
+ TAG: "Error",
48
+ _0: `--pull-request takes a number, not "` + match$1 + `"`
49
+ });
50
+ } else {
51
+ request = {
52
+ TAG: "Error",
53
+ _0: "--pull-request needs --base, the branch it targets"
54
+ };
55
+ }
56
+ } else {
57
+ request = match$2 !== undefined ? ({
58
+ TAG: "Error",
59
+ _0: "--ref and --pull-request are two different questions; ask one"
60
+ }) : ({
61
+ TAG: "Error",
62
+ _0: "say what deploys: --ref, or --pull-request with --base"
63
+ });
64
+ }
65
+ return Stdlib_Result.map(request, request => ({
66
+ file: CliArgs$Reventless.string(a, "file"),
67
+ manifest: CliArgs$Reventless.string(a, "manifest"),
68
+ request: request
69
+ }));
70
+ });
71
+ }
72
+
73
+ let usage = `
74
+ Usage: resolve-environment (--ref <ref> | --pull-request <n> --base <branch> [--draft])
75
+ [--file <path>] [--manifest <path>]
76
+
77
+ Which environment of the app's environments.yaml a deploy goes to.
78
+
79
+ --ref <ref> A push or a tag: refs/heads/<branch> or refs/tags/<tag>.
80
+ --pull-request <n> A pull request, by number ...
81
+ --base <branch> ... and the branch it targets.
82
+ --draft The pull request is a draft.
83
+ --file <path> The environments file. Defaults to ` + Environments$Reventless.fileName + `
84
+ beside the deploy manifest.
85
+ --manifest <path> The deploy manifest. Defaults to ` + DeployManifest$ReventlessAws.defaultFile + `
86
+ in the working directory.
87
+
88
+ Prints matched, environment, stack and purpose as key=value lines; a deploy that
89
+ matches nothing prints matched=false and exits 0.
90
+ `;
91
+
92
+ function answer(environments, request) {
93
+ let resolved;
94
+ resolved = request.TAG === "Push" ? Stdlib_Option.flatMap(Environments$Reventless.refOf(request._0), ref => Environments$Reventless.resolve(environments, ref)) : Environments$Reventless.resolvePullRequest(environments, request.base, request.number, request.draft);
95
+ if (resolved !== undefined) {
96
+ return [
97
+ "matched=true",
98
+ `environment=` + resolved.name,
99
+ `stack=` + resolved.stack,
100
+ `purpose=` + Environments$Reventless.purposeName(resolved.purpose)
101
+ ];
102
+ } else {
103
+ return ["matched=false"];
104
+ }
105
+ }
106
+
107
+ function run(args) {
108
+ let file = args.file;
109
+ let file$1 = file !== undefined ? file : Environments$Reventless.pathBeside(Stdlib_Option.getOr(args.manifest, Nodepath.join(process.cwd(), DeployManifest$ReventlessAws.defaultFile)));
110
+ if (Nodefs.existsSync(file$1)) {
111
+ return Stdlib_Result.mapError(Stdlib_Result.map(Environments$Reventless.parseFile(file$1), environments => answer(environments, args.request)), message => file$1 + `: ` + message + `. The deploy reads any file of this name beside the deploy manifest as its environments; a file kept there for something else needs another name, or the workflow's environments input pointed elsewhere.`);
112
+ } else {
113
+ return {
114
+ TAG: "Error",
115
+ _0: `no environments file at ` + file$1 + `. Without one every branch deploys to the stack of its own name, and there is nothing to resolve; --file names another path.`
116
+ };
117
+ }
118
+ }
119
+
120
+ let cli = {
121
+ bin: "resolve-environment",
122
+ usage: usage,
123
+ parse: parseArgs
124
+ };
125
+
126
+ function main() {
127
+ return CliArgs$Reventless.run(cli, undefined, undefined, async args => {
128
+ let lines = run(args);
129
+ if (lines.TAG === "Ok") {
130
+ lines._0.forEach(line => {
131
+ console.log(line);
132
+ });
133
+ return;
134
+ }
135
+ console.error(`resolve-environment: ` + lines._0);
136
+ process.exit(1);
137
+ });
138
+ }
139
+
140
+ export {
141
+ parseArgs,
142
+ usage,
143
+ answer,
144
+ run,
145
+ cli,
146
+ main,
147
+ }
148
+ /* node:fs Not a pure module */
package/src/Platform.res CHANGED
@@ -107,6 +107,24 @@ let messagingEmailSenderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.ma
107
107
  `""` means the platform named none, which the runtime reads as its default. */
108
108
  let messagingEmailProviderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
109
109
 
110
+ /** Which group stands for which role, only where the two differ; every other
111
+ role maps to the group of its own name. Call before `Make()`: the platform
112
+ resolves roles while it is built, and a mapping stated later is refused. */
113
+ let roleGroups = Reventless.Role.setGroups
114
+
115
+ /** The groups of a user pool this deployment did not create, which no accounts
116
+ manifest names. They count as provided by the check that every role a plugin
117
+ needs has a group. */
118
+ let providedGroups = Reventless.Role.provideGroups
119
+
120
+ /** The group a role resolves to — the name this platform writes into its
121
+ directives, its shell configuration and the elevated groups. For code that
122
+ grants access by group, so it never spells one itself. */
123
+ let groupOf = Reventless.Role.groupOf
124
+
125
+ /** `groupOf(Role.admin)`. */
126
+ let adminGroup = Reventless.Role.adminGroup
127
+
110
128
  module MakeWithConfig = (
111
129
  Config: {
112
130
  let splitApi: bool
@@ -144,7 +162,15 @@ module MakeWithConfig = (
144
162
  // A default, never a forcing: an explicit `setElevatedGroups` in a platform
145
163
  // root — including one naming nobody — means what it says and wins. See
146
164
  // [Reventless.OwnerScope.defaultElevatedGroups].
147
- Reventless.OwnerScope.defaultElevatedGroups([Reventless.AdminGroup.name])
165
+ Reventless.OwnerScope.defaultElevatedRoles([Reventless.Role.admin])
166
+
167
+ // What this deployment provides for the roles its plugins need: the
168
+ // administrator group the stack declares, and the groups of the accounts
169
+ // provisioning creates. Read when each plugin is built, after the root has
170
+ // mapped its roles.
171
+ Reventless.Role.provideGroupsFrom(() =>
172
+ [Reventless.Role.adminGroup()]->Array.concat(Reventless.AccountsManifest.declaredGroups())
173
+ )
148
174
 
149
175
  // Dispatch per-flavor commandHandlerConfig records to the four runtime
150
176
  // builders. Every sub-record is optional; only branches the caller actually
@@ -308,13 +334,16 @@ module MakeWithConfig = (
308
334
  // Admin base as a source-API document — auth-decorated (all fields Admin,
309
335
  // Cognito-only; the deploy-time SigV4 system-caller fields died with the
310
336
  // fragment registry) plus the canonical stamp.
311
- let adminSourceSdl = (): string =>
337
+ let adminSourceSdl = (): string => {
338
+ // The directive below fixes the administrator's group for this deploy.
339
+ Reventless.Role.freeze()
312
340
  assembleCanonicalSourceSdl(
313
341
  ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
314
342
  ReventlessCore.Platform_AdminApi.baseFragment(~cloner=Config.cloner),
315
- ~group=Reventless.AdminGroup.name,
343
+ ~group=Reventless.Role.adminGroup(),
316
344
  ),
317
345
  )
346
+ }
318
347
 
319
348
  // Split-mode Domain source document: relay base types + Platform_ping — the
320
349
  // Domain merged API's canonical owner (plugin fields come from plugin sources).
@@ -13,10 +13,10 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
13
13
  import * as Pulumi from "@pulumi/pulumi";
14
14
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
15
15
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
16
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
16
17
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
17
18
  import * as Plugin$ReventlessAws from "./components/Plugin.res.mjs";
18
19
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
19
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
20
20
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
21
21
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
22
22
  import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
@@ -34,6 +34,7 @@ import * as Util_Bundle$ReventlessAws from "./util/Util_Bundle.res.mjs";
34
34
  import * as Plugin_Stack$ReventlessAws from "./plugin/stack/Plugin_Stack.res.mjs";
35
35
  import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
36
36
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
37
+ import * as AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
37
38
  import * as Platform_Casts$ReventlessAws from "./Platform_Casts.res.mjs";
38
39
  import * as Platform_Stack$ReventlessAws from "./Platform_Stack.res.mjs";
39
40
  import * as QueryDbBackend$ReventlessAws from "./adapter/QueryDb/QueryDbBackend.res.mjs";
@@ -152,7 +153,8 @@ let messagingEmailProviderRef = {
152
153
  };
153
154
 
154
155
  function MakeWithConfig(Config) {
155
- OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
156
+ OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
157
+ Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
156
158
  Stdlib_Option.forEach(Config.commandHandlerConfig.aggregates, param => {
157
159
  Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
158
160
  Stdlib_Option.forEach(param.async, AggregateRuntime_Builder_Single_Async$ReventlessAws.setConfig);
@@ -214,7 +216,8 @@ function MakeWithConfig(Config) {
214
216
  subscriptionSources: []
215
217
  });
216
218
  let adminSourceSdl = () => {
217
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(Config.cloner), AdminGroup$Reventless.name, undefined);
219
+ Role$Reventless.freeze();
220
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(Config.cloner), Role$Reventless.adminGroup(), undefined);
218
221
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
219
222
  };
220
223
  let match;
@@ -1566,7 +1569,8 @@ function MakeWithConfig(Config) {
1566
1569
 
1567
1570
  function Make($star) {
1568
1571
  let commandHandlerConfig = {};
1569
- OwnerScope$Reventless.defaultElevatedGroups([AdminGroup$Reventless.name]);
1572
+ OwnerScope$Reventless.defaultElevatedRoles([Role$Reventless.admin]);
1573
+ Role$Reventless.provideGroupsFrom(() => [Role$Reventless.adminGroup()].concat(AccountsManifest$Reventless.declaredGroups()));
1570
1574
  Stdlib_Option.forEach(commandHandlerConfig.aggregates, param => {
1571
1575
  Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
1572
1576
  Stdlib_Option.forEach(param.async, AggregateRuntime_Builder_Single_Async$ReventlessAws.setConfig);
@@ -1628,7 +1632,8 @@ function Make($star) {
1628
1632
  subscriptionSources: []
1629
1633
  });
1630
1634
  let adminSourceSdl = () => {
1631
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), AdminGroup$Reventless.name, undefined);
1635
+ Role$Reventless.freeze();
1636
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(Platform_AdminApi$ReventlessCore.baseFragment(false), Role$Reventless.adminGroup(), undefined);
1632
1637
  return AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
1633
1638
  };
1634
1639
  let match;
@@ -2958,6 +2963,14 @@ function Make($star) {
2958
2963
  };
2959
2964
  }
2960
2965
 
2966
+ let roleGroups = Role$Reventless.setGroups;
2967
+
2968
+ let providedGroups = Role$Reventless.provideGroups;
2969
+
2970
+ let groupOf = Role$Reventless.groupOf;
2971
+
2972
+ let adminGroup = Role$Reventless.adminGroup;
2973
+
2961
2974
  export {
2962
2975
  log,
2963
2976
  apiConfigRef,
@@ -2969,6 +2982,10 @@ export {
2969
2982
  geocoderPlaceIndexRef,
2970
2983
  messagingEmailSenderRef,
2971
2984
  messagingEmailProviderRef,
2985
+ roleGroups,
2986
+ providedGroups,
2987
+ groupOf,
2988
+ adminGroup,
2972
2989
  MakeWithConfig,
2973
2990
  Make,
2974
2991
  }
@@ -25,7 +25,7 @@ type cognitoUserPool = {
25
25
  * with SPA-friendly defaults (email sign-in unless `platform:loginIdentifier`
26
26
  * says otherwise, 12-char password policy, no MFA, and admin-only user
27
27
  * creation unless `platform:signUpMode` says otherwise), plus the
28
- * administrator group named by [Reventless.AdminGroup] and an elevated-groups
28
+ * administrator group named by [Reventless.Role.adminGroup] and an elevated-groups
29
29
  * default naming it. What it does **not** create is the first administrator:
30
30
  * accounts are not stack resources, and a pool full of them is not something a
31
31
  * `pulumi destroy` should be able to empty. `provision-admin` makes that one,
@@ -302,7 +302,7 @@ let _resolveUncached = (): cognitoUserPool => {
302
302
  // An ordinary child resource: the pool is ours, so this needs no out-of-band
303
303
  // call and Pulumi's state stays the authority for whether it exists.
304
304
  let _adminGroup = Util_CognitoGroupUser.addUserGroup(
305
- ~name=Reventless.AdminGroup.name,
305
+ ~name=Reventless.Role.adminGroup(),
306
306
  ~userPoolId=pool.id,
307
307
  )
308
308
 
@@ -400,7 +400,10 @@ let _resolveUncached = (): cognitoUserPool => {
400
400
  // name is what a caller needs either way, and an output that appeared in one
401
401
  // mode and not the other would read as "there is no administrator group here"
402
402
  // rather than "this stack did not create it".
403
- Pulumi.Pulumi.export("identityProviderAdminGroup", Pulumi.Output.make(Reventless.AdminGroup.name))
403
+ Pulumi.Pulumi.export(
404
+ "identityProviderAdminGroup",
405
+ Pulumi.Output.make(Reventless.Role.adminGroup()),
406
+ )
404
407
 
405
408
  // Exported so an operator can read back the store this provider resolved to —
406
409
  // the name is derived, not configured, so this is a convenience for checking
@@ -5,7 +5,7 @@ import * as Pulumi$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Pulumi
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
- import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
8
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
9
9
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
10
10
  import * as AWS_Tags$ReventlessAws from "./adapter/AWS_Tags.res.mjs";
11
11
  import * as Auth_SignUpMode$ReventlessAws from "./adapter/Auth/Auth_SignUpMode.res.mjs";
@@ -133,7 +133,7 @@ function _resolveUncached() {
133
133
  mfaConfiguration: "OFF",
134
134
  tags: AWS_Tags$ReventlessAws.make("HostUiPool", "Platform", "Auth", "Platform", undefined, undefined, undefined, undefined)
135
135
  });
136
- Util_CognitoGroupUser$ReventlessAws.addUserGroup(AdminGroup$Reventless.name, pool.id);
136
+ Util_CognitoGroupUser$ReventlessAws.addUserGroup(Role$Reventless.adminGroup(), pool.id);
137
137
  let tokenUnits2_accessToken = "minutes";
138
138
  let tokenUnits2_idToken = "minutes";
139
139
  let tokenUnits2_refreshToken = "days";
@@ -181,7 +181,7 @@ function _resolveUncached() {
181
181
  Pulumi$Pulumi.$$export("cognitoUserPoolArn", result.poolArn);
182
182
  Pulumi$Pulumi.$$export("cognitoRegion", regionOutput);
183
183
  Pulumi$Pulumi.$$export("cognitoUserPoolManaged", managedStr);
184
- Pulumi$Pulumi.$$export("identityProviderAdminGroup", Pulumi.output(AdminGroup$Reventless.name));
184
+ Pulumi$Pulumi.$$export("identityProviderAdminGroup", Pulumi.output(Role$Reventless.adminGroup()));
185
185
  Pulumi$Pulumi.$$export("activeRoleStore", result.activeRoleTable.name);
186
186
  return result;
187
187
  }
@@ -194,7 +194,7 @@ let make = (
194
194
  let adminEntryJson =
195
195
  ReventlessCore.Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
196
196
  ~pluginId=ReventlessCore.Platform_Admin_Structure.pluginId,
197
- ReventlessCore.Platform_Admin_Structure.structure,
197
+ ReventlessCore.Platform_Admin_Structure.structure(),
198
198
  )->JSON.stringify
199
199
 
200
200
  // Bundle reventless-aws (the compiled `_Ops` handler lives inside it) and
@@ -107,7 +107,7 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
107
107
  encodeSelections(j.selections)
108
108
  ]
109
109
  ]))) : "";
110
- let adminEntryJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(Platform_Admin_Structure$ReventlessCore.pluginId, undefined, Platform_Admin_Structure$ReventlessCore.structure));
110
+ let adminEntryJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(Platform_Admin_Structure$ReventlessCore.pluginId, undefined, Platform_Admin_Structure$ReventlessCore.structure()));
111
111
  let packageDirs = Object.fromEntries([[
112
112
  "@reventlessdev/reventless-aws",
113
113
  Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
@@ -6,7 +6,7 @@
6
6
  // ([Auth_ActiveRoleTrigger.res] bundles it and attaches it to the user pool).
7
7
  //
8
8
  // The narrowing has to happen here rather than in our own enforcement code
9
- // because `@authorize(AllowGroups([...]))` compiles to a field directive AppSync
9
+ // because `@authorize(AllowRoles([...]))` compiles to a field directive AppSync
10
10
  // evaluates against `cognito:groups` **before any of our code executes**. A
11
11
  // request header we invent could scope reads correctly and still leave every
12
12
  // group-gated mutation callable — right about the data, wrong about the writes.