@cripty2001/utils 0.0.211 → 0.0.212

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 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 { Dispatcher } from "../Dispatcher";
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
- constructor();
11
+ readonly code: string;
12
+ readonly payload: AppserverData;
13
+ constructor(code?: string, message?: string, payload?: AppserverData);
9
14
  }
10
- export declare class ClientServerError extends Error {
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: any[];
17
- constructor(errors: any[]);
21
+ errors: unknown;
22
+ constructor(errors: unknown);
18
23
  }
19
24
  export declare class Client {
20
25
  private url;
21
- private authToken;
22
- private setAuthToken;
23
- user: Dispatcher<string | null, AppserverData | null>;
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
- exec<I extends AppserverData, O extends AppserverData>(action: string, input: I): Promise<O>;
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
  }
@@ -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
- constructor() {
15
- super("Authentication error");
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 Error {
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
- authToken;
40
- setAuthToken;
41
- user;
42
- constructor(url) {
42
+ rpcMount;
43
+ token;
44
+ setToken;
45
+ loggedIn;
46
+ constructor(url, rpcMount) {
43
47
  this.url = url;
44
- const [_authToken, _setAuthToken] = whispr_1.Whispr.create(null);
45
- this.authToken = _authToken;
46
- this.setAuthToken = (t) => {
47
- if (t === this.authToken.value)
48
- return true;
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
- return new Client(url);
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.setAuthToken(token);
63
- return await this.user.data.wait(data => {
64
- if (data.loading)
65
- return;
66
- return data.ok;
67
- });
61
+ this.setToken(token);
62
+ return true;
68
63
  }
69
- async exec(action, input) {
70
- return await this.unsafeExec(`/exec/${action}`, input)
71
- .catch(e => {
72
- if (e instanceof ClientAuthError) {
73
- this.setAuthToken(null);
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
- throw e;
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.authToken.value;
86
- const res = await fetch(`${this.url}${action}`, {
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 ? { "Authorization": `Bearer ${testedToken}` } : {}),
99
+ ...(testedToken !== null ? { Authorization: `Bearer ${testedToken}` } : {}),
91
100
  },
92
- body: new Blob([new Uint8Array((0, msgpack_1.encode)(input))], { type: 'application/msgpack' }),
101
+ body: new Uint8Array((0, msgpack_1.encode)(wireBody)),
93
102
  });
94
- if (res.status === 401 || res.status === 403)
95
- throw new ClientError("Permission denied");
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
- responseData = decoded;
104
- throw new ClientValidationError(responseData.errors);
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 === 400 || res.status === 500) {
107
- responseData = decoded;
108
- throw new ClientServerError(responseData.code, responseData.error, responseData.payload);
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 AppserverPublicHandler<I extends AppserverData, O extends AppserverData> = (input: I) => Promise<O> | O;
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
- * Lightweight typed RPC runner that exposes POST `/exec/:action` endpoints using msgpack payloads
23
- * and a GET `/metrics` endpoint for Prometheus gauges.
24
- *
25
- * Usage:
26
- * const app = new Appserver(8080, parseToken, collectMetrics);
27
- * app.register('sum', SumSchema, true, async (input, user) => ({ total: input.a + input.b }));
28
- *
29
- * Request requirements:
30
- * - `Content-Type: application/vnd.msgpack`
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(action: string, mod: AppserverModule): void;
27
+ autoload(base: string): Promise<void>;
28
+ bind(app: express.Express, path: string): void;
29
+ private dispatch;
48
30
  }
@@ -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.Appserver = exports.AppserverHandledError = exports.AppserverAuthError = exports.Value = exports.Type = void 0;
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 typebox_2 = require("@sinclair/typebox");
14
- Object.defineProperty(exports, "Type", { enumerable: true, get: function () { return typebox_2.Type; } });
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("PERMISSION_DENIED", "You have no right to access this page", {}, 403);
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
- * Lightweight typed RPC runner that exposes POST `/exec/:action` endpoints using msgpack payloads
43
- * and a GET `/metrics` endpoint for Prometheus gauges.
44
- *
45
- * Usage:
46
- * const app = new Appserver(8080, parseToken, collectMetrics);
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
- registerAuth(app) {
78
- this.unsafeRegister('/auth/whoami', typebox_1.Type.Object({}), false, async (input, user) => {
79
- const mappedUser = Object.fromEntries(Object.entries(user ?? {})
80
- .filter(([key]) => !key.startsWith('_')));
81
- return {
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
- async handleMetricsRequest(req, res) {
87
- try {
88
- const metrics = this.getMetrics();
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
- catch (e) {
107
- console.log("Error generating metrics:", e);
108
- res
109
- .status(500)
110
- .type('text/plain')
111
- .send('# Error generating metrics\n');
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
- async parseInput(req) {
115
- if (req.headers['content-type'] !== 'application/vnd.msgpack')
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
- const { status, data } = await (async () => {
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
- const { data: unsafeData, user } = await this.parseInput(req);
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
- if (e instanceof AppserverError)
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
- res
185
- .status(status)
186
- .send((0, msgpack_1.encode)(data));
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.Appserver = Appserver;
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
  }