Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca06f56a6d |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.11.0"
|
||||
".": "0.12.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
## [0.12.0](https://github.com/useplunk/plunk/compare/v0.11.0...v0.12.0) (2026-05-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add disabledReason field to projects for better tracking of disable reasons ([94ceadb](https://github.com/useplunk/plunk/commit/94ceadbbe417f0cb3ab72c66bfad9428bdce2d11))
|
||||
* **api:** allow API key authentication for domain endpoints ([d2496bc](https://github.com/useplunk/plunk/commit/d2496bc51d17657380160dbfc20e099d3f4eadba))
|
||||
* **contacts:** make email cell a link to the contact detail page ([6759bff](https://github.com/useplunk/plunk/commit/6759bffd2a1f0bfb09b754bb0efdfe5c347ea2b3))
|
||||
* **contacts:** make email cell a link to the contact detail page ([6d98d51](https://github.com/useplunk/plunk/commit/6d98d512222ffb422bc2a4bef4284a644558b39d))
|
||||
* **EmailService:** add worker concurrency settings and improve email queue prioritization ([80beb2b](https://github.com/useplunk/plunk/commit/80beb2bb9937101d1723f2d54648060fdcbe6cef))
|
||||
* make detectCustomHtmlPatterns aware of TipTap's actual capabilities ([9797aed](https://github.com/useplunk/plunk/commit/9797aed47f520aba51bec4e09c9bc4b7762e02f8))
|
||||
* make detectCustomHtmlPatterns aware of TipTap's actual capabilities ([ba3813e](https://github.com/useplunk/plunk/commit/ba3813e2422d9fe4e252a90242393cada65644dc))
|
||||
* render template variables in WEBHOOK step url, headers and body ([bfecf04](https://github.com/useplunk/plunk/commit/bfecf04fa38309804ee19e99b9908caf51c5e039))
|
||||
* render template variables in WEBHOOK step url, headers and body ([c484da8](https://github.com/useplunk/plunk/commit/c484da88ab7b4635987868e2e3c6fdded68ade8c))
|
||||
* **SecurityService:** enhance phishing detection by verifying sender domains and institutional TLDs ([6ab4d77](https://github.com/useplunk/plunk/commit/6ab4d77ca9bebd01f75d8799dad23fdec7768b52))
|
||||
* **SecurityService:** enhance phishing detection by verifying sender domains and institutional TLDs ([edfc399](https://github.com/useplunk/plunk/commit/edfc399061cb7fd079b7f11167a2796df766d8ac))
|
||||
* **tests:** enhance test database setup and cleanup for improved isolation and performance ([32dd7bb](https://github.com/useplunk/plunk/commit/32dd7bba462a6f4a13d59b8fb03708f971d1eff0))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* coerce boolean and numeric values in custom CSV columns ([4a145f3](https://github.com/useplunk/plunk/commit/4a145f3488bee63c2d82a4a477f6dfaaaaede64b))
|
||||
* **filters:** land templates/workflows/campaigns search inputs at 32px to match filter buttons ([283f402](https://github.com/useplunk/plunk/commit/283f40239dcc81e78d35b39cb9b31f97b435b8d7))
|
||||
* make email templates, campaigns and workflow search inputs same height as the rest of the app ([8b3657d](https://github.com/useplunk/plunk/commit/8b3657d056938b17f0634c0fe8d664ae1e07cd73))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **database:** increase Prisma connection pool limits for improved test performance ([71e2277](https://github.com/useplunk/plunk/commit/71e227764319e6fc732aefdebd11f3e844257ef6))
|
||||
* **SecurityService:** update absolute count ceilings for new projects to improve spam detection ([4de40f4](https://github.com/useplunk/plunk/commit/4de40f40fa300c9e9ed2ed35f367ca4cd601f309))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add env-var sync rule to CLAUDE.md ([a348d37](https://github.com/useplunk/plunk/commit/a348d37c21ecf1d57845f31210f66a8113fadb8d))
|
||||
* add new recipe pages for waitlist and sync unsubscribes ([01ec34a](https://github.com/useplunk/plunk/commit/01ec34a8cbbe0f4a158398c0eb8300d847bc29ab))
|
||||
* correct PHISHING_CONFIDENCE_THRESHOLD default in CLAUDE.md ([5c16679](https://github.com/useplunk/plunk/commit/5c166797b57f2e7b714957824ee1acdcb22c05f2))
|
||||
* **env:** add wiki-documented vars to apps/api/.env.example ([81315ea](https://github.com/useplunk/plunk/commit/81315eac8eb32a7d2e61617c6a84f349afaf7860))
|
||||
* **env:** sync .env.self-host.example with missing variables ([1c1c95d](https://github.com/useplunk/plunk/commit/1c1c95d332fb9ab6c2f926f6d51071cfb245ed86))
|
||||
* **env:** sync env example files, fix CLAUDE.md drift, add process rule ([6ebbb50](https://github.com/useplunk/plunk/commit/6ebbb50f6817f1354f26cc7734631d3b6ded32ed))
|
||||
* **wiki:** document MAIL_FROM_SUBDOMAIN and NGINX_PORT env vars ([971b98a](https://github.com/useplunk/plunk/commit/971b98a4cc6145571204f5a4ad794aa3d09ea71c))
|
||||
|
||||
## [0.11.0](https://github.com/useplunk/plunk/compare/v0.10.0...v0.11.0) (2026-05-13)
|
||||
|
||||
|
||||
|
||||
@@ -3,20 +3,6 @@ import type {NextFunction, Request, Response} from 'express';
|
||||
import {databaseRequestLogger} from '../requestLogger.js';
|
||||
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||
|
||||
async function waitForLog(prisma: ReturnType<typeof getPrismaClient>, id: string, timeoutMs = 2000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const record = await prisma.apiRequest.findUnique({where: {id}});
|
||||
if (record) return record;
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
}
|
||||
return prisma.apiRequest.findUnique({where: {id}});
|
||||
}
|
||||
|
||||
async function waitForNoLog(ms = 200) {
|
||||
await new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
describe('Request Logger Middleware', () => {
|
||||
const prisma = getPrismaClient();
|
||||
let req: Partial<Request>;
|
||||
@@ -91,7 +77,13 @@ describe('Request Logger Middleware', () => {
|
||||
const responseBody = {success: true, data: {id: '123'}};
|
||||
await res.json!(responseBody);
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
|
||||
// Wait for async logging to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Verify database record was created
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-request-id-123'},
|
||||
});
|
||||
|
||||
expect(loggedRequest).toBeDefined();
|
||||
expect(loggedRequest?.method).toBe('POST');
|
||||
@@ -117,7 +109,11 @@ describe('Request Logger Middleware', () => {
|
||||
const responseBody = {success: true};
|
||||
await res.json!(responseBody);
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'public-request-id');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'public-request-id'},
|
||||
});
|
||||
|
||||
expect(loggedRequest).toBeDefined();
|
||||
expect(loggedRequest?.projectId).toBeNull();
|
||||
@@ -135,7 +131,11 @@ describe('Request Logger Middleware', () => {
|
||||
|
||||
await res.json!({success: true});
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-request-id-123'},
|
||||
});
|
||||
|
||||
// Allow for timer imprecision (especially in CI environments)
|
||||
expect(loggedRequest?.duration).toBeGreaterThanOrEqual(45);
|
||||
@@ -162,7 +162,11 @@ describe('Request Logger Middleware', () => {
|
||||
|
||||
await res.json!(errorResponse);
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-request-id-123'},
|
||||
});
|
||||
|
||||
expect(loggedRequest).toBeDefined();
|
||||
expect(loggedRequest?.statusCode).toBe(400);
|
||||
@@ -185,7 +189,11 @@ describe('Request Logger Middleware', () => {
|
||||
|
||||
await res.json!(errorResponse);
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-request-id-123'},
|
||||
});
|
||||
|
||||
expect(loggedRequest?.statusCode).toBe(500);
|
||||
expect(loggedRequest?.errorCode).toBe('INTERNAL_SERVER_ERROR');
|
||||
@@ -201,7 +209,11 @@ describe('Request Logger Middleware', () => {
|
||||
error: {code: 'RESOURCE_NOT_FOUND', message: 'Template not found'},
|
||||
});
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-request-id-123'},
|
||||
});
|
||||
|
||||
expect(loggedRequest?.statusCode).toBe(404);
|
||||
expect(loggedRequest?.errorCode).toBe('RESOURCE_NOT_FOUND');
|
||||
@@ -294,8 +306,11 @@ describe('Request Logger Middleware', () => {
|
||||
|
||||
databaseRequestLogger(req as Request, res as Response, next);
|
||||
await res.json!({success: true});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, `log-${path.replace(/\//g, '-')}`);
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: `log-${path.replace(/\//g, '-')}`},
|
||||
});
|
||||
|
||||
expect(loggedRequest).toBeDefined();
|
||||
expect(loggedRequest?.path).toBe(path);
|
||||
@@ -337,18 +352,17 @@ describe('Request Logger Middleware', () => {
|
||||
|
||||
await res.json!({success: true});
|
||||
|
||||
// Should create a record with generated UUID — poll for it
|
||||
const deadline = Date.now() + 2000;
|
||||
let allRequests: Awaited<ReturnType<typeof prisma.apiRequest.findMany>> = [];
|
||||
while (Date.now() < deadline) {
|
||||
allRequests = await prisma.apiRequest.findMany({
|
||||
where: {path: '/v1/send', method: 'POST'},
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Should create a record with generated UUID
|
||||
const allRequests = await prisma.apiRequest.findMany({
|
||||
where: {
|
||||
path: '/v1/send',
|
||||
method: 'POST',
|
||||
},
|
||||
orderBy: {createdAt: 'desc'},
|
||||
take: 1,
|
||||
});
|
||||
if (allRequests.length > 0) break;
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
}
|
||||
|
||||
expect(allRequests.length).toBeGreaterThan(0);
|
||||
expect(allRequests[0].id).toBeDefined();
|
||||
@@ -404,8 +418,11 @@ describe('Request Logger Middleware', () => {
|
||||
databaseRequestLogger(reqWithSize as Request, resWithId as Response, next);
|
||||
|
||||
await resWithId.json!({success: true});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-size-5000');
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-size-5000'},
|
||||
});
|
||||
|
||||
expect(loggedRequest?.requestSize).toBe(5000);
|
||||
});
|
||||
@@ -428,8 +445,11 @@ describe('Request Logger Middleware', () => {
|
||||
databaseRequestLogger(reqNoSize as Request, resWithId as Response, next);
|
||||
|
||||
await resWithId.json!({success: true});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-no-size');
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-no-size'},
|
||||
});
|
||||
|
||||
expect(loggedRequest?.requestSize).toBeNull();
|
||||
});
|
||||
@@ -454,8 +474,11 @@ describe('Request Logger Middleware', () => {
|
||||
};
|
||||
|
||||
await resLarge.json!(largeResponse);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const loggedRequest = await waitForLog(prisma, 'test-large-response');
|
||||
const loggedRequest = await prisma.apiRequest.findUnique({
|
||||
where: {id: 'test-large-response'},
|
||||
});
|
||||
|
||||
const expectedSize = JSON.stringify(largeResponse).length;
|
||||
expect(loggedRequest?.responseSize).toBe(expectedSize);
|
||||
|
||||
@@ -758,20 +758,8 @@ describe('WorkflowService', () => {
|
||||
});
|
||||
const contact = await factories.createContact({projectId});
|
||||
|
||||
// Insert a RUNNING execution directly to avoid racing with the background
|
||||
// step processor that startExecution kicks off (a trigger-only workflow can
|
||||
// transition to COMPLETED before the second call observes it as RUNNING).
|
||||
const triggerStep = await prisma.workflowStep.findFirst({
|
||||
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
|
||||
});
|
||||
await prisma.workflowExecution.create({
|
||||
data: {
|
||||
workflowId: workflow.id,
|
||||
contactId: contact.id,
|
||||
status: WorkflowExecutionStatus.RUNNING,
|
||||
currentStepId: triggerStep?.id,
|
||||
},
|
||||
});
|
||||
// Start first execution (still running)
|
||||
await WorkflowService.startExecution(projectId, workflow.id, contact.id);
|
||||
|
||||
// Second execution should fail (first still running)
|
||||
await expect(WorkflowService.startExecution(projectId, workflow.id, contact.id)).rejects.toThrow(
|
||||
|
||||
+58
-17
@@ -27,6 +27,39 @@ services:
|
||||
# Infrastructure Services
|
||||
# ============================================
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: plunk-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: plunk
|
||||
POSTGRES_USER: plunk
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme123}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U plunk" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- plunk
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: plunk-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- plunk
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: plunk-minio
|
||||
@@ -37,10 +70,10 @@ services:
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-plunkminiopass}
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
# ports:
|
||||
# # Expose Minio API (for S3 operations) and Console (for web UI)
|
||||
# - "${MINIO_API_PORT:-9000}:9000"
|
||||
# - "${MINIO_CONSOLE_PORT:-9001}:9001"
|
||||
ports:
|
||||
# Expose Minio API (for S3 operations) and Console (for web UI)
|
||||
- "${MINIO_API_PORT:-9000}:9000"
|
||||
- "${MINIO_CONSOLE_PORT:-9001}:9001"
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ]
|
||||
interval: 30s
|
||||
@@ -55,13 +88,13 @@ services:
|
||||
restart: unless-stopped
|
||||
command: serve
|
||||
environment:
|
||||
- TZ=CST
|
||||
- TZ=UTC
|
||||
volumes:
|
||||
- ntfy_cache:/var/cache/ntfy
|
||||
- ntfy_etc:/etc/ntfy
|
||||
# ports:
|
||||
# # Expose ntfy web UI and API
|
||||
# - "${NTFY_PORT:-8080}:80"
|
||||
ports:
|
||||
# Expose ntfy web UI and API
|
||||
- "${NTFY_PORT:-8080}:80"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1" ]
|
||||
interval: 30s
|
||||
@@ -85,11 +118,11 @@ services:
|
||||
NODE_ENV: production
|
||||
|
||||
# Database
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
DIRECT_DATABASE_URL: ${DIRECT_DATABASE_URL}
|
||||
DATABASE_URL: postgresql://plunk:${DB_PASSWORD:-changeme123}@postgres:5432/plunk
|
||||
DIRECT_DATABASE_URL: postgresql://plunk:${DB_PASSWORD:-changeme123}@postgres:5432/plunk
|
||||
|
||||
# Redis
|
||||
REDIS_URL: ${REDIS_URL}
|
||||
REDIS_URL: redis://redis:6379
|
||||
|
||||
# Security
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
@@ -181,15 +214,19 @@ services:
|
||||
|
||||
ports:
|
||||
# SMTP ports (for email relay)
|
||||
# - "${PORT_SECURE:-465}:465" # SMTPS (implicit TLS)
|
||||
# - "${PORT_SUBMISSION:-587}:587" # SMTP Submission (STARTTLS)
|
||||
- "${PORT_SECURE:-465}:465" # SMTPS (implicit TLS)
|
||||
- "${PORT_SUBMISSION:-587}:587" # SMTP Submission (STARTTLS)
|
||||
# Optional: Expose individual service ports for debugging
|
||||
- "6000:8080" # API
|
||||
- "6001:3000" # Web
|
||||
# - "6002:4000" # Landing
|
||||
# - "6003:1000" # Wiki
|
||||
# - "8080:8080" # API
|
||||
# - "3000:3000" # Web
|
||||
# - "4000:4000" # Landing
|
||||
# - "1000:1000" # Wiki
|
||||
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
ntfy:
|
||||
@@ -207,6 +244,10 @@ services:
|
||||
- plunk
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
minio_data:
|
||||
driver: local
|
||||
plunk_data:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plunk",
|
||||
"version": "0.11.0",
|
||||
"version": "0.12.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo build",
|
||||
|
||||
Reference in New Issue
Block a user