@o3co/auth-provider-foundation 0.16.0 → 0.17.0-rc.2
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/README.ja.md +154 -29
- package/README.md +482 -45
- package/config/reference.conf +20 -0
- package/dist/endpointUrl.d.mts +21 -47
- package/dist/endpointUrl.d.mts.map +1 -1
- package/dist/endpointUrl.mjs +23 -50
- package/dist/index.d.mts +5 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +18 -35
- package/dist/mfa/HttpMfaFactorStore.d.mts +55 -0
- package/dist/mfa/HttpMfaFactorStore.d.mts.map +1 -0
- package/dist/mfa/HttpMfaFactorStore.mjs +183 -0
- package/dist/mfa/markEnrolled.d.mts +4 -0
- package/dist/mfa/markEnrolled.d.mts.map +1 -0
- package/dist/mfa/markEnrolled.mjs +31 -0
- package/dist/mfa/module.d.mts +27 -0
- package/dist/mfa/module.d.mts.map +1 -0
- package/dist/mfa/module.mjs +56 -0
- package/dist/mfa/section.d.mts +45 -0
- package/dist/mfa/section.d.mts.map +1 -0
- package/dist/mfa/section.mjs +99 -0
- package/dist/mfa/storeFailure.d.mts +52 -0
- package/dist/mfa/storeFailure.d.mts.map +1 -0
- package/dist/mfa/storeFailure.mjs +107 -0
- package/dist/repositories/HttpUserRepository.d.mts +54 -79
- package/dist/repositories/HttpUserRepository.d.mts.map +1 -1
- package/dist/repositories/HttpUserRepository.mjs +117 -413
- package/dist/repositories/storeErrors.d.mts +18 -27
- package/dist/repositories/storeErrors.d.mts.map +1 -1
- package/dist/repositories/storeErrors.mjs +30 -40
- package/dist/repositories/wwwAuthenticate.d.mts.map +1 -1
- package/dist/repositories/wwwAuthenticate.mjs +12 -16
- package/dist/storeTransport.d.mts +97 -0
- package/dist/storeTransport.d.mts.map +1 -0
- package/dist/storeTransport.mjs +290 -0
- package/dist/testing/index.d.mts +28 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +58 -0
- package/package.json +18 -7
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Foundation defaults -- @o3co/auth-provider-foundation
|
|
2
|
+
#
|
|
3
|
+
# The section of the module that keeps a subject's MFA factors in the Store,
|
|
4
|
+
# `foundation-mfa-factor-store`: the Store's four MFA factor endpoints, each
|
|
5
|
+
# a POST of JSON (the package README, "The Store's MFA endpoints"). A
|
|
6
|
+
# composition root that installs a module reading the section layers this
|
|
7
|
+
# file between its own configuration and core's reference.conf.
|
|
8
|
+
#
|
|
9
|
+
# None has a default, and each is required: a module reading the section
|
|
10
|
+
# refuses the boot with any of them unset, naming each one missing. Each
|
|
11
|
+
# must be https, or http to a loopback host (localhost, 127.0.0.0/8, [::1]),
|
|
12
|
+
# and the endpoint that answers: no request follows a redirect. Each key has
|
|
13
|
+
# its variable, FOUNDATION_MFA_FACTOR_STORE_<KEY>.
|
|
14
|
+
|
|
15
|
+
foundation-mfa-factor-store {
|
|
16
|
+
listUrl = ${?FOUNDATION_MFA_FACTOR_STORE_LIST_URL}
|
|
17
|
+
createUrl = ${?FOUNDATION_MFA_FACTOR_STORE_CREATE_URL}
|
|
18
|
+
updateUrl = ${?FOUNDATION_MFA_FACTOR_STORE_UPDATE_URL}
|
|
19
|
+
deleteUrl = ${?FOUNDATION_MFA_FACTOR_STORE_DELETE_URL}
|
|
20
|
+
}
|
package/dist/endpointUrl.d.mts
CHANGED
|
@@ -1,41 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transport validation for the Store endpoints
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* force every local development setup and every in-process test fixture to
|
|
16
|
-
* provision a certificate for no security gain. Any other host — including a
|
|
17
|
-
* private-range address such as `10.0.0.5` or a service name on a container
|
|
18
|
-
* network — must use `https://`: those cross a network the deployment does not
|
|
19
|
-
* control end to end, and "internal" is not a synonym for "encrypted".
|
|
20
|
-
*
|
|
21
|
-
* The rule matches `oauth.jwt.issuer`'s (`checkCanonicalIssuer` in
|
|
22
|
-
* `@o3co/auth-provider-core`) so operators meet one policy, not two. It is kept
|
|
23
|
-
* separate rather than reused because the constraints genuinely differ: an
|
|
24
|
-
* issuer may not carry a query string or fragment (OIDC Discovery derives the
|
|
25
|
-
* metadata URL from it), while a Store endpoint is an ordinary POST target for
|
|
26
|
-
* which `?tenant=acme` is legitimate. The loopback *predicate* the carve-out
|
|
27
|
-
* runs on, though, is shared vocabulary: `isLoopbackHostname` comes from
|
|
28
|
-
* `@o3co/auth-provider-core` (`net/loopback`, #364), the same definition the
|
|
29
|
-
* session redirect policy consumes, so the carve-outs cannot drift apart.
|
|
30
|
-
*
|
|
31
|
-
* ## What a message names of an endpoint
|
|
32
|
-
*
|
|
33
|
-
* Every failure the repository throws names the endpoint it was talking to,
|
|
34
|
-
* and every caller logs what it throws. A message names the endpoint by its
|
|
35
|
-
* origin and path alone ({@link endpointForMessage}): a query string is
|
|
36
|
-
* accepted in a Store URL, and a deployment may carry a credential there
|
|
37
|
-
* (`?api_key=…`) despite the README's advice, so the query never reaches a
|
|
38
|
-
* message — nor does a fragment, which no request sends anyway.
|
|
2
|
+
* Transport validation for the Store endpoints the Store clients post to —
|
|
3
|
+
* `HttpUserRepository`'s and `HttpMfaFactorStore`'s: `https://`, or
|
|
4
|
+
* `http://` to a loopback host only (`localhost`, `127.0.0.0/8`, `[::1]`),
|
|
5
|
+
* because what they carry — credentials, verified identities, sealed factor
|
|
6
|
+
* records — must not cross a network in the clear. See README, Constructor
|
|
7
|
+
* validation.
|
|
8
|
+
*
|
|
9
|
+
* Kept separate from `oauth.jwt.issuer`'s rule (core's `checkCanonicalIssuer`)
|
|
10
|
+
* because an issuer may not carry a query string, while a Store endpoint may.
|
|
11
|
+
* The loopback predicate is core's `isLoopbackHostname`, the session redirect
|
|
12
|
+
* policy's too, so the carve-outs cannot drift apart. A message names an
|
|
13
|
+
* endpoint by origin and path alone ({@link endpointForMessage}): a query may
|
|
14
|
+
* carry a credential, and every caller logs what a client throws.
|
|
39
15
|
*/
|
|
40
16
|
import { isLoopbackHostname } from "@o3co/auth-provider-core";
|
|
41
17
|
export { isLoopbackHostname };
|
|
@@ -58,22 +34,20 @@ export declare function checkSecureEndpoint(value: unknown): EndpointRejection |
|
|
|
58
34
|
export declare function describeEndpointRejection(reason: EndpointRejection): string;
|
|
59
35
|
/**
|
|
60
36
|
* Returns `value` when it is a usable Store endpoint, otherwise throws naming
|
|
61
|
-
* `field` and the reason.
|
|
37
|
+
* `owner`, `field` and the reason.
|
|
62
38
|
*
|
|
63
39
|
* The rejected value is deliberately **not** echoed into the message: it is
|
|
64
40
|
* operator-supplied configuration that may embed a secret, and a boot error
|
|
65
41
|
* lands in logs.
|
|
66
42
|
*/
|
|
67
|
-
export declare function assertSecureEndpoint(value: unknown, field: string): string;
|
|
43
|
+
export declare function assertSecureEndpoint(value: unknown, field: string, owner?: string): string;
|
|
68
44
|
/**
|
|
69
|
-
* `url` as a message names it: its origin and path
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* constructor refuses such a Store URL anyway — but a caller can hand
|
|
76
|
-
* `StoreCredentialRefusedError` anything.
|
|
45
|
+
* `url` as a message names it: its origin and path, as the WHATWG URL parser
|
|
46
|
+
* normalises them (`https://store.example/authenticate`), and never its query
|
|
47
|
+
* or fragment, which may carry a credential. Anything that is not an http or
|
|
48
|
+
* https URL is named by what it is not, since its origin would be `"null"` and
|
|
49
|
+
* its path the rest of it (`data:…`): the constructor refuses such a Store URL,
|
|
50
|
+
* but a caller can hand `StoreCredentialRefusedError` anything.
|
|
77
51
|
*/
|
|
78
52
|
export declare function endpointForMessage(url: string): string;
|
|
79
53
|
//# sourceMappingURL=endpointUrl.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpointUrl.d.mts","sourceRoot":"","sources":["../src/endpointUrl.mts"],"names":[],"mappings":"AAgBA
|
|
1
|
+
{"version":3,"file":"endpointUrl.d.mts","sourceRoot":"","sources":["../src/endpointUrl.mts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,4EAA4E;AAC5E,MAAM,MAAM,iBAAiB,GAC1B,cAAc,GACd,OAAO,GACP,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,iBAAiB,CAAC;AAKrB;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,GAAG,IAAI,CAyB5E;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAwB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,KAAK,SAAuB,GAC1B,MAAM,CAQR;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD"}
|
package/dist/endpointUrl.mjs
CHANGED
|
@@ -14,48 +14,24 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
|
-
* Transport validation for the Store endpoints
|
|
17
|
+
* Transport validation for the Store endpoints the Store clients post to —
|
|
18
|
+
* `HttpUserRepository`'s and `HttpMfaFactorStore`'s: `https://`, or
|
|
19
|
+
* `http://` to a loopback host only (`localhost`, `127.0.0.0/8`, `[::1]`),
|
|
20
|
+
* because what they carry — credentials, verified identities, sealed factor
|
|
21
|
+
* records — must not cross a network in the clear. See README, Constructor
|
|
22
|
+
* validation.
|
|
18
23
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* ## The loopback carve-out
|
|
26
|
-
*
|
|
27
|
-
* `http://` is accepted for **loopback hosts only** — `localhost`, anything in
|
|
28
|
-
* `127.0.0.0/8`, and `[::1]`. Traffic to a loopback address never leaves the
|
|
29
|
-
* machine, so there is no path to eavesdrop on, and requiring TLS there would
|
|
30
|
-
* force every local development setup and every in-process test fixture to
|
|
31
|
-
* provision a certificate for no security gain. Any other host — including a
|
|
32
|
-
* private-range address such as `10.0.0.5` or a service name on a container
|
|
33
|
-
* network — must use `https://`: those cross a network the deployment does not
|
|
34
|
-
* control end to end, and "internal" is not a synonym for "encrypted".
|
|
35
|
-
*
|
|
36
|
-
* The rule matches `oauth.jwt.issuer`'s (`checkCanonicalIssuer` in
|
|
37
|
-
* `@o3co/auth-provider-core`) so operators meet one policy, not two. It is kept
|
|
38
|
-
* separate rather than reused because the constraints genuinely differ: an
|
|
39
|
-
* issuer may not carry a query string or fragment (OIDC Discovery derives the
|
|
40
|
-
* metadata URL from it), while a Store endpoint is an ordinary POST target for
|
|
41
|
-
* which `?tenant=acme` is legitimate. The loopback *predicate* the carve-out
|
|
42
|
-
* runs on, though, is shared vocabulary: `isLoopbackHostname` comes from
|
|
43
|
-
* `@o3co/auth-provider-core` (`net/loopback`, #364), the same definition the
|
|
44
|
-
* session redirect policy consumes, so the carve-outs cannot drift apart.
|
|
45
|
-
*
|
|
46
|
-
* ## What a message names of an endpoint
|
|
47
|
-
*
|
|
48
|
-
* Every failure the repository throws names the endpoint it was talking to,
|
|
49
|
-
* and every caller logs what it throws. A message names the endpoint by its
|
|
50
|
-
* origin and path alone ({@link endpointForMessage}): a query string is
|
|
51
|
-
* accepted in a Store URL, and a deployment may carry a credential there
|
|
52
|
-
* (`?api_key=…`) despite the README's advice, so the query never reaches a
|
|
53
|
-
* message — nor does a fragment, which no request sends anyway.
|
|
24
|
+
* Kept separate from `oauth.jwt.issuer`'s rule (core's `checkCanonicalIssuer`)
|
|
25
|
+
* because an issuer may not carry a query string, while a Store endpoint may.
|
|
26
|
+
* The loopback predicate is core's `isLoopbackHostname`, the session redirect
|
|
27
|
+
* policy's too, so the carve-outs cannot drift apart. A message names an
|
|
28
|
+
* endpoint by origin and path alone ({@link endpointForMessage}): a query may
|
|
29
|
+
* carry a credential, and every caller logs what a client throws.
|
|
54
30
|
*/
|
|
55
31
|
import { isLoopbackHostname } from "@o3co/auth-provider-core";
|
|
56
32
|
// Re-exported unchanged: this module's callers (and its tests) read the
|
|
57
33
|
// predicate as part of the endpoint-validation surface. The definition lives
|
|
58
|
-
// in core
|
|
34
|
+
// in core.
|
|
59
35
|
export { isLoopbackHostname };
|
|
60
36
|
/** Matches `scheme://` — the shape that distinguishes an absolute URL from a bare host. */
|
|
61
37
|
const ABSOLUTE_URL_PREFIX = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//;
|
|
@@ -114,7 +90,7 @@ export function describeEndpointRejection(reason) {
|
|
|
114
90
|
return ("must use https, or http for a loopback host (localhost, 127.0.0.0/8, [::1]) — " +
|
|
115
91
|
"no other scheme is accepted");
|
|
116
92
|
case "insecure-scheme":
|
|
117
|
-
return ("must use https — it carries
|
|
93
|
+
return ("must use https — what it carries must not cross a network in the clear; http is accepted only for " +
|
|
118
94
|
"a loopback host (localhost, 127.0.0.0/8, [::1])");
|
|
119
95
|
case "has-credentials":
|
|
120
96
|
return "must not embed credentials in the URL";
|
|
@@ -122,29 +98,26 @@ export function describeEndpointRejection(reason) {
|
|
|
122
98
|
}
|
|
123
99
|
/**
|
|
124
100
|
* Returns `value` when it is a usable Store endpoint, otherwise throws naming
|
|
125
|
-
* `field` and the reason.
|
|
101
|
+
* `owner`, `field` and the reason.
|
|
126
102
|
*
|
|
127
103
|
* The rejected value is deliberately **not** echoed into the message: it is
|
|
128
104
|
* operator-supplied configuration that may embed a secret, and a boot error
|
|
129
105
|
* lands in logs.
|
|
130
106
|
*/
|
|
131
|
-
export function assertSecureEndpoint(value, field) {
|
|
107
|
+
export function assertSecureEndpoint(value, field, owner = "HttpUserRepository") {
|
|
132
108
|
const rejection = checkSecureEndpoint(value);
|
|
133
109
|
if (rejection !== null) {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`(reason: ${rejection})`);
|
|
110
|
+
throw new Error(`${owner}: "${field}" ${describeEndpointRejection(rejection)} (reason: ${rejection})`);
|
|
136
111
|
}
|
|
137
112
|
return value;
|
|
138
113
|
}
|
|
139
114
|
/**
|
|
140
|
-
* `url` as a message names it: its origin and path
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* constructor refuses such a Store URL anyway — but a caller can hand
|
|
147
|
-
* `StoreCredentialRefusedError` anything.
|
|
115
|
+
* `url` as a message names it: its origin and path, as the WHATWG URL parser
|
|
116
|
+
* normalises them (`https://store.example/authenticate`), and never its query
|
|
117
|
+
* or fragment, which may carry a credential. Anything that is not an http or
|
|
118
|
+
* https URL is named by what it is not, since its origin would be `"null"` and
|
|
119
|
+
* its path the rest of it (`data:…`): the constructor refuses such a Store URL,
|
|
120
|
+
* but a caller can hand `StoreCredentialRefusedError` anything.
|
|
148
121
|
*/
|
|
149
122
|
export function endpointForMessage(url) {
|
|
150
123
|
try {
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,10 @@ import type { AdapterFactory, UserRepository } from "@o3co/auth-provider-core";
|
|
|
2
2
|
export declare const registerBuiltinAdapters: (factories: {
|
|
3
3
|
userFactory: AdapterFactory<UserRepository>;
|
|
4
4
|
}) => void;
|
|
5
|
-
export {
|
|
5
|
+
export { HttpMfaFactorStore, type HttpMfaFactorStoreOptions, } from "./mfa/HttpMfaFactorStore.mjs";
|
|
6
|
+
export { type FoundationMfaFactorStoreModuleOptions, foundationMfaFactorStoreModule, } from "./mfa/module.mjs";
|
|
7
|
+
export { MfaStoreError, type MfaStoreFailure, type MfaStoreOperation, } from "./mfa/storeFailure.mjs";
|
|
8
|
+
export { type FederatedIdentityLookupCoverage, HttpUserRepository, } from "./repositories/HttpUserRepository.mjs";
|
|
6
9
|
export { StoreCredentialRefusedError, StoreTransportError, type StoreTransportFailure, } from "./repositories/storeErrors.mjs";
|
|
10
|
+
export { DEFAULT_MAX_RESPONSE_BYTES } from "./storeTransport.mjs";
|
|
7
11
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAO/E,eAAO,MAAM,uBAAuB,GAAI,WAAW;IAClD,WAAW,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;CAC5C,KAAG,IA4CH,CAAC;AAEF,OAAO,EACN,kBAAkB,EAClB,KAAK,yBAAyB,GAC9B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACN,KAAK,qCAAqC,EAC1C,8BAA8B,GAC9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACtB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACN,KAAK,+BAA+B,EACpC,kBAAkB,GAClB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EACN,2BAA2B,EAC3B,mBAAmB,EACnB,KAAK,qBAAqB,GAC1B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.mjs
CHANGED
|
@@ -13,31 +13,8 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
19
|
-
/**
|
|
20
|
-
* Coerces a numeric config value that may arrive as a string.
|
|
21
|
-
*
|
|
22
|
-
* HOCON environment substitution yields strings, so `CLIENT_USER_TIMEOUT=1234`
|
|
23
|
-
* reaches the builder as `"1234"`. An *absent* key takes `fallback`; anything
|
|
24
|
-
* present but unreadable becomes `NaN` and is rejected by the constructor,
|
|
25
|
-
* which owns the range rules and names the field in its message.
|
|
26
|
-
*
|
|
27
|
-
* The distinction matters because HOCON substitutes a **blank** environment
|
|
28
|
-
* variable as `""` — a very ordinary shape in a `.env` file or an empty
|
|
29
|
-
* ConfigMap key — and `Number("")` is `0`. This used to fall back to the
|
|
30
|
-
* default, hiding the misconfiguration; it is now a boot failure (#285).
|
|
31
|
-
*/
|
|
32
|
-
const toNumber = (value, fallback) => {
|
|
33
|
-
if (value === undefined || value === null)
|
|
34
|
-
return fallback;
|
|
35
|
-
if (typeof value === "number")
|
|
36
|
-
return value;
|
|
37
|
-
if (typeof value === "string")
|
|
38
|
-
return Number(value.trim());
|
|
39
|
-
return Number.NaN;
|
|
40
|
-
};
|
|
16
|
+
import { HttpUserRepository, } from "./repositories/HttpUserRepository.mjs";
|
|
17
|
+
import { readStoreTransportConfig } from "./storeTransport.mjs";
|
|
41
18
|
export const registerBuiltinAdapters = (factories) => {
|
|
42
19
|
factories.userFactory.register("http", (config) => {
|
|
43
20
|
if (typeof config.authenticateUrl !== "string") {
|
|
@@ -52,11 +29,11 @@ export const registerBuiltinAdapters = (factories) => {
|
|
|
52
29
|
return new HttpUserRepository({
|
|
53
30
|
authenticateUrl: config.authenticateUrl,
|
|
54
31
|
authenticateByTokenUrl: config.authenticateByTokenUrl,
|
|
55
|
-
//
|
|
32
|
+
// Optional. Present → the repository can link a federated identity.
|
|
56
33
|
...(typeof config.linkFederatedIdentityUrl === "string"
|
|
57
34
|
? { linkFederatedIdentityUrl: config.linkFederatedIdentityUrl }
|
|
58
35
|
: {}),
|
|
59
|
-
//
|
|
36
|
+
// Optional. Forwarded whenever SET, not only when well-typed —
|
|
60
37
|
// the link URL above vanishes when misspelt, and for the lookup a value
|
|
61
38
|
// that vanishes is a deployment that believes itself covered and is
|
|
62
39
|
// not. The constructor refuses what is not a URL, or not a list.
|
|
@@ -65,20 +42,26 @@ export const registerBuiltinAdapters = (factories) => {
|
|
|
65
42
|
findSubjectByFederatedIdentityUrl: config.findSubjectByFederatedIdentityUrl,
|
|
66
43
|
}
|
|
67
44
|
: {}),
|
|
45
|
+
// Optional, forwarded whenever SET for the same reason: a witness URL
|
|
46
|
+
// that vanished leaves the MFA enrollment witness unwritten.
|
|
47
|
+
...(config.markMfaEnrolledUrl !== undefined
|
|
48
|
+
? { markMfaEnrolledUrl: config.markMfaEnrolledUrl }
|
|
49
|
+
: {}),
|
|
68
50
|
...(config.federatedIdentityLookupCoverage !== undefined
|
|
69
51
|
? {
|
|
70
52
|
federatedIdentityLookupCoverage: config.federatedIdentityLookupCoverage,
|
|
71
53
|
}
|
|
72
54
|
: {}),
|
|
73
|
-
//
|
|
74
|
-
// token that
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
...(config.bearerToken !== undefined ? { bearerToken: config.bearerToken } : {}),
|
|
78
|
-
timeout: toNumber(config.timeout, DEFAULT_TIMEOUT_MS),
|
|
79
|
-
maxResponseBytes: toNumber(config.maxResponseBytes, DEFAULT_MAX_RESPONSE_BYTES),
|
|
55
|
+
// The bearer token, the deadline and the response cap. The constructor
|
|
56
|
+
// refuses a token that is not a string, or is blank, malformed or too
|
|
57
|
+
// weak, and a deadline or cap that is not a positive integer.
|
|
58
|
+
...readStoreTransportConfig(config),
|
|
80
59
|
});
|
|
81
60
|
});
|
|
82
61
|
};
|
|
83
|
-
export {
|
|
62
|
+
export { HttpMfaFactorStore, } from "./mfa/HttpMfaFactorStore.mjs";
|
|
63
|
+
export { foundationMfaFactorStoreModule, } from "./mfa/module.mjs";
|
|
64
|
+
export { MfaStoreError, } from "./mfa/storeFailure.mjs";
|
|
65
|
+
export { HttpUserRepository, } from "./repositories/HttpUserRepository.mjs";
|
|
84
66
|
export { StoreCredentialRefusedError, StoreTransportError, } from "./repositories/storeErrors.mjs";
|
|
67
|
+
export { DEFAULT_MAX_RESPONSE_BYTES } from "./storeTransport.mjs";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MfaFactorStore` kept by the Store, over its four MFA factor endpoints
|
|
3
|
+
* (README, "The Store's MFA endpoints"), on the user repository's transport.
|
|
4
|
+
*
|
|
5
|
+
* Guarantees: every answer the contract does not give an operation throws —
|
|
6
|
+
* a list is never answered as fewer records, an update never as written; a
|
|
7
|
+
* list is read whole or refused (a record the provider cannot read, one of
|
|
8
|
+
* another subject, an id twice); an update's answer must be the record named,
|
|
9
|
+
* with the changes sent, at the expected version plus one; what is thrown is
|
|
10
|
+
* built from an allowlist (`storeFailure.mts`, `storeErrors.mts`) and carries
|
|
11
|
+
* nothing the Store sent; the sealed `data` it is handed is sent as it is,
|
|
12
|
+
* and nothing is sent that the wire codec would not read back.
|
|
13
|
+
*
|
|
14
|
+
* A record is created or removed only conditionally: every create and every
|
|
15
|
+
* single-record removal it sends carries `expectedGeneration`, and only the
|
|
16
|
+
* whole set's reset (`removeAllForSubject`) is sent without one.
|
|
17
|
+
*
|
|
18
|
+
* The factor set's members (`listVersioned`, `createIf`, `removeIf`) read
|
|
19
|
+
* every answer through core's codec alone, once: a status the operation does
|
|
20
|
+
* not give is `unexpected_status`, read before any body, and whatever the
|
|
21
|
+
* codec refuses — a `404` or `409` without its outcome body among them — is
|
|
22
|
+
* `malformed_answer`. A conditional write states its deadline on the wire
|
|
23
|
+
* (`deadlineMs`, the request timeout from just before it is sent), at or
|
|
24
|
+
* before the moment the transport gives up, whose timer starts after it: a
|
|
25
|
+
* write past it is never applied after the adapter stopped waiting. A Store
|
|
26
|
+
* that reads it as passed answers `408`, which is `unexpected_status`. A conditional write that is sent and then fails,
|
|
27
|
+
* its deadline included, is unknown: it may have committed.
|
|
28
|
+
*/
|
|
29
|
+
import { type ConditionalCreateAnswer, type ConditionalSetRemoveAnswer, type MfaFactorRecord, type MfaFactorRecordUpdate, type MfaFactorStore, type StoreGeneration, type VersionedSet } from "@o3co/auth-provider-core";
|
|
30
|
+
export interface HttpMfaFactorStoreOptions {
|
|
31
|
+
/** The Store's four MFA factor endpoints, each https or loopback http. */
|
|
32
|
+
readonly listUrl: string;
|
|
33
|
+
readonly createUrl: string;
|
|
34
|
+
readonly updateUrl: string;
|
|
35
|
+
readonly deleteUrl: string;
|
|
36
|
+
/** The user repository's credential: sent as `Authorization: Bearer <token>`. */
|
|
37
|
+
readonly bearerToken?: string;
|
|
38
|
+
/** The whole exchange's deadline, in milliseconds. */
|
|
39
|
+
readonly timeout: number;
|
|
40
|
+
/** The most bytes of an answer read. Default `DEFAULT_MAX_RESPONSE_BYTES`. */
|
|
41
|
+
readonly maxResponseBytes?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare class HttpMfaFactorStore implements MfaFactorStore {
|
|
44
|
+
#private;
|
|
45
|
+
readonly kind = "store";
|
|
46
|
+
constructor({ listUrl, createUrl, updateUrl, deleteUrl, bearerToken, timeout, maxResponseBytes, }: HttpMfaFactorStoreOptions);
|
|
47
|
+
list(subject: string): Promise<readonly MfaFactorRecord[]>;
|
|
48
|
+
listVersioned(subject: string): Promise<VersionedSet<MfaFactorRecord>>;
|
|
49
|
+
createIf(record: MfaFactorRecord, expected: StoreGeneration | null): Promise<ConditionalCreateAnswer>;
|
|
50
|
+
removeIf(subject: string, id: string, expected: StoreGeneration): Promise<ConditionalSetRemoveAnswer>;
|
|
51
|
+
update(subject: string, id: string, expectedVersion: number, next: MfaFactorRecordUpdate): Promise<MfaFactorRecord | null>;
|
|
52
|
+
/** The subject's whole set reset: done on a `2xx` or a `404`. */
|
|
53
|
+
removeAllForSubject(subject: string): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=HttpMfaFactorStore.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HttpMfaFactorStore.d.mts","sourceRoot":"","sources":["../../src/mfa/HttpMfaFactorStore.mts"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAE/B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EAUnB,KAAK,eAAe,EAIpB,KAAK,YAAY,EACjB,MAAM,0BAA0B,CAAC;AA6BlC,MAAM,WAAW,yBAAyB;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACnC;AAqBD,qBAAa,kBAAmB,YAAW,cAAc;;IACxD,QAAQ,CAAC,IAAI,WAAW;gBAKZ,EACX,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,EACX,OAAO,EACP,gBAA6C,GAC7C,EAAE,yBAAyB;IActB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC;IAc1D,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;IAOtE,QAAQ,CACb,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,eAAe,GAAG,IAAI,GAC9B,OAAO,CAAC,uBAAuB,CAAC;IAU7B,QAAQ,CACb,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,eAAe,GACvB,OAAO,CAAC,0BAA0B,CAAC;IAUhC,MAAM,CACX,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,qBAAqB,GACzB,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAyBlC,iEAAiE;IAC3D,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAsDzD"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* `MfaFactorStore` kept by the Store, over its four MFA factor endpoints
|
|
18
|
+
* (README, "The Store's MFA endpoints"), on the user repository's transport.
|
|
19
|
+
*
|
|
20
|
+
* Guarantees: every answer the contract does not give an operation throws —
|
|
21
|
+
* a list is never answered as fewer records, an update never as written; a
|
|
22
|
+
* list is read whole or refused (a record the provider cannot read, one of
|
|
23
|
+
* another subject, an id twice); an update's answer must be the record named,
|
|
24
|
+
* with the changes sent, at the expected version plus one; what is thrown is
|
|
25
|
+
* built from an allowlist (`storeFailure.mts`, `storeErrors.mts`) and carries
|
|
26
|
+
* nothing the Store sent; the sealed `data` it is handed is sent as it is,
|
|
27
|
+
* and nothing is sent that the wire codec would not read back.
|
|
28
|
+
*
|
|
29
|
+
* A record is created or removed only conditionally: every create and every
|
|
30
|
+
* single-record removal it sends carries `expectedGeneration`, and only the
|
|
31
|
+
* whole set's reset (`removeAllForSubject`) is sent without one.
|
|
32
|
+
*
|
|
33
|
+
* The factor set's members (`listVersioned`, `createIf`, `removeIf`) read
|
|
34
|
+
* every answer through core's codec alone, once: a status the operation does
|
|
35
|
+
* not give is `unexpected_status`, read before any body, and whatever the
|
|
36
|
+
* codec refuses — a `404` or `409` without its outcome body among them — is
|
|
37
|
+
* `malformed_answer`. A conditional write states its deadline on the wire
|
|
38
|
+
* (`deadlineMs`, the request timeout from just before it is sent), at or
|
|
39
|
+
* before the moment the transport gives up, whose timer starts after it: a
|
|
40
|
+
* write past it is never applied after the adapter stopped waiting. A Store
|
|
41
|
+
* that reads it as passed answers `408`, which is `unexpected_status`. A conditional write that is sent and then fails,
|
|
42
|
+
* its deadline included, is unknown: it may have committed.
|
|
43
|
+
*/
|
|
44
|
+
import { fromMfaStoreFactor, readMfaStoreCreateIfAnswer, readMfaStoreFactor, readMfaStoreListAnswer, readMfaStoreRemoveIfAnswer, readMfaStoreVersionedListAnswer, toMfaStoreCreateIfRequest, toMfaStoreRemoveIfRequest, toMfaStoreUpdateRequest, } from "@o3co/auth-provider-core";
|
|
45
|
+
import { assertSecureEndpoint } from "../endpointUrl.mjs";
|
|
46
|
+
import { bearerAuthorization, checkStoreResponseCap, checkStoreTimeout, DEFAULT_MAX_RESPONSE_BYTES, postToStore, } from "../storeTransport.mjs";
|
|
47
|
+
import { mfaStoreMalformedAnswer, mfaStoreRequestMessages, mfaStoreStatusError, mfaStoreUnreadableRecord, mfaStoreVersionSkipped, } from "./storeFailure.mjs";
|
|
48
|
+
/** What this adapter's messages lead with. */
|
|
49
|
+
const OWNER = "HttpMfaFactorStore";
|
|
50
|
+
/** The statuses a conditional create is answered with, each with its outcome body. */
|
|
51
|
+
const CREATE_IF_STATUSES = new Set([200, 409]);
|
|
52
|
+
/** The statuses a conditional removal is answered with, each with its outcome body. */
|
|
53
|
+
const REMOVE_IF_STATUSES = new Set([200, 404, 409]);
|
|
54
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
/** What JSON text parses to, or `NOT_JSON`. */
|
|
56
|
+
const NOT_JSON = Symbol("not JSON");
|
|
57
|
+
function parsed(text) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return NOT_JSON;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Whether `factor` holds exactly the changes an update sent. */
|
|
66
|
+
const wrote = (factor, changes) => factor.data === changes.data &&
|
|
67
|
+
factor.label === changes.label &&
|
|
68
|
+
factor.lastUsedAtMs === changes.lastUsedAtMs;
|
|
69
|
+
export class HttpMfaFactorStore {
|
|
70
|
+
kind = "store";
|
|
71
|
+
/** Private fields: `inspect()` and `JSON.stringify` of the store show neither the credential nor the endpoints. */
|
|
72
|
+
#urls;
|
|
73
|
+
#settings;
|
|
74
|
+
constructor({ listUrl, createUrl, updateUrl, deleteUrl, bearerToken, timeout, maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES, }) {
|
|
75
|
+
this.#urls = Object.freeze({
|
|
76
|
+
list: assertSecureEndpoint(listUrl, "listUrl", OWNER),
|
|
77
|
+
create: assertSecureEndpoint(createUrl, "createUrl", OWNER),
|
|
78
|
+
update: assertSecureEndpoint(updateUrl, "updateUrl", OWNER),
|
|
79
|
+
delete: assertSecureEndpoint(deleteUrl, "deleteUrl", OWNER),
|
|
80
|
+
});
|
|
81
|
+
this.#settings = Object.freeze({
|
|
82
|
+
authorization: bearerAuthorization(bearerToken, OWNER),
|
|
83
|
+
timeout: checkStoreTimeout(timeout, OWNER),
|
|
84
|
+
maxResponseBytes: checkStoreResponseCap(maxResponseBytes, OWNER),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async list(subject) {
|
|
88
|
+
const url = this.#urls.list;
|
|
89
|
+
const body = { subject };
|
|
90
|
+
const { response, text } = await this.#post("list", body, (status) => status === 200);
|
|
91
|
+
if (text === undefined)
|
|
92
|
+
throw mfaStoreStatusError("list", url, response);
|
|
93
|
+
const reading = readMfaStoreListAnswer(parsed(text), subject);
|
|
94
|
+
if (!reading.ok) {
|
|
95
|
+
throw reading.reason === "malformed"
|
|
96
|
+
? mfaStoreMalformedAnswer("list", url)
|
|
97
|
+
: mfaStoreUnreadableRecord(url);
|
|
98
|
+
}
|
|
99
|
+
return reading.factors.map(fromMfaStoreFactor);
|
|
100
|
+
}
|
|
101
|
+
async listVersioned(subject) {
|
|
102
|
+
const body = { subject };
|
|
103
|
+
const { response, text } = await this.#post("list", body, (status) => status === 200);
|
|
104
|
+
if (text === undefined)
|
|
105
|
+
throw mfaStoreStatusError("list", this.#urls.list, response);
|
|
106
|
+
return this.#read("list", () => readMfaStoreVersionedListAnswer(parsed(text), subject));
|
|
107
|
+
}
|
|
108
|
+
async createIf(record, expected) {
|
|
109
|
+
const { response, text } = await this.#postConditional("create", (deadlineMs) => toMfaStoreCreateIfRequest(record, expected, deadlineMs), (status) => CREATE_IF_STATUSES.has(status));
|
|
110
|
+
if (text === undefined)
|
|
111
|
+
throw mfaStoreStatusError("create", this.#urls.create, response);
|
|
112
|
+
return this.#read("create", () => readMfaStoreCreateIfAnswer(response.status, parsed(text)));
|
|
113
|
+
}
|
|
114
|
+
async removeIf(subject, id, expected) {
|
|
115
|
+
const { response, text } = await this.#postConditional("delete", (deadlineMs) => toMfaStoreRemoveIfRequest(subject, id, expected, deadlineMs), (status) => REMOVE_IF_STATUSES.has(status));
|
|
116
|
+
if (text === undefined)
|
|
117
|
+
throw mfaStoreStatusError("delete", this.#urls.delete, response);
|
|
118
|
+
return this.#read("delete", () => readMfaStoreRemoveIfAnswer(response.status, parsed(text)));
|
|
119
|
+
}
|
|
120
|
+
async update(subject, id, expectedVersion, next) {
|
|
121
|
+
const url = this.#urls.update;
|
|
122
|
+
const body = toMfaStoreUpdateRequest(subject, id, expectedVersion, next);
|
|
123
|
+
const { response, text } = await this.#post("update", body, (status) => status === 200);
|
|
124
|
+
if (response.status === 409 || response.status === 404)
|
|
125
|
+
return null;
|
|
126
|
+
if (text === undefined)
|
|
127
|
+
throw mfaStoreStatusError("update", url, response);
|
|
128
|
+
const answer = parsed(text);
|
|
129
|
+
const factor = isRecord(answer) && Object.hasOwn(answer, "factor")
|
|
130
|
+
? readMfaStoreFactor(answer.factor)
|
|
131
|
+
: undefined;
|
|
132
|
+
if (factor === undefined ||
|
|
133
|
+
factor.subject !== subject ||
|
|
134
|
+
factor.id !== id ||
|
|
135
|
+
!wrote(factor, body.changes)) {
|
|
136
|
+
throw mfaStoreMalformedAnswer("update", url);
|
|
137
|
+
}
|
|
138
|
+
if (factor.version !== expectedVersion + 1) {
|
|
139
|
+
throw mfaStoreVersionSkipped(url, { subject, id, expectedVersion });
|
|
140
|
+
}
|
|
141
|
+
return fromMfaStoreFactor(factor);
|
|
142
|
+
}
|
|
143
|
+
/** The subject's whole set reset: done on a `2xx` or a `404`. */
|
|
144
|
+
async removeAllForSubject(subject) {
|
|
145
|
+
const body = { subject, all: true };
|
|
146
|
+
const { response } = await this.#post("delete", body, () => false);
|
|
147
|
+
if (response.ok || response.status === 404)
|
|
148
|
+
return;
|
|
149
|
+
throw mfaStoreStatusError("delete", this.#urls.delete, response);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A conditional write to `operation`'s endpoint, its body built by
|
|
153
|
+
* `build` with its `deadlineMs`: the request timeout from now, taken just
|
|
154
|
+
* before the request is sent. Nothing yields between this and the
|
|
155
|
+
* transport's timer, which starts after it, so the deadline is at or
|
|
156
|
+
* before the moment the adapter gives up.
|
|
157
|
+
*/
|
|
158
|
+
#postConditional(operation, build, readsBody) {
|
|
159
|
+
const deadlineMs = Date.now() + this.#settings.timeout;
|
|
160
|
+
return this.#post(operation, build(deadlineMs), readsBody);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* What `read`, a reader of core's codec, makes of an answer of
|
|
164
|
+
* `operation`; the `TypeError` it throws for an answer outside the
|
|
165
|
+
* contract is `malformed_answer`, carrying none of the reader's words.
|
|
166
|
+
*/
|
|
167
|
+
#read(operation, read) {
|
|
168
|
+
try {
|
|
169
|
+
return read();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error instanceof TypeError) {
|
|
173
|
+
throw mfaStoreMalformedAnswer(operation, this.#urls[operation]);
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** `body` to `operation`'s endpoint, its answer read when `readsBody` says so. */
|
|
179
|
+
#post(operation, body, readsBody) {
|
|
180
|
+
const url = this.#urls[operation];
|
|
181
|
+
return postToStore(url, body, this.#settings, mfaStoreRequestMessages(OWNER, operation, url), readsBody);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type StoreRequestSettings } from "../storeTransport.mjs";
|
|
2
|
+
/** Posts `{ subject, enrolled }` to `url`; resolves on a `204` and throws on anything else. */
|
|
3
|
+
export declare function markMfaEnrolledAtStore(url: string, settings: StoreRequestSettings, owner: string, subject: string, enrolled: boolean): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=markEnrolled.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markEnrolled.d.mts","sourceRoot":"","sources":["../../src/mfa/markEnrolled.mts"],"names":[],"mappings":"AA8BA,OAAO,EAAe,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAG/E,+FAA+F;AAC/F,wBAAsB,sBAAsB,CAC3C,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,oBAAoB,EAC9B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,GACf,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { postToStore } from "../storeTransport.mjs";
|
|
17
|
+
import { mfaStoreRequestMessages, mfaStoreStatusError } from "./storeFailure.mjs";
|
|
18
|
+
/** Posts `{ subject, enrolled }` to `url`; resolves on a `204` and throws on anything else. */
|
|
19
|
+
export async function markMfaEnrolledAtStore(url, settings, owner, subject, enrolled) {
|
|
20
|
+
if (typeof subject !== "string" || subject.length === 0) {
|
|
21
|
+
throw new RangeError(`${owner}: markMfaEnrolled takes a non-empty subject`);
|
|
22
|
+
}
|
|
23
|
+
if (typeof enrolled !== "boolean") {
|
|
24
|
+
throw new RangeError(`${owner}: markMfaEnrolled takes enrolled as a boolean`);
|
|
25
|
+
}
|
|
26
|
+
const body = { subject, enrolled };
|
|
27
|
+
const { response } = await postToStore(url, body, settings, mfaStoreRequestMessages(owner, "markMfaEnrolled", url), () => false);
|
|
28
|
+
if (response.status === 204)
|
|
29
|
+
return;
|
|
30
|
+
throw mfaStoreStatusError("markMfaEnrolled", url, response);
|
|
31
|
+
}
|