@reventlessdev/reventless-local 3.0.0-alpha.298 → 3.0.0-alpha.300
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 +37 -0
- package/package.json +12 -12
- package/src/EmitCapabilities.res +3 -3
- package/src/LocalPlatformRegistry.res +1 -1
- package/src/LocalSeedTarget.res +1 -1
- package/src/Platform.res +57 -3
- package/src/Platform.res.mjs +61 -6
- package/src/ShellConfig.res +11 -1
- package/src/ShellConfig.res.mjs +10 -4
- package/src/adapter/Auth/Auth_GraphqlContext.res +18 -2
- package/src/adapter/Auth/Auth_GraphqlContext.res.mjs +8 -1
- package/src/adapter/Auth/UserStore.res +102 -3
- package/src/adapter/Auth/UserStore.res.mjs +128 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +33 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +26 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +11 -3
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res.mjs +3 -0
- package/src/adapter/DomainGraphQL_Server.res +1 -1
- package/src/adapter/GraphQL_CallerError.res +4 -0
- package/src/adapter/GraphQL_CallerError.res.mjs +9 -0
- package/src/adapter/LocalAnonymousLimit.res +35 -0
- package/src/adapter/LocalAnonymousLimit.res.mjs +52 -0
- package/src/adapter/LocalCapabilities.res +14 -2
- package/src/adapter/LocalCapabilities.res.mjs +21 -5
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +22 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +7 -0
- package/src/components/InboundTranslationSlice_Builder.res +1 -0
- package/src/components/InboundTranslationSlice_Builder.res.mjs +4 -1
- package/tests/EmitCapabilitiesTest.res +4 -6
- package/tests/EmitCapabilitiesTest.res.mjs +1 -2
- package/tests/HostShellDistTest.res.mjs +1 -1
- package/tests/LocalPlatformRegistryTest.res +3 -5
- package/tests/LocalPlatformRegistryTest.res.mjs +2 -4
- package/tests/LocalPlatformStartTest.res +2 -2
- package/tests/LocalPlatformStartTest.res.mjs +1 -2
- package/tests/ShellConfigTest.res.mjs +27 -27
- package/tests/adapter/InboundTranslationMutationTest.res +13 -2
- package/tests/adapter/InboundTranslationMutationTest.res.mjs +7 -4
- package/tests/adapter/LocalAnonymousLimitTest.res +70 -0
- package/tests/adapter/LocalAnonymousLimitTest.res.mjs +85 -0
- package/tests/adapter/LocalAuthLoginTest.res +2 -2
- package/tests/adapter/LocalAuthUserStoreTest.res +88 -0
- package/tests/adapter/LocalAuthUserStoreTest.res.mjs +83 -0
- package/tests/capability/LocalCapabilitiesTest.res +2 -1
- package/tests/capability/LocalCapabilitiesTest.res.mjs +3 -2
- package/tests/components/inboundtranslationslice/InboundTranslationCredentialTest.res +136 -0
- package/tests/components/inboundtranslationslice/InboundTranslationCredentialTest.res.mjs +183 -0
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +143 -11
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res.mjs +129 -22
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +18 -5
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res.mjs +12 -3
- package/tests/components/inboundtranslationslice/InboundTranslationWebhookTest.res +26 -6
- package/tests/components/inboundtranslationslice/InboundTranslationWebhookTest.res.mjs +48 -13
- package/tests/plugin/Plugin_Fixtures.res +0 -4
- package/tests/plugin/Plugin_Fixtures.res.mjs +13 -33
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,43 @@
|
|
|
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.300 (2026-10-25)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.299 (2026-10-25)
|
|
15
|
+
|
|
16
|
+
* refactor!: internal results and tool records whose parts may be absent are optional fields ([7009ddf](https://github.com/ReventlessDev/reventless-core/commit/7009ddf5d522f39ec9946a3b5cd062ceae889bbf))
|
|
17
|
+
* refactor(aws)!: command-line arguments a tool may not be given are optional fields ([82d5880](https://github.com/ReventlessDev/reventless-core/commit/82d5880e9aeda73fe9625dee73a1ca633b20a59a))
|
|
18
|
+
* refactor(spec)!: the plugin structure's optional parts are optional fields ([130bad5](https://github.com/ReventlessDev/reventless-core/commit/130bad581caf725a9d61b5a3df3d012e4f8ba402))
|
|
19
|
+
* feat!: an inbound translation is async and handed the platform's capabilities, and its audit row withholds sensitive fields ([dae6049](https://github.com/ReventlessDev/reventless-core/commit/dae6049e084f81408a494a2cd2011ec564dda65c))
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* a door open to anonymous callers is limited per source address, and answers Throttled past the limit ([259a671](https://github.com/ReventlessDev/reventless-core/commit/259a671969dd25579d26227a8fe80cba3194f0aa))
|
|
23
|
+
* a plugin declares per role how a person is admitted, and the local platform honors the self-service and provisioned halves ([9c37bf5](https://github.com/ReventlessDev/reventless-core/commit/9c37bf50f15cac1703bf2c351713518121a56211))
|
|
24
|
+
* a stash capability holds a value for minutes outside the log and hands it over once ([627f285](https://github.com/ReventlessDev/reventless-core/commit/627f28529ead2d2d31bdd091c9f42e7faca40f6f))
|
|
25
|
+
* **local:** the platform reads its accounts file again when it changes ([eeb2b5f](https://github.com/ReventlessDev/reventless-core/commit/eeb2b5f8ec9d5baca2d2d8bfb81260f07713a3a6))
|
|
26
|
+
|
|
27
|
+
### BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
* code that builds these records drops `name: None` and
|
|
30
|
+
writes `name: x` for `name: Some(x)`; a destructure of one of them binds a
|
|
31
|
+
field that may be absent with `?name`.
|
|
32
|
+
* code that builds these records drops `name: None` and
|
|
33
|
+
writes `name: x` for `name: Some(x)`.
|
|
34
|
+
* code that builds these records drops `name: None`, writes
|
|
35
|
+
`name: x` for `name: Some(x)`, and `name: ?x` for a value already of type
|
|
36
|
+
option.
|
|
37
|
+
* an inbound translation's translate takes ~capabilities and
|
|
38
|
+
returns a promise, and its spec must declare capabilityNeeds ([] for one that
|
|
39
|
+
only maps input). FromSlice steps return promises.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
6
43
|
# 3.0.0-alpha.298 (2026-10-23)
|
|
7
44
|
|
|
8
45
|
### Features
|
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.300",
|
|
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,24 +34,24 @@
|
|
|
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
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
39
37
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
40
|
-
"@reventlessdev/rescript-
|
|
38
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
39
|
+
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
40
|
+
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
41
41
|
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.309",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.257",
|
|
45
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.206",
|
|
46
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.173",
|
|
47
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.157",
|
|
48
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.37",
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.177"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.1",
|
|
53
53
|
"sury-ppx": "11.0.0",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.108"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"rescript": "^12.3.1"
|
package/src/EmitCapabilities.res
CHANGED
|
@@ -61,7 +61,7 @@ let usage = `Usage: emit-capabilities <srcDir> [<compositionModule>]
|
|
|
61
61
|
|
|
62
62
|
Run from the plugin package, after rescript build.`
|
|
63
63
|
|
|
64
|
-
type args = {srcDir: string, compositionModule
|
|
64
|
+
type args = {srcDir: string, compositionModule?: string}
|
|
65
65
|
|
|
66
66
|
let parseArgs = (argv: array<string>): result<args, string> =>
|
|
67
67
|
Reventless.CliArgs.parse(argv)
|
|
@@ -70,14 +70,14 @@ let parseArgs = (argv: array<string>): result<args, string> =>
|
|
|
70
70
|
let positionals = a->Reventless.CliArgs.positionals
|
|
71
71
|
switch positionals->Array.get(0) {
|
|
72
72
|
| None | Some("") => Error("<srcDir> is required.")
|
|
73
|
-
| Some(srcDir) => Ok({srcDir, compositionModule: positionals->Array.get(1)})
|
|
73
|
+
| Some(srcDir) => Ok({srcDir, compositionModule: ?(positionals->Array.get(1))})
|
|
74
74
|
}
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
let cli: Reventless.CliArgs.cli<args> = {bin: "emit-capabilities", usage, parse: parseArgs}
|
|
78
78
|
|
|
79
79
|
let main = () =>
|
|
80
|
-
Reventless.CliArgs.run(cli, async ({srcDir: srcDirArg, compositionModule}) => {
|
|
80
|
+
Reventless.CliArgs.run(cli, async ({srcDir: srcDirArg, ?compositionModule}) => {
|
|
81
81
|
// The local platform defaults to Debug-level logging; a build step should not.
|
|
82
82
|
switch NodeProcess.env->Dict.get("LOG_LEVEL") {
|
|
83
83
|
| Some(_) => ()
|
package/src/LocalSeedTarget.res
CHANGED
|
@@ -147,7 +147,7 @@ let announce = (t: t): unit =>
|
|
|
147
147
|
collapsed them would tell an in-memory platform to check its disk. */
|
|
148
148
|
let storePath = (t: t): result<string, string> =>
|
|
149
149
|
switch t.origin {
|
|
150
|
-
| Running({store: {kind: "sqlite", path: Some(path)}}) => Ok(path)
|
|
150
|
+
| Running({store: {kind: "sqlite", path: ?Some(path)}}) => Ok(path)
|
|
151
151
|
| Running({store: {kind: "memory"}, endpoint}) =>
|
|
152
152
|
Error(`the platform at ${endpoint} keeps its store in memory; restart it to empty it.`)
|
|
153
153
|
| Running({store: {kind: "postgres"}, endpoint}) =>
|
package/src/Platform.res
CHANGED
|
@@ -949,6 +949,34 @@ module MakeWithConfig = (
|
|
|
949
949
|
})
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
+
// Runs `f` once every plugin's structure has landed, with the plugin names
|
|
953
|
+
// and structures. The same moment the bake waits for, for the same reason: a
|
|
954
|
+
// structure only exists once its Output resolves, and anything read off all of
|
|
955
|
+
// them has to wait for the last.
|
|
956
|
+
let whenStructuresLanded = (
|
|
957
|
+
~pluginComponents: array<ReventlessCore.Plugin.component>,
|
|
958
|
+
f: array<(string, Reventless.Plugin.pluginStructure)> => unit,
|
|
959
|
+
) => {
|
|
960
|
+
let resolved: array<(string, Reventless.Plugin.pluginStructure)> = []
|
|
961
|
+
let expected = pluginComponents->Array.length
|
|
962
|
+
pluginComponents->Array.forEach(plugin => {
|
|
963
|
+
let outputs: ReventlessInfra.Plugin.outputs = plugin->ReventlessCore.Component.outputs
|
|
964
|
+
let _ =
|
|
965
|
+
(outputs.id, outputs.pluginStructure)
|
|
966
|
+
->Pulumi.Output.all2
|
|
967
|
+
->Pulumi.Output.apply(((id, ps)) => {
|
|
968
|
+
let name = id->String.split("@")->Array.get(0)->Option.getOr(id)
|
|
969
|
+
switch ps {
|
|
970
|
+
| Some(def) => resolved->Array.push((name, def))
|
|
971
|
+
| None => ()
|
|
972
|
+
}
|
|
973
|
+
if resolved->Array.length === expected {
|
|
974
|
+
f(resolved)
|
|
975
|
+
}
|
|
976
|
+
})
|
|
977
|
+
})
|
|
978
|
+
}
|
|
979
|
+
|
|
952
980
|
// The object stores a plugin's fields declared, as `{plugin}.{store}` keys.
|
|
953
981
|
let declaredStoresOf = (structure: Reventless.Plugin.pluginStructure) =>
|
|
954
982
|
structure.requiredStores->Option.getOr([])
|
|
@@ -1238,7 +1266,6 @@ module MakeWithConfig = (
|
|
|
1238
1266
|
~store="pluginApiFragments",
|
|
1239
1267
|
),
|
|
1240
1268
|
),
|
|
1241
|
-
apiTarget: None,
|
|
1242
1269
|
structure: pluginStructure->Option.map(
|
|
1243
1270
|
s =>
|
|
1244
1271
|
s->ReventlessCore.Plugin_Helpers.offloadPayload(
|
|
@@ -1246,7 +1273,6 @@ module MakeWithConfig = (
|
|
|
1246
1273
|
~store="pluginStructures",
|
|
1247
1274
|
),
|
|
1248
1275
|
),
|
|
1249
|
-
dcbEventLog: None,
|
|
1250
1276
|
kind: Domain,
|
|
1251
1277
|
}
|
|
1252
1278
|
// Dispatch Connect(def) to the Plugin aggregate (id = plugin NAME).
|
|
@@ -1384,7 +1410,7 @@ module MakeWithConfig = (
|
|
|
1384
1410
|
~port=domainPort,
|
|
1385
1411
|
~endpoint=`http://localhost:${domainPort->Int.toString}/graphql`,
|
|
1386
1412
|
~loginEndpoint=`http://localhost:${domainPort->Int.toString}/__inmemory/login`,
|
|
1387
|
-
~store={kind, path},
|
|
1413
|
+
~store={kind, ?path},
|
|
1388
1414
|
)
|
|
1389
1415
|
// After the register, and it fills the tap port in once the socket is bound:
|
|
1390
1416
|
// the port is ephemeral by default, so it is not known until `listen` calls
|
|
@@ -1771,6 +1797,34 @@ module MakeWithConfig = (
|
|
|
1771
1797
|
~uiSlotsFile=?hostUiBundle->Option.flatMap(cfg => cfg.uiSlotsFile),
|
|
1772
1798
|
~shellConfig=hostUiBundle->Option.flatMap(cfg => cfg.shellConfig),
|
|
1773
1799
|
)
|
|
1800
|
+
// What the plugins declare about admission is read once their structures
|
|
1801
|
+
// have landed. First the refusal: a role declared self-service or by
|
|
1802
|
+
// invitation that nothing here honors stops the platform, as a role no
|
|
1803
|
+
// group stands for does. Then the shell is told where the doors are and
|
|
1804
|
+
// which groups a visitor may sign up for, by writing `config.json` again
|
|
1805
|
+
// with the one key the declaration alone could not supply.
|
|
1806
|
+
whenStructuresLanded(~pluginComponents=plugins, structures => {
|
|
1807
|
+
Reventless.AdmissionCheck.check(
|
|
1808
|
+
~structures,
|
|
1809
|
+
~provisioned=LocalCapabilities.provisioned(),
|
|
1810
|
+
~elevatedGroups=Reventless.OwnerScope.elevatedGroups(),
|
|
1811
|
+
~adminGroup=Reventless.Role.adminGroup(),
|
|
1812
|
+
// This platform admits callers who are not signed in on every door
|
|
1813
|
+
// whose rule says so.
|
|
1814
|
+
~anonymousDoors=true,
|
|
1815
|
+
)
|
|
1816
|
+
ReventlessCore.Platform_SelfRegistration.fromStructures(
|
|
1817
|
+
structures,
|
|
1818
|
+
)->Option.forEach(selfRegistration =>
|
|
1819
|
+
ShellConfig.emit(
|
|
1820
|
+
~dir=?hostUiDir,
|
|
1821
|
+
~bakedManifest=hostUiBundle->Option.flatMap(cfg => cfg.bakedManifest),
|
|
1822
|
+
~uiSlotsFile=?hostUiBundle->Option.flatMap(cfg => cfg.uiSlotsFile),
|
|
1823
|
+
~shellConfig=hostUiBundle->Option.flatMap(cfg => cfg.shellConfig),
|
|
1824
|
+
~selfRegistration,
|
|
1825
|
+
)
|
|
1826
|
+
)
|
|
1827
|
+
})
|
|
1774
1828
|
// Unconditional for the same reason, and one file over: it is also what
|
|
1775
1829
|
// restores the shell package's own hints for a platform that has stopped
|
|
1776
1830
|
// declaring its own.
|
package/src/Platform.res.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import * as AnsiStyle$Reventless from "@reventlessdev/reventless-spec/src/AnsiSt
|
|
|
18
18
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
19
19
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
20
20
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
21
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
21
22
|
import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
|
|
22
23
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
23
24
|
import * as Backend$ReventlessLocal from "./adapter/Backend.res.mjs";
|
|
@@ -25,6 +26,7 @@ import * as UiHints$ReventlessLocal from "./UiHints.res.mjs";
|
|
|
25
26
|
import * as UiSlots$ReventlessLocal from "./UiSlots.res.mjs";
|
|
26
27
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
27
28
|
import * as LocalBus$ReventlessLocal from "./adapter/LocalBus.res.mjs";
|
|
29
|
+
import * as AdmissionCheck$Reventless from "@reventlessdev/reventless-spec/src/types/AdmissionCheck.res.mjs";
|
|
28
30
|
import * as Api_Naming$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Naming.res.mjs";
|
|
29
31
|
import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
|
|
30
32
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
@@ -94,6 +96,7 @@ import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventl
|
|
|
94
96
|
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
95
97
|
import * as LocalCommandTopicChannel$ReventlessLocal from "./adapter/CommandTopic/LocalCommandTopicChannel.res.mjs";
|
|
96
98
|
import * as LocalEventTopicPublisher$ReventlessLocal from "./adapter/EventTopic/LocalEventTopicPublisher.res.mjs";
|
|
99
|
+
import * as Platform_SelfRegistration$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_SelfRegistration.res.mjs";
|
|
97
100
|
import * as StateChangeSlice_Builder$ReventlessLocal from "./components/StateChangeSlice_Builder.res.mjs";
|
|
98
101
|
import * as DcbEventLogStorage_Sqlite$ReventlessLocal from "./adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs";
|
|
99
102
|
import * as LocalEventCollectorChannel$ReventlessLocal from "./adapter/EventCollector/LocalEventCollectorChannel.res.mjs";
|
|
@@ -884,6 +887,30 @@ function MakeWithConfig(Config) {
|
|
|
884
887
|
});
|
|
885
888
|
});
|
|
886
889
|
};
|
|
890
|
+
let whenStructuresLanded = (pluginComponents, f) => {
|
|
891
|
+
let resolved = [];
|
|
892
|
+
let expected = pluginComponents.length;
|
|
893
|
+
pluginComponents.forEach(plugin => {
|
|
894
|
+
let outputs = Component$ReventlessCore.outputs(plugin);
|
|
895
|
+
Pulumi.all([
|
|
896
|
+
outputs.id,
|
|
897
|
+
outputs.pluginStructure
|
|
898
|
+
]).apply(param => {
|
|
899
|
+
let ps = param[1];
|
|
900
|
+
let id = param[0];
|
|
901
|
+
let name = Stdlib_Option.getOr(id.split("@")[0], id);
|
|
902
|
+
if (ps !== undefined) {
|
|
903
|
+
resolved.push([
|
|
904
|
+
name,
|
|
905
|
+
ps
|
|
906
|
+
]);
|
|
907
|
+
}
|
|
908
|
+
if (resolved.length === expected) {
|
|
909
|
+
return f(resolved);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
};
|
|
887
914
|
let seedPluginStructuresStore = pluginComponents => {
|
|
888
915
|
pluginComponents.forEach(plugin => {
|
|
889
916
|
let outputs = Component$ReventlessCore.outputs(plugin);
|
|
@@ -1043,9 +1070,7 @@ function MakeWithConfig(Config) {
|
|
|
1043
1070
|
eventCollector: param[2].name,
|
|
1044
1071
|
extensionProtocols: [],
|
|
1045
1072
|
apiSchemaFragment: Stdlib_Option.map(param[5], f => Plugin_Helpers$ReventlessCore.offloadPayload(f, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments")),
|
|
1046
|
-
apiTarget: undefined,
|
|
1047
1073
|
structure: Stdlib_Option.map(param[7], s => Plugin_Helpers$ReventlessCore.offloadPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures")),
|
|
1048
|
-
dcbEventLog: undefined,
|
|
1049
1074
|
kind: "Domain"
|
|
1050
1075
|
};
|
|
1051
1076
|
dispatchPluginCommand(pluginName, {
|
|
@@ -1341,7 +1366,11 @@ function MakeWithConfig(Config) {
|
|
|
1341
1366
|
});
|
|
1342
1367
|
subscribeToPluginEvents();
|
|
1343
1368
|
let hostUiDir = HostShellDist$ReventlessLocal.resolve(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.assetsDir));
|
|
1344
|
-
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), hostUiDir);
|
|
1369
|
+
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), undefined, hostUiDir);
|
|
1370
|
+
whenStructuresLanded(plugins$1, structures => {
|
|
1371
|
+
AdmissionCheck$Reventless.check(structures, LocalCapabilities$ReventlessLocal.provisioned(), OwnerScope$Reventless.elevatedGroups(), Role$Reventless.adminGroup(), true);
|
|
1372
|
+
Stdlib_Option.forEach(Platform_SelfRegistration$ReventlessCore.fromStructures(structures), selfRegistration => ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), selfRegistration, hostUiDir));
|
|
1373
|
+
});
|
|
1345
1374
|
UiHints$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir);
|
|
1346
1375
|
UiHints$ReventlessLocal.watch(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir, LocalEvents_Server$ReventlessLocal.broadcastUiHintsChanged);
|
|
1347
1376
|
UiSlots$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), hostUiDir);
|
|
@@ -2653,6 +2682,30 @@ function Make($star) {
|
|
|
2653
2682
|
});
|
|
2654
2683
|
});
|
|
2655
2684
|
};
|
|
2685
|
+
let whenStructuresLanded = (pluginComponents, f) => {
|
|
2686
|
+
let resolved = [];
|
|
2687
|
+
let expected = pluginComponents.length;
|
|
2688
|
+
pluginComponents.forEach(plugin => {
|
|
2689
|
+
let outputs = Component$ReventlessCore.outputs(plugin);
|
|
2690
|
+
Pulumi.all([
|
|
2691
|
+
outputs.id,
|
|
2692
|
+
outputs.pluginStructure
|
|
2693
|
+
]).apply(param => {
|
|
2694
|
+
let ps = param[1];
|
|
2695
|
+
let id = param[0];
|
|
2696
|
+
let name = Stdlib_Option.getOr(id.split("@")[0], id);
|
|
2697
|
+
if (ps !== undefined) {
|
|
2698
|
+
resolved.push([
|
|
2699
|
+
name,
|
|
2700
|
+
ps
|
|
2701
|
+
]);
|
|
2702
|
+
}
|
|
2703
|
+
if (resolved.length === expected) {
|
|
2704
|
+
return f(resolved);
|
|
2705
|
+
}
|
|
2706
|
+
});
|
|
2707
|
+
});
|
|
2708
|
+
};
|
|
2656
2709
|
let seedPluginStructuresStore = pluginComponents => {
|
|
2657
2710
|
pluginComponents.forEach(plugin => {
|
|
2658
2711
|
let outputs = Component$ReventlessCore.outputs(plugin);
|
|
@@ -2812,9 +2865,7 @@ function Make($star) {
|
|
|
2812
2865
|
eventCollector: param[2].name,
|
|
2813
2866
|
extensionProtocols: [],
|
|
2814
2867
|
apiSchemaFragment: Stdlib_Option.map(param[5], f => Plugin_Helpers$ReventlessCore.offloadPayload(f, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments")),
|
|
2815
|
-
apiTarget: undefined,
|
|
2816
2868
|
structure: Stdlib_Option.map(param[7], s => Plugin_Helpers$ReventlessCore.offloadPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures")),
|
|
2817
|
-
dcbEventLog: undefined,
|
|
2818
2869
|
kind: "Domain"
|
|
2819
2870
|
};
|
|
2820
2871
|
dispatchPluginCommand(pluginName, {
|
|
@@ -3105,7 +3156,11 @@ function Make($star) {
|
|
|
3105
3156
|
});
|
|
3106
3157
|
subscribeToPluginEvents();
|
|
3107
3158
|
let hostUiDir = HostShellDist$ReventlessLocal.resolve(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.assetsDir));
|
|
3108
|
-
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), hostUiDir);
|
|
3159
|
+
ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), undefined, hostUiDir);
|
|
3160
|
+
whenStructuresLanded(plugins$1, structures => {
|
|
3161
|
+
AdmissionCheck$Reventless.check(structures, LocalCapabilities$ReventlessLocal.provisioned(), OwnerScope$Reventless.elevatedGroups(), Role$Reventless.adminGroup(), true);
|
|
3162
|
+
Stdlib_Option.forEach(Platform_SelfRegistration$ReventlessCore.fromStructures(structures), selfRegistration => ShellConfig$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.shellConfig), selfRegistration, hostUiDir));
|
|
3163
|
+
});
|
|
3109
3164
|
UiHints$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir);
|
|
3110
3165
|
UiHints$ReventlessLocal.watch(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiHintsFile), hostUiDir, LocalEvents_Server$ReventlessLocal.broadcastUiHintsChanged);
|
|
3111
3166
|
UiSlots$ReventlessLocal.emit(Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.uiSlotsFile), hostUiDir);
|
package/src/ShellConfig.res
CHANGED
|
@@ -50,6 +50,7 @@ let computedKeys = [
|
|
|
50
50
|
journeyManifestsKey,
|
|
51
51
|
ReventlessCore.Platform_UiSlots.configKey,
|
|
52
52
|
ReventlessCore.Platform_AdminGroup.configKey,
|
|
53
|
+
ReventlessCore.Platform_SelfRegistration.configKey,
|
|
53
54
|
]
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -63,6 +64,9 @@ let overlay = (
|
|
|
63
64
|
~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>,
|
|
64
65
|
~uiSlotsFile: option<string>=?,
|
|
65
66
|
~shellConfig: option<dict<JSON.t>>,
|
|
67
|
+
// Read off the plugin structures once they have landed, so the first write
|
|
68
|
+
// (from the declaration alone) omits it and the second carries it.
|
|
69
|
+
~selfRegistration: option<ReventlessCore.Platform_SelfRegistration.config>=?,
|
|
66
70
|
): dict<JSON.t> => {
|
|
67
71
|
let out =
|
|
68
72
|
Reventless.ShellEnvironment.local
|
|
@@ -83,6 +87,11 @@ let overlay = (
|
|
|
83
87
|
|
|
84
88
|
ReventlessCore.Platform_AdminGroup.configEntry()->Option.forEach(((k, v)) => out->Dict.set(k, v))
|
|
85
89
|
|
|
90
|
+
selfRegistration->Option.forEach(c => {
|
|
91
|
+
let (k, v) = ReventlessCore.Platform_SelfRegistration.configEntry(c)
|
|
92
|
+
out->Dict.set(k, v)
|
|
93
|
+
})
|
|
94
|
+
|
|
86
95
|
bakedManifest->Option.forEach(config => {
|
|
87
96
|
out->Dict.set("manifestUrl", JSON.Encode.string(manifestUrlOf(config)))
|
|
88
97
|
// Omitted entirely when nothing is declared, so a single-audience deployment
|
|
@@ -138,10 +147,11 @@ let emit = (
|
|
|
138
147
|
~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>,
|
|
139
148
|
~uiSlotsFile: option<string>=?,
|
|
140
149
|
~shellConfig: option<dict<JSON.t>>,
|
|
150
|
+
~selfRegistration: option<ReventlessCore.Platform_SelfRegistration.config>=?,
|
|
141
151
|
// The platform's chosen `assetsDir`; unset, the host-shell package's `dist/`.
|
|
142
152
|
~dir: option<string>=?,
|
|
143
153
|
) => {
|
|
144
|
-
let overlay = overlay(~bakedManifest, ~uiSlotsFile?, ~shellConfig)
|
|
154
|
+
let overlay = overlay(~bakedManifest, ~uiSlotsFile?, ~shellConfig, ~selfRegistration?)
|
|
145
155
|
let declared = overlay->Dict.keysToArray->Array.filter(k => !(environmentKeys->Array.includes(k)))
|
|
146
156
|
|
|
147
157
|
switch switch dir {
|
package/src/ShellConfig.res.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import * as HostShellDist$ReventlessLocal from "./HostShellDist.res.mjs";
|
|
|
13
13
|
import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
|
|
14
14
|
import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
|
|
15
15
|
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
16
|
+
import * as Platform_SelfRegistration$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_SelfRegistration.res.mjs";
|
|
16
17
|
|
|
17
18
|
let log = Logger$ReventlessCore.fromEnv();
|
|
18
19
|
|
|
@@ -37,11 +38,12 @@ let computedKeys = Belt_Array.concatMany([
|
|
|
37
38
|
"manifestUrl",
|
|
38
39
|
journeyManifestsKey,
|
|
39
40
|
Platform_UiSlots$ReventlessCore.configKey,
|
|
40
|
-
Platform_AdminGroup$ReventlessCore.configKey
|
|
41
|
+
Platform_AdminGroup$ReventlessCore.configKey,
|
|
42
|
+
Platform_SelfRegistration$ReventlessCore.configKey
|
|
41
43
|
]
|
|
42
44
|
]);
|
|
43
45
|
|
|
44
|
-
function overlay(bakedManifest, uiSlotsFile, shellConfig) {
|
|
46
|
+
function overlay(bakedManifest, uiSlotsFile, shellConfig, selfRegistration) {
|
|
45
47
|
let out = Object.fromEntries(ShellEnvironment$Reventless.configFields(ShellEnvironment$Reventless.local));
|
|
46
48
|
Stdlib_Option.forEach(uiSlotsFile, param => {
|
|
47
49
|
out[Platform_UiSlots$ReventlessCore.configKey] = Platform_UiSlots$ReventlessCore.url;
|
|
@@ -49,6 +51,10 @@ function overlay(bakedManifest, uiSlotsFile, shellConfig) {
|
|
|
49
51
|
Stdlib_Option.forEach(Platform_AdminGroup$ReventlessCore.configEntry(), param => {
|
|
50
52
|
out[param[0]] = param[1];
|
|
51
53
|
});
|
|
54
|
+
Stdlib_Option.forEach(selfRegistration, c => {
|
|
55
|
+
let match = Platform_SelfRegistration$ReventlessCore.configEntry(c);
|
|
56
|
+
out[match[0]] = match[1];
|
|
57
|
+
});
|
|
52
58
|
Stdlib_Option.forEach(bakedManifest, config => {
|
|
53
59
|
out["manifestUrl"] = Platform_BakedManifest$ReventlessCore.urlForKey(config.key);
|
|
54
60
|
let urls = Platform_BakedManifest$ReventlessCore.journeyUrls(config);
|
|
@@ -86,8 +92,8 @@ function readObject(path, label) {
|
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
function emit(bakedManifest, uiSlotsFile, shellConfig, dir) {
|
|
90
|
-
let overlay$1 = overlay(bakedManifest, uiSlotsFile, shellConfig);
|
|
95
|
+
function emit(bakedManifest, uiSlotsFile, shellConfig, selfRegistration, dir) {
|
|
96
|
+
let overlay$1 = overlay(bakedManifest, uiSlotsFile, shellConfig, selfRegistration);
|
|
91
97
|
let declared = Object.keys(overlay$1).filter(k => !environmentKeys.includes(k));
|
|
92
98
|
let dir$1 = dir !== undefined ? dir : HostShellDist$ReventlessLocal.dir();
|
|
93
99
|
if (dir$1 === undefined) {
|
|
@@ -20,7 +20,19 @@ module YG = GraphqlYoga
|
|
|
20
20
|
|
|
21
21
|
type fetchHeaders
|
|
22
22
|
type fetchRequest = {headers: fetchHeaders}
|
|
23
|
-
type
|
|
23
|
+
type socket = {remoteAddress?: string}
|
|
24
|
+
type nodeRequest = {socket?: socket}
|
|
25
|
+
// `req` is the Node request yoga is handed when it serves over `http`; absent
|
|
26
|
+
// when the context is built some other way, such as in a test.
|
|
27
|
+
type yogaInitialCtx = {request: fetchRequest, req?: nodeRequest}
|
|
28
|
+
|
|
29
|
+
/** Where a request came from, as the socket says — not a forwarded header, which
|
|
30
|
+
the caller writes. What the limit on anonymous doors counts by. */
|
|
31
|
+
let sourceAddressOf = (ctx: yogaInitialCtx): string =>
|
|
32
|
+
ctx.req
|
|
33
|
+
->Option.flatMap(r => r.socket)
|
|
34
|
+
->Option.flatMap(s => s.remoteAddress)
|
|
35
|
+
->Option.getOr("unknown")
|
|
24
36
|
|
|
25
37
|
@send
|
|
26
38
|
external headersForEach: (fetchHeaders, (string, string) => unit) => unit = "forEach"
|
|
@@ -65,7 +77,11 @@ let buildAuthContext = async (initial: YG.initialContext): JSON.t => {
|
|
|
65
77
|
let requestContext: ReventlessCore.Auth_Adapter.requestContext = {headers: headers}
|
|
66
78
|
let result = await LocalAuth.authenticate(requestContext)
|
|
67
79
|
let identity = identityFromAuthResult(result)
|
|
68
|
-
Obj.magic({
|
|
80
|
+
Obj.magic({
|
|
81
|
+
"identity": identity,
|
|
82
|
+
"authenticated": isAuthenticated(result),
|
|
83
|
+
"sourceAddress": sourceAddressOf(ctx),
|
|
84
|
+
})
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
// Read the identity attached by `buildAuthContext` from the resolver context.
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Graphql from "graphql";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
5
6
|
import * as LocalAuth$ReventlessLocal from "./LocalAuth.res.mjs";
|
|
6
7
|
import * as Auth_RefusalVocabulary$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_RefusalVocabulary.res.mjs";
|
|
7
8
|
|
|
9
|
+
function sourceAddressOf(ctx) {
|
|
10
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(ctx.req, r => r.socket), s => s.remoteAddress), "unknown");
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
function extractHeaders(headers) {
|
|
9
14
|
let acc = {};
|
|
10
15
|
headers.forEach((value, key) => {
|
|
@@ -38,7 +43,8 @@ async function buildAuthContext(initial) {
|
|
|
38
43
|
let identity = identityFromAuthResult(result);
|
|
39
44
|
return {
|
|
40
45
|
identity: identity,
|
|
41
|
-
authenticated: isAuthenticated(result)
|
|
46
|
+
authenticated: isAuthenticated(result),
|
|
47
|
+
sourceAddress: sourceAddressOf(initial)
|
|
42
48
|
};
|
|
43
49
|
}
|
|
44
50
|
|
|
@@ -105,6 +111,7 @@ let YG;
|
|
|
105
111
|
|
|
106
112
|
export {
|
|
107
113
|
YG,
|
|
114
|
+
sourceAddressOf,
|
|
108
115
|
extractHeaders,
|
|
109
116
|
identityFromAuthResult,
|
|
110
117
|
isAuthenticated,
|
|
@@ -26,6 +26,10 @@ let parseFile = AccountsManifest.parseFile
|
|
|
26
26
|
|
|
27
27
|
// ── Resolution & loading ────────────────────────────────────────────────────
|
|
28
28
|
|
|
29
|
+
/** The usernames the accounts file gave, so a reload can forget those it no
|
|
30
|
+
longer lists without touching accounts registered some other way. */
|
|
31
|
+
let fromFile: ref<array<string>> = ref([])
|
|
32
|
+
|
|
29
33
|
let _registerEntries = (entries: array<entry>): unit =>
|
|
30
34
|
entries->Array.forEach(({username, password, groups, ?userId}) => {
|
|
31
35
|
let id: Identity.t = {
|
|
@@ -63,6 +67,7 @@ let pluginChecked: ref<bool> = ref(false)
|
|
|
63
67
|
let resetResolution = (): unit => {
|
|
64
68
|
resolved := false
|
|
65
69
|
pluginChecked := false
|
|
70
|
+
fromFile := []
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
/**
|
|
@@ -95,6 +100,7 @@ let load = (~users: option<array<entry>>=?, ~usersFile: option<string>=?, ()): r
|
|
|
95
100
|
| Some(path) =>
|
|
96
101
|
parseFile(path)->Result.map(entries => {
|
|
97
102
|
_registerEntries(entries)
|
|
103
|
+
fromFile := entries->Array.map(e => e.username)
|
|
98
104
|
UsersFile(path)
|
|
99
105
|
})
|
|
100
106
|
| None =>
|
|
@@ -102,6 +108,7 @@ let load = (~users: option<array<entry>>=?, ~usersFile: option<string>=?, ()): r
|
|
|
102
108
|
if NodeFs.existsSync(defaultPath) {
|
|
103
109
|
parseFile(defaultPath)->Result.map(entries => {
|
|
104
110
|
_registerEntries(entries)
|
|
111
|
+
fromFile := entries->Array.map(e => e.username)
|
|
105
112
|
DefaultFile(defaultPath)
|
|
106
113
|
})
|
|
107
114
|
} else {
|
|
@@ -114,13 +121,105 @@ let load = (~users: option<array<entry>>=?, ~usersFile: option<string>=?, ()): r
|
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
// ── Reloading ───────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
Reads the accounts file again: accounts it adds can sign in, changed ones sign in
|
|
128
|
+
with their new password and groups, and ones it no longer lists are forgotten. A
|
|
129
|
+
token already issued stays valid until it expires, as it carries its identity.
|
|
130
|
+
A file that does not parse changes nothing, so a half-saved edit never signs
|
|
131
|
+
everyone out. Says what it did, for the log.
|
|
132
|
+
*/
|
|
133
|
+
let reload = (path: string): result<string, string> =>
|
|
134
|
+
switch parseFile(path) {
|
|
135
|
+
| Error(e) => Error(e)
|
|
136
|
+
| Ok(entries) =>
|
|
137
|
+
let names = entries->Array.map(e => e.username)
|
|
138
|
+
let dropped = fromFile.contents->Array.filter(n => !(names->Array.includes(n)))
|
|
139
|
+
dropped->Array.forEach(username => LocalAuth.Login.removeCredentials(~username))
|
|
140
|
+
_registerEntries(entries)
|
|
141
|
+
fromFile := names
|
|
142
|
+
Ok(
|
|
143
|
+
`${names->Array.length->Int.toString} accounts` ++ (
|
|
144
|
+
dropped == [] ? "" : `, removed: ${dropped->Array.join(", ")}`
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The roles the plugins need that the accounts no longer provide, as the
|
|
150
|
+
platform's start would have refused them; empty when all are provided. */
|
|
151
|
+
let unmetAfterReload = (): array<RoleCoverage.need> =>
|
|
152
|
+
switch Role.providedGroups() {
|
|
153
|
+
| None => []
|
|
154
|
+
| Some(provided) => RoleCoverage.unmet(RoleCoverage.checked.contents, ~provided)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let _watcher: ref<option<NodeFs.watcher>> = ref(None)
|
|
158
|
+
let _pending: ref<option<timeoutId>> = ref(None)
|
|
159
|
+
|
|
160
|
+
/** Stops following the accounts file. */
|
|
161
|
+
let unwatch = (): unit => {
|
|
162
|
+
_watcher.contents->Option.forEach(NodeFs.watcherClose)
|
|
163
|
+
_watcher := None
|
|
164
|
+
_pending.contents->Option.forEach(clearTimeout)
|
|
165
|
+
_pending := None
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
Follows the accounts file, so an account the tools or the developer add can sign
|
|
170
|
+
in without a restart. The folder is watched, not the file, as an editor that
|
|
171
|
+
saves by replacing the file would silence a watch on it. One save fires several
|
|
172
|
+
events; they are read once, a moment after the last.
|
|
173
|
+
*/
|
|
174
|
+
let watch = (path: string): unit => {
|
|
175
|
+
unwatch()
|
|
176
|
+
let dir = NodePath.dirname(path)
|
|
177
|
+
let base = NodePath.basename(path)
|
|
178
|
+
let last = ref(NodeFs.existsSync(path) ? NodeFs.readFileSync(path) : "")
|
|
179
|
+
switch NodeFs.watch(dir, (_, file) =>
|
|
180
|
+
if file->Nullable.toOption->Option.mapOr(true, f => f == base) {
|
|
181
|
+
_pending.contents->Option.forEach(clearTimeout)
|
|
182
|
+
_pending := Some(setTimeout(() => {
|
|
183
|
+
_pending := None
|
|
184
|
+
switch NodeFs.readFileSync(path) {
|
|
185
|
+
| text if text != last.contents =>
|
|
186
|
+
last := text
|
|
187
|
+
switch reload(path) {
|
|
188
|
+
| Ok(said) =>
|
|
189
|
+
Console.log(`[LocalAuth] ${base} changed — reloaded ${said}.`)
|
|
190
|
+
switch unmetAfterReload() {
|
|
191
|
+
| [] => ()
|
|
192
|
+
| missing =>
|
|
193
|
+
Console.warn(
|
|
194
|
+
`[LocalAuth] No account is in the group of: ` ++
|
|
195
|
+
missing
|
|
196
|
+
->Array.map(RoleCoverage.describe)
|
|
197
|
+
->Array.join("; ") ++ ". Every such caller is refused until one is.",
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
| Error(e) => Console.warn(`[LocalAuth] ${base} changed but was not read: ${e}`)
|
|
201
|
+
}
|
|
202
|
+
| _ => ()
|
|
203
|
+
| exception _ => ()
|
|
204
|
+
}
|
|
205
|
+
}, 100))
|
|
206
|
+
}
|
|
207
|
+
) {
|
|
208
|
+
| w => _watcher := Some(w->NodeFs.watcherUnref)
|
|
209
|
+
| exception _ => ()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
117
213
|
/**
|
|
118
214
|
* Idempotent auto-discovery. Platform.startServers calls this before
|
|
119
215
|
* `DomainGraphQL_Server.start`, so a `.reventless/users.yaml` in cwd
|
|
120
|
-
* hydrates the Login store automatically
|
|
121
|
-
* called from anywhere.
|
|
216
|
+
* hydrates the Login store automatically, and follows the file from then on.
|
|
217
|
+
* No-op once `load` has been called from anywhere.
|
|
122
218
|
*/
|
|
123
219
|
let autoLoadOnce = (): unit =>
|
|
124
220
|
if !resolved.contents {
|
|
125
|
-
|
|
221
|
+
switch load() {
|
|
222
|
+
| Ok(DefaultFile(path)) | Ok(UsersFile(path)) => watch(path)
|
|
223
|
+
| _ => ()
|
|
224
|
+
}
|
|
126
225
|
}
|