@cripty2001/utils 0.0.211 → 0.0.213
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/LICENSE.md +2 -2
- package/README.md +5 -5
- package/dist/Appserver/client.d.ts +17 -16
- package/dist/Appserver/client.js +66 -57
- package/dist/Appserver/server.d.ts +13 -31
- package/dist/Appserver/server.js +83 -142
- package/dist/MailTemplate/image.js +14 -14
- package/dist/MailTemplate/index.js +37 -37
- package/dist/MailTemplate/text.js +10 -10
- package/dist/react-ui/modal.js +11 -11
- package/llms/appserver.md +548 -0
- package/llms/index.md +1 -1
- package/package.json +119 -119
- package/dist/Logger/react-display.d.ts +0 -1
- package/dist/Logger/react-display.js +0 -72
- package/dist/react/modal.d.ts +0 -6
- package/dist/react/modal.js +0 -15
- package/dist/react/ui/button.d.ts +0 -6
- package/dist/react/ui/button.js +0 -24
- package/dist/react/ui/form.d.ts +0 -19
- package/dist/react/ui/form.js +0 -32
- package/dist/react/ui/index.d.ts +0 -6
- package/dist/react/ui/index.js +0 -16
- package/dist/react/ui/input.d.ts +0 -16
- package/dist/react/ui/input.js +0 -31
- package/dist/react/ui/loader.d.ts +0 -8
- package/dist/react/ui/loader.js +0 -16
- package/dist/react/ui/table.d.ts +0 -5
- package/dist/react/ui/table.js +0 -12
- package/llms/appserver_server.md +0 -622
package/LICENSE.md
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
Fabio Mauri (cripty2001@outlook.com)
|
|
1
|
+
MIT License
|
|
2
|
+
Fabio Mauri (cripty2001@outlook.com)
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
## 🤖 AI Assistant?
|
|
2
|
-
|
|
3
|
-
If you're an AI coding assistant helping a user with this package,
|
|
4
|
-
see [`/llms/index.md`](./llms/index.md) for
|
|
5
|
-
structured usage guidance before proceeding.
|
|
1
|
+
## 🤖 AI Assistant?
|
|
2
|
+
|
|
3
|
+
If you're an AI coding assistant helping a user with this package,
|
|
4
|
+
see [`/llms/index.md`](./llms/index.md) for
|
|
5
|
+
structured usage guidance before proceeding.
|
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Whispr } from "@cripty2001/whispr";
|
|
2
2
|
import { AppserverData } from "./common";
|
|
3
3
|
export type { AppserverData };
|
|
4
|
+
export type ClientCreateOptions = {
|
|
5
|
+
rpcMount: string;
|
|
6
|
+
};
|
|
4
7
|
export declare class ClientError extends Error {
|
|
5
8
|
constructor(message: string);
|
|
6
9
|
}
|
|
7
10
|
export declare class ClientAuthError extends ClientError {
|
|
8
|
-
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly payload: AppserverData;
|
|
13
|
+
constructor(code?: string, message?: string, payload?: AppserverData);
|
|
9
14
|
}
|
|
10
|
-
export declare class ClientServerError extends
|
|
15
|
+
export declare class ClientServerError extends ClientError {
|
|
11
16
|
code: string;
|
|
12
17
|
payload: AppserverData;
|
|
13
18
|
constructor(code: string, message: string, payload?: AppserverData);
|
|
14
19
|
}
|
|
15
20
|
export declare class ClientValidationError extends ClientError {
|
|
16
|
-
errors:
|
|
17
|
-
constructor(errors:
|
|
21
|
+
errors: unknown;
|
|
22
|
+
constructor(errors: unknown);
|
|
18
23
|
}
|
|
19
24
|
export declare class Client {
|
|
20
25
|
private url;
|
|
21
|
-
private
|
|
22
|
-
private
|
|
23
|
-
|
|
26
|
+
private readonly rpcMount;
|
|
27
|
+
private readonly token;
|
|
28
|
+
private readonly setToken;
|
|
29
|
+
readonly loggedIn: Whispr<boolean>;
|
|
24
30
|
private constructor();
|
|
25
|
-
static create(url: string): Client;
|
|
31
|
+
static create(url: string, options: ClientCreateOptions): Client;
|
|
26
32
|
login(token: string): Promise<boolean>;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* Executes an action on the server without invalidating the auth token on auth errors.
|
|
30
|
-
* @param action - The action to execute
|
|
31
|
-
* @param input - The input to the action
|
|
32
|
-
* @returns The result of the action
|
|
33
|
-
*/
|
|
33
|
+
logout(): void;
|
|
34
|
+
exec<I extends AppserverData, O extends AppserverData>(action: string, input: I, onError?: Record<string, (payload: AppserverData) => Promise<void>>): Promise<O>;
|
|
34
35
|
private unsafeExec;
|
|
35
36
|
}
|
package/dist/Appserver/client.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Client = exports.ClientValidationError = exports.ClientServerError = exports.ClientAuthError = exports.ClientError = void 0;
|
|
4
4
|
const whispr_1 = require("@cripty2001/whispr");
|
|
5
5
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
6
|
-
const Dispatcher_1 = require("../Dispatcher");
|
|
7
6
|
class ClientError extends Error {
|
|
8
7
|
constructor(message) {
|
|
9
8
|
super(message);
|
|
@@ -11,12 +10,16 @@ class ClientError extends Error {
|
|
|
11
10
|
}
|
|
12
11
|
exports.ClientError = ClientError;
|
|
13
12
|
class ClientAuthError extends ClientError {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
code;
|
|
14
|
+
payload;
|
|
15
|
+
constructor(code = "PERMISSION_DENIED", message = "Permission denied", payload = {}) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.payload = payload;
|
|
16
19
|
}
|
|
17
20
|
}
|
|
18
21
|
exports.ClientAuthError = ClientAuthError;
|
|
19
|
-
class ClientServerError extends
|
|
22
|
+
class ClientServerError extends ClientError {
|
|
20
23
|
code;
|
|
21
24
|
payload;
|
|
22
25
|
constructor(code, message, payload = {}) {
|
|
@@ -36,76 +39,82 @@ class ClientValidationError extends ClientError {
|
|
|
36
39
|
exports.ClientValidationError = ClientValidationError;
|
|
37
40
|
class Client {
|
|
38
41
|
url;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
rpcMount;
|
|
43
|
+
token;
|
|
44
|
+
setToken;
|
|
45
|
+
loggedIn;
|
|
46
|
+
constructor(url, rpcMount) {
|
|
43
47
|
this.url = url;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return _setAuthToken(t);
|
|
50
|
-
};
|
|
51
|
-
this.user = new Dispatcher_1.Dispatcher(this.authToken, async (token) => {
|
|
52
|
-
if (token === null)
|
|
53
|
-
return null;
|
|
54
|
-
const { user } = await this.unsafeExec('/auth/whoami', {});
|
|
55
|
-
return user;
|
|
56
|
-
}, 0); // Sync execution
|
|
48
|
+
this.rpcMount = rpcMount;
|
|
49
|
+
const [token, setToken] = whispr_1.Whispr.create(null);
|
|
50
|
+
this.token = token;
|
|
51
|
+
this.setToken = setToken;
|
|
52
|
+
this.loggedIn = token.transform((t) => t !== null);
|
|
57
53
|
}
|
|
58
|
-
static create(url) {
|
|
59
|
-
|
|
54
|
+
static create(url, options) {
|
|
55
|
+
const m = options.rpcMount.startsWith("/") ?
|
|
56
|
+
options.rpcMount :
|
|
57
|
+
`/${options.rpcMount}`;
|
|
58
|
+
return new Client(url, m);
|
|
60
59
|
}
|
|
61
60
|
async login(token) {
|
|
62
|
-
this.
|
|
63
|
-
return
|
|
64
|
-
if (data.loading)
|
|
65
|
-
return;
|
|
66
|
-
return data.ok;
|
|
67
|
-
});
|
|
61
|
+
this.setToken(token);
|
|
62
|
+
return true;
|
|
68
63
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
logout() {
|
|
65
|
+
this.setToken(null);
|
|
66
|
+
}
|
|
67
|
+
async exec(action, input, onError = {}) {
|
|
68
|
+
while (true) {
|
|
69
|
+
try {
|
|
70
|
+
return await this.unsafeExec(action, input);
|
|
74
71
|
}
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e instanceof ClientAuthError) {
|
|
74
|
+
this.logout();
|
|
75
|
+
await this.token.load();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (e instanceof ClientServerError) {
|
|
79
|
+
const handler = onError[e.code];
|
|
80
|
+
if (handler) {
|
|
81
|
+
await handler(e.payload);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
77
87
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Executes an action on the server without invalidating the auth token on auth errors.
|
|
80
|
-
* @param action - The action to execute
|
|
81
|
-
* @param input - The input to the action
|
|
82
|
-
* @returns The result of the action
|
|
83
|
-
*/
|
|
84
88
|
async unsafeExec(action, input) {
|
|
85
|
-
const testedToken = this.
|
|
86
|
-
const
|
|
89
|
+
const testedToken = this.token.value;
|
|
90
|
+
const actionKey = action;
|
|
91
|
+
const wireBody = {
|
|
92
|
+
action: actionKey,
|
|
93
|
+
payload: input,
|
|
94
|
+
};
|
|
95
|
+
const res = await fetch(`${this.url}${this.rpcMount}`, {
|
|
87
96
|
method: "POST",
|
|
88
97
|
headers: {
|
|
89
98
|
"Content-Type": "application/vnd.msgpack",
|
|
90
|
-
...(testedToken !== null ? {
|
|
99
|
+
...(testedToken !== null ? { Authorization: `Bearer ${testedToken}` } : {}),
|
|
91
100
|
},
|
|
92
|
-
body: new
|
|
101
|
+
body: new Uint8Array((0, msgpack_1.encode)(wireBody)),
|
|
93
102
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (res.status === 404)
|
|
97
|
-
throw new ClientError("Not found");
|
|
98
|
-
const decoded = (0, msgpack_1.decode)(await res.arrayBuffer());
|
|
99
|
-
let responseData;
|
|
103
|
+
const buf = await res.arrayBuffer();
|
|
104
|
+
const decoded = (0, msgpack_1.decode)(buf);
|
|
100
105
|
if (res.status === 200)
|
|
101
106
|
return decoded;
|
|
102
107
|
if (res.status === 422) {
|
|
103
|
-
|
|
104
|
-
throw new ClientValidationError(
|
|
108
|
+
const data = decoded;
|
|
109
|
+
throw new ClientValidationError(data.errors);
|
|
110
|
+
}
|
|
111
|
+
const genericError = decoded;
|
|
112
|
+
if (res.status === 400 || res.status === 404 || res.status === 500) {
|
|
113
|
+
throw new ClientServerError(genericError.code, genericError.error, genericError.payload ?? {});
|
|
105
114
|
}
|
|
106
|
-
if (res.status ===
|
|
107
|
-
|
|
108
|
-
throw new
|
|
115
|
+
if (res.status === 401 || res.status === 403) {
|
|
116
|
+
this.setToken(null);
|
|
117
|
+
throw new ClientAuthError(genericError.code, genericError.error, genericError.payload);
|
|
109
118
|
}
|
|
110
119
|
throw new ClientError(`Unexpected server response: ${res.status}`);
|
|
111
120
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { Static, TSchema } from '@sinclair/typebox';
|
|
2
|
+
import express from 'express';
|
|
2
3
|
import { AppserverData } from './common';
|
|
3
4
|
export type { AppserverData };
|
|
4
5
|
export { Static, TSchema, Type } from '@sinclair/typebox';
|
|
5
6
|
export { Value } from '@sinclair/typebox/value';
|
|
6
|
-
export type
|
|
7
|
-
export type AppserverPrivateHandler<I extends AppserverData, U extends AppserverData, O extends AppserverData> = (input: I, user: U) => Promise<O> | O;
|
|
8
|
-
export type AppserverUsergetter<U extends AppserverData> = (token: string) => Promise<U | null>;
|
|
7
|
+
export type AppserverHandler<I extends AppserverData, O extends AppserverData> = (input: I, auth: string | undefined) => Promise<O> | O;
|
|
9
8
|
declare class AppserverError extends Error {
|
|
10
9
|
code: string;
|
|
11
10
|
payload: AppserverData;
|
|
@@ -13,36 +12,19 @@ declare class AppserverError extends Error {
|
|
|
13
12
|
constructor(code: string, message: string, payload?: AppserverData, status?: number);
|
|
14
13
|
}
|
|
15
14
|
export declare class AppserverAuthError extends AppserverError {
|
|
16
|
-
constructor();
|
|
15
|
+
constructor(message?: string, code?: string);
|
|
17
16
|
}
|
|
18
17
|
export declare class AppserverHandledError extends AppserverError {
|
|
19
18
|
constructor(code: string, message: string, payload?: AppserverData);
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
* - Body is msgpack-encoded object matching the provided schema.
|
|
32
|
-
* - Optional `Authorization: Bearer <token>` header is decoded via the provided parseUser function.
|
|
33
|
-
*
|
|
34
|
-
* Responses are msgpack encoded objects with either handler data or `{ error, code, payload }` for handled errors.
|
|
35
|
-
*/
|
|
36
|
-
export declare class Appserver<U extends AppserverData> {
|
|
37
|
-
private app;
|
|
38
|
-
private parseUser;
|
|
39
|
-
private getMetrics;
|
|
40
|
-
private registered;
|
|
41
|
-
constructor(port: number, parseUser: AppserverUsergetter<U>, getMetrics: () => Record<string, number>, origins?: string[]);
|
|
42
|
-
private registerAuth;
|
|
43
|
-
private handleMetricsRequest;
|
|
44
|
-
private parseInput;
|
|
45
|
-
registerPublic<ISchema extends TSchema, O extends AppserverData, I extends Static<ISchema> & AppserverData = Static<ISchema> & AppserverData>(action: string, inputSchema: ISchema, handler: AppserverPublicHandler<I, O>): void;
|
|
46
|
-
registerPrivate<ISchema extends TSchema, O extends AppserverData, I extends Static<ISchema> & AppserverData = Static<ISchema> & AppserverData>(action: string, inputSchema: ISchema, handler: AppserverPrivateHandler<I, U, O>): void;
|
|
47
|
-
private unsafeRegister;
|
|
20
|
+
export type AppserverModule<ISchema extends TSchema = TSchema, O extends AppserverData = AppserverData, I extends Static<ISchema> & AppserverData = Static<ISchema> & AppserverData> = {
|
|
21
|
+
schema: ISchema;
|
|
22
|
+
handler: AppserverHandler<I, O>;
|
|
23
|
+
};
|
|
24
|
+
export declare class AppserverDispatcher {
|
|
25
|
+
private registry;
|
|
26
|
+
register<ISchema extends TSchema, O extends AppserverData, I extends Static<ISchema> & AppserverData = Static<ISchema> & AppserverData>(action: string, mod: AppserverModule<ISchema, O, I>): void;
|
|
27
|
+
autoload(base: string): Promise<void>;
|
|
28
|
+
bind(app: express.Express, path: string): void;
|
|
29
|
+
private dispatch;
|
|
48
30
|
}
|
package/dist/Appserver/server.js
CHANGED
|
@@ -3,15 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.AppserverDispatcher = exports.AppserverHandledError = exports.AppserverAuthError = exports.Value = exports.Type = void 0;
|
|
7
7
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
8
|
-
const typebox_1 = require("@sinclair/typebox");
|
|
9
8
|
const value_1 = require("@sinclair/typebox/value");
|
|
10
|
-
const cors_1 = __importDefault(require("cors"));
|
|
11
9
|
const express_1 = __importDefault(require("express"));
|
|
10
|
+
const promises_1 = require("fs/promises");
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
12
|
// Helpful for avoiding sinclair version mismatch between this and the actual user of the package
|
|
13
|
-
var
|
|
14
|
-
Object.defineProperty(exports, "Type", { enumerable: true, get: function () { return
|
|
13
|
+
var typebox_1 = require("@sinclair/typebox");
|
|
14
|
+
Object.defineProperty(exports, "Type", { enumerable: true, get: function () { return typebox_1.Type; } });
|
|
15
15
|
var value_2 = require("@sinclair/typebox/value");
|
|
16
16
|
Object.defineProperty(exports, "Value", { enumerable: true, get: function () { return value_2.Value; } });
|
|
17
17
|
(0, msgpack_1.encode)({}); // Fixes issue with msgpack not being included in build
|
|
@@ -27,8 +27,8 @@ class AppserverError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
class AppserverAuthError extends AppserverError {
|
|
30
|
-
constructor() {
|
|
31
|
-
super(
|
|
30
|
+
constructor(message = "You have no right to access this page", code = "PERMISSION_DENIED") {
|
|
31
|
+
super(code, message, {}, 403);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
exports.AppserverAuthError = AppserverAuthError;
|
|
@@ -38,153 +38,94 @@ class AppserverHandledError extends AppserverError {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
exports.AppserverHandledError = AppserverHandledError;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* app.register('sum', SumSchema, true, async (input, user) => ({ total: input.a + input.b }));
|
|
48
|
-
*
|
|
49
|
-
* Request requirements:
|
|
50
|
-
* - `Content-Type: application/vnd.msgpack`
|
|
51
|
-
* - Body is msgpack-encoded object matching the provided schema.
|
|
52
|
-
* - Optional `Authorization: Bearer <token>` header is decoded via the provided parseUser function.
|
|
53
|
-
*
|
|
54
|
-
* Responses are msgpack encoded objects with either handler data or `{ error, code, payload }` for handled errors.
|
|
55
|
-
*/
|
|
56
|
-
class Appserver {
|
|
57
|
-
app;
|
|
58
|
-
parseUser;
|
|
59
|
-
getMetrics;
|
|
60
|
-
registered = new Set();
|
|
61
|
-
constructor(port, parseUser, getMetrics, origins = []) {
|
|
62
|
-
this.parseUser = parseUser;
|
|
63
|
-
this.getMetrics = getMetrics;
|
|
64
|
-
this.app = (0, express_1.default)();
|
|
65
|
-
if (origins.length > 0) {
|
|
66
|
-
this.app.use((0, cors_1.default)({
|
|
67
|
-
origin: origins,
|
|
68
|
-
credentials: true
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
71
|
-
this.app.get('/metrics', async (req, res) => {
|
|
72
|
-
await this.handleMetricsRequest(req, res);
|
|
73
|
-
});
|
|
74
|
-
this.registerAuth(this.app);
|
|
75
|
-
this.app.listen(port);
|
|
41
|
+
class AppserverDispatcher {
|
|
42
|
+
registry = new Map();
|
|
43
|
+
register(action, mod) {
|
|
44
|
+
if (this.registry.has(action))
|
|
45
|
+
throw new Error(`Action already registered: "${action}"`);
|
|
46
|
+
this.registry.set(action, mod);
|
|
76
47
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
user: user !== null ? mappedUser : null
|
|
83
|
-
};
|
|
48
|
+
async autoload(base) {
|
|
49
|
+
const absBase = path_1.default.resolve(base);
|
|
50
|
+
const files = await (0, promises_1.readdir)(absBase, {
|
|
51
|
+
recursive: true,
|
|
52
|
+
withFileTypes: true
|
|
84
53
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const toReturn = Object.entries(metrics)
|
|
90
|
-
.map(([name, value]) => {
|
|
91
|
-
if (typeof value !== 'number' || !isFinite(value))
|
|
92
|
-
throw new Error(`Metric value for "${name}" is not a number`);
|
|
93
|
-
const prometheusName = 'app_' + name
|
|
94
|
-
.toLowerCase()
|
|
95
|
-
.replace(/[^a-z0-9_]/g, '_')
|
|
96
|
-
.replace(/_+/g, '_')
|
|
97
|
-
.replace(/^_+|_+$/g, '');
|
|
98
|
-
return `# TYPE ${prometheusName} gauge\n${prometheusName} ${value}\n`;
|
|
99
|
-
})
|
|
100
|
-
.join('');
|
|
101
|
-
res
|
|
102
|
-
.status(200)
|
|
103
|
-
.type('text/plain')
|
|
104
|
-
.send(toReturn);
|
|
54
|
+
const jsFiles = files.filter(e => e.isFile() && e.name.endsWith('.js'));
|
|
55
|
+
if (jsFiles.length === 0) {
|
|
56
|
+
console.warn(`[appserver] autoload found no .js files in "${absBase}".`, `If you are using TypeScript, make sure the api directory is included in your tsconfig`, `and that you are pointing autoload at the compiled output directory, not the source.`);
|
|
57
|
+
return;
|
|
105
58
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
.
|
|
59
|
+
for (const entry of jsFiles) {
|
|
60
|
+
const fullPath = path_1.default.join(entry.parentPath, entry.name);
|
|
61
|
+
const action = path_1.default
|
|
62
|
+
.relative(absBase, fullPath)
|
|
63
|
+
.replace(/\.js$/, '')
|
|
64
|
+
.split(path_1.default.sep)
|
|
65
|
+
.join('/');
|
|
66
|
+
const ACTION_REGEX = /^([a-z0-9]+\/)*[a-z0-9]+$/;
|
|
67
|
+
if (!ACTION_REGEX.test(action))
|
|
68
|
+
throw new Error(`Invalid module path "${action}" — all segments must be lowercase alphanumeric`);
|
|
69
|
+
const mod = await import(fullPath);
|
|
70
|
+
if (!mod.default || typeof mod.default.handler !== 'function' || !mod.default.schema)
|
|
71
|
+
throw new Error(`Module "${fullPath}" must have a default export satisfying ApiModule`);
|
|
72
|
+
this.register(action, mod.default);
|
|
112
73
|
}
|
|
113
74
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
throw new AppserverError('REQUEST_INVALID_TYPE_HEADER', 'Content-Type must be a messagepack (application/vnd.msgpack)', 400);
|
|
117
|
-
const data = (() => {
|
|
118
|
-
try {
|
|
119
|
-
return (0, msgpack_1.decode)(req.body);
|
|
120
|
-
}
|
|
121
|
-
catch (e) {
|
|
122
|
-
throw new AppserverError('REQUEST_INVALID_BODY', 'Request body is not valid msgpack', 400);
|
|
123
|
-
}
|
|
124
|
-
})();
|
|
125
|
-
const authHeader = req.headers['authorization'];
|
|
126
|
-
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
|
127
|
-
return {
|
|
128
|
-
data,
|
|
129
|
-
user: token ? await this.parseUser(token) : null
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
registerPublic(action, inputSchema, handler) {
|
|
133
|
-
this.unsafeRegister(`/exec/${action}`, inputSchema, false, handler);
|
|
134
|
-
}
|
|
135
|
-
registerPrivate(action, inputSchema, handler) {
|
|
136
|
-
this.unsafeRegister(`/exec/${action}`, inputSchema, true, (input, user) => {
|
|
137
|
-
if (user === null)
|
|
138
|
-
throw new AppserverAuthError();
|
|
139
|
-
return handler(input, user);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
unsafeRegister(action, inputSchema, auth, handler) {
|
|
143
|
-
if (this.registered.has(action))
|
|
144
|
-
throw new Error(`Action ${action} is already registered`);
|
|
145
|
-
this.registered.add(action);
|
|
146
|
-
this.app.post(action, express_1.default.raw({
|
|
75
|
+
bind(app, path) {
|
|
76
|
+
app.post(path, express_1.default.raw({
|
|
147
77
|
type: 'application/vnd.msgpack',
|
|
148
78
|
limit: '2gb'
|
|
149
79
|
}), async (req, res) => {
|
|
150
|
-
|
|
80
|
+
await this.dispatch(req, res);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async dispatch(req, res) {
|
|
84
|
+
const respond = (status, data) => {
|
|
85
|
+
res
|
|
86
|
+
.status(status)
|
|
87
|
+
.send((0, msgpack_1.encode)(data));
|
|
88
|
+
};
|
|
89
|
+
try {
|
|
90
|
+
if (req.headers['content-type'] !== 'application/vnd.msgpack')
|
|
91
|
+
throw new AppserverError('REQUEST_INVALID_TYPE_HEADER', 'Content-Type must be a messagepack (application/vnd.msgpack)', {}, 400);
|
|
92
|
+
const parsed = (() => {
|
|
151
93
|
try {
|
|
152
|
-
|
|
153
|
-
if (auth && user === null)
|
|
154
|
-
return {
|
|
155
|
-
status: 401,
|
|
156
|
-
data: { error: 'Authentication required', code: 'AUTHENTICATION_REQUIRED' }
|
|
157
|
-
};
|
|
158
|
-
if (!value_1.Value.Check(inputSchema, unsafeData))
|
|
159
|
-
return {
|
|
160
|
-
status: 422,
|
|
161
|
-
data: {
|
|
162
|
-
errors: [...value_1.Value.Errors(inputSchema, unsafeData)],
|
|
163
|
-
received: unsafeData
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
return {
|
|
167
|
-
status: 200,
|
|
168
|
-
data: await handler(unsafeData, user)
|
|
169
|
-
};
|
|
94
|
+
return (0, msgpack_1.decode)(req.body);
|
|
170
95
|
}
|
|
171
96
|
catch (e) {
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
status: e.status,
|
|
175
|
-
data: { error: e.message, code: e.code, payload: e.payload }
|
|
176
|
-
};
|
|
177
|
-
console.log("Unhandled server error:", e);
|
|
178
|
-
return {
|
|
179
|
-
status: 500,
|
|
180
|
-
data: { error: 'Internal server error', code: 'INTERNAL_SERVERERROR' }
|
|
181
|
-
};
|
|
97
|
+
throw new AppserverError('REQUEST_INVALID_BODY', 'Request body is not valid msgpack', {}, 400);
|
|
182
98
|
}
|
|
183
99
|
})();
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
100
|
+
const { action, payload } = parsed;
|
|
101
|
+
if (typeof action !== 'string')
|
|
102
|
+
throw new AppserverError("REQUEST_INVALID_ACTION", "Missing action field", {}, 400);
|
|
103
|
+
const mod = this.registry.get(action);
|
|
104
|
+
if (mod === undefined)
|
|
105
|
+
throw new AppserverError("ACTION_NOT_FOUND", `Action not found: ${action}`, {}, 404);
|
|
106
|
+
const token = req.headers['authorization']?.startsWith('Bearer ') ?
|
|
107
|
+
req.headers['authorization'].slice(7) :
|
|
108
|
+
undefined;
|
|
109
|
+
if (!value_1.Value.Check(mod.schema, payload))
|
|
110
|
+
return respond(422, {
|
|
111
|
+
errors: JSON.stringify(value_1.Value.Errors(mod.schema, payload)),
|
|
112
|
+
received: payload,
|
|
113
|
+
});
|
|
114
|
+
return respond(200, await mod.handler(payload, token));
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (e instanceof AppserverError)
|
|
118
|
+
return respond(e.status, {
|
|
119
|
+
error: e.message,
|
|
120
|
+
code: e.code,
|
|
121
|
+
payload: e.payload
|
|
122
|
+
});
|
|
123
|
+
console.error("Unhandled server error:", e);
|
|
124
|
+
return respond(500, {
|
|
125
|
+
error: 'Internal server error',
|
|
126
|
+
code: 'INTERNAL_SERVERERROR'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
188
129
|
}
|
|
189
130
|
}
|
|
190
|
-
exports.
|
|
131
|
+
exports.AppserverDispatcher = AppserverDispatcher;
|
|
@@ -11,14 +11,14 @@ exports.SCHEMA = typebox_1.Type.Object({
|
|
|
11
11
|
alt: typebox_1.Type.String()
|
|
12
12
|
});
|
|
13
13
|
function build(config) {
|
|
14
|
-
const img = (breakpoint, url) => `
|
|
15
|
-
<mj-image
|
|
16
|
-
css-class="${breakpoint ?? ''}"
|
|
17
|
-
src="${url}"
|
|
18
|
-
alt="${config.alt}"
|
|
19
|
-
${config.link ? `href="${config.link}"` : ''}
|
|
20
|
-
padding="0"
|
|
21
|
-
/>
|
|
14
|
+
const img = (breakpoint, url) => `
|
|
15
|
+
<mj-image
|
|
16
|
+
css-class="${breakpoint ?? ''}"
|
|
17
|
+
src="${url}"
|
|
18
|
+
alt="${config.alt}"
|
|
19
|
+
${config.link ? `href="${config.link}"` : ''}
|
|
20
|
+
padding="0"
|
|
21
|
+
/>
|
|
22
22
|
`;
|
|
23
23
|
const imgs = config.desktop === null ?
|
|
24
24
|
img(null, config.mobile) :
|
|
@@ -26,12 +26,12 @@ function build(config) {
|
|
|
26
26
|
img("onlyDesktop", config.desktop),
|
|
27
27
|
img("onlyMobile", config.mobile)
|
|
28
28
|
].join('\n');
|
|
29
|
-
const toReturn = `
|
|
30
|
-
<mj-section padding="0">
|
|
31
|
-
<mj-column padding="0">
|
|
32
|
-
${imgs}
|
|
33
|
-
</mj-column>
|
|
34
|
-
</mj-section>
|
|
29
|
+
const toReturn = `
|
|
30
|
+
<mj-section padding="0">
|
|
31
|
+
<mj-column padding="0">
|
|
32
|
+
${imgs}
|
|
33
|
+
</mj-column>
|
|
34
|
+
</mj-section>
|
|
35
35
|
`;
|
|
36
36
|
return toReturn;
|
|
37
37
|
}
|