feat: add fluxer upstream source and self-hosting documentation

- Clone of github.com/fluxerapp/fluxer (official upstream)
- SELF_HOSTING.md: full VM rebuild procedure, architecture overview,
  service reference, step-by-step setup, troubleshooting, seattle reference
- dev/.env.example: all env vars with secrets redacted and generation instructions
- dev/livekit.yaml: LiveKit config template with placeholder keys
- fluxer-seattle/: existing seattle deployment setup scripts
This commit is contained in:
Vish
2026-03-13 00:55:14 -07:00
parent 5ceda343b8
commit 3b9d759b4b
5859 changed files with 1923440 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {DefaultUserOnly, LoginRequired} from '@fluxer/api/src/middleware/AuthMiddleware';
import {RateLimitMiddleware} from '@fluxer/api/src/middleware/RateLimitMiddleware';
import {OpenAPI} from '@fluxer/api/src/middleware/ResponseTypeMiddleware';
import {RateLimitConfigs} from '@fluxer/api/src/RateLimitConfig';
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
import {Validator} from '@fluxer/api/src/Validator';
import {ThemeCreateRequest, ThemeCreateResponse} from '@fluxer/schema/src/domains/theme/ThemeSchemas';
export function ThemeController(app: HonoApp) {
app.post(
'/users/@me/themes',
RateLimitMiddleware(RateLimitConfigs.THEME_SHARE_CREATE),
LoginRequired,
DefaultUserOnly,
OpenAPI({
operationId: 'create_theme',
summary: 'Create theme',
responseSchema: ThemeCreateResponse,
statusCode: 201,
security: ['botToken', 'bearerToken', 'sessionToken'],
tags: ['Themes'],
description: 'Creates a new custom theme with CSS styling that can be shared with other users.',
}),
Validator('json', ThemeCreateRequest),
async (ctx) => {
const {css} = ctx.req.valid('json');
const theme = await ctx.get('themeService').createTheme(css);
return ctx.json(theme, 201);
},
);
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {randomBytes} from 'node:crypto';
import {Config} from '@fluxer/api/src/Config';
import type {IStorageService} from '@fluxer/api/src/infrastructure/IStorageService';
import {getMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
import {FileSizeTooLargeError} from '@fluxer/errors/src/domains/core/FileSizeTooLargeError';
const MAX_CSS_BYTES = 8 * 1024 * 1024;
export class ThemeService {
constructor(private readonly storageService: IStorageService) {}
async createTheme(css: string): Promise<{id: string}> {
const cssBytes = Buffer.from(css, 'utf-8');
if (cssBytes.length > MAX_CSS_BYTES) {
throw new FileSizeTooLargeError();
}
const themeId = randomBytes(8).toString('hex');
await this.storageService.uploadObject({
bucket: Config.s3.buckets.cdn,
key: `themes/${themeId}.css`,
body: cssBytes,
contentType: 'text/css; charset=utf-8',
});
getMetricsService().counter({
name: 'fluxer.themes.custom_theme_created',
dimensions: {
css_size_bytes: cssBytes.length.toString(),
},
});
return {id: themeId};
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createFakeAuthToken} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
describe('Theme authentication required', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('rejects request without authentication', async () => {
await createBuilderWithoutAuth(harness)
.post('/users/@me/themes')
.body({css: '.test { color: red; }'})
.expect(HTTP_STATUS.UNAUTHORIZED, 'UNAUTHORIZED')
.execute();
});
it('rejects request with invalid token', async () => {
const fakeToken = createFakeAuthToken();
await createBuilder(harness, fakeToken)
.post('/users/@me/themes')
.body({css: '.test { color: red; }'})
.expect(HTTP_STATUS.UNAUTHORIZED, 'UNAUTHORIZED')
.execute();
});
it('rejects request with empty authorization header', async () => {
await createBuilder(harness, '')
.post('/users/@me/themes')
.body({css: '.test { color: red; }'})
.expect(HTTP_STATUS.UNAUTHORIZED, 'UNAUTHORIZED')
.execute();
});
it('rejects request with malformed token', async () => {
await createBuilder(harness, 'not-a-valid-token')
.post('/users/@me/themes')
.body({css: '.test { color: red; }'})
.expect(HTTP_STATUS.UNAUTHORIZED, 'UNAUTHORIZED')
.execute();
});
});

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createTestBotAccount} from '@fluxer/api/src/bot/tests/BotTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
describe('Theme bot user denied', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('rejects theme creation from bot users', async () => {
const botAccount = await createTestBotAccount(harness);
await createBuilder(harness, `Bot ${botAccount.botToken}`)
.post('/users/@me/themes')
.body({css: '.test { color: red; }'})
.expect(HTTP_STATUS.FORBIDDEN, 'ACCESS_DENIED')
.execute();
});
});

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
interface ThemeCreateResponse {
id: string;
}
describe('Theme creation', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('successfully creates a theme with valid CSS', async () => {
const user = await createTestAccount(harness);
const theme = await createBuilder<ThemeCreateResponse>(harness, user.token)
.post('/users/@me/themes')
.body({css: '.test { color: red; }'})
.expect(HTTP_STATUS.CREATED)
.execute();
expect(theme.id).toBeDefined();
expect(typeof theme.id).toBe('string');
expect(theme.id.length).toBe(16);
});
it('successfully creates a theme with complex CSS', async () => {
const user = await createTestAccount(harness);
const complexCss = `
:root {
--background-primary: #36393f;
--background-secondary: #2f3136;
--text-normal: #dcddde;
}
.container {
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (max-width: 768px) {
.container {
flex-direction: row;
}
}
`;
const theme = await createBuilder<ThemeCreateResponse>(harness, user.token)
.post('/users/@me/themes')
.body({css: complexCss})
.expect(HTTP_STATUS.CREATED)
.execute();
expect(theme.id).toBeDefined();
});
it('successfully creates a theme with minimal CSS (1 character)', async () => {
const user = await createTestAccount(harness);
const theme = await createBuilder<ThemeCreateResponse>(harness, user.token)
.post('/users/@me/themes')
.body({css: 'a'})
.expect(HTTP_STATUS.CREATED)
.execute();
expect(theme.id).toBeDefined();
});
it('successfully creates a theme with unicode CSS content', async () => {
const user = await createTestAccount(harness);
const unicodeCss = '.test::before { content: "\u2764\ufe0f"; }';
const theme = await createBuilder<ThemeCreateResponse>(harness, user.token)
.post('/users/@me/themes')
.body({css: unicodeCss})
.expect(HTTP_STATUS.CREATED)
.execute();
expect(theme.id).toBeDefined();
});
});

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
interface ThemeCreateResponse {
id: string;
}
const MAX_CSS_BYTES = 8 * 1024 * 1024;
describe('Theme CSS size limits', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('rejects CSS that exceeds the 8MB limit', async () => {
const user = await createTestAccount(harness);
const oversizedCss = 'a'.repeat(MAX_CSS_BYTES + 1);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: oversizedCss})
.expect(HTTP_STATUS.BAD_REQUEST, 'FILE_SIZE_TOO_LARGE')
.execute();
});
it('accepts CSS at exactly the 8MB limit', async () => {
const user = await createTestAccount(harness);
const maxCss = 'a'.repeat(MAX_CSS_BYTES);
const theme = await createBuilder<ThemeCreateResponse>(harness, user.token)
.post('/users/@me/themes')
.body({css: maxCss})
.expect(HTTP_STATUS.CREATED)
.execute();
expect(theme.id).toBeDefined();
});
it('accepts CSS just under the 8MB limit', async () => {
const user = await createTestAccount(harness);
const nearMaxCss = 'a'.repeat(MAX_CSS_BYTES - 1);
const theme = await createBuilder<ThemeCreateResponse>(harness, user.token)
.post('/users/@me/themes')
.body({css: nearMaxCss})
.expect(HTTP_STATUS.CREATED)
.execute();
expect(theme.id).toBeDefined();
});
it('rejects CSS exceeding limit with multibyte unicode characters', async () => {
const user = await createTestAccount(harness);
const unicodeChar = '\u{1F600}';
const bytesPerChar = Buffer.from(unicodeChar, 'utf-8').length;
const charsNeeded = Math.ceil((MAX_CSS_BYTES + 1) / bytesPerChar);
const oversizedUnicodeCss = unicodeChar.repeat(charsNeeded);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: oversizedUnicodeCss})
.expect(HTTP_STATUS.BAD_REQUEST, 'FILE_SIZE_TOO_LARGE')
.execute();
});
});

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
describe('Theme validation', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
});
afterAll(async () => {
await harness?.shutdown();
});
it('rejects request with missing css field', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
it('rejects request with empty css string', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: ''})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
it('rejects request with null css value', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: null})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
it('rejects request with numeric css value', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: 12345})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
it('rejects request with array css value', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: ['body { color: red; }']})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
it('rejects request with object css value', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: {content: 'body { color: red; }'}})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
it('rejects request with boolean css value', async () => {
const user = await createTestAccount(harness);
await createBuilder(harness, user.token)
.post('/users/@me/themes')
.body({css: true})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
});