feat: ONLYOFFICE inline editor integration with JWT auth and docker-compose
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Carbone Express Server - Standalone microservice
|
||||
* Runs on port 8771 alongside the FastAPI app on 8770
|
||||
* Also serves ONLYOFFICE callback endpoints
|
||||
*/
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const { router } = require('./carbone-service');
|
||||
const onlyofficeRouter = require('./onlyoffice-service');
|
||||
|
||||
const app = express();
|
||||
const PORT = 8771;
|
||||
@@ -16,9 +18,12 @@ app.use(express.json({ limit: '50mb' }));
|
||||
// Mount Carbone routes
|
||||
app.use('/api/carbone', router);
|
||||
|
||||
// Mount ONLYOFFICE routes
|
||||
app.use('/api/onlyoffice', onlyofficeRouter);
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => res.json({ status: 'ok', service: 'carbone' }));
|
||||
app.get('/health', (req, res) => res.json({ status: 'ok', service: 'carbone+onlyoffice' }));
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Carbone service running on http://0.0.0.0:${PORT}`);
|
||||
console.log(`Carbone + ONLYOFFICE service running on http://0.0.0.0:${PORT}`);
|
||||
});
|
||||
332
renderer/onlyoffice-service.js
Normal file
332
renderer/onlyoffice-service.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* ONLYOFFICE Integration Service
|
||||
*
|
||||
* Handles:
|
||||
* - Serving .docx templates to ONLYOFFICE for inline editing
|
||||
* - Receiving ONLYOFFICE save callbacks (status=2 -> download updated file)
|
||||
* - Generating JWT-signed editor config for the browser
|
||||
* - Managing the "Edit in ONLYOFFICE" session lifecycle
|
||||
*
|
||||
* Docker Networking:
|
||||
* - Public URL (browser -> ONLYOFFICE): http://<host>:8080/
|
||||
* - Internal URL (backend -> ONLYOFFICE): http://onlyoffice-server/
|
||||
* - The backend serves the .docx file at a publicly-accessible URL so
|
||||
* ONLYOFFICE can fetch it, and receives callbacks at a publicly-accessible URL.
|
||||
*
|
||||
* JWT Security:
|
||||
* - The JWT_SECRET env var is shared between this backend and the ONLYOFFICE container.
|
||||
* - ALL communication (editor config, callbacks) is signed with this secret.
|
||||
* - ONLYOFFICE validates the JWT on incoming requests and signs outgoing callbacks.
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const axios = require('axios');
|
||||
const multer = require('multer');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ============================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* ONLYOFFICE URLs - these are different for browser vs backend:
|
||||
*
|
||||
* ONLYOFFICE_PUBLIC_URL: The URL the user's BROWSER uses to load the ONLYOFFICE
|
||||
* JavaScript API (e.g., http://100.91.25.139:8080/ or https://docs.example.com/)
|
||||
* This is set via env var and must be accessible from the user's browser.
|
||||
*
|
||||
* ONLYOFFICE_INTERNAL_URL: The URL our BACKEND uses to talk to ONLYOFFICE
|
||||
* over the Docker network (e.g., http://onlyoffice-server/).
|
||||
* In dev mode (no Docker), this is the same as the public URL.
|
||||
*/
|
||||
const ONLYOFFICE_PUBLIC_URL = process.env.ONLYOFFICE_PUBLIC_URL || 'http://localhost:8080';
|
||||
const ONLYOFFICE_INTERNAL_URL = process.env.ONLYOFFICE_INTERNAL_URL || ONLYOFFICE_PUBLIC_URL;
|
||||
|
||||
/**
|
||||
* BACKEND_PUBLIC_URL: The URL that ONLYOFFICE uses to fetch the .docx template
|
||||
* and send callbacks back to. This must be accessible from the ONLYOFFICE
|
||||
* container's network perspective.
|
||||
* - In Docker: http://cv-backend:8771 (internal Docker DNS)
|
||||
* - In dev: http://<host>:8771 (same machine)
|
||||
*/
|
||||
const BACKEND_PUBLIC_URL = process.env.BACKEND_PUBLIC_URL || 'http://localhost:8771';
|
||||
|
||||
/**
|
||||
* JWT_SECRET: Shared secret between this backend and ONLYOFFICE.
|
||||
* Must be identical in both containers for JWT validation to work.
|
||||
*/
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'cvapp_onlyoffice_jwt_secret_2026';
|
||||
|
||||
// Template storage (shared with carbone-service)
|
||||
const TEMPLATES_DIR = path.join(__dirname, 'carbone-templates');
|
||||
fs.mkdirSync(TEMPLATES_DIR, { recursive: true });
|
||||
|
||||
// Load registry (shared with carbone-service)
|
||||
const REGISTRY_PATH = path.join(TEMPLATES_DIR, 'registry.json');
|
||||
function loadRegistry() {
|
||||
try { return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf-8')); }
|
||||
catch { return []; }
|
||||
}
|
||||
function saveRegistry(registry) {
|
||||
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// JWT TOKEN HELPERS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Sign a payload with the JWT secret.
|
||||
* ONLYOFFICE expects the token in the Authorization header as "Bearer <token>".
|
||||
*/
|
||||
function signToken(payload) {
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a JWT token from ONLYOFFICE callbacks.
|
||||
* ONLYOFFICE sends the token in the Authorization header.
|
||||
*/
|
||||
function verifyToken(token) {
|
||||
try {
|
||||
// Remove "Bearer " prefix if present
|
||||
if (token && token.startsWith('Bearer ')) {
|
||||
token = token.substring(7);
|
||||
}
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique document key.
|
||||
* ONLYOFFICE uses this to identify document sessions and for caching.
|
||||
* Must be unique per document + version (so re-edits get fresh sessions).
|
||||
*/
|
||||
function generateDocKey(templateId) {
|
||||
return `cvtemplate_${templateId}_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINT: GET /api/onlyoffice/:templateId/config
|
||||
// Returns the JWT-signed editor config for the browser to initialize
|
||||
// ============================================================
|
||||
|
||||
router.get('/:templateId/config', (req, res) => {
|
||||
const templateId = req.params.templateId;
|
||||
const registry = loadRegistry();
|
||||
const template = registry.find(t => t.id === templateId);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({ error: 'Template not found' });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(template.path)) {
|
||||
return res.status(404).json({ error: 'Template file missing' });
|
||||
}
|
||||
|
||||
const docKey = generateDocKey(templateId);
|
||||
const fileName = template.originalName || template.name + '.docx';
|
||||
|
||||
// Build the editor config
|
||||
// The document URL must be accessible from ONLYOFFICE's perspective.
|
||||
// In Docker: http://cv-backend:8771/api/onlyoffice/<id>/download
|
||||
// In dev: http://<host>:8771/api/onlyoffice/<id>/download
|
||||
const documentUrl = `${BACKEND_PUBLIC_URL}/api/onlyoffice/${templateId}/download`;
|
||||
const callbackUrl = `${BACKEND_PUBLIC_URL}/api/onlyoffice/${templateId}/callback`;
|
||||
|
||||
const config = {
|
||||
document: {
|
||||
fileType: 'docx',
|
||||
key: docKey,
|
||||
title: fileName,
|
||||
url: documentUrl,
|
||||
permissions: {
|
||||
edit: true,
|
||||
download: true,
|
||||
review: true,
|
||||
print: false,
|
||||
},
|
||||
},
|
||||
editorConfig: {
|
||||
mode: 'edit',
|
||||
lang: 'en',
|
||||
callbackUrl: callbackUrl,
|
||||
user: {
|
||||
id: 'cv-app-user',
|
||||
name: 'CV App Editor',
|
||||
},
|
||||
customization: {
|
||||
autosave: true,
|
||||
forcesave: true,
|
||||
compactHeader: false,
|
||||
toolbarNoTabs: false,
|
||||
// Hide features not relevant to CV templates
|
||||
hideRightMenu: true,
|
||||
hideRulers: false,
|
||||
},
|
||||
},
|
||||
// The type of document editor to load
|
||||
documentType: 'text',
|
||||
};
|
||||
|
||||
// Sign the entire config with JWT
|
||||
// ONLYOFFICE validates this token when the editor loads
|
||||
const token = signToken(config);
|
||||
config.token = token;
|
||||
|
||||
res.json({
|
||||
config: config,
|
||||
onlyofficeUrl: ONLYOFFICE_PUBLIC_URL,
|
||||
templateId: templateId,
|
||||
templateName: template.name,
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINT: GET /api/onlyoffice/:templateId/download
|
||||
// ONLYOFFICE fetches the .docx file from this URL
|
||||
// ============================================================
|
||||
|
||||
router.get('/:templateId/download', (req, res) => {
|
||||
const templateId = req.params.templateId;
|
||||
const registry = loadRegistry();
|
||||
const template = registry.find(t => t.id === templateId);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({ error: 'Template not found' });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(template.path)) {
|
||||
return res.status(404).json({ error: 'Template file missing' });
|
||||
}
|
||||
|
||||
const fileName = template.originalName || template.name + '.docx';
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||
|
||||
const fileStream = fs.createReadStream(template.path);
|
||||
fileStream.pipe(res);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINT: POST /api/onlyoffice/:templateId/callback
|
||||
//
|
||||
// ONLYOFFICE calls this endpoint when the document state changes.
|
||||
// Status codes:
|
||||
// 0 = Document being edited (no action needed)
|
||||
// 1 = Document ready for saving (prepare to receive)
|
||||
// 2 = Document saved, URL contains the updated file (DOWNLOAD AND OVERWRITE)
|
||||
// 3 = Document save error
|
||||
// 4 = Document closed with no changes (no action needed)
|
||||
// 6 = Document is being edited but force-save was requested
|
||||
// 7 = Force-save error
|
||||
//
|
||||
// When status === 2 or 6, the payload contains a "url" field with a
|
||||
// direct download link to the updated .docx file. We fetch it and
|
||||
// overwrite the stored template.
|
||||
// ============================================================
|
||||
|
||||
router.post('/:templateId/callback', async (req, res) => {
|
||||
const templateId = req.params.templateId;
|
||||
|
||||
// ONLYOFFICE sends JWT in the Authorization header
|
||||
// Verify the token for security
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (authHeader) {
|
||||
const decoded = verifyToken(authHeader);
|
||||
if (!decoded) {
|
||||
console.error('ONLYOFFICE callback: Invalid JWT token');
|
||||
return res.status(403).json({ error: 'Invalid token' });
|
||||
}
|
||||
}
|
||||
|
||||
// The callback payload from ONLYOFFICE
|
||||
const payload = req.body;
|
||||
const status = payload.status;
|
||||
const downloadUrl = payload.url;
|
||||
const key = payload.key;
|
||||
|
||||
console.log(`ONLYOFFICE callback: templateId=${templateId}, status=${status}, key=${key}`);
|
||||
|
||||
const registry = loadRegistry();
|
||||
const template = registry.find(t => t.id === templateId);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({ error: 'Template not found' });
|
||||
}
|
||||
|
||||
// Status 2 = saved, 6 = force-saved -> download the updated file
|
||||
if ((status === 2 || status === 6) && downloadUrl) {
|
||||
try {
|
||||
console.log(`Downloading updated template from ONLYOFFICE: ${downloadUrl}`);
|
||||
|
||||
// Fetch the updated .docx file from ONLYOFFICE's URL
|
||||
const response = await axios.get(downloadUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Overwrite the stored template file
|
||||
const templatePath = template.path;
|
||||
fs.writeFileSync(templatePath, response.data);
|
||||
|
||||
// Update the registry timestamp
|
||||
template.updatedAt = new Date().toISOString();
|
||||
saveRegistry(registry);
|
||||
|
||||
console.log(`Template "${template.name}" updated successfully (${response.data.length} bytes)`);
|
||||
|
||||
// ONLYOFFICE expects a JSON response acknowledging the save
|
||||
return res.json({
|
||||
error: 0,
|
||||
key: key
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to download updated template:', err.message);
|
||||
return res.status(500).json({
|
||||
error: 1,
|
||||
message: 'Failed to save: ' + err.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Status 4 = closed without changes, 0 = still editing, etc.
|
||||
// Just acknowledge
|
||||
return res.json({
|
||||
error: 0,
|
||||
key: key
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// ENDPOINT: GET /api/onlyoffice/health
|
||||
// Health check for the ONLYOFFICE integration
|
||||
// ============================================================
|
||||
|
||||
router.get('/health', async (req, res) => {
|
||||
let onlyofficeStatus = 'unknown';
|
||||
try {
|
||||
const resp = await axios.get(`${ONLYOFFICE_INTERNAL_URL}/healthcheck`, { timeout: 5000 });
|
||||
onlyofficeStatus = resp.status === 200 ? 'healthy' : 'unhealthy';
|
||||
} catch (err) {
|
||||
onlyofficeStatus = 'offline';
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
onlyoffice: onlyofficeStatus,
|
||||
onlyofficePublicUrl: ONLYOFFICE_PUBLIC_URL,
|
||||
onlyofficeInternalUrl: ONLYOFFICE_INTERNAL_URL,
|
||||
backendUrl: BACKEND_PUBLIC_URL,
|
||||
jwtEnabled: !!JWT_SECRET,
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
253
renderer/package-lock.json
generated
253
renderer/package-lock.json
generated
@@ -10,9 +10,11 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"archiver": "^8.0.0",
|
||||
"axios": "^1.18.1",
|
||||
"carbone": "^3.8.2",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^2.2.0",
|
||||
"puppeteer": "^25.3.0",
|
||||
"uuid": "^14.0.1"
|
||||
@@ -96,6 +98,18 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
@@ -177,6 +191,24 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
|
||||
@@ -434,6 +466,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -538,6 +576,18 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-7.0.1.tgz",
|
||||
@@ -712,6 +762,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -741,6 +800,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -792,6 +860,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@@ -987,6 +1070,42 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1096,6 +1215,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
@@ -1128,6 +1262,19 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
@@ -1212,6 +1359,49 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
@@ -1266,6 +1456,48 @@
|
||||
"url": "https://github.com/sponsors/antonk52"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1485,6 +1717,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "25.3.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.3.0.tgz",
|
||||
@@ -1638,6 +1879,18 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"archiver": "^8.0.0",
|
||||
"axios": "^1.18.1",
|
||||
"carbone": "^3.8.2",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"multer": "^2.2.0",
|
||||
"puppeteer": "^25.3.0",
|
||||
"uuid": "^14.0.1"
|
||||
|
||||
Reference in New Issue
Block a user