@reventlessdev/reventless-local 3.0.0-alpha.299 → 3.0.0-alpha.301
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 +16 -0
- package/package.json +10 -10
- package/src/Platform.res +6 -6
- package/src/Platform.res.mjs +8 -8
- package/src/adapter/Auth/{UserStore.res → AccountStore.res} +17 -17
- package/src/adapter/Auth/{UserStore.res.mjs → AccountStore.res.mjs} +11 -11
- package/src/adapter/Auth/LocalAuth.res +5 -5
- package/src/adapter/LocalCapabilities.res +2 -2
- package/src/adapter/LocalCapabilities.res.mjs +1 -1
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -1
- package/src/capability/{Capability_IdentityProvider_UserStore.res → Capability_IdentityProvider_AccountStore.res} +1 -1
- package/tests/adapter/{LocalAuthUserStoreTest.res → LocalAuthAccountStoreTest.res} +48 -48
- package/tests/adapter/{LocalAuthUserStoreTest.res.mjs → LocalAuthAccountStoreTest.res.mjs} +44 -44
- package/tests/capability/{Capability_IdentityProvider_UserStoreTest.res → Capability_IdentityProvider_AccountStoreTest.res} +15 -13
- package/tests/capability/{Capability_IdentityProvider_UserStoreTest.res.mjs → Capability_IdentityProvider_AccountStoreTest.res.mjs} +15 -15
- package/tests/capability/LocalCapabilitiesTest.res +2 -2
- package/tests/capability/LocalCapabilitiesTest.res.mjs +3 -3
- /package/src/capability/{Capability_IdentityProvider_UserStore.res.mjs → Capability_IdentityProvider_AccountStore.res.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.301 (2026-10-25)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.300 (2026-10-25)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @reventlessdev/reventless-local
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.299 (2026-10-25)
|
|
7
23
|
|
|
8
24
|
* refactor!: internal results and tool records whose parts may be absent are optional fields ([7009ddf](https://github.com/ReventlessDev/reventless-core/commit/7009ddf5d522f39ec9946a3b5cd062ceae889bbf))
|
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.301",
|
|
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": {
|
|
@@ -35,23 +35,23 @@
|
|
|
35
35
|
"sury": "11.0.0",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "1.0.0-alpha.1",
|
|
38
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
38
39
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.13",
|
|
39
40
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.24",
|
|
40
41
|
"@reventlessdev/rescript-node": "2.0.0-alpha.23",
|
|
41
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.29",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "3.0.0-alpha.0",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-gwt": "1.0.0-alpha.
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.310",
|
|
44
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.258",
|
|
45
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.158",
|
|
46
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.207",
|
|
47
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.38",
|
|
48
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.174",
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.178"
|
|
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/Platform.res
CHANGED
|
@@ -109,7 +109,7 @@ module MakeWithConfig = (
|
|
|
109
109
|
// What this platform provides for the roles its plugins need: every account it
|
|
110
110
|
// can sign in as, built-in and loaded alike, or the file the store will load
|
|
111
111
|
// when the servers start — after the plugins are built and checked.
|
|
112
|
-
Reventless.Role.provideGroupsFrom(
|
|
112
|
+
Reventless.Role.provideGroupsFrom(AccountStore.providedGroups)
|
|
113
113
|
// Activate Pulumi mock mode — must happen before any component creation.
|
|
114
114
|
// Idempotent, so safe to call even if TestRunner.setup() was already called.
|
|
115
115
|
let _ = TestRunner.setup()
|
|
@@ -1377,11 +1377,11 @@ module MakeWithConfig = (
|
|
|
1377
1377
|
// this is a no-op (start() is called inline inside makePlatform/deployPlugin).
|
|
1378
1378
|
let startServers = () => {
|
|
1379
1379
|
if Config.splitApi {
|
|
1380
|
-
// Hydrate Login store from .reventless/
|
|
1380
|
+
// Hydrate Login store from .reventless/accounts.yaml (or ~accounts/~accountsFile
|
|
1381
1381
|
// if provided programmatically) before the Domain server accepts
|
|
1382
1382
|
// requests. Unified mode does this in makePlatform; split mode defers
|
|
1383
1383
|
// server startup to here so it must do the same.
|
|
1384
|
-
|
|
1384
|
+
AccountStore.autoLoadOnce()
|
|
1385
1385
|
DomainGraphQL_Server.start(~port=domainPort, ())
|
|
1386
1386
|
DomainMCP_Server.start(~port=domainMcpPort, ())
|
|
1387
1387
|
PlatformGraphQL_Server.start(
|
|
@@ -1579,7 +1579,7 @@ module MakeWithConfig = (
|
|
|
1579
1579
|
geocoderPlaceIndex?: ReventlessInfra.Platform.geocoderIndex,
|
|
1580
1580
|
messagingSender?: ReventlessInfra.Platform.messagingSender,
|
|
1581
1581
|
// Local only, until the AWS platform has a provider to pass the same way:
|
|
1582
|
-
// `
|
|
1582
|
+
// `Capability_IdentityProvider_AccountStore.make()`, writing the accounts file
|
|
1583
1583
|
// the auth adapter loads.
|
|
1584
1584
|
identityProvider?: Reventless.IdentityProvider.t,
|
|
1585
1585
|
uploadBucket?: ReventlessInfra.Platform.objectStore,
|
|
@@ -2350,7 +2350,7 @@ module MakeWithConfig = (
|
|
|
2350
2350
|
// (domain and platform) can register their schema first.
|
|
2351
2351
|
// In unified mode, start immediately (backwards-compatible).
|
|
2352
2352
|
if !Config.splitApi {
|
|
2353
|
-
|
|
2353
|
+
AccountStore.autoLoadOnce()
|
|
2354
2354
|
DomainGraphQL_Server.start()
|
|
2355
2355
|
DomainMCP_Server.start()
|
|
2356
2356
|
}
|
|
@@ -2534,7 +2534,7 @@ module MakeWithConfig = (
|
|
|
2534
2534
|
|
|
2535
2535
|
// The role check below reads the accounts as they stand; a user store
|
|
2536
2536
|
// loaded from elsewhere after this would leave it compared with the wrong ones.
|
|
2537
|
-
|
|
2537
|
+
AccountStore.pluginChecked := true
|
|
2538
2538
|
|
|
2539
2539
|
// Set the active deploy target so resolveTargetGraphQL/MCP() and QueryDb serverRef/relayRef
|
|
2540
2540
|
// route registrations to the correct server. Mirrors AWS resolveTargetApi() pattern.
|
package/src/Platform.res.mjs
CHANGED
|
@@ -30,12 +30,12 @@ import * as AdmissionCheck$Reventless from "@reventlessdev/reventless-spec/src/t
|
|
|
30
30
|
import * as Api_Naming$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Naming.res.mjs";
|
|
31
31
|
import * as CommandOutcome$Reventless from "@reventlessdev/reventless-spec/src/components/CommandOutcome.res.mjs";
|
|
32
32
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
33
|
-
import * as UserStore$ReventlessLocal from "./adapter/Auth/UserStore.res.mjs";
|
|
34
33
|
import * as TestRunner$ReventlessLocal from "./test/TestRunner.res.mjs";
|
|
35
34
|
import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
|
|
36
35
|
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
37
36
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
38
37
|
import * as ShellConfig$ReventlessLocal from "./ShellConfig.res.mjs";
|
|
38
|
+
import * as AccountStore$ReventlessLocal from "./adapter/Auth/AccountStore.res.mjs";
|
|
39
39
|
import * as BackendState$ReventlessLocal from "./adapter/BackendState.res.mjs";
|
|
40
40
|
import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
|
|
41
41
|
import * as SqliteDriver$ReventlessLocal from "./adapter/SqliteDriver.res.mjs";
|
|
@@ -146,7 +146,7 @@ function decodeUiFragmentRegistryEventEnvelope(eventJson) {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
function MakeWithConfig(Config) {
|
|
149
|
-
Role$Reventless.provideGroupsFrom(
|
|
149
|
+
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
150
150
|
TestRunner$ReventlessLocal.setup();
|
|
151
151
|
let match = Config.backend;
|
|
152
152
|
if (typeof match !== "object") {
|
|
@@ -1151,7 +1151,7 @@ function MakeWithConfig(Config) {
|
|
|
1151
1151
|
};
|
|
1152
1152
|
let startServers = () => {
|
|
1153
1153
|
if (Config.splitApi) {
|
|
1154
|
-
|
|
1154
|
+
AccountStore$ReventlessLocal.autoLoadOnce();
|
|
1155
1155
|
DomainGraphQL_Server$ReventlessLocal.start(domainPort, undefined, undefined);
|
|
1156
1156
|
DomainMCP_Server$ReventlessLocal.start(domainMcpPort, undefined);
|
|
1157
1157
|
PlatformGraphQL_Server$ReventlessLocal.start(platformPort, Auth_GraphqlContext$ReventlessLocal.buildAuthContext, undefined);
|
|
@@ -1694,7 +1694,7 @@ function MakeWithConfig(Config) {
|
|
|
1694
1694
|
}
|
|
1695
1695
|
currentDeployTarget.contents = "Domain";
|
|
1696
1696
|
if (!Config.splitApi) {
|
|
1697
|
-
|
|
1697
|
+
AccountStore$ReventlessLocal.autoLoadOnce();
|
|
1698
1698
|
DomainGraphQL_Server$ReventlessLocal.start(undefined, undefined, undefined);
|
|
1699
1699
|
return DomainMCP_Server$ReventlessLocal.start(undefined, undefined);
|
|
1700
1700
|
}
|
|
@@ -1795,7 +1795,7 @@ function MakeWithConfig(Config) {
|
|
|
1795
1795
|
let tmp;
|
|
1796
1796
|
tmp = apiTarget === "Domain" ? "Domain" : "Platform";
|
|
1797
1797
|
log.info("Platform", undefined, `deployPlugin target=` + tmp);
|
|
1798
|
-
|
|
1798
|
+
AccountStore$ReventlessLocal.pluginChecked.contents = true;
|
|
1799
1799
|
currentDeployTarget.contents = apiTarget;
|
|
1800
1800
|
StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
|
|
1801
1801
|
let tmp$1;
|
|
@@ -1948,7 +1948,7 @@ function MakeWithConfig(Config) {
|
|
|
1948
1948
|
|
|
1949
1949
|
function Make($star) {
|
|
1950
1950
|
let backend = Backend$ReventlessLocal.fromEnv();
|
|
1951
|
-
Role$Reventless.provideGroupsFrom(
|
|
1951
|
+
Role$Reventless.provideGroupsFrom(AccountStore$ReventlessLocal.providedGroups);
|
|
1952
1952
|
TestRunner$ReventlessLocal.setup();
|
|
1953
1953
|
if (typeof backend !== "object") {
|
|
1954
1954
|
BackendState$ReventlessLocal.setMemory();
|
|
@@ -2945,7 +2945,7 @@ function Make($star) {
|
|
|
2945
2945
|
});
|
|
2946
2946
|
};
|
|
2947
2947
|
let startServers = () => {
|
|
2948
|
-
|
|
2948
|
+
AccountStore$ReventlessLocal.autoLoadOnce();
|
|
2949
2949
|
DomainGraphQL_Server$ReventlessLocal.start(domainPort, undefined, undefined);
|
|
2950
2950
|
DomainMCP_Server$ReventlessLocal.start(domainMcpPort, undefined);
|
|
2951
2951
|
PlatformGraphQL_Server$ReventlessLocal.start(platformPort, Auth_GraphqlContext$ReventlessLocal.buildAuthContext, undefined);
|
|
@@ -3576,7 +3576,7 @@ function Make($star) {
|
|
|
3576
3576
|
let tmp;
|
|
3577
3577
|
tmp = apiTarget === "Domain" ? "Domain" : "Platform";
|
|
3578
3578
|
log.info("Platform", undefined, `deployPlugin target=` + tmp);
|
|
3579
|
-
|
|
3579
|
+
AccountStore$ReventlessLocal.pluginChecked.contents = true;
|
|
3580
3580
|
currentDeployTarget.contents = apiTarget;
|
|
3581
3581
|
StateViewSliceMaker.QueryDbResolvers.serverRef.contents = resolveTargetGraphQL();
|
|
3582
3582
|
let tmp$1;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// In-memory user store loader. Hydrates `LocalAuth.Login.store` from
|
|
2
2
|
// three resolution paths (first match wins):
|
|
3
|
-
// 1. `~
|
|
4
|
-
// 2. `~
|
|
5
|
-
// 3. `.reventless/
|
|
3
|
+
// 1. `~accounts` arg — explicit list passed by the caller
|
|
4
|
+
// 2. `~accountsFile` arg — explicit YAML path
|
|
5
|
+
// 3. `.reventless/accounts.yaml` (relative to process.cwd()) — auto-discovery
|
|
6
6
|
//
|
|
7
7
|
// All three are silent on absence: when no path is provided and no default
|
|
8
8
|
// file exists, the store stays empty and `LocalAuth.Login.issue` rejects
|
|
9
9
|
// every credential. A one-line stdout hint is printed so developers know to
|
|
10
|
-
// either set the file or pass `~
|
|
10
|
+
// either set the file or pass `~accounts` programmatically.
|
|
11
11
|
|
|
12
12
|
open Reventless
|
|
13
13
|
|
|
@@ -45,13 +45,13 @@ let _defaultPath = AccountsManifest.defaultPath
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Hydrates the Login store. Returns the resolution path actually used
|
|
48
|
-
* (`
|
|
48
|
+
* (`InlineAccounts`, `AccountsFile(path)`, `DefaultFile(path)`, or `Empty`).
|
|
49
49
|
* Errors from a *resolved* path are returned as `Error`; absence of the
|
|
50
50
|
* auto-discovered default file is silent.
|
|
51
51
|
*/
|
|
52
52
|
type resolution =
|
|
53
|
-
|
|
|
54
|
-
|
|
|
53
|
+
| InlineAccounts
|
|
54
|
+
| AccountsFile(string)
|
|
55
55
|
| DefaultFile(string)
|
|
56
56
|
| Empty
|
|
57
57
|
|
|
@@ -79,29 +79,29 @@ makes it from). Never the working directory once another source was loaded.
|
|
|
79
79
|
let providedGroups = (): array<string> =>
|
|
80
80
|
LocalAuth.knownGroups()->Array.concat(resolved.contents ? [] : AccountsManifest.declaredGroups())
|
|
81
81
|
|
|
82
|
-
let load = (~
|
|
82
|
+
let load = (~accounts: option<array<entry>>=?, ~accountsFile: option<string>=?, ()): result<
|
|
83
83
|
resolution,
|
|
84
84
|
string,
|
|
85
85
|
> => {
|
|
86
86
|
// The default file is what the check already read; any other source arrives
|
|
87
87
|
// after it, and the check would have compared the plugins with other accounts.
|
|
88
|
-
if pluginChecked.contents && (
|
|
88
|
+
if pluginChecked.contents && (accounts->Option.isSome || accountsFile->Option.isSome) {
|
|
89
89
|
JsError.throwWithMessage(
|
|
90
|
-
"
|
|
90
|
+
"AccountStore.load(~accounts / ~accountsFile) after a plugin was built — the check that every role it needs has a group read other accounts. Call AccountStore.load before deploying plugins.",
|
|
91
91
|
)
|
|
92
92
|
}
|
|
93
93
|
resolved := true
|
|
94
|
-
switch
|
|
94
|
+
switch accounts {
|
|
95
95
|
| Some(entries) =>
|
|
96
96
|
_registerEntries(entries)
|
|
97
|
-
Ok(
|
|
97
|
+
Ok(InlineAccounts)
|
|
98
98
|
| None =>
|
|
99
|
-
switch
|
|
99
|
+
switch accountsFile {
|
|
100
100
|
| Some(path) =>
|
|
101
101
|
parseFile(path)->Result.map(entries => {
|
|
102
102
|
_registerEntries(entries)
|
|
103
103
|
fromFile := entries->Array.map(e => e.username)
|
|
104
|
-
|
|
104
|
+
AccountsFile(path)
|
|
105
105
|
})
|
|
106
106
|
| None =>
|
|
107
107
|
let defaultPath = _defaultPath()
|
|
@@ -113,7 +113,7 @@ let load = (~users: option<array<entry>>=?, ~usersFile: option<string>=?, ()): r
|
|
|
113
113
|
})
|
|
114
114
|
} else {
|
|
115
115
|
Console.log(
|
|
116
|
-
"[LocalAuth] no
|
|
116
|
+
"[LocalAuth] no accounts configured — POST /__inmemory/login will reject all. " ++ "Provide ~accounts / ~accountsFile or create .reventless/accounts.yaml relative to cwd.",
|
|
117
117
|
)
|
|
118
118
|
Ok(Empty)
|
|
119
119
|
}
|
|
@@ -212,14 +212,14 @@ let watch = (path: string): unit => {
|
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* Idempotent auto-discovery. Platform.startServers calls this before
|
|
215
|
-
* `DomainGraphQL_Server.start`, so a `.reventless/
|
|
215
|
+
* `DomainGraphQL_Server.start`, so a `.reventless/accounts.yaml` in cwd
|
|
216
216
|
* hydrates the Login store automatically, and follows the file from then on.
|
|
217
217
|
* No-op once `load` has been called from anywhere.
|
|
218
218
|
*/
|
|
219
219
|
let autoLoadOnce = (): unit =>
|
|
220
220
|
if !resolved.contents {
|
|
221
221
|
switch load() {
|
|
222
|
-
| Ok(DefaultFile(path)) | Ok(
|
|
222
|
+
| Ok(DefaultFile(path)) | Ok(AccountsFile(path)) => watch(path)
|
|
223
223
|
| _ => ()
|
|
224
224
|
}
|
|
225
225
|
}
|
|
@@ -49,25 +49,25 @@ function providedGroups() {
|
|
|
49
49
|
return LocalAuth$ReventlessLocal.knownGroups().concat(resolved.contents ? [] : AccountsManifest$Reventless.declaredGroups());
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function load(
|
|
53
|
-
if (pluginChecked.contents && (Stdlib_Option.isSome(
|
|
54
|
-
Stdlib_JsError.throwWithMessage("
|
|
52
|
+
function load(accounts, accountsFile, param) {
|
|
53
|
+
if (pluginChecked.contents && (Stdlib_Option.isSome(accounts) || Stdlib_Option.isSome(accountsFile))) {
|
|
54
|
+
Stdlib_JsError.throwWithMessage("AccountStore.load(~accounts / ~accountsFile) after a plugin was built — the check that every role it needs has a group read other accounts. Call AccountStore.load before deploying plugins.");
|
|
55
55
|
}
|
|
56
56
|
resolved.contents = true;
|
|
57
|
-
if (
|
|
58
|
-
_registerEntries(
|
|
57
|
+
if (accounts !== undefined) {
|
|
58
|
+
_registerEntries(accounts);
|
|
59
59
|
return {
|
|
60
60
|
TAG: "Ok",
|
|
61
|
-
_0: "
|
|
61
|
+
_0: "InlineAccounts"
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
if (
|
|
65
|
-
return Stdlib_Result.map(AccountsManifest$Reventless.parseFile(
|
|
64
|
+
if (accountsFile !== undefined) {
|
|
65
|
+
return Stdlib_Result.map(AccountsManifest$Reventless.parseFile(accountsFile), entries => {
|
|
66
66
|
_registerEntries(entries);
|
|
67
67
|
fromFile.contents = entries.map(e => e.username);
|
|
68
68
|
return {
|
|
69
|
-
TAG: "
|
|
70
|
-
_0:
|
|
69
|
+
TAG: "AccountsFile",
|
|
70
|
+
_0: accountsFile
|
|
71
71
|
};
|
|
72
72
|
});
|
|
73
73
|
}
|
|
@@ -82,7 +82,7 @@ function load(users, usersFile, param) {
|
|
|
82
82
|
};
|
|
83
83
|
});
|
|
84
84
|
} else {
|
|
85
|
-
console.log("[LocalAuth] no
|
|
85
|
+
console.log("[LocalAuth] no accounts configured — POST /__inmemory/login will reject all. Provide ~accounts / ~accountsFile or create .reventless/accounts.yaml relative to cwd.");
|
|
86
86
|
return {
|
|
87
87
|
TAG: "Ok",
|
|
88
88
|
_0: "Empty"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// none of the above → Authenticated(defaultUser) (dev default)
|
|
9
9
|
//
|
|
10
10
|
// Stage A4 adds the `Login` submodule (token issuance + verification) and
|
|
11
|
-
// the companion `
|
|
11
|
+
// the companion `AccountStore` module (YAML hydration).
|
|
12
12
|
|
|
13
13
|
open Reventless
|
|
14
14
|
|
|
@@ -33,7 +33,7 @@ let adminUser = (): Identity.t => {
|
|
|
33
33
|
// ── User registry ─────────────────────────────────────────────────────────
|
|
34
34
|
//
|
|
35
35
|
// A `dict<Identity.t>` keyed by `username`. A4 hydrates this from
|
|
36
|
-
// `.reventless/
|
|
36
|
+
// `.reventless/accounts.yaml`; tests inject via `registerUser`. The built-in
|
|
37
37
|
// `admin` is answered on lookup, for the reason above, unless one is registered.
|
|
38
38
|
|
|
39
39
|
let initialUsers = () => Dict.fromArray([("user", defaultUser)])
|
|
@@ -86,7 +86,7 @@ let getHeader = (headers: dict<string>, name: string): option<string> =>
|
|
|
86
86
|
module Login = {
|
|
87
87
|
type credentials = {password: string, identity: Identity.t}
|
|
88
88
|
|
|
89
|
-
// Hydrated by `
|
|
89
|
+
// Hydrated by `AccountStore` at platform start; tests can inject directly.
|
|
90
90
|
let store: ref<dict<credentials>> = ref(Dict.make())
|
|
91
91
|
|
|
92
92
|
let setCredentials = (~username: string, ~password: string, ~identity: Identity.t): unit => {
|
|
@@ -127,13 +127,13 @@ module Login = {
|
|
|
127
127
|
// that has no `.reventless/` to write to.
|
|
128
128
|
//
|
|
129
129
|
// The directory is used but never created. Its presence is what distinguishes
|
|
130
|
-
// a platform someone develops against — `pnpm run setup` puts `
|
|
130
|
+
// a platform someone develops against — `pnpm run setup` puts `accounts.yaml`
|
|
131
131
|
// there, so anything with logins to preserve has one — from a unit test, and
|
|
132
132
|
// an auth module that made directories would leave one in every test's cwd.
|
|
133
133
|
//
|
|
134
134
|
// Local dev only, as the whole module is: these tokens are not security-grade
|
|
135
135
|
// and AWS never sees them. The file sits in a gitignored directory beside
|
|
136
|
-
// `
|
|
136
|
+
// `accounts.yaml`, which already holds plaintext dev passwords.
|
|
137
137
|
|
|
138
138
|
let _secretFileName = "token-secret"
|
|
139
139
|
let _secretDir = (): string => NodePath.join([NodeProcess.cwd(), ".reventless"])
|
|
@@ -31,7 +31,7 @@ let registerMessagingSender = (sender: string) => messagingSenderRef := sender
|
|
|
31
31
|
let identityProviderRef = ref(Reventless.Capabilities.none.identityProvider)
|
|
32
32
|
|
|
33
33
|
/** Provision the identity provider. Called by `makePlatform` from the handle a
|
|
34
|
-
root declared, `
|
|
34
|
+
root declared, `Capability_IdentityProvider_AccountStore.make()`. */
|
|
35
35
|
let registerIdentityProvider = (provider: Reventless.IdentityProvider.t) =>
|
|
36
36
|
identityProviderRef := provider
|
|
37
37
|
|
|
@@ -70,7 +70,7 @@ let unmetWarning = (~declared: array<(string, string)>): option<string> =>
|
|
|
70
70
|
| Messaging =>
|
|
71
71
|
`\n Messaging is \`Capability_Messaging_Log.make()\`, passed to the platform as ` ++ `\`~messagingSender\`.`
|
|
72
72
|
| IdentityProvider =>
|
|
73
|
-
`\n IdentityProvider is \`
|
|
73
|
+
`\n IdentityProvider is \`Capability_IdentityProvider_AccountStore.make()\`, passed to the ` ++ `platform as \`~identityProvider\`.`
|
|
74
74
|
// Never unmet here: the local platform always provisions one.
|
|
75
75
|
| Stash => ``
|
|
76
76
|
}
|
|
@@ -43,7 +43,7 @@ function unmetWarning(declared) {
|
|
|
43
43
|
case "Messaging" :
|
|
44
44
|
return `\n Messaging is \`Capability_Messaging_Log.make()\`, passed to the platform as \`~messagingSender\`.`;
|
|
45
45
|
case "IdentityProvider" :
|
|
46
|
-
return `\n IdentityProvider is \`
|
|
46
|
+
return `\n IdentityProvider is \`Capability_IdentityProvider_AccountStore.make()\`, passed to the platform as \`~identityProvider\`.`;
|
|
47
47
|
case "Stash" :
|
|
48
48
|
return ``;
|
|
49
49
|
}
|
|
@@ -205,7 +205,7 @@ let deleteOffloadAll = (~root: string): int => {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
// Removes the three trees, leaving the rest of the root (the database file, the
|
|
208
|
-
// dev `
|
|
208
|
+
// dev `accounts.yaml`) untouched.
|
|
209
209
|
let reset = (~root: string): unit =>
|
|
210
210
|
[objectsDir(~root), metaDir(~root), offloadDir(~root)]->Array.forEach(dir =>
|
|
211
211
|
try NodeFs.rmSync(dir, {recursive: true, force: true}) catch {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// `~identityProvider` in `~hostUiBundle`, and the platform puts it on the
|
|
5
5
|
// capability record every slice's `translate` receives.
|
|
6
6
|
//
|
|
7
|
-
// Its store is `.reventless/
|
|
7
|
+
// Its store is `.reventless/accounts.yaml`, the file the in-memory auth adapter
|
|
8
8
|
// already loads at startup, so an account made here survives a restart with no
|
|
9
9
|
// second store to keep in step. Every write goes to the file and to the live
|
|
10
10
|
// `LocalAuth.Login` store together, so the running process and the next one
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Stage A4 —
|
|
1
|
+
// Stage A4 — AccountStore.load resolution order and YAML parsing.
|
|
2
2
|
|
|
3
3
|
@@warning("-44")
|
|
4
4
|
|
|
@@ -7,12 +7,12 @@ open JestGlobals
|
|
|
7
7
|
let resetAll = () => {
|
|
8
8
|
LocalAuth.resetUsers()
|
|
9
9
|
LocalAuth.Login.resetStore()
|
|
10
|
-
|
|
10
|
+
AccountStore.resetResolution()
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
let _writeYamlFile = (contents: string): string => {
|
|
14
14
|
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-userstore-"]))
|
|
15
|
-
let path = NodePath.join([dir, "
|
|
15
|
+
let path = NodePath.join([dir, "accounts.yaml"])
|
|
16
16
|
NodeFs.writeFileSync(path, contents)
|
|
17
17
|
path
|
|
18
18
|
}
|
|
@@ -26,7 +26,7 @@ testPromise("parseString decodes a well-formed YAML document", async () => {
|
|
|
26
26
|
password: bob-pw
|
|
27
27
|
groups: []
|
|
28
28
|
`
|
|
29
|
-
switch
|
|
29
|
+
switch AccountStore.parseString(yaml) {
|
|
30
30
|
| Ok(entries) =>
|
|
31
31
|
expect(entries->Array.length)->toEqual(2)
|
|
32
32
|
let first = entries->Array.getUnsafe(0)
|
|
@@ -38,7 +38,7 @@ testPromise("parseString decodes a well-formed YAML document", async () => {
|
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
testPromise("parseString rejects malformed YAML", async () => {
|
|
41
|
-
switch
|
|
41
|
+
switch AccountStore.parseString("not: valid: yaml: at all: ::") {
|
|
42
42
|
| Error(_) => ()
|
|
43
43
|
| Ok(_) => JsError.throwWithMessage("expected Error for malformed YAML")
|
|
44
44
|
}
|
|
@@ -49,21 +49,21 @@ testPromise("parseString rejects entries missing required fields", async () => {
|
|
|
49
49
|
- username: alice
|
|
50
50
|
groups: [Editor]
|
|
51
51
|
`
|
|
52
|
-
switch
|
|
52
|
+
switch AccountStore.parseString(yaml) {
|
|
53
53
|
| Error(_) => ()
|
|
54
54
|
| Ok(_) => JsError.throwWithMessage("expected Error for missing password")
|
|
55
55
|
}
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
testPromise("load(~
|
|
58
|
+
testPromise("load(~accounts) registers credentials and returns InlineAccounts", async () => {
|
|
59
59
|
resetAll()
|
|
60
|
-
let entries: array<
|
|
60
|
+
let entries: array<AccountStore.entry> = [
|
|
61
61
|
{username: "alice", password: "alice-pw", groups: ["Editor"]},
|
|
62
62
|
{username: "bob", password: "bob-pw", groups: [], userId: "u-bob"},
|
|
63
63
|
]
|
|
64
|
-
switch
|
|
65
|
-
| Ok(
|
|
66
|
-
| Ok(_) => JsError.throwWithMessage("expected
|
|
64
|
+
switch AccountStore.load(~accounts=entries, ()) {
|
|
65
|
+
| Ok(InlineAccounts) => ()
|
|
66
|
+
| Ok(_) => JsError.throwWithMessage("expected InlineAccounts")
|
|
67
67
|
| Error(msg) => JsError.throwWithMessage("load failed: " ++ msg)
|
|
68
68
|
}
|
|
69
69
|
// Verify Login.issue accepts the credentials.
|
|
@@ -79,16 +79,16 @@ testPromise("load(~users) registers credentials and returns InlineUsers", async
|
|
|
79
79
|
}
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
testPromise("load(~
|
|
82
|
+
testPromise("load(~accountsFile) reads a YAML file from disk", async () => {
|
|
83
83
|
resetAll()
|
|
84
84
|
let path = _writeYamlFile(`
|
|
85
85
|
- username: carol
|
|
86
86
|
password: carol-pw
|
|
87
87
|
groups: [Admin]
|
|
88
88
|
`)
|
|
89
|
-
switch
|
|
90
|
-
| Ok(
|
|
91
|
-
| Ok(_) => JsError.throwWithMessage("expected
|
|
89
|
+
switch AccountStore.load(~accountsFile=path, ()) {
|
|
90
|
+
| Ok(AccountsFile(p)) => expect(p)->toEqual(path)
|
|
91
|
+
| Ok(_) => JsError.throwWithMessage("expected AccountsFile resolution")
|
|
92
92
|
| Error(msg) => JsError.throwWithMessage("load failed: " ++ msg)
|
|
93
93
|
}
|
|
94
94
|
let result = await LocalAuth.Login.issue(~username="carol", ~password="carol-pw")
|
|
@@ -99,9 +99,9 @@ testPromise("load(~usersFile) reads a YAML file from disk", async () => {
|
|
|
99
99
|
NodeFs.rmSync(path, {force: true})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
testPromise("load(~
|
|
102
|
+
testPromise("load(~accountsFile) returns Error when the file is missing", async () => {
|
|
103
103
|
resetAll()
|
|
104
|
-
switch
|
|
104
|
+
switch AccountStore.load(~accountsFile="/tmp/does-not-exist-reventless.yaml", ()) {
|
|
105
105
|
| Error(_) => ()
|
|
106
106
|
| Ok(_) => JsError.throwWithMessage("expected Error for missing file")
|
|
107
107
|
}
|
|
@@ -109,12 +109,12 @@ testPromise("load(~usersFile) returns Error when the file is missing", async ()
|
|
|
109
109
|
|
|
110
110
|
testPromise("load() with no args + no default file returns Empty", async () => {
|
|
111
111
|
resetAll()
|
|
112
|
-
// The test's cwd has no .reventless/
|
|
112
|
+
// The test's cwd has no .reventless/accounts.yaml — verify Empty is returned.
|
|
113
113
|
// (The package's own cwd is reventless-local/ during `pnpm test`, where
|
|
114
114
|
// no such file exists.)
|
|
115
|
-
switch
|
|
115
|
+
switch AccountStore.load() {
|
|
116
116
|
| Ok(Empty) => ()
|
|
117
|
-
| Ok(DefaultFile(_)) => // If a .reventless/
|
|
117
|
+
| Ok(DefaultFile(_)) => // If a .reventless/accounts.yaml happens to exist in CI, accept it.
|
|
118
118
|
()
|
|
119
119
|
| Ok(_) => JsError.throwWithMessage("expected Empty or DefaultFile")
|
|
120
120
|
| Error(msg) => JsError.throwWithMessage("load failed: " ++ msg)
|
|
@@ -128,12 +128,12 @@ testPromise("inline users win over a file path", async () => {
|
|
|
128
128
|
password: file-pw
|
|
129
129
|
groups: []
|
|
130
130
|
`)
|
|
131
|
-
let inline: array<
|
|
131
|
+
let inline: array<AccountStore.entry> = [
|
|
132
132
|
{username: "inline-user", password: "inline-pw", groups: []},
|
|
133
133
|
]
|
|
134
|
-
switch
|
|
135
|
-
| Ok(
|
|
136
|
-
| Ok(_) => JsError.throwWithMessage("expected
|
|
134
|
+
switch AccountStore.load(~accounts=inline, ~accountsFile=path, ()) {
|
|
135
|
+
| Ok(InlineAccounts) => ()
|
|
136
|
+
| Ok(_) => JsError.throwWithMessage("expected InlineAccounts (inline should win)")
|
|
137
137
|
| Error(msg) => JsError.throwWithMessage("load failed: " ++ msg)
|
|
138
138
|
}
|
|
139
139
|
// file-user must NOT be in the registry.
|
|
@@ -150,10 +150,10 @@ testPromise("inline users win over a file path", async () => {
|
|
|
150
150
|
|
|
151
151
|
testPromise("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
152
152
|
resetAll()
|
|
153
|
-
let _ =
|
|
154
|
-
// Second call should not crash even if .reventless/
|
|
153
|
+
let _ = AccountStore.load(~accounts=[{username: "x", password: "x", groups: []}], ())
|
|
154
|
+
// Second call should not crash even if .reventless/accounts.yaml doesn't
|
|
155
155
|
// exist; resolved=true prevents the default-discovery branch.
|
|
156
|
-
|
|
156
|
+
AccountStore.autoLoadOnce()
|
|
157
157
|
// Sanity: explicit-load registration persists.
|
|
158
158
|
switch LocalAuth.lookupUser("x") {
|
|
159
159
|
| Some(_) => ()
|
|
@@ -162,7 +162,7 @@ testPromise("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
|
162
162
|
})
|
|
163
163
|
|
|
164
164
|
// What the role check counts as provided, before the servers load the store.
|
|
165
|
-
describe("
|
|
165
|
+
describe("AccountStore.providedGroups", () => {
|
|
166
166
|
// Runs `f` in a directory holding `files` (path relative to it → contents).
|
|
167
167
|
let inDir = (files: array<(string, string)>, f) => {
|
|
168
168
|
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-provided-"]))
|
|
@@ -188,8 +188,8 @@ describe("UserStore.providedGroups", () => {
|
|
|
188
188
|
testSync("counts the default file the servers will load", () => {
|
|
189
189
|
resetAll()
|
|
190
190
|
let groups = inDir(
|
|
191
|
-
[(".reventless/
|
|
192
|
-
() =>
|
|
191
|
+
[(".reventless/accounts.yaml", manifest("Merchandiser"))],
|
|
192
|
+
() => AccountStore.providedGroups(),
|
|
193
193
|
)
|
|
194
194
|
expect(has("Merchandiser", groups))->toBe(true)
|
|
195
195
|
})
|
|
@@ -197,27 +197,27 @@ describe("UserStore.providedGroups", () => {
|
|
|
197
197
|
testSync("counts the template where setup has not made the file yet", () => {
|
|
198
198
|
resetAll()
|
|
199
199
|
let groups = inDir(
|
|
200
|
-
[("
|
|
201
|
-
() =>
|
|
200
|
+
[("accounts.example.yaml", manifest("Fulfilment"))],
|
|
201
|
+
() => AccountStore.providedGroups(),
|
|
202
202
|
)
|
|
203
203
|
expect(has("Fulfilment", groups))->toBe(true)
|
|
204
204
|
})
|
|
205
205
|
|
|
206
206
|
testSync("once loaded from elsewhere, counts those accounts and not the directory's", () => {
|
|
207
207
|
resetAll()
|
|
208
|
-
let _ =
|
|
208
|
+
let _ = AccountStore.load(~accounts=[{username: "i", password: "i", groups: ["Inline"]}], ())
|
|
209
209
|
let groups = inDir(
|
|
210
|
-
[(".reventless/
|
|
211
|
-
() =>
|
|
210
|
+
[(".reventless/accounts.yaml", manifest("Merchandiser"))],
|
|
211
|
+
() => AccountStore.providedGroups(),
|
|
212
212
|
)
|
|
213
213
|
expect((has("Inline", groups), has("Merchandiser", groups)))->toEqual((true, false))
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
testSync("loading other accounts after a plugin was checked is refused", () => {
|
|
217
217
|
resetAll()
|
|
218
|
-
|
|
219
|
-
let refused = switch
|
|
220
|
-
~
|
|
218
|
+
AccountStore.pluginChecked := true
|
|
219
|
+
let refused = switch AccountStore.load(
|
|
220
|
+
~accounts=[{username: "late", password: "l", groups: ["Late"]}],
|
|
221
221
|
(),
|
|
222
222
|
) {
|
|
223
223
|
| _ => false
|
|
@@ -228,13 +228,13 @@ describe("UserStore.providedGroups", () => {
|
|
|
228
228
|
|
|
229
229
|
testSync("the default load after a plugin was checked is the one the check read", () => {
|
|
230
230
|
resetAll()
|
|
231
|
-
|
|
232
|
-
let loaded = inDir([], () =>
|
|
231
|
+
AccountStore.pluginChecked := true
|
|
232
|
+
let loaded = inDir([], () => AccountStore.load()->Result.isOk)
|
|
233
233
|
expect(loaded)->toBe(true)
|
|
234
234
|
})
|
|
235
235
|
})
|
|
236
236
|
|
|
237
|
-
describe("
|
|
237
|
+
describe("AccountStore.reload", () => {
|
|
238
238
|
let signsIn = async (~username, ~password) =>
|
|
239
239
|
switch await LocalAuth.Login.issue(~username, ~password) {
|
|
240
240
|
| Ok(_) => true
|
|
@@ -251,7 +251,7 @@ describe("UserStore.reload", () => {
|
|
|
251
251
|
password: b1
|
|
252
252
|
groups: []
|
|
253
253
|
`)
|
|
254
|
-
let _ =
|
|
254
|
+
let _ = AccountStore.load(~accountsFile=path, ())
|
|
255
255
|
NodeFs.writeFileSync(
|
|
256
256
|
path,
|
|
257
257
|
`
|
|
@@ -263,7 +263,7 @@ describe("UserStore.reload", () => {
|
|
|
263
263
|
groups: [Driver]
|
|
264
264
|
`,
|
|
265
265
|
)
|
|
266
|
-
switch
|
|
266
|
+
switch AccountStore.reload(path) {
|
|
267
267
|
| Ok(said) => expect(said)->toEqual("2 accounts, removed: bob")
|
|
268
268
|
| Error(e) => JsError.throwWithMessage(e)
|
|
269
269
|
}
|
|
@@ -281,9 +281,9 @@ describe("UserStore.reload", () => {
|
|
|
281
281
|
password: a1
|
|
282
282
|
groups: [Editor]
|
|
283
283
|
`)
|
|
284
|
-
let _ =
|
|
284
|
+
let _ = AccountStore.load(~accountsFile=path, ())
|
|
285
285
|
NodeFs.writeFileSync(path, "- username: alice\n groups: [Editor]\n")
|
|
286
|
-
expect(
|
|
286
|
+
expect(AccountStore.reload(path)->Result.isError)->toBe(true)
|
|
287
287
|
expect(await signsIn(~username="alice", ~password="a1"))->toBe(true)
|
|
288
288
|
})
|
|
289
289
|
|
|
@@ -294,8 +294,8 @@ describe("UserStore.reload", () => {
|
|
|
294
294
|
password: a1
|
|
295
295
|
groups: [Editor]
|
|
296
296
|
`)
|
|
297
|
-
let _ =
|
|
298
|
-
|
|
297
|
+
let _ = AccountStore.load(~accountsFile=path, ())
|
|
298
|
+
AccountStore.watch(path)
|
|
299
299
|
NodeFs.writeFileSync(
|
|
300
300
|
path,
|
|
301
301
|
`
|
|
@@ -317,7 +317,7 @@ describe("UserStore.reload", () => {
|
|
|
317
317
|
await waitFor(tries - 1)
|
|
318
318
|
}
|
|
319
319
|
let added = await waitFor(60)
|
|
320
|
-
|
|
320
|
+
AccountStore.unwatch()
|
|
321
321
|
expect(added)->toBe(true)
|
|
322
322
|
})
|
|
323
323
|
})
|
|
@@ -7,23 +7,23 @@ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
|
7
7
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
8
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
9
9
|
import * as LocalAuth$ReventlessLocal from "../../src/adapter/Auth/LocalAuth.res.mjs";
|
|
10
|
-
import * as
|
|
10
|
+
import * as AccountStore$ReventlessLocal from "../../src/adapter/Auth/AccountStore.res.mjs";
|
|
11
11
|
|
|
12
12
|
function resetAll() {
|
|
13
13
|
LocalAuth$ReventlessLocal.resetUsers();
|
|
14
14
|
LocalAuth$ReventlessLocal.Login.resetStore();
|
|
15
|
-
|
|
15
|
+
AccountStore$ReventlessLocal.resetResolution();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function _writeYamlFile(contents) {
|
|
19
19
|
let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-userstore-"));
|
|
20
|
-
let path = Nodepath.join(dir, "
|
|
20
|
+
let path = Nodepath.join(dir, "accounts.yaml");
|
|
21
21
|
Nodefs.writeFileSync(path, contents, "utf8");
|
|
22
22
|
return path;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
globalThis.test("parseString decodes a well-formed YAML document", async () => {
|
|
26
|
-
let entries =
|
|
26
|
+
let entries = AccountStore$ReventlessLocal.parseString(`
|
|
27
27
|
- username: alice
|
|
28
28
|
password: alice-pw
|
|
29
29
|
groups: [Editor]
|
|
@@ -43,14 +43,14 @@ globalThis.test("parseString decodes a well-formed YAML document", async () => {
|
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
globalThis.test("parseString rejects malformed YAML", async () => {
|
|
46
|
-
let match =
|
|
46
|
+
let match = AccountStore$ReventlessLocal.parseString("not: valid: yaml: at all: ::");
|
|
47
47
|
if (match.TAG === "Ok") {
|
|
48
48
|
return Stdlib_JsError.throwWithMessage("expected Error for malformed YAML");
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
globalThis.test("parseString rejects entries missing required fields", async () => {
|
|
53
|
-
let match =
|
|
53
|
+
let match = AccountStore$ReventlessLocal.parseString(`
|
|
54
54
|
- username: alice
|
|
55
55
|
groups: [Editor]
|
|
56
56
|
`);
|
|
@@ -59,7 +59,7 @@ globalThis.test("parseString rejects entries missing required fields", async ()
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
globalThis.test("load(~
|
|
62
|
+
globalThis.test("load(~accounts) registers credentials and returns InlineAccounts", async () => {
|
|
63
63
|
resetAll();
|
|
64
64
|
let entries = [
|
|
65
65
|
{
|
|
@@ -74,11 +74,11 @@ globalThis.test("load(~users) registers credentials and returns InlineUsers", as
|
|
|
74
74
|
userId: "u-bob"
|
|
75
75
|
}
|
|
76
76
|
];
|
|
77
|
-
let msg =
|
|
77
|
+
let msg = AccountStore$ReventlessLocal.load(entries, undefined, undefined);
|
|
78
78
|
if (msg.TAG === "Ok") {
|
|
79
79
|
let tmp = msg._0;
|
|
80
|
-
if (typeof tmp === "object" || tmp !== "
|
|
81
|
-
Stdlib_JsError.throwWithMessage("expected
|
|
80
|
+
if (typeof tmp === "object" || tmp !== "InlineAccounts") {
|
|
81
|
+
Stdlib_JsError.throwWithMessage("expected InlineAccounts");
|
|
82
82
|
}
|
|
83
83
|
} else {
|
|
84
84
|
Stdlib_JsError.throwWithMessage("load failed: " + msg._0);
|
|
@@ -96,18 +96,18 @@ globalThis.test("load(~users) registers credentials and returns InlineUsers", as
|
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
globalThis.test("load(~
|
|
99
|
+
globalThis.test("load(~accountsFile) reads a YAML file from disk", async () => {
|
|
100
100
|
resetAll();
|
|
101
101
|
let path = _writeYamlFile(`
|
|
102
102
|
- username: carol
|
|
103
103
|
password: carol-pw
|
|
104
104
|
groups: [Admin]
|
|
105
105
|
`);
|
|
106
|
-
let msg =
|
|
106
|
+
let msg = AccountStore$ReventlessLocal.load(undefined, path, undefined);
|
|
107
107
|
if (msg.TAG === "Ok") {
|
|
108
108
|
let p = msg._0;
|
|
109
|
-
if (typeof p !== "object" || p.TAG !== "
|
|
110
|
-
Stdlib_JsError.throwWithMessage("expected
|
|
109
|
+
if (typeof p !== "object" || p.TAG !== "AccountsFile") {
|
|
110
|
+
Stdlib_JsError.throwWithMessage("expected AccountsFile resolution");
|
|
111
111
|
} else {
|
|
112
112
|
globalThis.expect(p._0).toEqual(path);
|
|
113
113
|
}
|
|
@@ -123,9 +123,9 @@ globalThis.test("load(~usersFile) reads a YAML file from disk", async () => {
|
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
globalThis.test("load(~
|
|
126
|
+
globalThis.test("load(~accountsFile) returns Error when the file is missing", async () => {
|
|
127
127
|
resetAll();
|
|
128
|
-
let match =
|
|
128
|
+
let match = AccountStore$ReventlessLocal.load(undefined, "/tmp/does-not-exist-reventless.yaml", undefined);
|
|
129
129
|
if (match.TAG === "Ok") {
|
|
130
130
|
return Stdlib_JsError.throwWithMessage("expected Error for missing file");
|
|
131
131
|
}
|
|
@@ -133,18 +133,18 @@ globalThis.test("load(~usersFile) returns Error when the file is missing", async
|
|
|
133
133
|
|
|
134
134
|
globalThis.test("load() with no args + no default file returns Empty", async () => {
|
|
135
135
|
resetAll();
|
|
136
|
-
let msg =
|
|
136
|
+
let msg = AccountStore$ReventlessLocal.load(undefined, undefined, undefined);
|
|
137
137
|
if (msg.TAG !== "Ok") {
|
|
138
138
|
return Stdlib_JsError.throwWithMessage("load failed: " + msg._0);
|
|
139
139
|
}
|
|
140
140
|
let tmp = msg._0;
|
|
141
141
|
if (typeof tmp !== "object") {
|
|
142
|
-
if (tmp === "
|
|
142
|
+
if (tmp === "InlineAccounts") {
|
|
143
143
|
return Stdlib_JsError.throwWithMessage("expected Empty or DefaultFile");
|
|
144
144
|
} else {
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
|
-
} else if (tmp.TAG === "
|
|
147
|
+
} else if (tmp.TAG === "AccountsFile") {
|
|
148
148
|
return Stdlib_JsError.throwWithMessage("expected Empty or DefaultFile");
|
|
149
149
|
} else {
|
|
150
150
|
return;
|
|
@@ -163,11 +163,11 @@ globalThis.test("inline users win over a file path", async () => {
|
|
|
163
163
|
password: "inline-pw",
|
|
164
164
|
groups: []
|
|
165
165
|
}];
|
|
166
|
-
let msg =
|
|
166
|
+
let msg = AccountStore$ReventlessLocal.load(inline, path, undefined);
|
|
167
167
|
if (msg.TAG === "Ok") {
|
|
168
168
|
let tmp = msg._0;
|
|
169
|
-
if (typeof tmp === "object" || tmp !== "
|
|
170
|
-
Stdlib_JsError.throwWithMessage("expected
|
|
169
|
+
if (typeof tmp === "object" || tmp !== "InlineAccounts") {
|
|
170
|
+
Stdlib_JsError.throwWithMessage("expected InlineAccounts (inline should win)");
|
|
171
171
|
}
|
|
172
172
|
} else {
|
|
173
173
|
Stdlib_JsError.throwWithMessage("load failed: " + msg._0);
|
|
@@ -189,12 +189,12 @@ globalThis.test("inline users win over a file path", async () => {
|
|
|
189
189
|
|
|
190
190
|
globalThis.test("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
191
191
|
resetAll();
|
|
192
|
-
|
|
192
|
+
AccountStore$ReventlessLocal.load([{
|
|
193
193
|
username: "x",
|
|
194
194
|
password: "x",
|
|
195
195
|
groups: []
|
|
196
196
|
}], undefined, undefined);
|
|
197
|
-
|
|
197
|
+
AccountStore$ReventlessLocal.autoLoadOnce();
|
|
198
198
|
let match = LocalAuth$ReventlessLocal.lookupUser("x");
|
|
199
199
|
if (match !== undefined) {
|
|
200
200
|
return;
|
|
@@ -203,7 +203,7 @@ globalThis.test("autoLoadOnce is a no-op after an explicit load", async () => {
|
|
|
203
203
|
}
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
globalThis.describe("
|
|
206
|
+
globalThis.describe("AccountStore.providedGroups", () => {
|
|
207
207
|
let inDir = (files, f) => {
|
|
208
208
|
let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-provided-"));
|
|
209
209
|
files.forEach(param => {
|
|
@@ -232,30 +232,30 @@ globalThis.describe("UserStore.providedGroups", () => {
|
|
|
232
232
|
globalThis.test("counts the default file the servers will load", () => {
|
|
233
233
|
resetAll();
|
|
234
234
|
let groups = inDir([[
|
|
235
|
-
".reventless/
|
|
235
|
+
".reventless/accounts.yaml",
|
|
236
236
|
`- username: m\n password: m\n groups: [` + "Merchandiser" + `]\n`
|
|
237
|
-
]], () =>
|
|
237
|
+
]], () => AccountStore$ReventlessLocal.providedGroups());
|
|
238
238
|
globalThis.expect(groups.includes("Merchandiser")).toBe(true);
|
|
239
239
|
});
|
|
240
240
|
globalThis.test("counts the template where setup has not made the file yet", () => {
|
|
241
241
|
resetAll();
|
|
242
242
|
let groups = inDir([[
|
|
243
|
-
"
|
|
243
|
+
"accounts.example.yaml",
|
|
244
244
|
`- username: m\n password: m\n groups: [` + "Fulfilment" + `]\n`
|
|
245
|
-
]], () =>
|
|
245
|
+
]], () => AccountStore$ReventlessLocal.providedGroups());
|
|
246
246
|
globalThis.expect(groups.includes("Fulfilment")).toBe(true);
|
|
247
247
|
});
|
|
248
248
|
globalThis.test("once loaded from elsewhere, counts those accounts and not the directory's", () => {
|
|
249
249
|
resetAll();
|
|
250
|
-
|
|
250
|
+
AccountStore$ReventlessLocal.load([{
|
|
251
251
|
username: "i",
|
|
252
252
|
password: "i",
|
|
253
253
|
groups: ["Inline"]
|
|
254
254
|
}], undefined, undefined);
|
|
255
255
|
let groups = inDir([[
|
|
256
|
-
".reventless/
|
|
256
|
+
".reventless/accounts.yaml",
|
|
257
257
|
`- username: m\n password: m\n groups: [` + "Merchandiser" + `]\n`
|
|
258
|
-
]], () =>
|
|
258
|
+
]], () => AccountStore$ReventlessLocal.providedGroups());
|
|
259
259
|
globalThis.expect([
|
|
260
260
|
groups.includes("Inline"),
|
|
261
261
|
groups.includes("Merchandiser")
|
|
@@ -266,10 +266,10 @@ globalThis.describe("UserStore.providedGroups", () => {
|
|
|
266
266
|
});
|
|
267
267
|
globalThis.test("loading other accounts after a plugin was checked is refused", () => {
|
|
268
268
|
resetAll();
|
|
269
|
-
|
|
269
|
+
AccountStore$ReventlessLocal.pluginChecked.contents = true;
|
|
270
270
|
let refused;
|
|
271
271
|
try {
|
|
272
|
-
|
|
272
|
+
AccountStore$ReventlessLocal.load([{
|
|
273
273
|
username: "late",
|
|
274
274
|
password: "l",
|
|
275
275
|
groups: ["Late"]
|
|
@@ -287,13 +287,13 @@ globalThis.describe("UserStore.providedGroups", () => {
|
|
|
287
287
|
});
|
|
288
288
|
globalThis.test("the default load after a plugin was checked is the one the check read", () => {
|
|
289
289
|
resetAll();
|
|
290
|
-
|
|
291
|
-
let loaded = inDir([], () => Stdlib_Result.isOk(
|
|
290
|
+
AccountStore$ReventlessLocal.pluginChecked.contents = true;
|
|
291
|
+
let loaded = inDir([], () => Stdlib_Result.isOk(AccountStore$ReventlessLocal.load(undefined, undefined, undefined)));
|
|
292
292
|
globalThis.expect(loaded).toBe(true);
|
|
293
293
|
});
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
-
globalThis.describe("
|
|
296
|
+
globalThis.describe("AccountStore.reload", () => {
|
|
297
297
|
let signsIn = async (username, password) => {
|
|
298
298
|
let match = await LocalAuth$ReventlessLocal.Login.issue(username, password, undefined);
|
|
299
299
|
return match.TAG === "Ok";
|
|
@@ -308,7 +308,7 @@ globalThis.describe("UserStore.reload", () => {
|
|
|
308
308
|
password: b1
|
|
309
309
|
groups: []
|
|
310
310
|
`);
|
|
311
|
-
|
|
311
|
+
AccountStore$ReventlessLocal.load(undefined, path, undefined);
|
|
312
312
|
Nodefs.writeFileSync(path, `
|
|
313
313
|
- username: alice
|
|
314
314
|
password: a2
|
|
@@ -317,7 +317,7 @@ globalThis.describe("UserStore.reload", () => {
|
|
|
317
317
|
password: driver
|
|
318
318
|
groups: [Driver]
|
|
319
319
|
`, "utf8");
|
|
320
|
-
let said =
|
|
320
|
+
let said = AccountStore$ReventlessLocal.reload(path);
|
|
321
321
|
if (said.TAG === "Ok") {
|
|
322
322
|
globalThis.expect(said._0).toEqual("2 accounts, removed: bob");
|
|
323
323
|
} else {
|
|
@@ -336,9 +336,9 @@ globalThis.describe("UserStore.reload", () => {
|
|
|
336
336
|
password: a1
|
|
337
337
|
groups: [Editor]
|
|
338
338
|
`);
|
|
339
|
-
|
|
339
|
+
AccountStore$ReventlessLocal.load(undefined, path, undefined);
|
|
340
340
|
Nodefs.writeFileSync(path, "- username: alice\n groups: [Editor]\n", "utf8");
|
|
341
|
-
globalThis.expect(Stdlib_Result.isError(
|
|
341
|
+
globalThis.expect(Stdlib_Result.isError(AccountStore$ReventlessLocal.reload(path))).toBe(true);
|
|
342
342
|
globalThis.expect(await signsIn("alice", "a1")).toBe(true);
|
|
343
343
|
});
|
|
344
344
|
globalThis.test("watch reads the file again once it is saved", async () => {
|
|
@@ -348,8 +348,8 @@ globalThis.describe("UserStore.reload", () => {
|
|
|
348
348
|
password: a1
|
|
349
349
|
groups: [Editor]
|
|
350
350
|
`);
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
AccountStore$ReventlessLocal.load(undefined, path, undefined);
|
|
352
|
+
AccountStore$ReventlessLocal.watch(path);
|
|
353
353
|
Nodefs.writeFileSync(path, `
|
|
354
354
|
- username: alice
|
|
355
355
|
password: a1
|
|
@@ -371,7 +371,7 @@ globalThis.describe("UserStore.reload", () => {
|
|
|
371
371
|
}
|
|
372
372
|
};
|
|
373
373
|
let added = await waitFor(60);
|
|
374
|
-
|
|
374
|
+
AccountStore$ReventlessLocal.unwatch();
|
|
375
375
|
globalThis.expect(added).toBe(true);
|
|
376
376
|
});
|
|
377
377
|
});
|
|
@@ -8,26 +8,28 @@ let email = address => Reventless.Messaging.ToEmail(Reventless.Email.unsafe(addr
|
|
|
8
8
|
|
|
9
9
|
let freshFile = (): string => {
|
|
10
10
|
let dir = NodeFs.mkdtempSync(NodePath.join([NodeOs.tmpdir(), "reventless-identity-"]))
|
|
11
|
-
NodePath.join([dir, ".reventless", "
|
|
11
|
+
NodePath.join([dir, ".reventless", "accounts.yaml"])
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
let resetAll = () => {
|
|
15
15
|
LocalAuth.resetUsers()
|
|
16
16
|
LocalAuth.Login.resetStore()
|
|
17
|
-
|
|
17
|
+
AccountStore.resetResolution()
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
let restartFrom = (file: string): unit => {
|
|
21
21
|
resetAll()
|
|
22
|
-
switch
|
|
22
|
+
switch AccountStore.load(~accountsFile=file, ()) {
|
|
23
23
|
| Ok(_) => ()
|
|
24
24
|
| Error(message) => JsError.throwWithMessage(`reload failed: ${message}`)
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
describe("
|
|
28
|
+
describe("Capability_IdentityProvider_AccountStore", () => {
|
|
29
29
|
testSync("publishes all four operations", () =>
|
|
30
|
-
expect(
|
|
30
|
+
expect(
|
|
31
|
+
Capability_IdentityProvider_AccountStore.make(~path=freshFile(), ()).operations,
|
|
32
|
+
)->toEqual([
|
|
31
33
|
Reventless.IdentityProvider.CreatePrincipal,
|
|
32
34
|
AddToGroup,
|
|
33
35
|
RemoveFromGroup,
|
|
@@ -38,7 +40,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
38
40
|
testPromise("a created principal can log in now and after a restart", async () => {
|
|
39
41
|
resetAll()
|
|
40
42
|
let file = freshFile()
|
|
41
|
-
let provider =
|
|
43
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=file, ())
|
|
42
44
|
let created = await provider.createPrincipal(
|
|
43
45
|
~contact=email("new@example.com"),
|
|
44
46
|
~credential=Password("secret-1"),
|
|
@@ -66,7 +68,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
66
68
|
|
|
67
69
|
testPromise("the same contact twice is a Conflict, not a second account", async () => {
|
|
68
70
|
resetAll()
|
|
69
|
-
let provider =
|
|
71
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=freshFile(), ())
|
|
70
72
|
let create = () =>
|
|
71
73
|
provider.createPrincipal(
|
|
72
74
|
~contact=email("twice@example.com"),
|
|
@@ -81,7 +83,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
81
83
|
// exists for the next provisioning step without a credential anyone holds.
|
|
82
84
|
testPromise("NoCredential writes an account no password opens", async () => {
|
|
83
85
|
resetAll()
|
|
84
|
-
let provider =
|
|
86
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=freshFile(), ())
|
|
85
87
|
let _ = await provider.createPrincipal(
|
|
86
88
|
~contact=email("later@example.com"),
|
|
87
89
|
~credential=NoCredential,
|
|
@@ -94,7 +96,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
94
96
|
})
|
|
95
97
|
|
|
96
98
|
testPromise("a push address is refused, since nothing signs in with one", async () => {
|
|
97
|
-
let provider =
|
|
99
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=freshFile(), ())
|
|
98
100
|
let answer = await provider.createPrincipal(
|
|
99
101
|
~contact=ToPush(Fcm({registrationToken: "tok"})),
|
|
100
102
|
~credential=NoCredential,
|
|
@@ -111,7 +113,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
111
113
|
testPromise("groups round-trip through the file", async () => {
|
|
112
114
|
resetAll()
|
|
113
115
|
let file = freshFile()
|
|
114
|
-
let provider =
|
|
116
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=file, ())
|
|
115
117
|
let principal: Reventless.IdentityProvider.principal = {providerId: "grp@example.com"}
|
|
116
118
|
let _ = await provider.createPrincipal(
|
|
117
119
|
~contact=email("grp@example.com"),
|
|
@@ -133,7 +135,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
133
135
|
|
|
134
136
|
testPromise("grouping a principal that does not exist is refused", async () => {
|
|
135
137
|
resetAll()
|
|
136
|
-
let provider =
|
|
138
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=freshFile(), ())
|
|
137
139
|
let answer = await provider.addToGroup(~principal={providerId: "ghost"}, ~group="Shopper")
|
|
138
140
|
expect(answer)->toEqual(Error(Reventless.IdentityProvider.Refused("no principal ghost")))
|
|
139
141
|
})
|
|
@@ -141,7 +143,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
141
143
|
testPromise("deleting removes the account from the file and the login, twice", async () => {
|
|
142
144
|
resetAll()
|
|
143
145
|
let file = freshFile()
|
|
144
|
-
let provider =
|
|
146
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=file, ())
|
|
145
147
|
let principal: Reventless.IdentityProvider.principal = {providerId: "gone@example.com"}
|
|
146
148
|
let _ = await provider.createPrincipal(
|
|
147
149
|
~contact=email("gone@example.com"),
|
|
@@ -166,7 +168,7 @@ describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
166
168
|
groups: [Admin]
|
|
167
169
|
`,
|
|
168
170
|
)
|
|
169
|
-
let provider =
|
|
171
|
+
let provider = Capability_IdentityProvider_AccountStore.make(~path=file, ())
|
|
170
172
|
let _ = await provider.createPrincipal(
|
|
171
173
|
~contact=email("c@example.com"),
|
|
172
174
|
~credential=Password("pw"),
|
|
@@ -7,9 +7,9 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
7
7
|
import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
8
8
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
9
|
import * as LocalAuth$ReventlessLocal from "../../src/adapter/Auth/LocalAuth.res.mjs";
|
|
10
|
-
import * as UserStore$ReventlessLocal from "../../src/adapter/Auth/UserStore.res.mjs";
|
|
11
10
|
import * as Auth_UserId$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_UserId.res.mjs";
|
|
12
|
-
import * as
|
|
11
|
+
import * as AccountStore$ReventlessLocal from "../../src/adapter/Auth/AccountStore.res.mjs";
|
|
12
|
+
import * as Capability_IdentityProvider_AccountStore$ReventlessLocal from "../../src/capability/Capability_IdentityProvider_AccountStore.res.mjs";
|
|
13
13
|
|
|
14
14
|
function email(address) {
|
|
15
15
|
return {
|
|
@@ -20,18 +20,18 @@ function email(address) {
|
|
|
20
20
|
|
|
21
21
|
function freshFile() {
|
|
22
22
|
let dir = Nodefs.mkdtempSync(Nodepath.join(Nodeos.tmpdir(), "reventless-identity-"));
|
|
23
|
-
return Nodepath.join(dir, ".reventless", "
|
|
23
|
+
return Nodepath.join(dir, ".reventless", "accounts.yaml");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function resetAll() {
|
|
27
27
|
LocalAuth$ReventlessLocal.resetUsers();
|
|
28
28
|
LocalAuth$ReventlessLocal.Login.resetStore();
|
|
29
|
-
|
|
29
|
+
AccountStore$ReventlessLocal.resetResolution();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function restartFrom(file) {
|
|
33
33
|
resetAll();
|
|
34
|
-
let message =
|
|
34
|
+
let message = AccountStore$ReventlessLocal.load(undefined, file, undefined);
|
|
35
35
|
if (message.TAG === "Ok") {
|
|
36
36
|
return;
|
|
37
37
|
} else {
|
|
@@ -39,9 +39,9 @@ function restartFrom(file) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
globalThis.describe("
|
|
42
|
+
globalThis.describe("Capability_IdentityProvider_AccountStore", () => {
|
|
43
43
|
globalThis.test("publishes all four operations", () => {
|
|
44
|
-
globalThis.expect(
|
|
44
|
+
globalThis.expect(Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined).operations).toEqual([
|
|
45
45
|
"CreatePrincipal",
|
|
46
46
|
"AddToGroup",
|
|
47
47
|
"RemoveFromGroup",
|
|
@@ -51,7 +51,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
51
51
|
globalThis.test("a created principal can log in now and after a restart", async () => {
|
|
52
52
|
resetAll();
|
|
53
53
|
let file = freshFile();
|
|
54
|
-
let provider =
|
|
54
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
|
|
55
55
|
let created = await provider.createPrincipal({
|
|
56
56
|
TAG: "ToEmail",
|
|
57
57
|
_0: "new@example.com"
|
|
@@ -75,7 +75,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
75
75
|
});
|
|
76
76
|
globalThis.test("the same contact twice is a Conflict, not a second account", async () => {
|
|
77
77
|
resetAll();
|
|
78
|
-
let provider =
|
|
78
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
|
|
79
79
|
let create = () => provider.createPrincipal({
|
|
80
80
|
TAG: "ToEmail",
|
|
81
81
|
_0: "twice@example.com"
|
|
@@ -88,7 +88,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
88
88
|
});
|
|
89
89
|
globalThis.test("NoCredential writes an account no password opens", async () => {
|
|
90
90
|
resetAll();
|
|
91
|
-
let provider =
|
|
91
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
|
|
92
92
|
await provider.createPrincipal({
|
|
93
93
|
TAG: "ToEmail",
|
|
94
94
|
_0: "later@example.com"
|
|
@@ -97,7 +97,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
97
97
|
globalThis.expect(Stdlib_Result.isOk(await LocalAuth$ReventlessLocal.Login.issue("later@example.com", "", undefined))).toBe(true);
|
|
98
98
|
});
|
|
99
99
|
globalThis.test("a push address is refused, since nothing signs in with one", async () => {
|
|
100
|
-
let provider =
|
|
100
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
|
|
101
101
|
let answer = await provider.createPrincipal({
|
|
102
102
|
TAG: "ToPush",
|
|
103
103
|
_0: {
|
|
@@ -117,7 +117,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
117
117
|
globalThis.test("groups round-trip through the file", async () => {
|
|
118
118
|
resetAll();
|
|
119
119
|
let file = freshFile();
|
|
120
|
-
let provider =
|
|
120
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
|
|
121
121
|
let principal = {
|
|
122
122
|
providerId: "grp@example.com"
|
|
123
123
|
};
|
|
@@ -150,7 +150,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
150
150
|
});
|
|
151
151
|
globalThis.test("grouping a principal that does not exist is refused", async () => {
|
|
152
152
|
resetAll();
|
|
153
|
-
let provider =
|
|
153
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(freshFile(), undefined);
|
|
154
154
|
let answer = await provider.addToGroup({
|
|
155
155
|
providerId: "ghost"
|
|
156
156
|
}, "Shopper");
|
|
@@ -165,7 +165,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
165
165
|
globalThis.test("deleting removes the account from the file and the login, twice", async () => {
|
|
166
166
|
resetAll();
|
|
167
167
|
let file = freshFile();
|
|
168
|
-
let provider =
|
|
168
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
|
|
169
169
|
let principal = {
|
|
170
170
|
providerId: "gone@example.com"
|
|
171
171
|
};
|
|
@@ -198,7 +198,7 @@ globalThis.describe("Capability_IdentityProvider_UserStore", () => {
|
|
|
198
198
|
password: pw
|
|
199
199
|
groups: [Admin]
|
|
200
200
|
`, "utf8");
|
|
201
|
-
let provider =
|
|
201
|
+
let provider = Capability_IdentityProvider_AccountStore$ReventlessLocal.make(file, undefined);
|
|
202
202
|
await provider.createPrincipal({
|
|
203
203
|
TAG: "ToEmail",
|
|
204
204
|
_0: "c@example.com"
|
|
@@ -22,14 +22,14 @@ describe("LocalCapabilities.unmetWarning", () => {
|
|
|
22
22
|
let message = LocalCapabilities.unmetWarning(~declared)->Option.getOr("")
|
|
23
23
|
expect(message->String.includes("Geocoding (GeocodeAddress)"))->toBe(true)
|
|
24
24
|
expect(message->String.includes("Capability_Messaging_Log.make()"))->toBe(true)
|
|
25
|
-
expect(message->String.includes("
|
|
25
|
+
expect(message->String.includes("Capability_IdentityProvider_AccountStore.make()"))->toBe(true)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
testSync("a registered provider leaves the warning", () => {
|
|
29
29
|
reset()
|
|
30
30
|
LocalCapabilities.registerMessagingSender("Shop <shop@example.test>")
|
|
31
31
|
LocalCapabilities.registerIdentityProvider(
|
|
32
|
-
|
|
32
|
+
Capability_IdentityProvider_AccountStore.make(~path="/nonexistent/accounts.yaml", ()),
|
|
33
33
|
)
|
|
34
34
|
expect(LocalCapabilities.provisioned())->toEqual([
|
|
35
35
|
Reventless.CapabilityNeed.Messaging,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
4
|
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
5
5
|
import * as LocalCapabilities$ReventlessLocal from "../../src/adapter/LocalCapabilities.res.mjs";
|
|
6
|
-
import * as
|
|
6
|
+
import * as Capability_IdentityProvider_AccountStore$ReventlessLocal from "../../src/capability/Capability_IdentityProvider_AccountStore.res.mjs";
|
|
7
7
|
|
|
8
8
|
let declared = [
|
|
9
9
|
[
|
|
@@ -32,12 +32,12 @@ globalThis.describe("LocalCapabilities.unmetWarning", () => {
|
|
|
32
32
|
let message = Stdlib_Option.getOr(LocalCapabilities$ReventlessLocal.unmetWarning(declared), "");
|
|
33
33
|
globalThis.expect(message.includes("Geocoding (GeocodeAddress)")).toBe(true);
|
|
34
34
|
globalThis.expect(message.includes("Capability_Messaging_Log.make()")).toBe(true);
|
|
35
|
-
globalThis.expect(message.includes("
|
|
35
|
+
globalThis.expect(message.includes("Capability_IdentityProvider_AccountStore.make()")).toBe(true);
|
|
36
36
|
});
|
|
37
37
|
globalThis.test("a registered provider leaves the warning", () => {
|
|
38
38
|
reset();
|
|
39
39
|
LocalCapabilities$ReventlessLocal.registerMessagingSender("Shop <shop@example.test>");
|
|
40
|
-
LocalCapabilities$ReventlessLocal.registerIdentityProvider(
|
|
40
|
+
LocalCapabilities$ReventlessLocal.registerIdentityProvider(Capability_IdentityProvider_AccountStore$ReventlessLocal.make("/nonexistent/accounts.yaml", undefined));
|
|
41
41
|
globalThis.expect(LocalCapabilities$ReventlessLocal.provisioned()).toEqual([
|
|
42
42
|
"Messaging",
|
|
43
43
|
"IdentityProvider",
|