wirejs-resources 0.1.7-alpha → 0.1.8-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/dist/adapters/context.d.ts +21 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/cookie-jar.d.ts +30 -0
- package/dist/adapters/cookie-jar.js +55 -0
- package/dist/index.js +6 -0
- package/dist/overrides.d.ts +11 -0
- package/{lib → dist}/overrides.js +1 -1
- package/dist/resource.d.ts +6 -0
- package/dist/resource.js +20 -0
- package/dist/resources/secret.d.ts +7 -0
- package/dist/resources/secret.js +28 -0
- package/dist/services/authentication.d.ts +67 -0
- package/dist/services/authentication.js +286 -0
- package/dist/services/file.d.ts +16 -0
- package/dist/services/file.js +38 -0
- package/package.json +16 -7
- package/lib/adapters/context.js +0 -98
- package/lib/adapters/cookie-jar.js +0 -94
- package/lib/resource.js +0 -32
- package/lib/resources/secret.js +0 -54
- package/lib/services/authentication.js +0 -439
- package/lib/services/file.js +0 -78
- package/lib/types.ts +0 -16
- /package/{lib/index.js → dist/index.d.ts} +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CookieJar } from "./cookie-jar.js";
|
|
2
|
+
type ApiMethod = (...args: any) => any;
|
|
3
|
+
type ApiNamespace = {
|
|
4
|
+
[K in string]: ApiMethod | ApiNamespace;
|
|
5
|
+
};
|
|
6
|
+
type ContextfulApiMethod<T> = T extends ((...args: infer ARGS) => infer RT) ? ((context: Context | boolean, ...args: ARGS) => RT extends Promise<any> ? RT : Promise<RT>) : never;
|
|
7
|
+
type ContextfulApiNamespace<T> = {
|
|
8
|
+
[K in keyof T]: T[K] extends ApiMethod ? ContextfulApiMethod<T[K]> : ContextfulApiNamespace<T[K]>;
|
|
9
|
+
};
|
|
10
|
+
type ContextWrapped<T extends ApiNamespace | ApiMethod> = T extends ApiMethod ? ContextfulApiMethod<T> : ContextfulApiNamespace<T>;
|
|
11
|
+
export declare function withContext<T extends ApiMethod | ApiNamespace>(contextWrapper: (context: Context) => T, path?: string[]): ContextWrapped<T>;
|
|
12
|
+
export declare function requiresContext(fnOrNS: Object): fnOrNS is (context: Context) => any;
|
|
13
|
+
export declare class Context {
|
|
14
|
+
cookies: CookieJar;
|
|
15
|
+
location: URL;
|
|
16
|
+
constructor({ cookies, location }: {
|
|
17
|
+
cookies: CookieJar;
|
|
18
|
+
location: URL;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const __requiresContext = Symbol('__requiresContext');
|
|
2
|
+
export function withContext(contextWrapper, path = []) {
|
|
3
|
+
// first param needs to be a function, which enables `Proxy` to implement `apply()`.
|
|
4
|
+
const fnOrNs = new Proxy(function () { }, {
|
|
5
|
+
apply(_target, _thisArg, args) {
|
|
6
|
+
const [context, ...remainingArgs] = args;
|
|
7
|
+
let functionOrNamespaceObject = contextWrapper(context);
|
|
8
|
+
console.log({ context, args, functionOrNamespaceObject, path });
|
|
9
|
+
for (const k of path) {
|
|
10
|
+
functionOrNamespaceObject = functionOrNamespaceObject[k];
|
|
11
|
+
}
|
|
12
|
+
return functionOrNamespaceObject(...remainingArgs);
|
|
13
|
+
},
|
|
14
|
+
get(_target, prop) {
|
|
15
|
+
if (prop === __requiresContext)
|
|
16
|
+
return true;
|
|
17
|
+
return withContext(contextWrapper, [...path, prop]);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return fnOrNs;
|
|
21
|
+
}
|
|
22
|
+
export function requiresContext(fnOrNS) {
|
|
23
|
+
return fnOrNS[__requiresContext] === true;
|
|
24
|
+
}
|
|
25
|
+
export class Context {
|
|
26
|
+
cookies;
|
|
27
|
+
location;
|
|
28
|
+
constructor({ cookies, location }) {
|
|
29
|
+
this.cookies = cookies;
|
|
30
|
+
this.location = location;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type Cookie = {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
httpOnly?: boolean;
|
|
5
|
+
secure?: boolean;
|
|
6
|
+
maxAge?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare class CookieJar {
|
|
9
|
+
#private;
|
|
10
|
+
/**
|
|
11
|
+
* Initialize
|
|
12
|
+
*/
|
|
13
|
+
constructor(cookie?: string);
|
|
14
|
+
set(cookie: Cookie): void;
|
|
15
|
+
get(name: string): {
|
|
16
|
+
name: string;
|
|
17
|
+
value: string;
|
|
18
|
+
httpOnly?: boolean;
|
|
19
|
+
secure?: boolean;
|
|
20
|
+
maxAge?: number;
|
|
21
|
+
} | undefined;
|
|
22
|
+
delete(name: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Gets a copy of all cookies.
|
|
25
|
+
*
|
|
26
|
+
* Changes made to this copy are not reflected
|
|
27
|
+
*/
|
|
28
|
+
getAll(): Record<string, string>;
|
|
29
|
+
getSetCookies(): Cookie[];
|
|
30
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export class CookieJar {
|
|
2
|
+
#cookies = {};
|
|
3
|
+
/**
|
|
4
|
+
* The list of cookies that have been set with `set()` which need to be
|
|
5
|
+
* sent to the client.
|
|
6
|
+
*/
|
|
7
|
+
#setCookies = new Set();
|
|
8
|
+
/**
|
|
9
|
+
* Initialize
|
|
10
|
+
*/
|
|
11
|
+
constructor(cookie) {
|
|
12
|
+
this.#cookies = Object.fromEntries((cookie || '')
|
|
13
|
+
.split(/;/g)
|
|
14
|
+
.map(c => {
|
|
15
|
+
const [k, v] = c.split('=').map(p => decodeURIComponent(p.trim()));
|
|
16
|
+
return [k, {
|
|
17
|
+
name: k,
|
|
18
|
+
value: v
|
|
19
|
+
}];
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
set(cookie) {
|
|
23
|
+
this.#cookies[cookie.name] = { ...cookie };
|
|
24
|
+
this.#setCookies.add(cookie.name);
|
|
25
|
+
}
|
|
26
|
+
get(name) {
|
|
27
|
+
return this.#cookies[name] ? { ...this.#cookies[name] } : undefined;
|
|
28
|
+
}
|
|
29
|
+
delete(name) {
|
|
30
|
+
if (this.#cookies[name]) {
|
|
31
|
+
this.#cookies[name].value = '-- deleted --';
|
|
32
|
+
this.#cookies[name].maxAge = 0;
|
|
33
|
+
this.#setCookies.add(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Gets a copy of all cookies.
|
|
38
|
+
*
|
|
39
|
+
* Changes made to this copy are not reflected
|
|
40
|
+
*/
|
|
41
|
+
getAll() {
|
|
42
|
+
const all = {};
|
|
43
|
+
for (const cookie of Object.values(this.#cookies)) {
|
|
44
|
+
all[cookie.name] = cookie.value;
|
|
45
|
+
}
|
|
46
|
+
return all;
|
|
47
|
+
}
|
|
48
|
+
getSetCookies() {
|
|
49
|
+
const all = [];
|
|
50
|
+
for (const name of this.#setCookies) {
|
|
51
|
+
all.push({ ...this.#cookies[name] });
|
|
52
|
+
}
|
|
53
|
+
return all;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { FileService } from './services/file.js';
|
|
2
|
+
export { AuthenticationService } from './services/authentication.js';
|
|
3
|
+
export { CookieJar } from './adapters/cookie-jar.js';
|
|
4
|
+
export { withContext, requiresContext, Context } from './adapters/context.js';
|
|
5
|
+
export { Resource } from './resource.js';
|
|
6
|
+
export { overrides } from './overrides.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FileService } from "./services/file";
|
|
2
|
+
import type { AuthenticationService } from "./services/authentication";
|
|
3
|
+
import type { Secret } from "./resources/secret";
|
|
4
|
+
/**
|
|
5
|
+
* Used by hosting providers to provide service overrides.
|
|
6
|
+
*/
|
|
7
|
+
export declare const overrides: {
|
|
8
|
+
FileService?: typeof FileService;
|
|
9
|
+
AuthenticationService?: typeof AuthenticationService;
|
|
10
|
+
Secret?: typeof Secret;
|
|
11
|
+
};
|
package/dist/resource.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class Resource {
|
|
2
|
+
scope;
|
|
3
|
+
id;
|
|
4
|
+
constructor(scope, id) {
|
|
5
|
+
this.scope = scope;
|
|
6
|
+
this.id = id;
|
|
7
|
+
}
|
|
8
|
+
get absoluteId() {
|
|
9
|
+
const sanitizedId = encodeURIComponent(this.id);
|
|
10
|
+
if (typeof this.scope === 'string') {
|
|
11
|
+
return `${encodeURIComponent(this.scope)}/${sanitizedId}`;
|
|
12
|
+
}
|
|
13
|
+
else if (typeof this.scope?.id === 'string') {
|
|
14
|
+
return `${this.scope.absoluteId}/${sanitizedId}`;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
throw new Error("Resources must defined within a scope. Provide either a namespace string or parent resource.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { Resource } from '../resource.js';
|
|
3
|
+
import { FileService } from '../services/file.js';
|
|
4
|
+
import { overrides } from '../overrides.js';
|
|
5
|
+
const FILENAME = 'secret';
|
|
6
|
+
export class Secret extends Resource {
|
|
7
|
+
#fileService;
|
|
8
|
+
#initPromise;
|
|
9
|
+
constructor(scope, id) {
|
|
10
|
+
super(scope, id);
|
|
11
|
+
this.#fileService = new (overrides.FileService || FileService)(this, 'files');
|
|
12
|
+
}
|
|
13
|
+
#initialize() {
|
|
14
|
+
this.#initPromise = this.#initPromise || this.#fileService.write(FILENAME, JSON.stringify(crypto.randomBytes(64).toString('base64url')), { onlyIfNotExists: true }).catch(error => {
|
|
15
|
+
if (!this.#fileService.isAlreadyExistsError(error))
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
return this.#initPromise;
|
|
19
|
+
}
|
|
20
|
+
async read() {
|
|
21
|
+
await this.#initialize();
|
|
22
|
+
return JSON.parse(await this.#fileService.read(FILENAME));
|
|
23
|
+
}
|
|
24
|
+
async write(data) {
|
|
25
|
+
await this.#initialize();
|
|
26
|
+
await this.#fileService.write(FILENAME, JSON.stringify(data));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Resource } from '../resource.js';
|
|
2
|
+
import type { CookieJar } from '../adapters/cookie-jar.js';
|
|
3
|
+
type AuthenticationInput = {
|
|
4
|
+
label: string;
|
|
5
|
+
type: 'text' | 'password';
|
|
6
|
+
isRequired?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type Action = {
|
|
9
|
+
name: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
inputs?: Record<string, AuthenticationInput>;
|
|
14
|
+
buttons?: string[];
|
|
15
|
+
};
|
|
16
|
+
type AuthenticationBaseState = {
|
|
17
|
+
state: 'authenticated' | 'unauthenticated';
|
|
18
|
+
user: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
type AuthenticationState = {
|
|
21
|
+
state: AuthenticationBaseState;
|
|
22
|
+
message?: string;
|
|
23
|
+
actions: Record<string, Action>;
|
|
24
|
+
};
|
|
25
|
+
type PerformActionParameter = {
|
|
26
|
+
key: string;
|
|
27
|
+
inputs: Record<string, string | number | boolean>;
|
|
28
|
+
verb: string;
|
|
29
|
+
};
|
|
30
|
+
type AuthenticationError = {
|
|
31
|
+
message: string;
|
|
32
|
+
field?: string;
|
|
33
|
+
};
|
|
34
|
+
type AuthenticationServiceOptions = {
|
|
35
|
+
/**
|
|
36
|
+
* The number of seconds the authentication session stays alive.
|
|
37
|
+
*/
|
|
38
|
+
duration?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Whether to automatically extend (keep alive) an authentication session when used.
|
|
41
|
+
*/
|
|
42
|
+
keepalive?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The name of the cookie to use to store the authentication state JWT.
|
|
45
|
+
*/
|
|
46
|
+
cookie?: string;
|
|
47
|
+
};
|
|
48
|
+
export declare class AuthenticationService extends Resource {
|
|
49
|
+
#private;
|
|
50
|
+
constructor(scope: Resource | string, id: string, { duration, keepalive, cookie }?: AuthenticationServiceOptions);
|
|
51
|
+
getSigningSecret(): Promise<Uint8Array<ArrayBufferLike>>;
|
|
52
|
+
get signingSecret(): Promise<Uint8Array<ArrayBufferLike>>;
|
|
53
|
+
getBaseState(cookies: CookieJar): Promise<AuthenticationBaseState>;
|
|
54
|
+
getState(cookies: CookieJar): Promise<AuthenticationState>;
|
|
55
|
+
setBaseState(cookies: CookieJar, user?: string): Promise<void>;
|
|
56
|
+
missingFieldErrors(input: Record<string, string | number | boolean>, fields: string[]): AuthenticationError[] | undefined;
|
|
57
|
+
setState(cookies: CookieJar, { key, inputs, verb: _verb }: PerformActionParameter): Promise<AuthenticationState | {
|
|
58
|
+
errors: AuthenticationError[];
|
|
59
|
+
}>;
|
|
60
|
+
buildApi(this: AuthenticationService): {
|
|
61
|
+
getState: (context: import("../adapters/context.js").Context | boolean) => Promise<AuthenticationState>;
|
|
62
|
+
setState: (context: import("../adapters/context.js").Context | boolean, options: PerformActionParameter) => Promise<AuthenticationState | {
|
|
63
|
+
errors: AuthenticationError[];
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { scrypt, randomBytes } from 'crypto';
|
|
2
|
+
import * as jose from 'jose';
|
|
3
|
+
import { Resource } from '../resource.js';
|
|
4
|
+
import { FileService } from './file.js';
|
|
5
|
+
import { Secret } from '../resources/secret.js';
|
|
6
|
+
import { withContext } from '../adapters/context.js';
|
|
7
|
+
import { overrides } from '../overrides.js';
|
|
8
|
+
function hash(password, salt) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const finalSalt = salt || randomBytes(16).toString('hex');
|
|
11
|
+
scrypt(password, finalSalt, 64, (err, key) => {
|
|
12
|
+
if (err) {
|
|
13
|
+
reject(err);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
resolve(`${finalSalt}$${key.toString('hex')}`);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function verifyHash(password, passwordHash) {
|
|
22
|
+
const [saltPart, _hashPart] = passwordHash.split('$');
|
|
23
|
+
const rehashed = await hash(password, saltPart);
|
|
24
|
+
return rehashed === passwordHash;
|
|
25
|
+
}
|
|
26
|
+
// #endregion
|
|
27
|
+
const ONE_WEEK = 7 * 24 * 60 * 60; // days * hours/day * minutes/hour * seconds/minute
|
|
28
|
+
export class AuthenticationService extends Resource {
|
|
29
|
+
#duration;
|
|
30
|
+
;
|
|
31
|
+
#keepalive;
|
|
32
|
+
#cookieName;
|
|
33
|
+
#rawSigningSecret;
|
|
34
|
+
#signingSecret;
|
|
35
|
+
#users;
|
|
36
|
+
constructor(scope, id, { duration, keepalive, cookie } = {}) {
|
|
37
|
+
super(scope, id);
|
|
38
|
+
this.#duration = duration || ONE_WEEK;
|
|
39
|
+
this.#keepalive = !!keepalive;
|
|
40
|
+
this.#cookieName = cookie ?? 'identity';
|
|
41
|
+
this.#rawSigningSecret = new (overrides.Secret || Secret)(this, 'jwt-signing-secret');
|
|
42
|
+
const fileService = new (overrides.FileService || FileService)(this, 'files');
|
|
43
|
+
this.#users = {
|
|
44
|
+
id,
|
|
45
|
+
async get(username) {
|
|
46
|
+
try {
|
|
47
|
+
const data = await fileService.read(this.filenameFor(username));
|
|
48
|
+
return JSON.parse(data);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async set(username, details) {
|
|
55
|
+
await fileService.write(this.filenameFor(username), JSON.stringify(details));
|
|
56
|
+
},
|
|
57
|
+
async has(username) {
|
|
58
|
+
const user = await this.get(username);
|
|
59
|
+
return !!user;
|
|
60
|
+
},
|
|
61
|
+
filenameFor(username) {
|
|
62
|
+
return `${username}.json`;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async getSigningSecret() {
|
|
67
|
+
const secretAsString = await this.#rawSigningSecret.read();
|
|
68
|
+
return new TextEncoder().encode(secretAsString);
|
|
69
|
+
}
|
|
70
|
+
get signingSecret() {
|
|
71
|
+
if (!this.#signingSecret) {
|
|
72
|
+
this.#signingSecret = this.getSigningSecret();
|
|
73
|
+
}
|
|
74
|
+
return this.#signingSecret;
|
|
75
|
+
}
|
|
76
|
+
async getBaseState(cookies) {
|
|
77
|
+
let idCookie;
|
|
78
|
+
let user;
|
|
79
|
+
try {
|
|
80
|
+
idCookie = cookies.get(this.#cookieName)?.value;
|
|
81
|
+
const idPayload = idCookie ? (await jose.jwtVerify(idCookie, await this.signingSecret)) : undefined;
|
|
82
|
+
user = idPayload ? idPayload.payload.sub : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
// jose doesn't like our cookie.
|
|
86
|
+
console.error(err);
|
|
87
|
+
}
|
|
88
|
+
if (user) {
|
|
89
|
+
return {
|
|
90
|
+
state: 'authenticated',
|
|
91
|
+
user
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return {
|
|
96
|
+
state: 'unauthenticated',
|
|
97
|
+
user: undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async getState(cookies) {
|
|
102
|
+
const state = await this.getBaseState(cookies);
|
|
103
|
+
if (state.state === 'authenticated') {
|
|
104
|
+
if (this.#keepalive)
|
|
105
|
+
this.setBaseState(cookies, state.user);
|
|
106
|
+
return {
|
|
107
|
+
state,
|
|
108
|
+
actions: {
|
|
109
|
+
changepassword: {
|
|
110
|
+
name: "Change Password",
|
|
111
|
+
inputs: {
|
|
112
|
+
existingPassword: {
|
|
113
|
+
label: 'Old Password',
|
|
114
|
+
type: 'password',
|
|
115
|
+
},
|
|
116
|
+
newPassword: {
|
|
117
|
+
label: 'New Password',
|
|
118
|
+
type: 'password',
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
buttons: ['Change Password']
|
|
122
|
+
},
|
|
123
|
+
signout: {
|
|
124
|
+
name: "Sign out"
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return {
|
|
131
|
+
state,
|
|
132
|
+
actions: {
|
|
133
|
+
signin: {
|
|
134
|
+
name: "Sign In",
|
|
135
|
+
inputs: {
|
|
136
|
+
username: {
|
|
137
|
+
label: 'Username',
|
|
138
|
+
type: 'text',
|
|
139
|
+
},
|
|
140
|
+
password: {
|
|
141
|
+
label: 'Password',
|
|
142
|
+
type: 'password',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
buttons: ['Sign In']
|
|
146
|
+
},
|
|
147
|
+
signup: {
|
|
148
|
+
name: "Sign Up",
|
|
149
|
+
inputs: {
|
|
150
|
+
username: {
|
|
151
|
+
label: 'Username',
|
|
152
|
+
type: 'text',
|
|
153
|
+
},
|
|
154
|
+
password: {
|
|
155
|
+
label: 'Password',
|
|
156
|
+
type: 'password',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
buttons: ['Sign Up']
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async setBaseState(cookies, user) {
|
|
166
|
+
if (!user) {
|
|
167
|
+
cookies.delete(this.#cookieName);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const jwt = await new jose.SignJWT({})
|
|
171
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
172
|
+
.setIssuedAt()
|
|
173
|
+
.setSubject(user)
|
|
174
|
+
.setExpirationTime(`${this.#duration}s`)
|
|
175
|
+
.sign(await this.signingSecret);
|
|
176
|
+
cookies.set({
|
|
177
|
+
name: this.#cookieName,
|
|
178
|
+
value: jwt,
|
|
179
|
+
httpOnly: true,
|
|
180
|
+
secure: true,
|
|
181
|
+
maxAge: this.#duration
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
missingFieldErrors(input, fields) {
|
|
186
|
+
const errors = [];
|
|
187
|
+
for (const field of fields) {
|
|
188
|
+
if (!input[field])
|
|
189
|
+
errors.push({
|
|
190
|
+
field,
|
|
191
|
+
message: "Field is required."
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return errors.length > 0 ? errors : undefined;
|
|
195
|
+
}
|
|
196
|
+
async setState(cookies, { key, inputs, verb: _verb }) {
|
|
197
|
+
if (key === 'signout') {
|
|
198
|
+
await this.setBaseState(cookies, undefined);
|
|
199
|
+
return this.getState(cookies);
|
|
200
|
+
}
|
|
201
|
+
else if (key === 'signup') {
|
|
202
|
+
const errors = this.missingFieldErrors(inputs, ['username', 'password']);
|
|
203
|
+
if (errors) {
|
|
204
|
+
return { errors };
|
|
205
|
+
}
|
|
206
|
+
else if (await this.#users.has(inputs.username)) {
|
|
207
|
+
return { errors: [{
|
|
208
|
+
field: 'username',
|
|
209
|
+
message: 'User already exists.'
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
await this.#users.set(inputs.username, {
|
|
215
|
+
id: inputs.username,
|
|
216
|
+
password: await hash(inputs.password)
|
|
217
|
+
});
|
|
218
|
+
await this.setBaseState(cookies, inputs.username);
|
|
219
|
+
return this.getState(cookies);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (key === 'signin') {
|
|
223
|
+
const user = await this.#users.get(inputs.username);
|
|
224
|
+
if (!user) {
|
|
225
|
+
return { errors: [{
|
|
226
|
+
field: 'username',
|
|
227
|
+
message: `User doesn't exist.`
|
|
228
|
+
}]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
else if (await verifyHash(inputs.password, user.password)) {
|
|
232
|
+
// a real authentication service will use password hashing.
|
|
233
|
+
// this is an in-memory just-for-testing user pool.
|
|
234
|
+
await this.setBaseState(cookies, inputs.username);
|
|
235
|
+
return this.getState(cookies);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
return { errors: [{
|
|
239
|
+
field: 'password',
|
|
240
|
+
message: "Incorrect password."
|
|
241
|
+
}]
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (key === 'changepassword') {
|
|
246
|
+
const state = await this.getBaseState(cookies);
|
|
247
|
+
const user = await this.#users.get(state.user);
|
|
248
|
+
if (!user) {
|
|
249
|
+
return { errors: [{
|
|
250
|
+
field: 'username',
|
|
251
|
+
message: `You're not signed in as a recognized user.`
|
|
252
|
+
}]
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
else if (await verifyHash(inputs.existingPassword, user.password)) {
|
|
256
|
+
await this.#users.set(user.id, {
|
|
257
|
+
...user,
|
|
258
|
+
password: await hash(inputs.newPassword)
|
|
259
|
+
});
|
|
260
|
+
return {
|
|
261
|
+
message: "Password updated.",
|
|
262
|
+
...await this.getState(cookies)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
return { errors: [{
|
|
267
|
+
field: 'existingPassword',
|
|
268
|
+
message: "The provided existing password is incorrect."
|
|
269
|
+
}]
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
return { errors: [{
|
|
275
|
+
message: 'Unrecognized authentication action.'
|
|
276
|
+
}]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
buildApi() {
|
|
281
|
+
return withContext(context => ({
|
|
282
|
+
getState: () => this.getState(context.cookies),
|
|
283
|
+
setState: (options) => this.setState(context.cookies, options),
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Resource } from '../resource.js';
|
|
2
|
+
export declare class FileService extends Resource {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(scope: Resource | string, id: string);
|
|
5
|
+
read(filename: string, encoding?: BufferEncoding): Promise<string>;
|
|
6
|
+
write(filename: string, data: string, { onlyIfNotExists }?: {
|
|
7
|
+
onlyIfNotExists?: boolean | undefined;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
delete(filename: string): Promise<void>;
|
|
10
|
+
list({ prefix }?: {
|
|
11
|
+
prefix?: string | undefined;
|
|
12
|
+
}): AsyncGenerator<string, void, unknown>;
|
|
13
|
+
isAlreadyExistsError(error: {
|
|
14
|
+
code: any;
|
|
15
|
+
}): boolean;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import process from 'process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { Resource } from '../resource.js';
|
|
5
|
+
const CWD = process.cwd();
|
|
6
|
+
const ALREADY_EXISTS_CODE = 'EEXIST';
|
|
7
|
+
export class FileService extends Resource {
|
|
8
|
+
constructor(scope, id) {
|
|
9
|
+
super(scope, id);
|
|
10
|
+
}
|
|
11
|
+
#fullNameFor(filename) {
|
|
12
|
+
const sanitizedId = this.absoluteId.replace('~', '-').replace(/\.+/g, '.');
|
|
13
|
+
const sanitizedName = filename.replace('~', '-').replace(/\.+/g, '.');
|
|
14
|
+
return path.join(CWD, 'temp', 'wirejs-services', sanitizedId, sanitizedName);
|
|
15
|
+
}
|
|
16
|
+
async read(filename, encoding = 'utf8') {
|
|
17
|
+
return fs.promises.readFile(this.#fullNameFor(filename), { encoding });
|
|
18
|
+
}
|
|
19
|
+
async write(filename, data, { onlyIfNotExists = false } = {}) {
|
|
20
|
+
const fullname = this.#fullNameFor(filename);
|
|
21
|
+
const flag = onlyIfNotExists ? 'wx' : 'w';
|
|
22
|
+
await fs.promises.mkdir(path.dirname(fullname), { recursive: true });
|
|
23
|
+
return fs.promises.writeFile(fullname, data, { flag });
|
|
24
|
+
}
|
|
25
|
+
async delete(filename) {
|
|
26
|
+
return fs.promises.unlink(this.#fullNameFor(filename));
|
|
27
|
+
}
|
|
28
|
+
async *list({ prefix = '' } = {}) {
|
|
29
|
+
const all = await fs.promises.readdir(CWD, { recursive: true });
|
|
30
|
+
for (const name of all) {
|
|
31
|
+
if (prefix === undefined || name.startsWith(prefix))
|
|
32
|
+
yield name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
isAlreadyExistsError(error) {
|
|
36
|
+
return error.code === ALREADY_EXISTS_CODE;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wirejs-resources",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8-alpha",
|
|
4
4
|
"description": "Basic services and server-side resources for wirejs apps",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/types.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"default": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"scripts": {
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc"
|
|
16
|
+
},
|
|
15
17
|
"repository": {
|
|
16
18
|
"type": "git",
|
|
17
19
|
"url": "git+https://github.com/svidgen/create-wirejs-app.git"
|
|
@@ -24,5 +26,12 @@
|
|
|
24
26
|
"homepage": "https://github.com/svidgen/create-wirejs-app#readme",
|
|
25
27
|
"dependencies": {
|
|
26
28
|
"jose": "^5.9.6"
|
|
27
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.7.3"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"package.json",
|
|
35
|
+
"dist/*"
|
|
36
|
+
]
|
|
28
37
|
}
|