@byline/admin 2.4.0 → 2.4.1
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/dist/abilities.js +5 -24
- package/dist/index.js +8 -30
- package/dist/lib/assert-admin-actor.js +13 -74
- package/dist/lib/create-command.js +6 -16
- package/dist/modules/admin-account/commands.js +35 -24
- package/dist/modules/admin-account/components/change-password.d.ts +8 -0
- package/dist/modules/admin-account/components/change-password.js +192 -0
- package/dist/modules/admin-account/components/change-password.module.js +8 -0
- package/dist/modules/admin-account/components/change-password_module.css +27 -0
- package/dist/modules/admin-account/components/container.d.ts +29 -0
- package/dist/modules/admin-account/components/container.js +298 -0
- package/dist/modules/admin-account/components/container.module.js +28 -0
- package/dist/modules/admin-account/components/container_module.css +106 -0
- package/dist/modules/admin-account/components/update.d.ts +8 -0
- package/dist/modules/admin-account/components/update.js +207 -0
- package/dist/modules/admin-account/components/update.module.js +8 -0
- package/dist/modules/admin-account/components/update_module.css +27 -0
- package/dist/modules/admin-account/errors.js +14 -45
- package/dist/modules/admin-account/index.js +4 -34
- package/dist/modules/admin-account/schemas.js +25 -59
- package/dist/modules/admin-account/service.js +56 -61
- package/dist/modules/admin-permissions/abilities.js +6 -24
- package/dist/modules/admin-permissions/commands.js +42 -28
- package/dist/modules/admin-permissions/components/inspector.d.ts +4 -0
- package/dist/modules/admin-permissions/components/inspector.js +284 -0
- package/dist/modules/admin-permissions/components/inspector.module.js +56 -0
- package/dist/modules/admin-permissions/components/inspector_module.css +238 -0
- package/dist/modules/admin-permissions/dto.js +3 -16
- package/dist/modules/admin-permissions/errors.js +14 -27
- package/dist/modules/admin-permissions/index.js +6 -26
- package/dist/modules/admin-permissions/repository.js +1 -8
- package/dist/modules/admin-permissions/schemas.js +33 -70
- package/dist/modules/admin-permissions/service.js +88 -92
- package/dist/modules/admin-roles/abilities.js +8 -30
- package/dist/modules/admin-roles/commands.js +89 -55
- package/dist/modules/admin-roles/components/create.d.ts +7 -0
- package/dist/modules/admin-roles/components/create.js +177 -0
- package/dist/modules/admin-roles/components/create.module.js +8 -0
- package/dist/modules/admin-roles/components/create_module.css +27 -0
- package/dist/modules/admin-roles/components/permissions.d.ts +10 -0
- package/dist/modules/admin-roles/components/permissions.js +303 -0
- package/dist/modules/admin-roles/components/permissions.module.js +44 -0
- package/dist/modules/admin-roles/components/permissions_module.css +192 -0
- package/dist/modules/admin-roles/components/update.d.ts +8 -0
- package/dist/modules/admin-roles/components/update.js +166 -0
- package/dist/modules/admin-roles/components/update.module.js +8 -0
- package/dist/modules/admin-roles/components/update_module.css +27 -0
- package/dist/modules/admin-roles/dto.js +3 -16
- package/dist/modules/admin-roles/errors.js +16 -40
- package/dist/modules/admin-roles/index.js +6 -26
- package/dist/modules/admin-roles/repository.js +1 -8
- package/dist/modules/admin-roles/schemas.js +41 -71
- package/dist/modules/admin-roles/service.js +79 -82
- package/dist/modules/admin-users/abilities.js +9 -38
- package/dist/modules/admin-users/commands.js +92 -50
- package/dist/modules/admin-users/components/create.d.ts +8 -0
- package/dist/modules/admin-users/components/create.js +268 -0
- package/dist/modules/admin-users/components/create.module.js +10 -0
- package/dist/modules/admin-users/components/create_module.css +45 -0
- package/dist/modules/admin-users/components/roles.d.ts +11 -0
- package/dist/modules/admin-users/components/roles.js +148 -0
- package/dist/modules/admin-users/components/roles.module.js +18 -0
- package/dist/modules/admin-users/components/roles_module.css +75 -0
- package/dist/modules/admin-users/components/set-password.d.ts +8 -0
- package/dist/modules/admin-users/components/set-password.js +170 -0
- package/dist/modules/admin-users/components/set-password.module.js +9 -0
- package/dist/modules/admin-users/components/set-password_module.css +31 -0
- package/dist/modules/admin-users/components/update.d.ts +8 -0
- package/dist/modules/admin-users/components/update.js +254 -0
- package/dist/modules/admin-users/components/update.module.js +9 -0
- package/dist/modules/admin-users/components/update_module.css +34 -0
- package/dist/modules/admin-users/dto.js +3 -18
- package/dist/modules/admin-users/errors.js +17 -43
- package/dist/modules/admin-users/index.js +7 -27
- package/dist/modules/admin-users/repository.js +1 -8
- package/dist/modules/admin-users/schemas.js +44 -75
- package/dist/modules/admin-users/seed-super-admin.js +9 -34
- package/dist/modules/admin-users/service.js +76 -91
- package/dist/modules/auth/components/sign-in-form.d.ts +12 -0
- package/dist/modules/auth/components/sign-in-form.js +115 -0
- package/dist/modules/auth/components/sign-in-form.module.js +12 -0
- package/dist/modules/auth/components/sign-in-form_module.css +41 -0
- package/dist/modules/auth/index.js +3 -24
- package/dist/modules/auth/jwt-session-provider.js +179 -149
- package/dist/modules/auth/password.js +11 -53
- package/dist/modules/auth/phc.js +21 -54
- package/dist/modules/auth/refresh-tokens-repository.js +1 -8
- package/dist/modules/auth/resolve-actor.js +6 -28
- package/dist/services/admin-services-context.d.ts +16 -0
- package/dist/services/admin-services-context.js +13 -0
- package/dist/services/admin-services-types.d.ts +129 -0
- package/dist/services/admin-services-types.js +1 -0
- package/dist/store.js +1 -8
- package/dist/vendor/noble-argon2/_blake.js +277 -45
- package/dist/vendor/noble-argon2/_md.js +81 -136
- package/dist/vendor/noble-argon2/_u64.js +65 -67
- package/dist/vendor/noble-argon2/argon2.js +181 -342
- package/dist/vendor/noble-argon2/blake2.js +252 -327
- package/dist/vendor/noble-argon2/utils.js +110 -490
- package/dist/vendor/noble-argon2/utils.js.LICENSE.txt +1 -0
- package/package.json +89 -10
- package/src/abilities.ts +32 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +39 -0
- package/src/lib/assert-admin-actor.ts +90 -0
- package/src/lib/create-command.ts +109 -0
- package/src/modules/admin-account/commands.ts +76 -0
- package/src/modules/admin-account/components/change-password.module.css +40 -0
- package/src/modules/admin-account/components/change-password.tsx +232 -0
- package/src/modules/admin-account/components/container.module.css +158 -0
- package/src/modules/admin-account/components/container.tsx +229 -0
- package/src/modules/admin-account/components/update.module.css +40 -0
- package/src/modules/admin-account/components/update.tsx +263 -0
- package/src/modules/admin-account/errors.ts +75 -0
- package/src/modules/admin-account/index.ts +60 -0
- package/src/modules/admin-account/schemas.ts +84 -0
- package/src/modules/admin-account/service.ts +92 -0
- package/src/modules/admin-permissions/abilities.ts +46 -0
- package/src/modules/admin-permissions/commands.ts +103 -0
- package/src/modules/admin-permissions/components/inspector.module.css +326 -0
- package/src/modules/admin-permissions/components/inspector.tsx +298 -0
- package/src/modules/admin-permissions/dto.ts +28 -0
- package/src/modules/admin-permissions/errors.ts +57 -0
- package/src/modules/admin-permissions/index.ts +72 -0
- package/src/modules/admin-permissions/repository.ts +49 -0
- package/src/modules/admin-permissions/schemas.ts +128 -0
- package/src/modules/admin-permissions/service.ts +137 -0
- package/src/modules/admin-roles/abilities.ts +62 -0
- package/src/modules/admin-roles/commands.ts +161 -0
- package/src/modules/admin-roles/components/create.module.css +40 -0
- package/src/modules/admin-roles/components/create.tsx +218 -0
- package/src/modules/admin-roles/components/permissions.module.css +279 -0
- package/src/modules/admin-roles/components/permissions.tsx +396 -0
- package/src/modules/admin-roles/components/update.module.css +40 -0
- package/src/modules/admin-roles/components/update.tsx +218 -0
- package/src/modules/admin-roles/dto.ts +30 -0
- package/src/modules/admin-roles/errors.ts +76 -0
- package/src/modules/admin-roles/index.ts +81 -0
- package/src/modules/admin-roles/repository.ts +96 -0
- package/src/modules/admin-roles/schemas.ts +139 -0
- package/src/modules/admin-roles/service.ts +136 -0
- package/src/modules/admin-users/abilities.ts +76 -0
- package/src/modules/admin-users/commands.ts +157 -0
- package/src/modules/admin-users/components/create.module.css +63 -0
- package/src/modules/admin-users/components/create.tsx +323 -0
- package/src/modules/admin-users/components/roles.module.css +119 -0
- package/src/modules/admin-users/components/roles.tsx +172 -0
- package/src/modules/admin-users/components/set-password.module.css +46 -0
- package/src/modules/admin-users/components/set-password.tsx +199 -0
- package/src/modules/admin-users/components/update.module.css +49 -0
- package/src/modules/admin-users/components/update.tsx +328 -0
- package/src/modules/admin-users/dto.ts +39 -0
- package/src/modules/admin-users/errors.ts +84 -0
- package/src/modules/admin-users/index.ts +91 -0
- package/src/modules/admin-users/repository.ts +161 -0
- package/src/modules/admin-users/schemas.ts +168 -0
- package/src/modules/admin-users/seed-super-admin.ts +102 -0
- package/src/modules/admin-users/service.ts +166 -0
- package/src/modules/auth/components/sign-in-form.module.css +62 -0
- package/src/modules/auth/components/sign-in-form.tsx +132 -0
- package/src/modules/auth/index.ts +31 -0
- package/src/modules/auth/jwt-session-provider.ts +301 -0
- package/src/modules/auth/password.ts +94 -0
- package/src/modules/auth/phc.ts +121 -0
- package/src/modules/auth/refresh-tokens-repository.ts +74 -0
- package/src/modules/auth/resolve-actor.ts +42 -0
- package/src/services/admin-services-context.tsx +52 -0
- package/src/services/admin-services-types.ts +177 -0
- package/src/store.ts +32 -0
- package/src/vendor/noble-argon2/LICENSE +21 -0
- package/src/vendor/noble-argon2/README.md +87 -0
- package/src/vendor/noble-argon2/_blake.ts +58 -0
- package/src/vendor/noble-argon2/_md.ts +223 -0
- package/src/vendor/noble-argon2/_u64.ts +118 -0
- package/src/vendor/noble-argon2/argon2.ts +668 -0
- package/src/vendor/noble-argon2/blake2.ts +583 -0
- package/src/vendor/noble-argon2/utils.ts +849 -0
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* `@byline/admin/auth` — session handling for the built-in admin realm.
|
|
10
|
-
*
|
|
11
|
-
* Hosts the reference `JwtSessionProvider` and the sign-in / refresh /
|
|
12
|
-
* revoke orchestration that consumes it, along with password hashing
|
|
13
|
-
* (`hashPassword` / `verifyPassword`) and the `RefreshTokensRepository`
|
|
14
|
-
* contract the provider drives.
|
|
15
|
-
*
|
|
16
|
-
* The `SessionProvider` **interface** lives in `@byline/auth` so the
|
|
17
|
-
* pluggability contract stays narrow; this module supplies the
|
|
18
|
-
* Byline-native implementation. Third-party providers (Lucia, WorkOS,
|
|
19
|
-
* Clerk, institutional SSO) should be shipped as separate packages
|
|
20
|
-
* against `@byline/auth` rather than added here.
|
|
21
|
-
*/
|
|
22
|
-
export { JwtSessionProvider } from './jwt-session-provider.js';
|
|
23
|
-
export { hashPassword, verifyPassword } from './password.js';
|
|
24
|
-
export { resolveActor } from './resolve-actor.js';
|
|
1
|
+
export { JwtSessionProvider } from "./jwt-session-provider.js";
|
|
2
|
+
export { hashPassword, verifyPassword } from "./password.js";
|
|
3
|
+
export { resolveActor } from "./resolve-actor.js";
|
|
@@ -1,214 +1,244 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { ERR_ACCOUNT_DISABLED, ERR_INVALID_CREDENTIALS, ERR_INVALID_TOKEN, ERR_REVOKED_TOKEN } from "@byline/auth";
|
|
3
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
4
|
+
import { v7 } from "uuid";
|
|
5
|
+
import { verifyPassword } from "./password.js";
|
|
6
|
+
import { resolveActor } from "./resolve-actor.js";
|
|
7
|
+
function _check_private_redeclaration(obj, privateCollection) {
|
|
8
|
+
if (privateCollection.has(obj)) throw new TypeError("Cannot initialize the same private elements twice on an object");
|
|
9
|
+
}
|
|
10
|
+
function _class_apply_descriptor_get(receiver, descriptor) {
|
|
11
|
+
if (descriptor.get) return descriptor.get.call(receiver);
|
|
12
|
+
return descriptor.value;
|
|
13
|
+
}
|
|
14
|
+
function _class_apply_descriptor_set(receiver, descriptor, value) {
|
|
15
|
+
if (descriptor.set) descriptor.set.call(receiver, value);
|
|
16
|
+
else {
|
|
17
|
+
if (!descriptor.writable) throw new TypeError("attempted to set read only private field");
|
|
18
|
+
descriptor.value = value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function _class_extract_field_descriptor(receiver, privateMap, action) {
|
|
22
|
+
if (!privateMap.has(receiver)) throw new TypeError("attempted to " + action + " private field on non-instance");
|
|
23
|
+
return privateMap.get(receiver);
|
|
24
|
+
}
|
|
25
|
+
function _class_private_field_get(receiver, privateMap) {
|
|
26
|
+
var descriptor = _class_extract_field_descriptor(receiver, privateMap, "get");
|
|
27
|
+
return _class_apply_descriptor_get(receiver, descriptor);
|
|
28
|
+
}
|
|
29
|
+
function _class_private_field_init(obj, privateMap, value) {
|
|
30
|
+
_check_private_redeclaration(obj, privateMap);
|
|
31
|
+
privateMap.set(obj, value);
|
|
32
|
+
}
|
|
33
|
+
function _class_private_field_set(receiver, privateMap, value) {
|
|
34
|
+
var descriptor = _class_extract_field_descriptor(receiver, privateMap, "set");
|
|
35
|
+
_class_apply_descriptor_set(receiver, descriptor, value);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function _class_private_method_get(receiver, privateSet, fn) {
|
|
39
|
+
if (!privateSet.has(receiver)) throw new TypeError("attempted to get private field on non-instance");
|
|
40
|
+
return fn;
|
|
41
|
+
}
|
|
42
|
+
function _class_private_method_init(obj, privateSet) {
|
|
43
|
+
_check_private_redeclaration(obj, privateSet);
|
|
44
|
+
privateSet.add(obj);
|
|
45
|
+
}
|
|
14
46
|
const DEFAULT_ISSUER = 'byline';
|
|
15
|
-
const DEFAULT_ACCESS_TOKEN_TTL_SECONDS =
|
|
16
|
-
const DEFAULT_REFRESH_TOKEN_TTL_SECONDS =
|
|
47
|
+
const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 900;
|
|
48
|
+
const DEFAULT_REFRESH_TOKEN_TTL_SECONDS = 2592000;
|
|
17
49
|
const CAPABILITIES = {
|
|
18
50
|
passwordChange: true,
|
|
19
51
|
magicLink: false,
|
|
20
|
-
sso: false
|
|
52
|
+
sso: false
|
|
21
53
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#store;
|
|
25
|
-
#signingKey;
|
|
26
|
-
#issuer;
|
|
27
|
-
#accessTtl;
|
|
28
|
-
#refreshTtl;
|
|
29
|
-
#now;
|
|
30
|
-
constructor(config) {
|
|
31
|
-
this.#store = config.store;
|
|
32
|
-
this.#signingKey =
|
|
33
|
-
typeof config.signingSecret === 'string'
|
|
34
|
-
? new TextEncoder().encode(config.signingSecret)
|
|
35
|
-
: config.signingSecret;
|
|
36
|
-
if (this.#signingKey.byteLength < 32) {
|
|
37
|
-
throw new Error('JwtSessionProvider: signingSecret must carry at least 32 bytes of entropy (256 bits)');
|
|
38
|
-
}
|
|
39
|
-
this.#issuer = config.issuer ?? DEFAULT_ISSUER;
|
|
40
|
-
this.#accessTtl = config.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
|
|
41
|
-
this.#refreshTtl = config.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS;
|
|
42
|
-
this.#now = config.now ?? (() => new Date());
|
|
43
|
-
}
|
|
44
|
-
// -----------------------------------------------------------------------
|
|
45
|
-
// SessionProvider
|
|
46
|
-
// -----------------------------------------------------------------------
|
|
54
|
+
var _store = /*#__PURE__*/ new WeakMap(), _signingKey = /*#__PURE__*/ new WeakMap(), _issuer = /*#__PURE__*/ new WeakMap(), _accessTtl = /*#__PURE__*/ new WeakMap(), _refreshTtl = /*#__PURE__*/ new WeakMap(), _now = /*#__PURE__*/ new WeakMap(), _issueTokens = /*#__PURE__*/ new WeakSet(), _signAccessToken = /*#__PURE__*/ new WeakSet();
|
|
55
|
+
class JwtSessionProvider {
|
|
47
56
|
async signInWithPassword(args) {
|
|
48
|
-
const users = this
|
|
57
|
+
const users = _class_private_field_get(this, _store).adminUsers;
|
|
49
58
|
const row = await users.getByEmailForSignIn(args.email);
|
|
50
|
-
// Uniform error response for unknown email vs. wrong password — don't
|
|
51
|
-
// leak which one. Still do a real verify against a dummy hash so the
|
|
52
|
-
// timing is comparable; the argon2 cost dominates regardless.
|
|
53
59
|
if (!row) {
|
|
54
60
|
await verifyPassword(args.password, DUMMY_HASH_FOR_TIMING);
|
|
55
|
-
throw ERR_INVALID_CREDENTIALS({
|
|
61
|
+
throw ERR_INVALID_CREDENTIALS({
|
|
62
|
+
message: 'invalid credentials'
|
|
63
|
+
});
|
|
56
64
|
}
|
|
57
65
|
const ok = await verifyPassword(args.password, row.password_hash);
|
|
58
66
|
if (!ok) {
|
|
59
67
|
await users.recordLoginFailure(row.id);
|
|
60
|
-
throw ERR_INVALID_CREDENTIALS({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw ERR_ACCOUNT_DISABLED({ message: 'account disabled' });
|
|
68
|
+
throw ERR_INVALID_CREDENTIALS({
|
|
69
|
+
message: 'invalid credentials'
|
|
70
|
+
});
|
|
64
71
|
}
|
|
72
|
+
if (!row.is_enabled) throw ERR_ACCOUNT_DISABLED({
|
|
73
|
+
message: 'account disabled'
|
|
74
|
+
});
|
|
65
75
|
await users.recordLoginSuccess(row.id, args.ip ?? null);
|
|
66
|
-
const actor = await resolveActor(this
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
throw ERR_ACCOUNT_DISABLED({ message: 'account disabled' });
|
|
72
|
-
}
|
|
73
|
-
const tokens = await this.#issueTokens({
|
|
76
|
+
const actor = await resolveActor(_class_private_field_get(this, _store), row.id);
|
|
77
|
+
if (!actor) throw ERR_ACCOUNT_DISABLED({
|
|
78
|
+
message: 'account disabled'
|
|
79
|
+
});
|
|
80
|
+
const tokens = await _class_private_method_get(this, _issueTokens, issueTokens).call(this, {
|
|
74
81
|
adminUserId: row.id,
|
|
75
82
|
ip: args.ip ?? null,
|
|
76
|
-
userAgent: args.userAgent ?? null
|
|
83
|
+
userAgent: args.userAgent ?? null
|
|
77
84
|
});
|
|
78
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
...tokens,
|
|
87
|
+
actor
|
|
88
|
+
};
|
|
79
89
|
}
|
|
80
90
|
async verifyAccessToken(token) {
|
|
81
91
|
let payload;
|
|
82
92
|
try {
|
|
83
|
-
const result = await jwtVerify(token, this
|
|
84
|
-
issuer: this
|
|
93
|
+
const result = await jwtVerify(token, _class_private_field_get(this, _signingKey), {
|
|
94
|
+
issuer: _class_private_field_get(this, _issuer)
|
|
85
95
|
});
|
|
86
96
|
payload = result.payload;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw ERR_INVALID_TOKEN({
|
|
99
|
+
message: 'access token verification failed',
|
|
100
|
+
cause: err
|
|
101
|
+
});
|
|
87
102
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return { actor };
|
|
103
|
+
if ('access' !== payload.typ) throw ERR_INVALID_TOKEN({
|
|
104
|
+
message: 'unexpected token type'
|
|
105
|
+
});
|
|
106
|
+
const actor = await resolveActor(_class_private_field_get(this, _store), payload.sub);
|
|
107
|
+
if (!actor) throw ERR_ACCOUNT_DISABLED({
|
|
108
|
+
message: 'account disabled or deleted'
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
actor
|
|
112
|
+
};
|
|
100
113
|
}
|
|
101
114
|
async refreshSession(args) {
|
|
102
|
-
const refreshTokens = this
|
|
115
|
+
const refreshTokens = _class_private_field_get(this, _store).refreshTokens;
|
|
103
116
|
const hash = hashToken(args.refreshToken);
|
|
104
117
|
const row = await refreshTokens.findByHash(hash);
|
|
105
|
-
if (!row) {
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
const now = this
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (row.rotated_to_id != null) {
|
|
112
|
-
// Rotated token replayed — the chain is compromised. Revoke every
|
|
113
|
-
// descendant so the attacker and the legitimate holder are both
|
|
114
|
-
// signed out.
|
|
118
|
+
if (!row) throw ERR_INVALID_TOKEN({
|
|
119
|
+
message: 'refresh token not recognised'
|
|
120
|
+
});
|
|
121
|
+
const now = _class_private_field_get(this, _now).call(this);
|
|
122
|
+
if (null != row.revoked_at) {
|
|
123
|
+
if (null != row.rotated_to_id) {
|
|
115
124
|
await refreshTokens.revokeChain(row.id, now);
|
|
116
125
|
throw ERR_REVOKED_TOKEN({
|
|
117
|
-
message: 'refresh token was already rotated — chain revoked'
|
|
126
|
+
message: 'refresh token was already rotated — chain revoked'
|
|
118
127
|
});
|
|
119
128
|
}
|
|
120
|
-
throw ERR_REVOKED_TOKEN({
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
throw ERR_INVALID_TOKEN({ message: 'refresh token expired' });
|
|
129
|
+
throw ERR_REVOKED_TOKEN({
|
|
130
|
+
message: 'refresh token has been revoked'
|
|
131
|
+
});
|
|
124
132
|
}
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
if (row.expires_at.getTime() <= now.getTime()) throw ERR_INVALID_TOKEN({
|
|
134
|
+
message: 'refresh token expired'
|
|
135
|
+
});
|
|
136
|
+
const newId = v7();
|
|
127
137
|
const newRefreshPlain = generateOpaqueToken();
|
|
128
138
|
const newRefreshHash = hashToken(newRefreshPlain);
|
|
129
|
-
const refreshExpiresAt = new Date(now.getTime() +
|
|
139
|
+
const refreshExpiresAt = new Date(now.getTime() + 1000 * _class_private_field_get(this, _refreshTtl));
|
|
130
140
|
await refreshTokens.issue({
|
|
131
141
|
id: newId,
|
|
132
142
|
admin_user_id: row.admin_user_id,
|
|
133
143
|
token_hash: newRefreshHash,
|
|
134
144
|
expires_at: refreshExpiresAt,
|
|
135
145
|
user_agent: args.userAgent ?? null,
|
|
136
|
-
ip: args.ip ?? null
|
|
146
|
+
ip: args.ip ?? null
|
|
137
147
|
});
|
|
138
148
|
await refreshTokens.markRotated(row.id, newId, now);
|
|
139
|
-
const accessToken = await this
|
|
140
|
-
const accessExpiresAt = new Date(now.getTime() +
|
|
149
|
+
const accessToken = await _class_private_method_get(this, _signAccessToken, signAccessToken).call(this, row.admin_user_id, now);
|
|
150
|
+
const accessExpiresAt = new Date(now.getTime() + 1000 * _class_private_field_get(this, _accessTtl));
|
|
141
151
|
return {
|
|
142
152
|
accessToken,
|
|
143
153
|
refreshToken: newRefreshPlain,
|
|
144
154
|
accessTokenExpiresAt: accessExpiresAt,
|
|
145
|
-
refreshTokenExpiresAt: refreshExpiresAt
|
|
155
|
+
refreshTokenExpiresAt: refreshExpiresAt
|
|
146
156
|
};
|
|
147
157
|
}
|
|
148
158
|
async revokeSession(refreshToken) {
|
|
149
|
-
const refreshTokens = this
|
|
159
|
+
const refreshTokens = _class_private_field_get(this, _store).refreshTokens;
|
|
150
160
|
const row = await refreshTokens.findByHash(hashToken(refreshToken));
|
|
151
|
-
if (!row)
|
|
152
|
-
|
|
153
|
-
await refreshTokens.revoke(row.id, this.#now());
|
|
161
|
+
if (!row) return;
|
|
162
|
+
await refreshTokens.revoke(row.id, _class_private_field_get(this, _now).call(this));
|
|
154
163
|
}
|
|
155
164
|
async resolveActor(adminUserId) {
|
|
156
|
-
return resolveActor(this
|
|
165
|
+
return resolveActor(_class_private_field_get(this, _store), adminUserId);
|
|
157
166
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const accessToken = await this.#signAccessToken(input.adminUserId, now);
|
|
165
|
-
const accessExpiresAt = new Date(now.getTime() + this.#accessTtl * 1000);
|
|
166
|
-
const refreshPlain = generateOpaqueToken();
|
|
167
|
-
const refreshHash = hashToken(refreshPlain);
|
|
168
|
-
const refreshExpiresAt = new Date(now.getTime() + this.#refreshTtl * 1000);
|
|
169
|
-
await refreshTokens.issue({
|
|
170
|
-
id: uuidv7(),
|
|
171
|
-
admin_user_id: input.adminUserId,
|
|
172
|
-
token_hash: refreshHash,
|
|
173
|
-
expires_at: refreshExpiresAt,
|
|
174
|
-
user_agent: input.userAgent,
|
|
175
|
-
ip: input.ip,
|
|
167
|
+
constructor(config){
|
|
168
|
+
_class_private_method_init(this, _issueTokens);
|
|
169
|
+
_class_private_method_init(this, _signAccessToken);
|
|
170
|
+
_class_private_field_init(this, _store, {
|
|
171
|
+
writable: true,
|
|
172
|
+
value: void 0
|
|
176
173
|
});
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
174
|
+
_class_private_field_init(this, _signingKey, {
|
|
175
|
+
writable: true,
|
|
176
|
+
value: void 0
|
|
177
|
+
});
|
|
178
|
+
_class_private_field_init(this, _issuer, {
|
|
179
|
+
writable: true,
|
|
180
|
+
value: void 0
|
|
181
|
+
});
|
|
182
|
+
_class_private_field_init(this, _accessTtl, {
|
|
183
|
+
writable: true,
|
|
184
|
+
value: void 0
|
|
185
|
+
});
|
|
186
|
+
_class_private_field_init(this, _refreshTtl, {
|
|
187
|
+
writable: true,
|
|
188
|
+
value: void 0
|
|
189
|
+
});
|
|
190
|
+
_class_private_field_init(this, _now, {
|
|
191
|
+
writable: true,
|
|
192
|
+
value: void 0
|
|
193
|
+
});
|
|
194
|
+
this.capabilities = CAPABILITIES;
|
|
195
|
+
_class_private_field_set(this, _store, config.store);
|
|
196
|
+
_class_private_field_set(this, _signingKey, 'string' == typeof config.signingSecret ? new TextEncoder().encode(config.signingSecret) : config.signingSecret);
|
|
197
|
+
if (_class_private_field_get(this, _signingKey).byteLength < 32) throw new Error('JwtSessionProvider: signingSecret must carry at least 32 bytes of entropy (256 bits)');
|
|
198
|
+
_class_private_field_set(this, _issuer, config.issuer ?? DEFAULT_ISSUER);
|
|
199
|
+
_class_private_field_set(this, _accessTtl, config.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS);
|
|
200
|
+
_class_private_field_set(this, _refreshTtl, config.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS);
|
|
201
|
+
_class_private_field_set(this, _now, config.now ?? (()=>new Date()));
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
async function issueTokens(input) {
|
|
205
|
+
const now = _class_private_field_get(this, _now).call(this);
|
|
206
|
+
const refreshTokens = _class_private_field_get(this, _store).refreshTokens;
|
|
207
|
+
const accessToken = await _class_private_method_get(this, _signAccessToken, signAccessToken).call(this, input.adminUserId, now);
|
|
208
|
+
const accessExpiresAt = new Date(now.getTime() + 1000 * _class_private_field_get(this, _accessTtl));
|
|
209
|
+
const refreshPlain = generateOpaqueToken();
|
|
210
|
+
const refreshHash = hashToken(refreshPlain);
|
|
211
|
+
const refreshExpiresAt = new Date(now.getTime() + 1000 * _class_private_field_get(this, _refreshTtl));
|
|
212
|
+
await refreshTokens.issue({
|
|
213
|
+
id: v7(),
|
|
214
|
+
admin_user_id: input.adminUserId,
|
|
215
|
+
token_hash: refreshHash,
|
|
216
|
+
expires_at: refreshExpiresAt,
|
|
217
|
+
user_agent: input.userAgent,
|
|
218
|
+
ip: input.ip
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
accessToken,
|
|
222
|
+
refreshToken: refreshPlain,
|
|
223
|
+
accessTokenExpiresAt: accessExpiresAt,
|
|
224
|
+
refreshTokenExpiresAt: refreshExpiresAt
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function signAccessToken(adminUserId, now) {
|
|
228
|
+
const iat = Math.floor(now.getTime() / 1000);
|
|
229
|
+
const exp = iat + _class_private_field_get(this, _accessTtl);
|
|
230
|
+
return new SignJWT({
|
|
231
|
+
typ: 'access'
|
|
232
|
+
}).setProtectedHeader({
|
|
233
|
+
alg: 'HS256',
|
|
234
|
+
typ: 'JWT'
|
|
235
|
+
}).setSubject(adminUserId).setIssuer(_class_private_field_get(this, _issuer)).setIssuedAt(iat).setExpirationTime(exp).setJti(randomUUID()).sign(_class_private_field_get(this, _signingKey));
|
|
236
|
+
}
|
|
201
237
|
function generateOpaqueToken() {
|
|
202
238
|
return randomBytes(32).toString('base64url');
|
|
203
239
|
}
|
|
204
|
-
/** SHA-256 hex digest of the raw refresh-token string. */
|
|
205
240
|
function hashToken(token) {
|
|
206
241
|
return createHash('sha256').update(token).digest('hex');
|
|
207
242
|
}
|
|
208
|
-
/**
|
|
209
|
-
* A stable argon2id hash used only to equalise sign-in timing on the
|
|
210
|
-
* unknown-email code path. The plaintext here is arbitrary — we never
|
|
211
|
-
* succeed against it. This is pre-generated at module-load time so the
|
|
212
|
-
* first sign-in call doesn't pay the generation cost.
|
|
213
|
-
*/
|
|
214
243
|
const DUMMY_HASH_FOR_TIMING = '$argon2id$v=19$m=19456,t=2,p=1$c2lkZS1jaGFubmVsLW1pdGlnYXRpb24$0Hqf2vQKZqSfZZ4nJRr7K5IOjn9ngjzaQjV+yTG6iNY';
|
|
244
|
+
export { JwtSessionProvider };
|
|
@@ -1,62 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Password hashing — argon2id via the vendored `@noble/hashes` copy at
|
|
10
|
-
* `../../vendor/noble-argon2/`. Pure-JS, runs anywhere with a modern JS
|
|
11
|
-
* runtime (Node, Workers, Deno, Bun, browsers).
|
|
12
|
-
*
|
|
13
|
-
* Stores the full PHC string (`$argon2id$v=19$m=…$…$…`) in the
|
|
14
|
-
* `byline_admin_users.password` column. That makes the algorithm and
|
|
15
|
-
* parameters self-describing, so upgrading params later (or migrating off
|
|
16
|
-
* argon2id entirely) is a straightforward re-hash on next successful sign-in.
|
|
17
|
-
*
|
|
18
|
-
* Defaults follow OWASP 2023 guidance for argon2id: memory 19 MiB,
|
|
19
|
-
* iterations 2, parallelism 1. These are reasonable for typical server
|
|
20
|
-
* hardware; tune if sign-in latency becomes a concern under load.
|
|
21
|
-
*
|
|
22
|
-
* Note: pure-JS argon2id is meaningfully slower than the previous
|
|
23
|
-
* `@node-rs/argon2` Rust binding (~50–150 ms vs ~10 ms at these params on
|
|
24
|
-
* modern server hardware). For interactive sign-in this is fine; for
|
|
25
|
-
* high-throughput auth services consider tuning `HASH_OPTIONS` or
|
|
26
|
-
* reintroducing a native binding behind a runtime-feature check.
|
|
27
|
-
*/
|
|
28
|
-
import { argon2idAsync } from '../../vendor/noble-argon2/argon2.js';
|
|
29
|
-
import { decodeArgon2idPhc, encodeArgon2idPhc, timingSafeEqual } from './phc.js';
|
|
30
|
-
/** Argon2id cost parameters. Matches the prior `@node-rs/argon2` defaults. */
|
|
1
|
+
import { argon2idAsync } from "../../vendor/noble-argon2/argon2.js";
|
|
2
|
+
import { decodeArgon2idPhc, encodeArgon2idPhc, timingSafeEqual } from "./phc.js";
|
|
31
3
|
const HASH_OPTIONS = {
|
|
32
|
-
/** Memory cost in KiB (19 MiB). */
|
|
33
4
|
memoryCost: 19456,
|
|
34
|
-
/** Iterations. */
|
|
35
5
|
timeCost: 2,
|
|
36
|
-
/** Parallelism (lanes). */
|
|
37
6
|
parallelism: 1,
|
|
38
|
-
/** Derived-key length in bytes — 32 matches the prior stored hashes. */
|
|
39
7
|
hashLength: 32,
|
|
40
|
-
|
|
41
|
-
saltLength: 16,
|
|
8
|
+
saltLength: 16
|
|
42
9
|
};
|
|
43
|
-
/** Argon2 v1.3 (RFC 9106). */
|
|
44
10
|
const ARGON2_VERSION = 0x13;
|
|
45
11
|
function randomSalt(length) {
|
|
46
12
|
return crypto.getRandomValues(new Uint8Array(length));
|
|
47
13
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (plaintext.length === 0) {
|
|
51
|
-
throw new Error('hashPassword: plaintext must be non-empty');
|
|
52
|
-
}
|
|
14
|
+
async function hashPassword(plaintext) {
|
|
15
|
+
if (0 === plaintext.length) throw new Error('hashPassword: plaintext must be non-empty');
|
|
53
16
|
const salt = randomSalt(HASH_OPTIONS.saltLength);
|
|
54
17
|
const hash = await argon2idAsync(plaintext, salt, {
|
|
55
18
|
m: HASH_OPTIONS.memoryCost,
|
|
56
19
|
t: HASH_OPTIONS.timeCost,
|
|
57
20
|
p: HASH_OPTIONS.parallelism,
|
|
58
21
|
dkLen: HASH_OPTIONS.hashLength,
|
|
59
|
-
version: ARGON2_VERSION
|
|
22
|
+
version: ARGON2_VERSION
|
|
60
23
|
});
|
|
61
24
|
return encodeArgon2idPhc({
|
|
62
25
|
algorithm: 'argon2id',
|
|
@@ -65,24 +28,19 @@ export async function hashPassword(plaintext) {
|
|
|
65
28
|
timeCost: HASH_OPTIONS.timeCost,
|
|
66
29
|
parallelism: HASH_OPTIONS.parallelism,
|
|
67
30
|
salt,
|
|
68
|
-
hash
|
|
31
|
+
hash
|
|
69
32
|
});
|
|
70
33
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* on mismatch — never throws for a normal mismatch. Re-throws on malformed
|
|
74
|
-
* hash strings or underlying library errors so those get surfaced.
|
|
75
|
-
*/
|
|
76
|
-
export async function verifyPassword(plaintext, phc) {
|
|
77
|
-
if (plaintext.length === 0 || phc.length === 0)
|
|
78
|
-
return false;
|
|
34
|
+
async function verifyPassword(plaintext, phc) {
|
|
35
|
+
if (0 === plaintext.length || 0 === phc.length) return false;
|
|
79
36
|
const decoded = decodeArgon2idPhc(phc);
|
|
80
37
|
const candidate = await argon2idAsync(plaintext, decoded.salt, {
|
|
81
38
|
m: decoded.memoryCost,
|
|
82
39
|
t: decoded.timeCost,
|
|
83
40
|
p: decoded.parallelism,
|
|
84
41
|
dkLen: decoded.hash.length,
|
|
85
|
-
version: decoded.version
|
|
42
|
+
version: decoded.version
|
|
86
43
|
});
|
|
87
44
|
return timingSafeEqual(candidate, decoded.hash);
|
|
88
45
|
}
|
|
46
|
+
export { hashPassword, verifyPassword };
|
package/dist/modules/auth/phc.js
CHANGED
|
@@ -1,68 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
1
|
function bytesToB64NoPad(bytes) {
|
|
9
2
|
let bin = '';
|
|
10
|
-
for
|
|
11
|
-
bin += String.fromCharCode(bytes[i]);
|
|
3
|
+
for(let i = 0; i < bytes.length; i++)bin += String.fromCharCode(bytes[i]);
|
|
12
4
|
return btoa(bin).replace(/=+$/, '');
|
|
13
5
|
}
|
|
14
6
|
function b64NoPadToBytes(b64) {
|
|
15
|
-
const padded = b64 + '='.repeat((4 -
|
|
7
|
+
const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
|
|
16
8
|
const bin = atob(padded);
|
|
17
9
|
const out = new Uint8Array(bin.length);
|
|
18
|
-
for
|
|
19
|
-
out[i] = bin.charCodeAt(i);
|
|
10
|
+
for(let i = 0; i < bin.length; i++)out[i] = bin.charCodeAt(i);
|
|
20
11
|
return out;
|
|
21
12
|
}
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
`$v=${phc.version}` +
|
|
25
|
-
`$m=${phc.memoryCost},t=${phc.timeCost},p=${phc.parallelism}` +
|
|
26
|
-
`$${bytesToB64NoPad(phc.salt)}` +
|
|
27
|
-
`$${bytesToB64NoPad(phc.hash)}`);
|
|
13
|
+
function encodeArgon2idPhc(phc) {
|
|
14
|
+
return `$${phc.algorithm}$v=${phc.version}$m=${phc.memoryCost},t=${phc.timeCost},p=${phc.parallelism}$${bytesToB64NoPad(phc.salt)}$${bytesToB64NoPad(phc.hash)}`;
|
|
28
15
|
}
|
|
29
|
-
|
|
30
|
-
// Leading `$` produces an empty first segment, so a valid argon2id PHC string
|
|
31
|
-
// splits into exactly 6 parts: ['', algo, 'v=…', 'm=…,t=…,p=…', salt, hash].
|
|
16
|
+
function decodeArgon2idPhc(s) {
|
|
32
17
|
const parts = s.split('$');
|
|
33
|
-
if (parts.length
|
|
34
|
-
throw new Error('decodeArgon2idPhc: malformed PHC string');
|
|
35
|
-
}
|
|
18
|
+
if (6 !== parts.length || '' !== parts[0]) throw new Error('decodeArgon2idPhc: malformed PHC string');
|
|
36
19
|
const algorithm = parts[1] ?? '';
|
|
37
20
|
const versionField = parts[2] ?? '';
|
|
38
21
|
const paramsField = parts[3] ?? '';
|
|
39
22
|
const saltB64 = parts[4] ?? '';
|
|
40
23
|
const hashB64 = parts[5] ?? '';
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
if (!versionField.startsWith('v=')) {
|
|
45
|
-
throw new Error('decodeArgon2idPhc: missing version field');
|
|
46
|
-
}
|
|
24
|
+
if ('argon2id' !== algorithm) throw new Error(`decodeArgon2idPhc: unsupported algorithm "${algorithm}"`);
|
|
25
|
+
if (!versionField.startsWith('v=')) throw new Error('decodeArgon2idPhc: missing version field');
|
|
47
26
|
const version = Number.parseInt(versionField.slice(2), 10);
|
|
48
|
-
if (!Number.isInteger(version)) {
|
|
49
|
-
throw new Error(`decodeArgon2idPhc: invalid version "${versionField}"`);
|
|
50
|
-
}
|
|
27
|
+
if (!Number.isInteger(version)) throw new Error(`decodeArgon2idPhc: invalid version "${versionField}"`);
|
|
51
28
|
const params = {};
|
|
52
|
-
for (const kv of paramsField.split(','))
|
|
29
|
+
for (const kv of paramsField.split(',')){
|
|
53
30
|
const eq = kv.indexOf('=');
|
|
54
|
-
if (eq <= 0)
|
|
55
|
-
throw new Error(`decodeArgon2idPhc: malformed param "${kv}"`);
|
|
31
|
+
if (eq <= 0) throw new Error(`decodeArgon2idPhc: malformed param "${kv}"`);
|
|
56
32
|
const k = kv.slice(0, eq);
|
|
57
33
|
const v = Number.parseInt(kv.slice(eq + 1), 10);
|
|
58
|
-
if (!Number.isInteger(v))
|
|
59
|
-
|
|
60
|
-
if (k === 'm' || k === 't' || k === 'p')
|
|
61
|
-
params[k] = v;
|
|
62
|
-
}
|
|
63
|
-
if (params.m === undefined || params.t === undefined || params.p === undefined) {
|
|
64
|
-
throw new Error('decodeArgon2idPhc: missing required m/t/p params');
|
|
34
|
+
if (!Number.isInteger(v)) throw new Error(`decodeArgon2idPhc: malformed param value "${kv}"`);
|
|
35
|
+
if ('m' === k || 't' === k || 'p' === k) params[k] = v;
|
|
65
36
|
}
|
|
37
|
+
if (void 0 === params.m || void 0 === params.t || void 0 === params.p) throw new Error('decodeArgon2idPhc: missing required m/t/p params');
|
|
66
38
|
return {
|
|
67
39
|
algorithm: 'argon2id',
|
|
68
40
|
version,
|
|
@@ -70,18 +42,13 @@ export function decodeArgon2idPhc(s) {
|
|
|
70
42
|
timeCost: params.t,
|
|
71
43
|
parallelism: params.p,
|
|
72
44
|
salt: b64NoPadToBytes(saltB64),
|
|
73
|
-
hash: b64NoPadToBytes(hashB64)
|
|
45
|
+
hash: b64NoPadToBytes(hashB64)
|
|
74
46
|
};
|
|
75
47
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* same length and every byte matches. Intended for hash verification.
|
|
79
|
-
*/
|
|
80
|
-
export function timingSafeEqual(a, b) {
|
|
81
|
-
if (a.length !== b.length)
|
|
82
|
-
return false;
|
|
48
|
+
function timingSafeEqual(a, b) {
|
|
49
|
+
if (a.length !== b.length) return false;
|
|
83
50
|
let diff = 0;
|
|
84
|
-
for
|
|
85
|
-
|
|
86
|
-
return diff === 0;
|
|
51
|
+
for(let i = 0; i < a.length; i++)diff |= a[i] ^ b[i];
|
|
52
|
+
return 0 === diff;
|
|
87
53
|
}
|
|
54
|
+
export { decodeArgon2idPhc, encodeArgon2idPhc, timingSafeEqual };
|
|
@@ -1,8 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
-
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
-
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
-
*
|
|
6
|
-
* Copyright (c) Infonomic Company Limited
|
|
7
|
-
*/
|
|
8
|
-
export {};
|
|
1
|
+
export { };
|