rlz-engine 0.0.1

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.
Files changed (39) hide show
  1. package/dist/back/auth/controllers.d.ts +7 -0
  2. package/dist/back/auth/controllers.js +39 -0
  3. package/dist/back/auth/model.d.ts +14 -0
  4. package/dist/back/auth/model.js +1 -0
  5. package/dist/back/auth/storage.d.ts +21 -0
  6. package/dist/back/auth/storage.js +91 -0
  7. package/dist/back/auth/utils.d.ts +8 -0
  8. package/dist/back/auth/utils.js +80 -0
  9. package/dist/back/config.d.ts +1 -0
  10. package/dist/back/config.js +1 -0
  11. package/dist/back/logger.d.ts +3 -0
  12. package/dist/back/logger.js +36 -0
  13. package/dist/back/server.d.ts +11 -0
  14. package/dist/back/server.js +75 -0
  15. package/dist/back/storage/db.d.ts +14 -0
  16. package/dist/back/storage/db.js +43 -0
  17. package/dist/back/storage/model.d.ts +33 -0
  18. package/dist/back/storage/model.js +7 -0
  19. package/dist/back/storage/sync.d.ts +5 -0
  20. package/dist/back/storage/sync.js +16 -0
  21. package/dist/client/api/api.d.ts +9 -0
  22. package/dist/client/api/api.js +57 -0
  23. package/dist/client/api/auth.d.ts +5 -0
  24. package/dist/client/api/auth.js +21 -0
  25. package/dist/client/screens/404.d.ts +2 -0
  26. package/dist/client/screens/404.js +10 -0
  27. package/dist/client/screens/SignupSigninScreen.d.ts +6 -0
  28. package/dist/client/screens/SignupSigninScreen.js +129 -0
  29. package/dist/client/state/auth.d.ts +12 -0
  30. package/dist/client/state/auth.js +74 -0
  31. package/dist/client/sync.d.ts +13 -0
  32. package/dist/client/sync.js +37 -0
  33. package/dist/shared/api/auth.d.ts +43 -0
  34. package/dist/shared/api/auth.js +16 -0
  35. package/dist/shared/api/sync.d.ts +43 -0
  36. package/dist/shared/api/sync.js +21 -0
  37. package/dist/shared/utils/datetime.d.ts +3 -0
  38. package/dist/shared/utils/datetime.js +15 -0
  39. package/package.json +65 -0
@@ -0,0 +1,7 @@
1
+ import { FastifyInstance, RawServerBase } from 'fastify';
2
+ import { AuthStorage } from './storage';
3
+ interface AuthEndpointsOpts {
4
+ storage: AuthStorage;
5
+ }
6
+ export declare const AUTH_API: <T extends RawServerBase>(app: FastifyInstance<T>, { storage }: AuthEndpointsOpts) => Promise<void>;
7
+ export {};
@@ -0,0 +1,39 @@
1
+ import { httpErrors } from '@fastify/sensible';
2
+ import fastifyPlugin from 'fastify-plugin';
3
+ import zodToJsonSchema from 'zod-to-json-schema';
4
+ import { API_AUTH_RESPONSE_SCHEMA_V0, API_SIGNIN_REQUEST_SCHEMA_V0, API_SIGNUP_REQUEST_SCHEMA_V0 } from '../../shared/api/auth';
5
+ import { logout, signin, signup } from './utils';
6
+ export const AUTH_API = fastifyPlugin(async function authApi(app, { storage }) {
7
+ app.post('/api/v0/signup', {
8
+ schema: {
9
+ body: zodToJsonSchema(API_SIGNUP_REQUEST_SCHEMA_V0),
10
+ response: { 200: zodToJsonSchema(API_AUTH_RESPONSE_SCHEMA_V0) }
11
+ }
12
+ }, async (req, _resp) => {
13
+ const body = API_SIGNUP_REQUEST_SCHEMA_V0.parse(req.body);
14
+ return await signup(storage, body.name, body.email, body.password);
15
+ });
16
+ app.post('/api/v0/signin', {
17
+ schema: {
18
+ body: zodToJsonSchema(API_SIGNIN_REQUEST_SCHEMA_V0),
19
+ response: { 200: zodToJsonSchema(API_AUTH_RESPONSE_SCHEMA_V0) }
20
+ }
21
+ }, async (req, _resp) => {
22
+ const body = API_SIGNIN_REQUEST_SCHEMA_V0.parse(req.body);
23
+ const r = await signin(storage, body.name, body.password);
24
+ if (r === null) {
25
+ return httpErrors.unauthorized();
26
+ }
27
+ return r;
28
+ });
29
+ app.post('/api/v0/logout', {}, async (req, resp) => {
30
+ await storage.auth(req.headers);
31
+ const authHeader = req.headers.authorization;
32
+ if (authHeader === undefined) {
33
+ throw httpErrors.forbidden();
34
+ }
35
+ const [userId, tempPassword] = authHeader.split(':');
36
+ await logout(storage, userId, tempPassword);
37
+ return resp.code(204).send();
38
+ });
39
+ });
@@ -0,0 +1,14 @@
1
+ import { Binary } from 'mongodb';
2
+ export interface StorageUser {
3
+ _id: string;
4
+ name: string;
5
+ email: string;
6
+ passwordSalt: Binary;
7
+ passwordHash: Binary;
8
+ lastActivityDate: Date;
9
+ }
10
+ export interface StorageTempPassword {
11
+ userId: string;
12
+ passwordHash: Binary;
13
+ validUntil: Date;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { FastifyRequest } from 'fastify';
2
+ import { Binary, Collection } from 'mongodb';
3
+ import { MongoStorage } from '../storage/db';
4
+ import { StorageTempPassword, StorageUser } from './model';
5
+ export declare class AuthStorage {
6
+ private readonly logger;
7
+ private readonly mongo;
8
+ constructor(mongo: MongoStorage);
9
+ init(): Promise<void>;
10
+ createUser(id: string, name: string, email: string, passwordSalt: Binary, passwordHash: Binary): Promise<void>;
11
+ getUser(name: string): Promise<StorageUser | null>;
12
+ markUserActive(userId: string): Promise<void>;
13
+ pushTempPassword(userId: string, passwordHash: Binary, validUntil: Date): Promise<void>;
14
+ deleteTempPassword(userId: string, passwordHash: Binary): Promise<void>;
15
+ getUserByTempPassword(userId: string, passwordHash: Binary): Promise<StorageUser | null>;
16
+ private createCollections;
17
+ private createIndexes;
18
+ get users(): Collection<StorageUser>;
19
+ get tempPasswords(): Collection<StorageTempPassword>;
20
+ get auth(): (headers: FastifyRequest["headers"]) => Promise<string>;
21
+ }
@@ -0,0 +1,91 @@
1
+ import { DateTime } from 'luxon';
2
+ import { logger } from '../logger';
3
+ import { auth } from './utils';
4
+ export class AuthStorage {
5
+ logger = logger('AuthStorage');
6
+ mongo;
7
+ constructor(mongo) {
8
+ this.mongo = mongo;
9
+ }
10
+ async init() {
11
+ await Promise.all([
12
+ 'users', 'temp-passwords'
13
+ ].map(i => this.mongo.createCollection(i)));
14
+ await this.createIndexes();
15
+ }
16
+ async createUser(id, name, email, passwordSalt, passwordHash) {
17
+ await this.users.insertOne({
18
+ _id: id,
19
+ name,
20
+ email,
21
+ passwordSalt,
22
+ passwordHash,
23
+ lastActivityDate: DateTime.utc().toJSDate()
24
+ });
25
+ }
26
+ async getUser(name) {
27
+ return await this.users.findOne({ name });
28
+ }
29
+ async markUserActive(userId) {
30
+ await this.users.updateOne({ _id: userId }, { $set: { lastActivityDate: DateTime.utc().toJSDate() } });
31
+ }
32
+ async pushTempPassword(userId, passwordHash, validUntil) {
33
+ await this.tempPasswords.insertOne({
34
+ userId,
35
+ passwordHash,
36
+ validUntil
37
+ });
38
+ }
39
+ async deleteTempPassword(userId, passwordHash) {
40
+ await this.tempPasswords.deleteOne({ userId, passwordHash });
41
+ }
42
+ async getUserByTempPassword(userId, passwordHash) {
43
+ const t = await this.tempPasswords.findOne({ userId, passwordHash });
44
+ if (t === null) {
45
+ return null;
46
+ }
47
+ if (DateTime.utc() > DateTime.fromJSDate(t.validUntil)) {
48
+ await this.tempPasswords.deleteOne({ _id: t._id });
49
+ return null;
50
+ }
51
+ return await this.users.findOne({ _id: t.userId });
52
+ }
53
+ async createCollections() {
54
+ }
55
+ async createIndexes() {
56
+ await this.mongo.createIndexes(this.users, [
57
+ {
58
+ name: 'name_v0',
59
+ key: {
60
+ name: 1
61
+ },
62
+ unique: true
63
+ }
64
+ ]);
65
+ await this.mongo.createIndexes(this.tempPasswords, [
66
+ {
67
+ name: 'userId_v0',
68
+ key: {
69
+ userId: 1
70
+ }
71
+ },
72
+ {
73
+ name: 'ttl_v0',
74
+ key: {
75
+ passwordHash: 1
76
+ },
77
+ // Temp passwords also have expirity dates, make sure they are syncronized with this expirity index
78
+ expireAfterSeconds: 60 * 60 * 24 * 7 // 7 days
79
+ }
80
+ ]);
81
+ }
82
+ get users() {
83
+ return this.mongo.db.collection('users');
84
+ }
85
+ get tempPasswords() {
86
+ return this.mongo.db.collection('temp-passwords');
87
+ }
88
+ get auth() {
89
+ return (headers) => auth(headers, this);
90
+ }
91
+ }
@@ -0,0 +1,8 @@
1
+ import { FastifyRequest } from 'fastify';
2
+ import { ApiAuthResponseV0 } from '../../shared/api/auth';
3
+ import { AuthStorage } from './storage';
4
+ export declare function signup(storage: AuthStorage, name: string, email: string, password: string): Promise<ApiAuthResponseV0>;
5
+ export declare function signin(storage: AuthStorage, name: string, password: string): Promise<ApiAuthResponseV0 | null>;
6
+ export declare function logout(storage: AuthStorage, userId: string, tempPassword: string): Promise<void>;
7
+ export declare function verifyTempPassword(storage: AuthStorage, userId: string, tempPassword: string): Promise<string>;
8
+ export declare function auth(headers: FastifyRequest['headers'], storage: AuthStorage): Promise<string>;
@@ -0,0 +1,80 @@
1
+ import { httpErrors } from '@fastify/sensible';
2
+ import { randomBytes, scryptSync } from 'crypto';
3
+ import { DateTime } from 'luxon';
4
+ import { Binary, MongoServerError } from 'mongodb';
5
+ import { uuidv7 } from 'uuidv7';
6
+ const TEMP_PASSWORD_SALT = Buffer.from('cashmony-temp-password-salt', 'utf8');
7
+ export async function signup(storage, name, email, password) {
8
+ const salt = randomBytes(64);
9
+ const hash = calcHash(password, salt);
10
+ const id = uuidv7();
11
+ try {
12
+ await storage.createUser(id, name, email, new Binary(salt), new Binary(hash));
13
+ }
14
+ catch (e) {
15
+ if (e instanceof MongoServerError && e.code === 11000) {
16
+ // duplicate key error
17
+ throw httpErrors.conflict();
18
+ }
19
+ throw e;
20
+ }
21
+ const tempPassword = await makeTempPassword(storage, id);
22
+ return {
23
+ id,
24
+ name,
25
+ email,
26
+ tempPassword
27
+ };
28
+ }
29
+ export async function signin(storage, name, password) {
30
+ const u = await storage.getUser(name);
31
+ if (u === null) {
32
+ return null;
33
+ }
34
+ const hash = calcHash(password, u.passwordSalt.value());
35
+ if (!hash.equals(u.passwordHash.value())) {
36
+ return null;
37
+ }
38
+ const tempPassword = await makeTempPassword(storage, u._id);
39
+ await storage.markUserActive(u._id);
40
+ return {
41
+ id: u._id,
42
+ name: u.name,
43
+ email: u.email,
44
+ tempPassword
45
+ };
46
+ }
47
+ export async function logout(storage, userId, tempPassword) {
48
+ const hash = calcHash(tempPassword, TEMP_PASSWORD_SALT);
49
+ await storage.deleteTempPassword(userId, new Binary(hash));
50
+ }
51
+ export async function verifyTempPassword(storage, userId, tempPassword) {
52
+ const hash = calcTempPasswordHash(Buffer.from(tempPassword, 'base64'));
53
+ const u = await storage.getUserByTempPassword(userId, new Binary(hash));
54
+ if (u === null) {
55
+ throw httpErrors.forbidden();
56
+ }
57
+ await storage.markUserActive(u._id);
58
+ return u._id;
59
+ }
60
+ async function makeTempPassword(storage, userId) {
61
+ const password = randomBytes(128);
62
+ const passwordHash = calcTempPasswordHash(password);
63
+ // Temp passwords also are removed from Mongo by TTL index, make sure expirity dates are syncronised
64
+ await storage.pushTempPassword(userId, new Binary(passwordHash), DateTime.utc().plus({ days: 7 }).toJSDate());
65
+ return password.toString('base64');
66
+ }
67
+ export async function auth(headers, storage) {
68
+ const authHeader = headers.authorization;
69
+ if (authHeader === undefined) {
70
+ throw httpErrors.forbidden();
71
+ }
72
+ const [userId, tempPassword] = authHeader.split(':');
73
+ return await verifyTempPassword(storage, userId, tempPassword);
74
+ }
75
+ function calcHash(password, salt) {
76
+ return scryptSync(password, salt, 512, { N: 1024 });
77
+ }
78
+ function calcTempPasswordHash(tempPassword) {
79
+ return scryptSync(tempPassword, TEMP_PASSWORD_SALT, 512, { N: 1024 });
80
+ }
@@ -0,0 +1 @@
1
+ export declare const PRODUCTION: boolean;
@@ -0,0 +1 @@
1
+ export const PRODUCTION = process.env.NODE_ENV === 'production';
@@ -0,0 +1,3 @@
1
+ import { Logger, LoggerOptions } from 'pino';
2
+ export declare function logger(name: string): Logger;
3
+ export declare function loggerOptions(name: string): LoggerOptions;
@@ -0,0 +1,36 @@
1
+ import { pino } from 'pino';
2
+ import { PRODUCTION } from './config';
3
+ export function logger(name) {
4
+ return pino(loggerOptions(name));
5
+ }
6
+ export function loggerOptions(name) {
7
+ if (PRODUCTION) {
8
+ return {
9
+ name,
10
+ transport: {
11
+ targets: [
12
+ {
13
+ level: 'info',
14
+ target: 'pino/file',
15
+ options: { destination: 1 }
16
+ }
17
+ // {
18
+ // level: 'info',
19
+ // target: 'pino-'
20
+ // }
21
+ ]
22
+ }
23
+ };
24
+ }
25
+ return {
26
+ name,
27
+ transport: {
28
+ targets: [
29
+ {
30
+ level: 'trace',
31
+ target: 'pino-pretty'
32
+ }
33
+ ]
34
+ }
35
+ };
36
+ }
@@ -0,0 +1,11 @@
1
+ import { FastifyInstance, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerBase } from 'fastify';
2
+ import { Logger } from 'pino';
3
+ export type InitServerFuncType = <S extends RawServerBase>(server: FastifyInstance<S, RawRequestDefaultExpression<S>, RawReplyDefaultExpression<S>, Logger>) => Promise<void>;
4
+ export interface RunServerParams {
5
+ production: boolean;
6
+ domain: string;
7
+ certDir: string;
8
+ staticDir: string;
9
+ init: InitServerFuncType;
10
+ }
11
+ export declare function runServer({ production, domain, certDir, staticDir, init }: RunServerParams): Promise<void>;
@@ -0,0 +1,75 @@
1
+ import fastifyCompress from '@fastify/compress';
2
+ import fastifyCors from '@fastify/cors';
3
+ import fastifyResponseValidation from '@fastify/response-validation';
4
+ import fastifySensible, { httpErrors } from '@fastify/sensible';
5
+ import fastifyStatic from '@fastify/static';
6
+ import formatsPlugin from 'ajv-formats';
7
+ import fastify from 'fastify';
8
+ import { fastifyAcmeSecurePlugin, fastifyAcmeUnsecurePlugin, getCertAndKey } from 'fastify-acme';
9
+ import { createReadStream } from 'fs';
10
+ import { installIntoGlobal } from 'iterator-helpers-polyfill';
11
+ import path from 'path';
12
+ import { logger } from './logger';
13
+ installIntoGlobal();
14
+ export async function runServer({ production, domain, certDir, staticDir, init }) {
15
+ const httpServer = fastify({
16
+ loggerInstance: logger('http'),
17
+ ajv: { plugins: [formatsPlugin] }
18
+ });
19
+ if (!production) {
20
+ await httpServer.register(fastifyCors, {
21
+ methods: ['get', 'post']
22
+ });
23
+ await httpServer.register(fastifyCompress);
24
+ await httpServer.register(fastifySensible);
25
+ await httpServer.register(fastifyResponseValidation, {
26
+ ajv: { plugins: [formatsPlugin] }
27
+ });
28
+ await init(httpServer);
29
+ httpServer.all('/api/*', async () => {
30
+ return httpErrors.notFound();
31
+ });
32
+ addStaticEndpoints(httpServer, staticDir);
33
+ await httpServer.listen({ host: 'localhost', port: 8080 });
34
+ return;
35
+ }
36
+ httpServer.register(fastifyAcmeUnsecurePlugin, { redirectDomain: domain });
37
+ await httpServer.listen({ port: 80 });
38
+ const certAndKey = await getCertAndKey(certDir, domain);
39
+ const httpsServer = fastify({
40
+ http2: true,
41
+ https: {
42
+ allowHTTP1: true,
43
+ cert: certAndKey.cert,
44
+ key: certAndKey.pkey
45
+ },
46
+ loggerInstance: logger('https'),
47
+ ajv: { plugins: [formatsPlugin] }
48
+ });
49
+ await httpsServer.register(fastifyAcmeSecurePlugin, {
50
+ certDir,
51
+ domain
52
+ });
53
+ await httpsServer.register(fastifyCors, {
54
+ methods: ['get', 'post']
55
+ });
56
+ await httpsServer.register(fastifyCompress);
57
+ await httpsServer.register(fastifySensible);
58
+ await httpsServer.register(fastifyResponseValidation, { ajv: { plugins: [formatsPlugin] } });
59
+ await init(httpsServer);
60
+ httpsServer.all('/api/*', async () => {
61
+ return httpErrors.notFound();
62
+ });
63
+ addStaticEndpoints(httpsServer, staticDir);
64
+ }
65
+ function addStaticEndpoints(server, staticPath) {
66
+ const absStaticPath = path.resolve(staticPath);
67
+ const absIndexPath = path.join(absStaticPath, 'index.html');
68
+ server.register((s) => {
69
+ s.register(fastifyStatic, { root: absStaticPath });
70
+ s.setNotFoundHandler(async (_req, resp) => {
71
+ await resp.type('text/html')
72
+ .send(createReadStream(absIndexPath));
73
+ });
74
+ });
75
+ }
@@ -0,0 +1,14 @@
1
+ import { Collection, Db, Document, IndexDescription } from 'mongodb';
2
+ type StorageIndexDescription = IndexDescription & {
3
+ name: string;
4
+ };
5
+ export declare class MongoStorage {
6
+ private readonly logger;
7
+ private readonly client;
8
+ constructor();
9
+ get db(): Db;
10
+ createCollection(name: string): Promise<void>;
11
+ createIndexes<T extends Document>(collection: Collection<T>, indexes: StorageIndexDescription[]): Promise<void>;
12
+ private listIndexes;
13
+ }
14
+ export {};
@@ -0,0 +1,43 @@
1
+ import { MongoClient } from 'mongodb';
2
+ import { logger } from '../logger';
3
+ export class MongoStorage {
4
+ logger;
5
+ client;
6
+ constructor() {
7
+ this.logger = logger('MongoStorage');
8
+ this.client = new MongoClient('mongodb://localhost');
9
+ }
10
+ get db() {
11
+ return this.client.db('app-data');
12
+ }
13
+ async createCollection(name) {
14
+ await this.db.createCollection(name);
15
+ }
16
+ async createIndexes(collection, indexes) {
17
+ this.logger.info({ indexes: indexes?.map(i => i.name) ?? null, collection: collection.collectionName }, 'Configured indexes');
18
+ const knownIndexes = await this.listIndexes(collection);
19
+ this.logger.info({ indexes: Array.from(knownIndexes), collection: collection.collectionName }, 'Known indexes');
20
+ knownIndexes.delete('_id_');
21
+ for (const index of indexes) {
22
+ if (knownIndexes.has(index.name)) {
23
+ knownIndexes.delete(index.name);
24
+ continue;
25
+ }
26
+ this.logger.info({ index, collection: collection.collectionName }, 'Create index');
27
+ await collection.createIndexes([index]);
28
+ }
29
+ for (const name of knownIndexes) {
30
+ this.logger.info({ index: name, collection: collection.collectionName }, 'Drop index');
31
+ await collection.dropIndex(name);
32
+ }
33
+ }
34
+ async listIndexes(collection) {
35
+ const indexes = await collection.listIndexes().toArray();
36
+ return new Set(indexes.map((i) => {
37
+ if (i.name === undefined) {
38
+ throw Error('Index with undefined name detected!');
39
+ }
40
+ return i.name;
41
+ }));
42
+ }
43
+ }
@@ -0,0 +1,33 @@
1
+ import { z } from 'zod';
2
+ export interface MongoObject<T> {
3
+ _id: string;
4
+ ownerId: string;
5
+ syncDate: Date;
6
+ data: T;
7
+ }
8
+ export declare const SYNC_OBJECT_SCHEMA: z.ZodObject<{
9
+ _id: z.ZodString;
10
+ data: z.ZodObject<{
11
+ lastModified: z.ZodString;
12
+ }, "strip", z.ZodTypeAny, {
13
+ lastModified: string;
14
+ }, {
15
+ lastModified: string;
16
+ }>;
17
+ }, "strip", z.ZodTypeAny, {
18
+ _id: string;
19
+ data: {
20
+ lastModified: string;
21
+ };
22
+ }, {
23
+ _id: string;
24
+ data: {
25
+ lastModified: string;
26
+ };
27
+ }>;
28
+ export interface SyncObject<T> {
29
+ _id: string;
30
+ ownerId: string;
31
+ syncDate: Date;
32
+ data: T;
33
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+ export const SYNC_OBJECT_SCHEMA = z.object({
3
+ _id: z.string().uuid(),
4
+ data: z.object({
5
+ lastModified: z.string().datetime()
6
+ })
7
+ });
@@ -0,0 +1,5 @@
1
+ import { DateTime } from 'luxon';
2
+ import { Collection } from 'mongodb';
3
+ import { ApiComparisonObjectV0 } from '../../shared/api/sync';
4
+ import { SyncObject } from './model';
5
+ export declare function getAll<T>(c: Collection<SyncObject<T>>, ownerId: string, syncAfter?: DateTime<true>): Promise<ApiComparisonObjectV0[]>;
@@ -0,0 +1,16 @@
1
+ import { SYNC_OBJECT_SCHEMA } from './model';
2
+ export async function getAll(c, ownerId, syncAfter) {
3
+ const items = [];
4
+ const query = syncAfter === undefined
5
+ ? { ownerId }
6
+ : { ownerId, syncDate: { $gt: syncAfter.toJSDate() } };
7
+ const cursor = c.find(query).project({ 'data.lastModified': 1 });
8
+ for await (const op of cursor) {
9
+ const parsed = SYNC_OBJECT_SCHEMA.parse(op);
10
+ items.push({
11
+ id: parsed._id,
12
+ lastModified: parsed.data.lastModified
13
+ });
14
+ }
15
+ return items;
16
+ }
@@ -0,0 +1,9 @@
1
+ import z, { ZodType } from 'zod';
2
+ export declare class Forbidden extends Error {
3
+ constructor(url: string);
4
+ }
5
+ export interface AuthParam {
6
+ userId: string;
7
+ tempPassword: string;
8
+ }
9
+ export declare function apiCall<T extends ZodType>(method: string, path: string, auth: AuthParam | null, queryString: Record<string, string> | null, request: object | null, validator: T): Promise<z.infer<T>>;
@@ -0,0 +1,57 @@
1
+ import { PRODUCTION } from '../../back/config';
2
+ const API_DOMAIN = PRODUCTION ? '/' : 'http://localhost:8080/';
3
+ export class Forbidden extends Error {
4
+ constructor(url) {
5
+ super(`Forbidden: ${url}`);
6
+ }
7
+ }
8
+ function url(path, queryString) {
9
+ const base = `${API_DOMAIN}api/v0/${path}`;
10
+ if (queryString === null) {
11
+ return base;
12
+ }
13
+ const query = Object.entries(queryString)
14
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
15
+ .join('&');
16
+ return `${base}?${query}`;
17
+ }
18
+ const GZIP_THRESHOLD = 16 * 1024;
19
+ async function prepareBody(request) {
20
+ const textBody = JSON.stringify(request);
21
+ if (textBody.length <= GZIP_THRESHOLD) {
22
+ return textBody;
23
+ }
24
+ const bodyStream = new Blob([textBody]).stream();
25
+ const bodyStreamCompressed = bodyStream.pipeThrough(new CompressionStream('gzip'));
26
+ return new Response(bodyStreamCompressed).blob();
27
+ }
28
+ export async function apiCall(method, path, auth, queryString, request, validator) {
29
+ const headers = {};
30
+ const body = request === null ? undefined : await prepareBody(request);
31
+ if (body !== undefined) {
32
+ headers['content-type'] = 'application/json';
33
+ if (typeof body !== 'string') {
34
+ headers['content-encoding'] = 'gzip';
35
+ }
36
+ }
37
+ if (auth !== null) {
38
+ headers['authorization'] = `${auth.userId}:${auth.tempPassword}`;
39
+ }
40
+ const u = url(path, queryString);
41
+ const resp = await fetch(u, {
42
+ method,
43
+ headers,
44
+ body
45
+ });
46
+ if (!resp.ok) {
47
+ if (resp.status === 403) {
48
+ throw new Forbidden(u);
49
+ }
50
+ throw Error(`Not ok resp (${resp.status} ${resp.statusText}): ${method} ${u}`);
51
+ }
52
+ if (resp.status === 204) {
53
+ return validator.parse(undefined);
54
+ }
55
+ const json = await resp.json();
56
+ return validator.parse(json);
57
+ }
@@ -0,0 +1,5 @@
1
+ import { ApiAuthResponseV0 } from '../../shared/api/auth';
2
+ import { AuthParam } from './api';
3
+ export declare function apiSignup(name: string, email: string, password: string): Promise<ApiAuthResponseV0>;
4
+ export declare function apiSignin(name: string, password: string): Promise<ApiAuthResponseV0>;
5
+ export declare function apiLogout(auth: AuthParam): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ import { API_AUTH_RESPONSE_SCHEMA_V0 } from '../../shared/api/auth';
3
+ import { apiCall } from './api';
4
+ export async function apiSignup(name, email, password) {
5
+ const req = {
6
+ name,
7
+ email,
8
+ password
9
+ };
10
+ return apiCall('post', 'signup', null, null, req, API_AUTH_RESPONSE_SCHEMA_V0);
11
+ }
12
+ export async function apiSignin(name, password) {
13
+ const req = {
14
+ name,
15
+ password
16
+ };
17
+ return apiCall('post', 'signin', null, null, req, API_AUTH_RESPONSE_SCHEMA_V0);
18
+ }
19
+ export async function apiLogout(auth) {
20
+ await apiCall('post', 'logout', auth, null, null, z.undefined());
21
+ }
@@ -0,0 +1,2 @@
1
+ import { JSX } from 'react';
2
+ export declare function NotFound(): JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { Box, Stack, Typography } from '@mui/material';
2
+ import React from 'react';
3
+ import { Link } from 'react-router-dom';
4
+ export function NotFound() {
5
+ return (React.createElement(Stack, { width: '100vw', height: '100vh', display: 'flex', justifyContent: 'center', alignItems: 'center' },
6
+ React.createElement(Box, { fontSize: 120 }, '404'),
7
+ React.createElement(Box, null,
8
+ React.createElement(Link, { to: '/' },
9
+ React.createElement(Typography, { color: 'warning.main', sx: { textDecoration: 'underline' } }, 'To main page')))));
10
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface SignupSigninScreenProps {
3
+ appName: string;
4
+ }
5
+ export declare const SignupSigninScreen: React.FunctionComponent<SignupSigninScreenProps>;
6
+ export {};
@@ -0,0 +1,129 @@
1
+ import { Box, Button, CircularProgress, Stack, Tab, Tabs, TextField, Typography } from '@mui/material';
2
+ import { observer } from 'mobx-react-lite';
3
+ import React from 'react';
4
+ import { useCallback, useState } from 'react';
5
+ import { useLocation, useNavigate } from 'react-router-dom';
6
+ import { z } from 'zod';
7
+ import { apiSignin, apiSignup } from '../api/auth';
8
+ import { useAuthState } from '../state/auth';
9
+ // eslint-disable-next-line @typescript-eslint/naming-convention
10
+ export const SignupSigninScreen = observer(function SignupSigninScreen({ appName }) {
11
+ const location = useLocation();
12
+ const navigate = useNavigate();
13
+ const tab = location.pathname.substring(1);
14
+ return (React.createElement(Stack, { p: 1, gap: 1, maxWidth: '500px', mx: 'auto', mt: 10 },
15
+ React.createElement(Typography, { variant: 'h3', textAlign: 'center' }, appName),
16
+ React.createElement(Tabs, { value: tab, variant: 'fullWidth', onChange: (_, tab) => {
17
+ void navigate(`/${tab}`);
18
+ } },
19
+ React.createElement(Tab, { value: 'signin', label: 'Signin' }),
20
+ React.createElement(Tab, { value: 'signup', label: 'Signup' })),
21
+ tab === 'signup'
22
+ && React.createElement(SignupForm, null),
23
+ tab === 'signin'
24
+ && React.createElement(SigninForm, null)));
25
+ });
26
+ function SignupForm() {
27
+ const authState = useAuthState();
28
+ const navigate = useNavigate();
29
+ const [syncInProgress, setSyncInProgress] = useState(false);
30
+ const [name, setName] = useState('');
31
+ const [nameActivated, setNameActivated] = useState(false);
32
+ const [email, setEmail] = useState('');
33
+ const [emailActivated, setEmailActivated] = useState(false);
34
+ const [password, setPassword] = useState('');
35
+ const [passwordActivated, setPasswordActivated] = useState(false);
36
+ const [password2, setPassword2] = useState('');
37
+ const [password2Activated, setPassword2Activated] = useState(false);
38
+ const doSignUp = useCallback(() => {
39
+ setSyncInProgress(true);
40
+ setTimeout(async () => {
41
+ try {
42
+ await signup(name, email, password, password2, authState);
43
+ void navigate('/');
44
+ }
45
+ finally {
46
+ setSyncInProgress(false);
47
+ }
48
+ }, 0);
49
+ }, [name, email, password, password2]);
50
+ return (React.createElement(Stack, { gap: 2, mt: 2 },
51
+ React.createElement(TextField, { label: 'Name', value: name, error: nameActivated && name === '', helperText: nameActivated ? (name === '' ? 'Name sholud not be empty' : 'ok') : 'required', onChange: (e) => {
52
+ setNameActivated(true);
53
+ setName(e.target.value);
54
+ }, autoFocus: true }),
55
+ React.createElement(TextField, { label: 'E-mail', type: 'email', value: email, error: emailActivated && !isValidEmail(email), helperText: emailActivated ? (isValidEmail(email) ? 'ok' : 'Invalid e-mail address') : 'required', onChange: e => setEmail(e.target.value), onFocus: () => setEmailActivated(true) }),
56
+ React.createElement(TextField, { label: 'Password', type: 'password', value: password, error: passwordActivated && password === '', helperText: passwordActivated ? (password === '' ? 'Empty password' : 'ok') : 'required', onChange: e => setPassword(e.target.value), onFocus: () => setPasswordActivated(true) }),
57
+ React.createElement(TextField, { label: 'Confirm password', type: 'password', value: password2, error: password2Activated && password2 !== password, helperText: password2Activated ? (password2 !== password ? 'Do not match' : 'ok') : 'required', onChange: e => setPassword2(e.target.value), onFocus: () => setPassword2Activated(true), onKeyDown: (e) => {
58
+ if (e.key === 'Enter') {
59
+ doSignUp();
60
+ }
61
+ } }),
62
+ React.createElement(Box, { sx: { m: 1, position: 'relative' } },
63
+ React.createElement(Button, { variant: 'contained', fullWidth: true, disabled: name === ''
64
+ || !isValidEmail(email)
65
+ || password === ''
66
+ || password2 !== password
67
+ || syncInProgress, onClick: doSignUp }, 'Signup'),
68
+ syncInProgress && (React.createElement(CircularProgress, { size: 24, sx: {
69
+ position: 'absolute',
70
+ top: '50%',
71
+ left: '50%',
72
+ marginTop: '-12px',
73
+ marginLeft: '-12px'
74
+ } })))));
75
+ }
76
+ async function signup(name, email, password, password2, authState) {
77
+ if (name === ''
78
+ || !isValidEmail(email)
79
+ || password === ''
80
+ || password2 !== password) {
81
+ return;
82
+ }
83
+ const resp = await apiSignup(name, email, password);
84
+ authState.login(resp.id, resp.name, resp.email, resp.tempPassword);
85
+ }
86
+ function SigninForm() {
87
+ const authState = useAuthState();
88
+ const [syncInProgress, setSyncInProgress] = useState(false);
89
+ const navigate = useNavigate();
90
+ const [name, setName] = useState('');
91
+ const [password, setPassword] = useState('');
92
+ const doSignIn = useCallback(() => {
93
+ setSyncInProgress(true);
94
+ setTimeout(async () => {
95
+ try {
96
+ await signin(name, password, authState);
97
+ void navigate('/');
98
+ }
99
+ finally {
100
+ setSyncInProgress(false);
101
+ }
102
+ }, 0);
103
+ }, [name, password]);
104
+ return (React.createElement(Stack, { gap: 2, mt: 2 },
105
+ React.createElement(TextField, { label: 'Name', value: name, onChange: e => setName(e.target.value), autoFocus: true }),
106
+ React.createElement(TextField, { label: 'Password', type: 'password', value: password, onChange: e => setPassword(e.target.value), onKeyDown: (e) => {
107
+ if (e.key === 'Enter') {
108
+ doSignIn();
109
+ }
110
+ } }),
111
+ React.createElement(Box, { sx: { m: 1, position: 'relative' } },
112
+ React.createElement(Button, { variant: 'contained', fullWidth: true, disabled: name === ''
113
+ || password === ''
114
+ || syncInProgress, onClick: doSignIn }, 'Signin'),
115
+ syncInProgress && (React.createElement(CircularProgress, { size: 24, sx: {
116
+ position: 'absolute',
117
+ top: '50%',
118
+ left: '50%',
119
+ marginTop: '-12px',
120
+ marginLeft: '-12px'
121
+ } })))));
122
+ }
123
+ async function signin(name, password, authState) {
124
+ const resp = await apiSignin(name, password);
125
+ authState.login(resp.id, resp.name, resp.email, resp.tempPassword);
126
+ }
127
+ function isValidEmail(s) {
128
+ return z.string().email().safeParse(s).success;
129
+ }
@@ -0,0 +1,12 @@
1
+ import { AuthParam } from '../api/api';
2
+ export declare class AuthState {
3
+ id: string | null;
4
+ name: string | null;
5
+ email: string | null;
6
+ tempPassword: string | null;
7
+ constructor();
8
+ logout(): void;
9
+ login(id: string, name: string, email: string, tempPassword: string): void;
10
+ get authParam(): AuthParam | null;
11
+ }
12
+ export declare function useAuthState(): AuthState;
@@ -0,0 +1,74 @@
1
+ import { autorun, makeAutoObservable } from 'mobx';
2
+ const AUTH_ID_KEY = 'AUTH_ID';
3
+ const AUTH_NAME_KEY = 'AUTH_NAME';
4
+ const AUTH_EMAIL_KEY = 'AUTH_EMAIL';
5
+ const AUTH_TEMP_PASSWORD_KEY = 'AUTH_TEMP_PASSWORD';
6
+ export class AuthState {
7
+ id = localStorage.getItem(AUTH_ID_KEY);
8
+ name = localStorage.getItem(AUTH_NAME_KEY);
9
+ email = localStorage.getItem(AUTH_EMAIL_KEY);
10
+ tempPassword = localStorage.getItem(AUTH_TEMP_PASSWORD_KEY);
11
+ constructor() {
12
+ makeAutoObservable(this);
13
+ autorun(() => {
14
+ if (this.id !== null) {
15
+ localStorage.setItem(AUTH_ID_KEY, this.id);
16
+ }
17
+ else {
18
+ localStorage.removeItem(AUTH_ID_KEY);
19
+ }
20
+ });
21
+ autorun(() => {
22
+ if (this.name !== null) {
23
+ localStorage.setItem(AUTH_NAME_KEY, this.name);
24
+ }
25
+ else {
26
+ localStorage.removeItem(AUTH_NAME_KEY);
27
+ }
28
+ });
29
+ autorun(() => {
30
+ if (this.email !== null) {
31
+ localStorage.setItem(AUTH_EMAIL_KEY, this.email);
32
+ }
33
+ else {
34
+ localStorage.removeItem(AUTH_EMAIL_KEY);
35
+ }
36
+ });
37
+ autorun(() => {
38
+ if (this.tempPassword !== null) {
39
+ localStorage.setItem(AUTH_TEMP_PASSWORD_KEY, this.tempPassword);
40
+ }
41
+ else {
42
+ localStorage.removeItem(AUTH_TEMP_PASSWORD_KEY);
43
+ }
44
+ });
45
+ }
46
+ logout() {
47
+ this.id = null;
48
+ this.name = null;
49
+ this.email = null;
50
+ this.tempPassword = null;
51
+ }
52
+ login(id, name, email, tempPassword) {
53
+ this.id = id;
54
+ this.name = name;
55
+ this.email = email;
56
+ this.tempPassword = tempPassword;
57
+ }
58
+ get authParam() {
59
+ if (this.id === null || this.tempPassword === null) {
60
+ return null;
61
+ }
62
+ return {
63
+ userId: this.id,
64
+ tempPassword: this.tempPassword
65
+ };
66
+ }
67
+ }
68
+ let state = null;
69
+ export function useAuthState() {
70
+ if (state === null) {
71
+ state = new AuthState();
72
+ }
73
+ return state;
74
+ }
@@ -0,0 +1,13 @@
1
+ import { DateTime } from 'luxon';
2
+ import { API_COMPARISON_OBJECT_SCHEMA_V0, ApiItemsResponseV0 } from '../shared/api/sync';
3
+ export declare function syncItems<T extends {
4
+ id: string;
5
+ lastModified: DateTime<true>;
6
+ }>({ getRemoteLastModified, localItems, pushRemote, getRemote, pushLocal, lastSyncDate }: {
7
+ getRemoteLastModified: () => Promise<ApiItemsResponseV0<typeof API_COMPARISON_OBJECT_SCHEMA_V0>>;
8
+ localItems: readonly T[];
9
+ pushRemote: (items: readonly T[]) => Promise<void>;
10
+ getRemote: (ids: readonly string[]) => Promise<T[]>;
11
+ pushLocal: (items: readonly T[]) => void;
12
+ lastSyncDate: DateTime<true> | null;
13
+ }): Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { DateTime } from 'luxon';
2
+ import { toValid } from '../shared/utils/datetime';
3
+ export async function syncItems({ getRemoteLastModified, localItems, pushRemote, getRemote, pushLocal, lastSyncDate }) {
4
+ const remoteItems = (await getRemoteLastModified()).items;
5
+ const remoteItemsMap = Object.fromEntries(remoteItems.map(i => [i.id, toValid(DateTime.fromISO(i.lastModified, { zone: 'utc' }))]));
6
+ const itemsToGet = [];
7
+ const itemsToPush = [];
8
+ for (const i of localItems) {
9
+ const ri = remoteItemsMap[i.id];
10
+ if (ri === undefined) {
11
+ if (lastSyncDate === null || i.lastModified > lastSyncDate) {
12
+ itemsToPush.push(i);
13
+ }
14
+ continue;
15
+ }
16
+ if (ri.equals(i.lastModified)) {
17
+ continue;
18
+ }
19
+ if (ri < i.lastModified) {
20
+ itemsToPush.push(i);
21
+ continue;
22
+ }
23
+ itemsToGet.push(i.id);
24
+ }
25
+ const knownItems = new Set(localItems.map(i => i.id));
26
+ for (const { id } of remoteItems) {
27
+ if (!knownItems.has(id)) {
28
+ itemsToGet.push(id);
29
+ }
30
+ }
31
+ if (itemsToPush.length > 0) {
32
+ await pushRemote(itemsToPush);
33
+ }
34
+ if (itemsToGet.length > 0) {
35
+ pushLocal((await getRemote(itemsToGet)));
36
+ }
37
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export declare const API_AUTH_RESPONSE_SCHEMA_V0: z.ZodObject<{
3
+ id: z.ZodString;
4
+ name: z.ZodString;
5
+ email: z.ZodString;
6
+ tempPassword: z.ZodString;
7
+ }, "strip", z.ZodTypeAny, {
8
+ id: string;
9
+ name: string;
10
+ email: string;
11
+ tempPassword: string;
12
+ }, {
13
+ id: string;
14
+ name: string;
15
+ email: string;
16
+ tempPassword: string;
17
+ }>;
18
+ export type ApiAuthResponseV0 = z.infer<typeof API_AUTH_RESPONSE_SCHEMA_V0>;
19
+ export declare const API_SIGNUP_REQUEST_SCHEMA_V0: z.ZodObject<{
20
+ name: z.ZodString;
21
+ email: z.ZodString;
22
+ password: z.ZodString;
23
+ }, "strip", z.ZodTypeAny, {
24
+ name: string;
25
+ email: string;
26
+ password: string;
27
+ }, {
28
+ name: string;
29
+ email: string;
30
+ password: string;
31
+ }>;
32
+ export type ApiSignupRequestV0 = z.infer<typeof API_SIGNUP_REQUEST_SCHEMA_V0>;
33
+ export declare const API_SIGNIN_REQUEST_SCHEMA_V0: z.ZodObject<{
34
+ name: z.ZodString;
35
+ password: z.ZodString;
36
+ }, "strip", z.ZodTypeAny, {
37
+ name: string;
38
+ password: string;
39
+ }, {
40
+ name: string;
41
+ password: string;
42
+ }>;
43
+ export type ApiSigninRequestV0 = z.infer<typeof API_SIGNIN_REQUEST_SCHEMA_V0>;
@@ -0,0 +1,16 @@
1
+ import { z } from 'zod';
2
+ export const API_AUTH_RESPONSE_SCHEMA_V0 = z.object({
3
+ id: z.string().uuid(),
4
+ name: z.string(),
5
+ email: z.string().email(),
6
+ tempPassword: z.string()
7
+ });
8
+ export const API_SIGNUP_REQUEST_SCHEMA_V0 = z.object({
9
+ name: z.string(),
10
+ email: z.string().email(),
11
+ password: z.string()
12
+ });
13
+ export const API_SIGNIN_REQUEST_SCHEMA_V0 = z.object({
14
+ name: z.string(),
15
+ password: z.string()
16
+ });
@@ -0,0 +1,43 @@
1
+ import z, { ZodType } from 'zod';
2
+ export declare const API_GET_OBJECTS_QUERY_STRING_SCHEMA_V0: z.ZodObject<{
3
+ syncAfter: z.ZodOptional<z.ZodString>;
4
+ }, "strip", z.ZodTypeAny, {
5
+ syncAfter?: string | undefined;
6
+ }, {
7
+ syncAfter?: string | undefined;
8
+ }>;
9
+ export declare function API_ITEMS_RESPONSE_SCHEMA_V0<T extends ZodType>(itemsSchema: T): z.ZodReadonly<z.ZodObject<{
10
+ items: z.ZodReadonly<z.ZodArray<T, "many">>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ items: readonly T["_output"][];
13
+ }, {
14
+ items: readonly T["_input"][];
15
+ }>>;
16
+ export type ApiItemsResponseV0<T extends ZodType> = z.infer<ReturnType<typeof API_ITEMS_RESPONSE_SCHEMA_V0<T>>>;
17
+ export declare const API_GET_OBJECTS_REQUEST_SCHEMA_V0: z.ZodReadonly<z.ZodObject<{
18
+ ids: z.ZodReadonly<z.ZodArray<z.ZodString, "many">>;
19
+ }, "strip", z.ZodTypeAny, {
20
+ ids: readonly string[];
21
+ }, {
22
+ ids: readonly string[];
23
+ }>>;
24
+ export type ApiGetObjectsRequestV0 = z.infer<typeof API_GET_OBJECTS_REQUEST_SCHEMA_V0>;
25
+ export declare const API_COMPARISON_OBJECT_SCHEMA_V0: z.ZodObject<{
26
+ id: z.ZodString;
27
+ lastModified: z.ZodString;
28
+ }, "strip", z.ZodTypeAny, {
29
+ lastModified: string;
30
+ id: string;
31
+ }, {
32
+ lastModified: string;
33
+ id: string;
34
+ }>;
35
+ export type ApiComparisonObjectV0 = z.infer<typeof API_COMPARISON_OBJECT_SCHEMA_V0>;
36
+ export declare function API_ITEMS_REQUEST_SCHEMA_V0<T extends ZodType>(itemsSchema: T): z.ZodReadonly<z.ZodObject<{
37
+ items: z.ZodReadonly<z.ZodArray<T, "many">>;
38
+ }, "strip", z.ZodTypeAny, {
39
+ items: readonly T["_output"][];
40
+ }, {
41
+ items: readonly T["_input"][];
42
+ }>>;
43
+ export type ApiItemsRequestV0<T extends ZodType> = z.infer<ReturnType<typeof API_ITEMS_REQUEST_SCHEMA_V0<T>>>;
@@ -0,0 +1,21 @@
1
+ import z from 'zod';
2
+ export const API_GET_OBJECTS_QUERY_STRING_SCHEMA_V0 = z.object({
3
+ syncAfter: z.string().datetime().optional()
4
+ });
5
+ export function API_ITEMS_RESPONSE_SCHEMA_V0(itemsSchema) {
6
+ return z.object({
7
+ items: z.array(itemsSchema).readonly()
8
+ }).readonly();
9
+ }
10
+ export const API_GET_OBJECTS_REQUEST_SCHEMA_V0 = z.object({
11
+ ids: z.array(z.string().uuid()).min(1).readonly()
12
+ }).readonly();
13
+ export const API_COMPARISON_OBJECT_SCHEMA_V0 = z.object({
14
+ id: z.string().uuid(),
15
+ lastModified: z.string().datetime()
16
+ });
17
+ export function API_ITEMS_REQUEST_SCHEMA_V0(itemsSchema) {
18
+ return z.object({
19
+ items: z.array(itemsSchema).min(1).readonly()
20
+ }).readonly();
21
+ }
@@ -0,0 +1,3 @@
1
+ import { DateTime } from 'luxon';
2
+ export declare function toValid(datetime: DateTime): DateTime<true>;
3
+ export declare function utcToday(): DateTime<true>;
@@ -0,0 +1,15 @@
1
+ import { DateTime } from 'luxon';
2
+ export function toValid(datetime) {
3
+ if (!datetime.isValid) {
4
+ throw Error('Invalid DateTime');
5
+ }
6
+ return datetime;
7
+ }
8
+ export function utcToday() {
9
+ const local = DateTime.local();
10
+ const utc = DateTime.utc(local.year, local.month, local.day);
11
+ if (!utc.isValid) {
12
+ throw Error('Invalid DateTime');
13
+ }
14
+ return utc;
15
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "rlz-engine",
3
+ "version": "0.0.1",
4
+ "description": "Deps and tools for my style of development",
5
+ "scripts": {
6
+ "build": "tsc"
7
+ },
8
+ "author": "Dmitry Maslennikov <maslennikovdm@gmail.com>",
9
+ "license": "ISC",
10
+ "homepage": "https://github.com/rlz/rlz-engine",
11
+ "bugs": {
12
+ "url": "https://github.com/rlz/rlz-engine/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/rlz/rlz-engine.git"
17
+ },
18
+ "type": "module",
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "devDependencies": {
23
+ "@stylistic/eslint-plugin": "^2.11.0",
24
+ "@types/luxon": "^3.4.2",
25
+ "@types/node": "^22.10.1",
26
+ "@typescript-eslint/parser": "^8.16.0",
27
+ "eslint": "^9.16.0",
28
+ "eslint-plugin-simple-import-sort": "^12.1.1",
29
+ "tsx": "^4.19.2",
30
+ "typescript": "^5.7.2",
31
+ "typescript-eslint": "^8.16.0"
32
+ },
33
+ "dependencies": {
34
+ "@emotion/react": "^11.14.0",
35
+ "@emotion/styled": "^11.14.0",
36
+ "@fastify/compress": "^8.0.1",
37
+ "@fastify/cors": "^10.0.1",
38
+ "@fastify/response-validation": "^3.0.2",
39
+ "@fastify/sensible": "^6.0.1",
40
+ "@fastify/static": "^8.0.3",
41
+ "@mui/icons-material": "^6.3.0",
42
+ "@mui/material": "^6.3.0",
43
+ "@types/react": "^19.0.2",
44
+ "@types/react-dom": "^19.0.2",
45
+ "@types/react-router-dom": "^5.3.3",
46
+ "acme-client": "^5.4.0",
47
+ "ajv-formats": "^3.0.1",
48
+ "fastify": "^5.1.0",
49
+ "fastify-acme": "^1.0.5",
50
+ "fastify-plugin": "^5.0.1",
51
+ "iterator-helpers-polyfill": "^3.0.1",
52
+ "luxon": "^3.5.0",
53
+ "mobx": "^6.13.5",
54
+ "mobx-react-lite": "^4.1.0",
55
+ "mongodb": "^6.12.0",
56
+ "pino": "^9.5.0",
57
+ "pino-pretty": "^13.0.0",
58
+ "react": "^19.0.0",
59
+ "react-dom": "^19.0.0",
60
+ "react-router-dom": "^7.1.1",
61
+ "uuidv7": "^1.0.2",
62
+ "zod": "^3.23.8",
63
+ "zod-to-json-schema": "^3.23.5"
64
+ }
65
+ }