SaaS Consolidation (#6384)

Co-authored-by: ConnorYoh <[email protected]>
This commit is contained in:
Anthony Stirling
2026-05-21 16:05:35 +01:00
committed by GitHub
co-authored by ConnorYoh
parent 089de247b4
commit 9d081d1792
208 changed files with 14064 additions and 242 deletions
@@ -1904,22 +1904,6 @@ secureWorkflowDesc = "Asegura documentos PDF eliminando contenido potencialmente
[autoRename]
description = "Esta herramienta renombrará automáticamente los archivos PDF en función de su contenido. Analiza el documento para encontrar el título más adecuado a partir del texto."
[pdfCommentAgent]
submit = "Generar comentarios"
prompt.label = "¿Sobre qué debería comentar la IA?"
prompt.placeholder = "p. ej., Señala fechas ambiguas y sugiere aclaraciones"
[pdfCommentAgent.settings]
title = "Instrucciones de comentarios"
[pdfCommentAgent.results]
title = "PDF con comentarios"
[pdfCommentAgent.error]
failed = "No se pudieron generar los comentarios"
emptyPrompt = "Describe sobre qué debería comentar la IA"
tooLong = "El prompt es demasiado largo (máximo {{max}} caracteres)"
[autoSizeSplitPDF]
tags = "pdf,dividir,documento,organización"
@@ -4141,11 +4125,6 @@ desc = "Renombra automáticamente un archivo PDF basándose en su encabezado det
tags = "auto-detectar,basado-en-encabezado,organizar,reetiquetar"
title = "Renombrar Automáticamente Archivo PDF"
[home.pdfCommentAgent]
desc = "Pide a la IA que anote un PDF con comentarios de notas adhesivas según tu indicación"
tags = "IA,agente,comentario,anotar,nota adhesiva,revisión,feedback,notas"
title = "Añadir comentarios con IA"
[home.autoSizeSplitPDF]
desc = "Divide un solo PDF en múltiples documentos según su tamaño, número de páginas, o número de documento"
tags = "auto,dividir,tamaño"
@@ -0,0 +1,123 @@
/**
* Live saas-backend smoke. Hits the actual Spring Boot saas backend at a configured port and
* verifies the fixes landed in this branch.
*
* Skipped automatically if the backend isn't reachable — CI doesn't boot one. To run locally:
* STIRLING_FLAVOR=saas ./gradlew :stirling-pdf:bootRun --args="--server.port=18083 --spring.profiles.include=dev"
* STIRLING_SAAS_URL=http://localhost:18083 npx playwright test --project=stubbed saas-backend-smoke
*/
import { test, expect, request } from "@playwright/test";
const SAAS_URL = process.env.STIRLING_SAAS_URL ?? "http://localhost:18083";
test.describe("SaaS backend smoke", () => {
test.beforeAll(async () => {
const ctx = await request.newContext();
try {
const r = await ctx.get(`${SAAS_URL}/actuator/health`, { timeout: 2000 });
if (r.status() !== 200) {
test.skip(true, `saas backend at ${SAAS_URL} returned ${r.status()}`);
}
} catch (err) {
test.skip(true, `saas backend at ${SAAS_URL} unreachable: ${err}`);
}
});
test("health endpoint returns UP", async () => {
const ctx = await request.newContext();
const r = await ctx.get(`${SAAS_URL}/actuator/health`);
expect(r.status()).toBe(200);
const body = await r.json();
expect(body.status).toBe("UP");
});
test("public endpoints return 200 / protected return 401", async () => {
const ctx = await request.newContext();
expect((await ctx.get(`${SAAS_URL}/api/v1/info/status`)).status()).toBe(
200,
);
expect(
(await ctx.get(`${SAAS_URL}/api/v1/config/app-config`)).status(),
).toBe(200);
expect((await ctx.get(`${SAAS_URL}/api/v1/credits`)).status()).toBe(401);
});
test("user-role webhook endpoints reject unauthenticated (regression #3)", async () => {
const ctx = await request.newContext();
for (const path of [
"/api/v1/user-role/upgrade?supabaseId=foo",
"/api/v1/user-role/downgrade?supabaseId=foo",
"/api/v1/user-role/enable-metered-billing?supabaseId=foo",
"/api/v1/user-role/disable-metered-billing?supabaseId=foo",
]) {
const r = await ctx.post(`${SAAS_URL}${path}`);
expect(r.status(), `${path}: expected 401, got ${r.status()}`).toBe(401);
expect(
r.status(),
`${path}: must not 500 — would indicate hasRole prefix bug returned`,
).not.toBe(500);
}
});
test("CORS preflight rejects unknown origin (regression #11)", async () => {
const ctx = await request.newContext();
const r = await ctx.fetch(`${SAAS_URL}/api/v1/credits`, {
method: "OPTIONS",
headers: {
Origin: "https://attacker.example.com",
"Access-Control-Request-Method": "GET",
},
});
expect(r.status()).toBe(403);
expect(r.headers()["access-control-allow-origin"]).toBeUndefined();
});
test("CORS preflight rejects tenant wildcard origin (regression #11)", async () => {
const ctx = await request.newContext();
const r = await ctx.fetch(`${SAAS_URL}/api/v1/credits`, {
method: "OPTIONS",
headers: {
Origin: "https://abandoned-tenant.ssl.stirlingpdf.cloud",
"Access-Control-Request-Method": "GET",
},
});
expect(r.status()).toBe(403);
expect(r.headers()["access-control-allow-origin"]).toBeUndefined();
});
test("CORS preflight accepts allowed origin", async () => {
const ctx = await request.newContext();
const r = await ctx.fetch(`${SAAS_URL}/api/v1/credits`, {
method: "OPTIONS",
headers: {
Origin: "https://app.stirling.com",
"Access-Control-Request-Method": "GET",
},
});
expect(r.status()).toBe(200);
expect(r.headers()["access-control-allow-origin"]).toBe(
"https://app.stirling.com",
);
expect(r.headers()["access-control-allow-credentials"]).toBe("true");
});
test("backend HTML loads in a real browser (no boot-time console errors)", async ({
page,
}) => {
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`));
page.on("console", (msg) => {
if (msg.type() === "error") errors.push(`console: ${msg.text()}`);
});
const resp = await page.goto(`${SAAS_URL}/`, { waitUntil: "load" });
expect(resp?.status()).toBe(200);
await page.waitForTimeout(750);
const real = errors.filter(
(e) => !/Failed to load resource.*401|\/api\/v1\/credits/i.test(e),
);
expect(real, real.join("\n")).toEqual([]);
});
});
@@ -1,3 +1,8 @@
// Supabase client. Relocated out of frontend/src/core/services/ during the SaaS<->OSS
// consolidation so the Supabase SDK never reaches the OSS core bundle. Lives in
// :proprietary because licensing/checkout/billing flows in proprietary mode use it; the
// :saas mode bundle picks it up via the existing @app/* path mapping (saas to proprietary
// to core).
import { createClient, SupabaseClient } from "@supabase/supabase-js";
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+18 -11
View File
@@ -26,16 +26,23 @@ export default defineConfig(async ({ mode }) => {
// `VITE_` prefix.
const env = loadEnv(mode, process.cwd(), "");
// Resolve the effective build mode.
// Explicit --mode flags take precedence; otherwise default to proprietary
// unless DISABLE_ADDITIONAL_FEATURES=true, in which case default to core.
const effectiveMode: BuildMode = (VALID_MODES as readonly string[]).includes(
mode,
)
// Effective mode: --mode > STIRLING_FLAVOR > ENABLE_SAAS > DISABLE_ADDITIONAL_FEATURES > proprietary.
const explicitMode = (VALID_MODES as readonly string[]).includes(mode)
? (mode as BuildMode)
: process.env.DISABLE_ADDITIONAL_FEATURES === "true"
? "core"
: "proprietary";
: null;
const flavor = (process.env.STIRLING_FLAVOR ?? "").toLowerCase();
const flavorMode: BuildMode | null =
flavor === "core" || flavor === "proprietary" || flavor === "saas"
? (flavor as BuildMode)
: null;
const effectiveMode: BuildMode =
explicitMode ??
flavorMode ??
(process.env.ENABLE_SAAS === "true"
? "saas"
: process.env.DISABLE_ADDITIONAL_FEATURES === "true"
? "core"
: "proprietary");
const tsconfigProject = TSCONFIG_MAP[effectiveMode];
@@ -96,14 +103,14 @@ export default defineConfig(async ({ mode }) => {
dest: "vendor/jscanify",
},
{
// pdfjs-dist CMap data for CJK / non-latin glyph mapping — required
// pdfjs-dist CMap data for CJK / non-latin glyph mapping. Required
// when rendering PDFs inside workers where the default DOM fetch paths
// aren't available.
src: "node_modules/pdfjs-dist/cmaps/*",
dest: "pdfjs/cmaps",
},
{
// pdfjs-dist standard font data (Helvetica/Times/etc.) needed so
// pdfjs-dist standard font data (Helvetica/Times/etc.) needed so
// workers can substitute non-embedded base 14 fonts without DOM access.
src: "node_modules/pdfjs-dist/standard_fonts/*",
dest: "pdfjs/standard_fonts",