wirejs-resources 0.1.1-alpha → 0.1.3-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/adapters/context.js +3 -3
- package/lib/index.js +3 -1
- package/lib/registry.js +13 -0
- package/lib/resource.js +32 -0
- package/lib/resources/secret.js +35 -33
- package/lib/services/authentication.js +62 -49
- 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/adapters/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { registry } from "../registry";
|
|
2
2
|
|
|
3
3
|
const contextWrappers = new Set();
|
|
4
4
|
|
|
@@ -76,7 +76,7 @@ export function requiresContext(fnOrNS) {
|
|
|
76
76
|
|
|
77
77
|
export class Context {
|
|
78
78
|
/**
|
|
79
|
-
* @type {CookieJar} cookies
|
|
79
|
+
* @type {typeof registry['CookieJar']} cookies
|
|
80
80
|
*/
|
|
81
81
|
cookies;
|
|
82
82
|
|
|
@@ -87,7 +87,7 @@ export class Context {
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* @param {{
|
|
90
|
-
* cookies: CookieJar;
|
|
90
|
+
* cookies: typeof registry['CookieJar'];
|
|
91
91
|
* location: URL;
|
|
92
92
|
* }}
|
|
93
93
|
*/
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
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';
|
|
6
|
+
export { registry } from './registry.js';
|
package/lib/registry.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Context } from './adapters/context.js';
|
|
2
|
+
import { CookieJar } from './adapters/cookie-jar.js';
|
|
3
|
+
import { Secret } from './resources/secret.js';
|
|
4
|
+
import { AuthenticationService } from './services/authentication.js'
|
|
5
|
+
import { FileService } from './services/file.js';
|
|
6
|
+
|
|
7
|
+
export const registry = /** @type {const} **/ ({
|
|
8
|
+
Context,
|
|
9
|
+
CookieJar,
|
|
10
|
+
Secret,
|
|
11
|
+
AuthenticationService,
|
|
12
|
+
FileService
|
|
13
|
+
});
|
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 { registry } from '../registry.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 registry.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
|
|
|
8
|
-
import {
|
|
9
|
-
import { CookieJar } from '../adapters/cookie-jar.js';
|
|
5
|
+
import { Resource } from '../resource.js';
|
|
10
6
|
import { withContext } from '../adapters/context.js';
|
|
7
|
+
import { registry } from '../registry.js';
|
|
11
8
|
|
|
12
|
-
const CWD = process.cwd();
|
|
13
|
-
const SALT_ROUNDS = 10;
|
|
14
9
|
|
|
15
|
-
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} password
|
|
12
|
+
* @param {string} [salt]
|
|
13
|
+
*/
|
|
14
|
+
function hash(password, salt) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const finalSalt = salt || randomBytes(16).toString('hex');
|
|
17
|
+
scrypt(password, finalSalt, 64, (err, key) => {
|
|
18
|
+
if (err) {
|
|
19
|
+
reject(err);
|
|
20
|
+
} else {
|
|
21
|
+
resolve(`${finalSalt}$${key.toString('hex')}`);
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} password
|
|
29
|
+
* @param {string} passwordHash
|
|
30
|
+
*/
|
|
31
|
+
async function verifyHash(password, passwordHash) {
|
|
32
|
+
const [saltPart, _hashPart] = passwordHash.split('$');
|
|
33
|
+
const rehashed = await hash(password, saltPart);
|
|
34
|
+
return rehashed === passwordHash;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// #region types
|
|
16
38
|
|
|
17
39
|
/**
|
|
18
40
|
* @typedef {{
|
|
@@ -77,33 +99,43 @@ 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();
|
|
102
|
+
// #endregion
|
|
84
103
|
|
|
85
104
|
const ONE_WEEK = 7 * 24 * 60 * 60; // days * hours/day * minutes/hour * seconds/minute
|
|
86
105
|
|
|
87
|
-
export class AuthenticationService {
|
|
88
|
-
id;
|
|
106
|
+
export class AuthenticationService extends Resource {
|
|
89
107
|
#duration;
|
|
90
108
|
#keepalive;
|
|
91
109
|
#cookieName;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @type {typeof registry['Secret']}
|
|
113
|
+
*/
|
|
114
|
+
#rawSigningSecret;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @type {Promise<Uint8Array<ArrayBufferLike>> | undefined}
|
|
118
|
+
*/
|
|
92
119
|
#signingSecret;
|
|
93
120
|
|
|
94
121
|
#users;
|
|
95
122
|
|
|
96
123
|
/**
|
|
97
124
|
*
|
|
125
|
+
* @param {Resource | string} scope
|
|
98
126
|
* @param {string} id
|
|
99
127
|
* @param {AuthenticationServiceOptions} [options]
|
|
100
128
|
*/
|
|
101
|
-
constructor(id, { duration, keepalive, cookie } = {}) {
|
|
102
|
-
|
|
129
|
+
constructor(scope, id, { duration, keepalive, cookie } = {}) {
|
|
130
|
+
super(scope, id);
|
|
131
|
+
|
|
103
132
|
this.#duration = duration || ONE_WEEK;
|
|
104
133
|
this.#keepalive = !!keepalive;
|
|
105
134
|
this.#cookieName = cookie ?? 'identity';
|
|
106
135
|
|
|
136
|
+
this.#rawSigningSecret = new registry.Secret(this, 'jwt-signing-secret');
|
|
137
|
+
const fileService = new registry.FileService(this, 'files');
|
|
138
|
+
|
|
107
139
|
this.#users = {
|
|
108
140
|
id,
|
|
109
141
|
|
|
@@ -113,7 +145,7 @@ export class AuthenticationService {
|
|
|
113
145
|
*/
|
|
114
146
|
async get(username) {
|
|
115
147
|
try {
|
|
116
|
-
const data = await
|
|
148
|
+
const data = await fileService.read(this.filenameFor(username));
|
|
117
149
|
return JSON.parse(data);
|
|
118
150
|
} catch {
|
|
119
151
|
return undefined;
|
|
@@ -125,14 +157,7 @@ export class AuthenticationService {
|
|
|
125
157
|
* @param {User} user
|
|
126
158
|
*/
|
|
127
159
|
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
|
-
);
|
|
160
|
+
await fileService.write(this.filenameFor(username), JSON.stringify(details));
|
|
136
161
|
},
|
|
137
162
|
|
|
138
163
|
/**
|
|
@@ -144,29 +169,17 @@ export class AuthenticationService {
|
|
|
144
169
|
},
|
|
145
170
|
|
|
146
171
|
/**
|
|
147
|
-
* @param {string} username
|
|
172
|
+
* @param {string} username
|
|
148
173
|
* @returns
|
|
149
174
|
*/
|
|
150
175
|
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
|
-
);
|
|
176
|
+
return `${username}.json`;
|
|
160
177
|
}
|
|
161
178
|
}
|
|
162
|
-
|
|
163
|
-
if (services.has(id)) {
|
|
164
|
-
services.set(id, this);
|
|
165
|
-
}
|
|
166
179
|
}
|
|
167
180
|
|
|
168
181
|
async getSigningSecret() {
|
|
169
|
-
const secretAsString = await
|
|
182
|
+
const secretAsString = await this.#rawSigningSecret.read();
|
|
170
183
|
return new TextEncoder().encode(secretAsString);
|
|
171
184
|
}
|
|
172
185
|
|
|
@@ -181,7 +194,7 @@ export class AuthenticationService {
|
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
/**
|
|
184
|
-
* @param {CookieJar} cookies
|
|
197
|
+
* @param {typeof registry['CookieJar']} cookies
|
|
185
198
|
* @returns {Promise<AuthenticationBaseState>}
|
|
186
199
|
*/
|
|
187
200
|
async getBaseState(cookies) {
|
|
@@ -212,7 +225,7 @@ export class AuthenticationService {
|
|
|
212
225
|
}
|
|
213
226
|
|
|
214
227
|
/**
|
|
215
|
-
* @param {CookieJar} cookies
|
|
228
|
+
* @param {typeof registry['CookieJar']} cookies
|
|
216
229
|
* @returns {Promise<AuthenticationState>}
|
|
217
230
|
*/
|
|
218
231
|
async getState(cookies) {
|
|
@@ -280,7 +293,7 @@ export class AuthenticationService {
|
|
|
280
293
|
|
|
281
294
|
/**
|
|
282
295
|
*
|
|
283
|
-
* @param {CookieJar} cookies
|
|
296
|
+
* @param {typeof registry['CookieJar']} cookies
|
|
284
297
|
* @param {string | undefined} [user]
|
|
285
298
|
*/
|
|
286
299
|
async setBaseState(cookies, user) {
|
|
@@ -325,7 +338,7 @@ export class AuthenticationService {
|
|
|
325
338
|
}
|
|
326
339
|
|
|
327
340
|
/**
|
|
328
|
-
* @param {CookieJar} cookies
|
|
341
|
+
* @param {typeof registry['CookieJar']} cookies
|
|
329
342
|
* @param {PerformActionParameter} params
|
|
330
343
|
* @returns {Promise<AuthenticationState | { errors: AuthenticationError[] }>}
|
|
331
344
|
*/
|
|
@@ -347,7 +360,7 @@ export class AuthenticationService {
|
|
|
347
360
|
} else {
|
|
348
361
|
await this.#users.set(inputs.username, {
|
|
349
362
|
id: inputs.username,
|
|
350
|
-
password: await
|
|
363
|
+
password: await hash(inputs.password)
|
|
351
364
|
});
|
|
352
365
|
await this.setBaseState(cookies, inputs.username);
|
|
353
366
|
return this.getState(cookies);
|
|
@@ -361,7 +374,7 @@ export class AuthenticationService {
|
|
|
361
374
|
message: `User doesn't exist.`
|
|
362
375
|
}]
|
|
363
376
|
};
|
|
364
|
-
} else if (await
|
|
377
|
+
} else if (await verifyHash(inputs.password, user.password)) {
|
|
365
378
|
// a real authentication service will use password hashing.
|
|
366
379
|
// this is an in-memory just-for-testing user pool.
|
|
367
380
|
await this.setBaseState(cookies, inputs.username);
|
|
@@ -384,10 +397,10 @@ export class AuthenticationService {
|
|
|
384
397
|
message: `You're not signed in as a recognized user.`
|
|
385
398
|
}]
|
|
386
399
|
};
|
|
387
|
-
} else if (await
|
|
400
|
+
} else if (await verifyHash(inputs.existingPassword, user.password)) {
|
|
388
401
|
await this.#users.set(user.id, {
|
|
389
402
|
...user,
|
|
390
|
-
password: await
|
|
403
|
+
password: await hash(inputs.newPassword)
|
|
391
404
|
});
|
|
392
405
|
return {
|
|
393
406
|
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.3-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
|
}
|