mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
feat: phase 6 production hardening + DB-to-DB transfer
Security:
- Graceful shutdown: http.Server + signal.NotifyContext, 10s drain,
retention sweeper cancelled before pool close
- Scoped API keys (migration 00006): ingest/read/write/admin enforced in
auth middleware; scope picker + badges in Settings UI; scopeless legacy
keys act as admin. Read-only keys now get 403 on ingest (verified live)
- nginx: baseline security headers on all responses, CSP scoped to SPA;
fixed add_header inheritance shadowing
- better-auth rate limit enabled unconditionally (was gated on NODE_ENV,
which the shipped .env sets to development)
- Mailpit moved to docker-compose.dev.yml; prod compose cannot expose it
- setup.sh generates PRIMORA_ENCRYPTION_KEY
Reliability/ops:
- Retention: per-project retention_{events,audit,webhook}_days
(migration 00007, 0=off) + hourly RetentionSweeper; edit UI in Projects
- GET /api/v1/metrics Prometheus exposition
- compose: healthchecks + restart policies everywhere, dragonfly pinned
to v2.0.0, depends_on service_healthy
- backup.sh/restore.sh (pg_dump -Fc + storage volume); restore verified
against scratch postgres+volume
- .dockerignore: exclude target dirs (build context was 4.7GB)
Correctness/testing:
- DBX transfer endpoint: POST /db-connections/:id/transfer — SQL or Redis
target, 1000-row bound, server-side creds (verified live pg→dragonfly)
- Pagination audit: ListComponents capped at 500 (auto-registers)
- CI: goose up/reset/up job + compose e2e-smoke job (smoke-e2e.sh
verified locally: signup, bootstrap, scoped keys, ingest, metrics)
- k6 load sanity script: 6650 reqs, ~217/s, 0 real failures, p95 221ms
(429s = configured rate limiter working)
Docs: DEPLOYMENT_GUIDE.md, PRODUCTION_READINESS.md; verify script
repaired (stale refs + set -e arithmetic abort). ROADMAP: phase 2 +
phase 6 marked complete.
Authored By: TDvorak <[email protected]>
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1b34dd43bb
commit
55edbf8b56
64 files changed
+2058
-165
No files matched your search
@@ -55,6 +55,8 @@ export type { DBRedisRequest } from './models/DBRedisRequest';
|
||||
export type { DBRedisResponse } from './models/DBRedisResponse';
|
||||
export type { DBSchemaContextResponse } from './models/DBSchemaContextResponse';
|
||||
export type { DBTableListResponse } from './models/DBTableListResponse';
|
||||
export type { DBTransferRequest } from './models/DBTransferRequest';
|
||||
export type { DBTransferResponse } from './models/DBTransferResponse';
|
||||
export type { DBXStatus } from './models/DBXStatus';
|
||||
export type { DeployMarker } from './models/DeployMarker';
|
||||
export type { Document } from './models/Document';
|
||||
@@ -168,6 +170,8 @@ export { $DBRedisRequest } from './schemas/$DBRedisRequest';
|
||||
export { $DBRedisResponse } from './schemas/$DBRedisResponse';
|
||||
export { $DBSchemaContextResponse } from './schemas/$DBSchemaContextResponse';
|
||||
export { $DBTableListResponse } from './schemas/$DBTableListResponse';
|
||||
export { $DBTransferRequest } from './schemas/$DBTransferRequest';
|
||||
export { $DBTransferResponse } from './schemas/$DBTransferResponse';
|
||||
export { $DBXStatus } from './schemas/$DBXStatus';
|
||||
export { $DeployMarker } from './schemas/$DeployMarker';
|
||||
export { $Document } from './schemas/$Document';
|
||||
|
||||
@@ -7,6 +7,7 @@ export type ApiKey = {
|
||||
project_id: string;
|
||||
name: string;
|
||||
prefix: string;
|
||||
scopes?: Array<'ingest' | 'read' | 'write' | 'admin'>;
|
||||
last_used_at?: string | null;
|
||||
revoked_at?: string | null;
|
||||
};
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
/* eslint-disable */
|
||||
export type CreateApiKeyRequest = {
|
||||
name: string;
|
||||
/**
|
||||
* Access scopes for the key. admin unlocks everything; write unlocks read + ingest + non-admin mutations; read and ingest unlock only themselves. Omitted means ["admin"] (full access, legacy behaviour).
|
||||
*/
|
||||
scopes?: Array<'ingest' | 'read' | 'write' | 'admin'>;
|
||||
};
|
||||
|
||||
@@ -7,5 +7,6 @@ export type CreatedApiKey = {
|
||||
prefix: string;
|
||||
secret: string;
|
||||
name: string;
|
||||
scopes?: Array<'ingest' | 'read' | 'write' | 'admin'>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DBTransferRequest = {
|
||||
target_connection_id: string;
|
||||
/**
|
||||
* SELECT run against the source connection.
|
||||
*/
|
||||
query: string;
|
||||
database?: string;
|
||||
/**
|
||||
* Destination table for SQL targets (schema-qualified allowed).
|
||||
*/
|
||||
target_table?: string;
|
||||
/**
|
||||
* Redis targets: SET key template with {column} placeholders, e.g. "user:{id}".
|
||||
*/
|
||||
key_pattern?: string;
|
||||
/**
|
||||
* Redis targets: column whose cell becomes the value. Omitted stores the whole row as JSON.
|
||||
*/
|
||||
value_column?: string;
|
||||
/**
|
||||
* Optional EX TTL applied to each Redis SET.
|
||||
*/
|
||||
ttl_seconds?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type DBTransferResponse = {
|
||||
transferred: number;
|
||||
truncated: boolean;
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
@@ -9,5 +9,8 @@ export type Project = {
|
||||
name: string;
|
||||
description?: string | null;
|
||||
membership_role?: string | null;
|
||||
retention_events_days?: number;
|
||||
retention_audit_days?: number;
|
||||
retention_webhook_days?: number;
|
||||
};
|
||||
|
||||
@@ -6,5 +6,17 @@ export type UpdateProjectRequest = {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string | null;
|
||||
/**
|
||||
* Telemetry event retention; 0 disables the sweep.
|
||||
*/
|
||||
retention_events_days?: number;
|
||||
/**
|
||||
* Audit log retention; 0 disables the sweep.
|
||||
*/
|
||||
retention_audit_days?: number;
|
||||
/**
|
||||
* Webhook delivery retention; 0 disables the sweep.
|
||||
*/
|
||||
retention_webhook_days?: number;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,12 @@ export const $ApiKey = {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
scopes: {
|
||||
type: 'array',
|
||||
contains: {
|
||||
type: 'Enum',
|
||||
},
|
||||
},
|
||||
last_used_at: {
|
||||
type: 'string',
|
||||
isNullable: true,
|
||||
|
||||
@@ -8,5 +8,11 @@ export const $CreateApiKeyRequest = {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
scopes: {
|
||||
type: 'array',
|
||||
contains: {
|
||||
type: 'Enum',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -21,5 +21,11 @@ export const $CreatedApiKey = {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
scopes: {
|
||||
type: 'array',
|
||||
contains: {
|
||||
type: 'Enum',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,42 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $DBTransferRequest = {
|
||||
properties: {
|
||||
target_connection_id: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
format: 'uuid',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: `SELECT run against the source connection.`,
|
||||
isRequired: true,
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
},
|
||||
target_table: {
|
||||
type: 'string',
|
||||
description: `Destination table for SQL targets (schema-qualified allowed).`,
|
||||
},
|
||||
key_pattern: {
|
||||
type: 'string',
|
||||
description: `Redis targets: SET key template with {column} placeholders, e.g. "user:{id}".`,
|
||||
},
|
||||
value_column: {
|
||||
type: 'string',
|
||||
description: `Redis targets: column whose cell becomes the value. Omitted stores the whole row as JSON.`,
|
||||
},
|
||||
ttl_seconds: {
|
||||
type: 'number',
|
||||
description: `Optional EX TTL applied to each Redis SET.`,
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
maximum: 1000,
|
||||
minimum: 1,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,24 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $DBTransferResponse = {
|
||||
properties: {
|
||||
transferred: {
|
||||
type: 'number',
|
||||
isRequired: true,
|
||||
},
|
||||
truncated: {
|
||||
type: 'boolean',
|
||||
isRequired: true,
|
||||
},
|
||||
source: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
target: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -30,5 +30,14 @@ export const $Project = {
|
||||
type: 'string',
|
||||
isNullable: true,
|
||||
},
|
||||
retention_events_days: {
|
||||
type: 'number',
|
||||
},
|
||||
retention_audit_days: {
|
||||
type: 'number',
|
||||
},
|
||||
retention_webhook_days: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -16,5 +16,20 @@ export const $UpdateProjectRequest = {
|
||||
type: 'string',
|
||||
isNullable: true,
|
||||
},
|
||||
retention_events_days: {
|
||||
type: 'number',
|
||||
description: `Telemetry event retention; 0 disables the sweep.`,
|
||||
maximum: 3650,
|
||||
},
|
||||
retention_audit_days: {
|
||||
type: 'number',
|
||||
description: `Audit log retention; 0 disables the sweep.`,
|
||||
maximum: 3650,
|
||||
},
|
||||
retention_webhook_days: {
|
||||
type: 'number',
|
||||
description: `Webhook delivery retention; 0 disables the sweep.`,
|
||||
maximum: 3650,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -15,6 +15,8 @@ import type { DBRedisRequest } from '../models/DBRedisRequest';
|
||||
import type { DBRedisResponse } from '../models/DBRedisResponse';
|
||||
import type { DBSchemaContextResponse } from '../models/DBSchemaContextResponse';
|
||||
import type { DBTableListResponse } from '../models/DBTableListResponse';
|
||||
import type { DBTransferRequest } from '../models/DBTransferRequest';
|
||||
import type { DBTransferResponse } from '../models/DBTransferResponse';
|
||||
import type { DBXStatus } from '../models/DBXStatus';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
@@ -333,4 +335,33 @@ export class DatabasesService {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Move query results from this connection to another saved connection
|
||||
* @returns DBTransferResponse Transfer result
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static transferDbRows({
|
||||
projectId,
|
||||
connectionId,
|
||||
requestBody,
|
||||
}: {
|
||||
projectId: string,
|
||||
connectionId: string,
|
||||
requestBody: DBTransferRequest,
|
||||
}): CancelablePromise<DBTransferResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/projects/{projectID}/db-connections/{connectionID}/transfer',
|
||||
path: {
|
||||
'projectID': projectId,
|
||||
'connectionID': connectionId,
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
404: `Error response`,
|
||||
502: `Error response`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -28,4 +28,15 @@ export class HealthService {
|
||||
url: '/health/readiness',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Prometheus text exposition of platform HTTP metrics
|
||||
* @returns string Prometheus metrics
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getMetrics(): CancelablePromise<string> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/metrics',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user