wirejs-resources 0.1.1-alpha → 0.1.2-alpha
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/lib/index.js +2 -1
- package/lib/resource.js +32 -0
- package/lib/resources/secret.js +35 -33
- package/lib/services/authentication.js +55 -44
- package/lib/services/file.js +21 -21
- package/lib/types.ts +3 -1
- package/package.json +1 -2
- package/lib/services/sendmail.js +0 -12
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { FileService } from './services/file.js';
|
|
2
2
|
export { AuthenticationService } from './services/authentication.js';
|
|
3
3
|
export { CookieJar } from './adapters/cookie-jar.js';
|
|
4
|
-
export { withContext, requiresContext, Context } from './adapters/context.js';
|
|
4
|
+
export { withContext, requiresContext, Context } from './adapters/context.js';
|
|
5
|
+
export { Resource } from './resource.js';
|
package/lib/resource.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class Resource {
|
|
2
|
+
/**
|
|
3
|
+
* @type {Resource | string}
|
|
4
|
+
*/
|
|
5
|
+
scope;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @type {string}
|
|
9
|
+
*/
|
|
10
|
+
id;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @param {Resource | string} scope
|
|
15
|
+
* @param {string} id
|
|
16
|
+
*/
|
|
17
|
+
constructor(scope, id) {
|
|
18
|
+
this.scope = scope;
|
|
19
|
+
this.id = id;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get absoluteId() {
|
|
23
|
+
const sanitizedId = encodeURIComponent(this.id);
|
|
24
|
+
if (typeof this.scope === 'string') {
|
|
25
|
+
return `${encodeURIComponent(this.scope)}/${sanitizedId}`;
|
|
26
|
+
} else if (typeof this.scope?.id === 'string') {
|
|
27
|
+
return `${this.scope.absoluteId}/${sanitizedId}`;
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error("Resources must defined within a scope. Provide either a namespace string or parent resource.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
package/lib/resources/secret.js
CHANGED
|
@@ -1,48 +1,50 @@
|
|
|
1
|
-
import process from 'process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
1
|
import crypto from 'crypto';
|
|
2
|
+
import { Resource } from '../resource.js';
|
|
3
|
+
import { FileService } from '../services/file.js';
|
|
5
4
|
|
|
6
|
-
const
|
|
5
|
+
const FILENAME = 'secret';
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export class Secret extends Resource {
|
|
8
|
+
/**
|
|
9
|
+
* @type {FileService}
|
|
10
|
+
*/
|
|
11
|
+
#fileService;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @type {Promise<any>}
|
|
15
|
+
*/
|
|
16
|
+
#initPromise;
|
|
15
17
|
|
|
16
18
|
/**
|
|
19
|
+
* @param {Resource | string}
|
|
17
20
|
* @param {string} id
|
|
18
|
-
* @param {string} [value]
|
|
19
21
|
*/
|
|
20
|
-
constructor(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#filename() {
|
|
33
|
-
const sanitizedId = this.id.replace('~', '-').replace(/\.+/g, '.');
|
|
34
|
-
return path.join(CWD, 'temp', 'wirejs-services', 'secrets', sanitizedId);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
get id() {
|
|
38
|
-
return this.#id;
|
|
22
|
+
constructor(scope, id) {
|
|
23
|
+
super(scope, id);
|
|
24
|
+
this.#fileService = new FileService(this, 'files');
|
|
25
|
+
|
|
26
|
+
this.#initPromise = this.#fileService.write(
|
|
27
|
+
FILENAME,
|
|
28
|
+
JSON.stringify(crypto.randomBytes(64).toString('base64url')),
|
|
29
|
+
{ onlyIfNotExists: true }
|
|
30
|
+
).catch(error => {
|
|
31
|
+
if (!this.#fileService.isAlreadyExistsError(error)) throw error;
|
|
32
|
+
});
|
|
39
33
|
}
|
|
40
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @returns {any}
|
|
37
|
+
*/
|
|
41
38
|
async read() {
|
|
42
|
-
|
|
39
|
+
await this.#initPromise;
|
|
40
|
+
return JSON.parse(await this.#fileService.read(FILENAME));
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/**
|
|
44
|
+
* @param {any} data
|
|
45
|
+
*/
|
|
46
|
+
async write(data) {
|
|
47
|
+
await this.#initPromise;
|
|
48
|
+
await this.#fileService.write(FILENAME, JSON.stringify(data));
|
|
47
49
|
}
|
|
48
50
|
}
|
|
@@ -1,18 +1,40 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
1
|
+
import { scrypt, randomBytes } from 'crypto';
|
|
4
2
|
|
|
5
3
|
import * as jose from 'jose';
|
|
6
|
-
import bcrypt from 'bcrypt';
|
|
7
4
|
|
|
5
|
+
import { Resource } from '../resource.js';
|
|
8
6
|
import { Secret } from '../resources/secret.js';
|
|
7
|
+
import { FileService } from './file.js';
|
|
9
8
|
import { CookieJar } from '../adapters/cookie-jar.js';
|
|
10
9
|
import { withContext } from '../adapters/context.js';
|
|
11
10
|
|
|
12
|
-
const CWD = process.cwd();
|
|
13
|
-
const SALT_ROUNDS = 10;
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} password
|
|
14
|
+
* @param {string} [salt]
|
|
15
|
+
*/
|
|
16
|
+
function hash(password, salt) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const finalSalt = salt || randomBytes(16).toString('hex');
|
|
19
|
+
scrypt(password, finalSalt, 64, (err, key) => {
|
|
20
|
+
if (err) {
|
|
21
|
+
reject(err);
|
|
22
|
+
} else {
|
|
23
|
+
resolve(`${finalSalt}$${key.toString('hex')}`);
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} password
|
|
31
|
+
* @param {string} passwordHash
|
|
32
|
+
*/
|
|
33
|
+
async function verifyHash(password, passwordHash) {
|
|
34
|
+
const [saltPart, _hashPart] = passwordHash.split('$');
|
|
35
|
+
const rehashed = await hash(password, saltPart);
|
|
36
|
+
return rehashed === passwordHash;
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
/**
|
|
18
40
|
* @typedef {{
|
|
@@ -77,33 +99,41 @@ const signingSecret = new Secret('wirejs-services/auth-jwt-signing-secret');
|
|
|
77
99
|
* @property {string} [cookie] - The name of the cookie to use to store the authentication state JWT.
|
|
78
100
|
*/
|
|
79
101
|
|
|
80
|
-
/**
|
|
81
|
-
* @type {Map<string, AuthService>}
|
|
82
|
-
*/
|
|
83
|
-
const services = new Map();
|
|
84
|
-
|
|
85
102
|
const ONE_WEEK = 7 * 24 * 60 * 60; // days * hours/day * minutes/hour * seconds/minute
|
|
86
103
|
|
|
87
|
-
export class AuthenticationService {
|
|
88
|
-
id;
|
|
104
|
+
export class AuthenticationService extends Resource {
|
|
89
105
|
#duration;
|
|
90
106
|
#keepalive;
|
|
91
107
|
#cookieName;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @type {Secret}
|
|
111
|
+
*/
|
|
112
|
+
#rawSigningSecret;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @type {Promise<Uint8Array<ArrayBufferLike>> | undefined}
|
|
116
|
+
*/
|
|
92
117
|
#signingSecret;
|
|
93
118
|
|
|
94
119
|
#users;
|
|
95
120
|
|
|
96
121
|
/**
|
|
97
122
|
*
|
|
123
|
+
* @param {Resource | string} scope
|
|
98
124
|
* @param {string} id
|
|
99
125
|
* @param {AuthenticationServiceOptions} [options]
|
|
100
126
|
*/
|
|
101
|
-
constructor(id, { duration, keepalive, cookie } = {}) {
|
|
102
|
-
|
|
127
|
+
constructor(scope, id, { duration, keepalive, cookie } = {}) {
|
|
128
|
+
super(scope, id);
|
|
129
|
+
|
|
103
130
|
this.#duration = duration || ONE_WEEK;
|
|
104
131
|
this.#keepalive = !!keepalive;
|
|
105
132
|
this.#cookieName = cookie ?? 'identity';
|
|
106
133
|
|
|
134
|
+
this.#rawSigningSecret = new Secret(this, 'jwt-signing-secret');
|
|
135
|
+
const fileService = new FileService(this, 'files');
|
|
136
|
+
|
|
107
137
|
this.#users = {
|
|
108
138
|
id,
|
|
109
139
|
|
|
@@ -113,7 +143,7 @@ export class AuthenticationService {
|
|
|
113
143
|
*/
|
|
114
144
|
async get(username) {
|
|
115
145
|
try {
|
|
116
|
-
const data = await
|
|
146
|
+
const data = await fileService.read(this.filenameFor(username));
|
|
117
147
|
return JSON.parse(data);
|
|
118
148
|
} catch {
|
|
119
149
|
return undefined;
|
|
@@ -125,14 +155,7 @@ export class AuthenticationService {
|
|
|
125
155
|
* @param {User} user
|
|
126
156
|
*/
|
|
127
157
|
async set(username, details) {
|
|
128
|
-
await
|
|
129
|
-
path.dirname(this.filenameFor(username)),
|
|
130
|
-
{ recursive: true }
|
|
131
|
-
);
|
|
132
|
-
await fs.promises.writeFile(
|
|
133
|
-
this.filenameFor(username),
|
|
134
|
-
JSON.stringify(details)
|
|
135
|
-
);
|
|
158
|
+
await fileService.write(this.filenameFor(username), JSON.stringify(details));
|
|
136
159
|
},
|
|
137
160
|
|
|
138
161
|
/**
|
|
@@ -144,29 +167,17 @@ export class AuthenticationService {
|
|
|
144
167
|
},
|
|
145
168
|
|
|
146
169
|
/**
|
|
147
|
-
* @param {string} username
|
|
170
|
+
* @param {string} username
|
|
148
171
|
* @returns
|
|
149
172
|
*/
|
|
150
173
|
filenameFor(username) {
|
|
151
|
-
|
|
152
|
-
const sanitizedName = username.replace('~', '-').replace(/\.+/g, '.');
|
|
153
|
-
return path.join(CWD,
|
|
154
|
-
'temp',
|
|
155
|
-
'wirejs-services',
|
|
156
|
-
'authentication',
|
|
157
|
-
sanitizedId,
|
|
158
|
-
`${sanitizedName}.json`
|
|
159
|
-
);
|
|
174
|
+
return `${username}.json`;
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
|
-
|
|
163
|
-
if (services.has(id)) {
|
|
164
|
-
services.set(id, this);
|
|
165
|
-
}
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
async getSigningSecret() {
|
|
169
|
-
const secretAsString = await
|
|
180
|
+
const secretAsString = await this.#rawSigningSecret.read();
|
|
170
181
|
return new TextEncoder().encode(secretAsString);
|
|
171
182
|
}
|
|
172
183
|
|
|
@@ -347,7 +358,7 @@ export class AuthenticationService {
|
|
|
347
358
|
} else {
|
|
348
359
|
await this.#users.set(inputs.username, {
|
|
349
360
|
id: inputs.username,
|
|
350
|
-
password: await
|
|
361
|
+
password: await hash(inputs.password)
|
|
351
362
|
});
|
|
352
363
|
await this.setBaseState(cookies, inputs.username);
|
|
353
364
|
return this.getState(cookies);
|
|
@@ -361,7 +372,7 @@ export class AuthenticationService {
|
|
|
361
372
|
message: `User doesn't exist.`
|
|
362
373
|
}]
|
|
363
374
|
};
|
|
364
|
-
} else if (await
|
|
375
|
+
} else if (await verifyHash(inputs.password, user.password)) {
|
|
365
376
|
// a real authentication service will use password hashing.
|
|
366
377
|
// this is an in-memory just-for-testing user pool.
|
|
367
378
|
await this.setBaseState(cookies, inputs.username);
|
|
@@ -384,10 +395,10 @@ export class AuthenticationService {
|
|
|
384
395
|
message: `You're not signed in as a recognized user.`
|
|
385
396
|
}]
|
|
386
397
|
};
|
|
387
|
-
} else if (await
|
|
398
|
+
} else if (await verifyHash(inputs.existingPassword, user.password)) {
|
|
388
399
|
await this.#users.set(user.id, {
|
|
389
400
|
...user,
|
|
390
|
-
password: await
|
|
401
|
+
password: await hash(inputs.newPassword)
|
|
391
402
|
});
|
|
392
403
|
return {
|
|
393
404
|
message: "Password updated.",
|
package/lib/services/file.js
CHANGED
|
@@ -2,27 +2,19 @@ import process from 'process';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { Resource } from '../resource.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
* @type {Map<string, FileService>}
|
|
9
|
-
*/
|
|
10
|
-
const services = new Map();
|
|
7
|
+
const CWD = process.cwd();
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
id;
|
|
9
|
+
const ALREADY_EXISTS_CODE = 'EEXIST';
|
|
14
10
|
|
|
11
|
+
export class FileService extends Resource {
|
|
15
12
|
/**
|
|
16
|
-
*
|
|
17
|
-
* @param {
|
|
18
|
-
* id: string
|
|
19
|
-
* }} options
|
|
13
|
+
* @param {Resource | string} scope
|
|
14
|
+
* @param {string} id
|
|
20
15
|
*/
|
|
21
|
-
constructor(id) {
|
|
22
|
-
|
|
23
|
-
if (!services.has(id)) {
|
|
24
|
-
services.set(id, this);
|
|
25
|
-
}
|
|
16
|
+
constructor(scope, id) {
|
|
17
|
+
super(scope, id);
|
|
26
18
|
}
|
|
27
19
|
|
|
28
20
|
/**
|
|
@@ -30,9 +22,9 @@ export class FileService {
|
|
|
30
22
|
* @returns
|
|
31
23
|
*/
|
|
32
24
|
#fullNameFor(filename) {
|
|
33
|
-
const sanitizedId = this.
|
|
25
|
+
const sanitizedId = this.absoluteId.replace('~', '-').replace(/\.+/g, '.');
|
|
34
26
|
const sanitizedName = filename.replace('~', '-').replace(/\.+/g, '.');
|
|
35
|
-
return path.join(CWD, 'temp', 'wirejs-services',
|
|
27
|
+
return path.join(CWD, 'temp', 'wirejs-services', sanitizedId, sanitizedName);
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
/**
|
|
@@ -47,12 +39,16 @@ export class FileService {
|
|
|
47
39
|
/**
|
|
48
40
|
*
|
|
49
41
|
* @param {string} filename
|
|
50
|
-
* @param {string} data
|
|
42
|
+
* @param {string} data
|
|
43
|
+
* @param {{
|
|
44
|
+
* onlyIfNotExists?: boolean;
|
|
45
|
+
* }} [options]
|
|
51
46
|
*/
|
|
52
|
-
async write(filename, data) {
|
|
47
|
+
async write(filename, data, { onlyIfNotExists = false } = {}) {
|
|
53
48
|
const fullname = this.#fullNameFor(filename);
|
|
49
|
+
const flag = onlyIfNotExists ? 'wx' : 'w';
|
|
54
50
|
await fs.promises.mkdir(path.dirname(fullname), { recursive: true });
|
|
55
|
-
return fs.promises.writeFile(fullname, data);
|
|
51
|
+
return fs.promises.writeFile(fullname, data, { flag });
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
/**
|
|
@@ -75,4 +71,8 @@ export class FileService {
|
|
|
75
71
|
if (prefix === undefined || name.startsWith(prefix)) yield name;
|
|
76
72
|
}
|
|
77
73
|
}
|
|
74
|
+
|
|
75
|
+
isAlreadyExistsError(error) {
|
|
76
|
+
return error.code === ALREADY_EXISTS_CODE;
|
|
77
|
+
}
|
|
78
78
|
}
|
package/lib/types.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type {
|
|
|
2
2
|
AuthenticationService as AuthenticationServiceBase,
|
|
3
3
|
withContext as withContextBase,
|
|
4
4
|
FileService as FileServiceBase,
|
|
5
|
-
Context as ContextBase
|
|
5
|
+
Context as ContextBase,
|
|
6
|
+
Resource as ResourceBase,
|
|
6
7
|
} from './index.js';
|
|
7
8
|
|
|
9
|
+
export declare class Resource extends ResourceBase {};
|
|
8
10
|
export declare class Context extends ContextBase {};
|
|
9
11
|
export declare class AuthenticationService extends AuthenticationServiceBase {};
|
|
10
12
|
export declare class FileService extends FileServiceBase {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-resources",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2-alpha",
|
|
4
4
|
"description": "Basic services and server-side resources for wirejs apps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://github.com/svidgen/create-wirejs-app#readme",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"bcrypt": "^5.1.1",
|
|
27
26
|
"jose": "^5.9.6"
|
|
28
27
|
}
|
|
29
28
|
}
|