Initial import of verae-middleware from zapier monorepo
This commit is contained in:
commit
87cd4896c2
67 changed files with 6630 additions and 0 deletions
57
src/routes/authRoutes.js
Normal file
57
src/routes/authRoutes.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* @fileoverview Auth routes under /zapier/v1/auth
|
||||
* @module routes/authRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import { loginWithCredentials, loginWithApiKey, validateSession } from '../services/authService.js';
|
||||
import { getUsageSummary } from '../store/usage.js';
|
||||
import { getTenant } from '../store/tenants.js';
|
||||
import { extractBearerToken } from '../lib/tokens.js';
|
||||
|
||||
export const authRoutes = Router();
|
||||
|
||||
authRoutes.post(
|
||||
'/login',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { username, password, api_key: apiKey } = req.body ?? {};
|
||||
|
||||
if (apiKey) {
|
||||
const session = await loginWithApiKey(apiKey);
|
||||
return res.json(session);
|
||||
}
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('username and password are required', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
const session = await loginWithCredentials({ username, password });
|
||||
res.json(session);
|
||||
}),
|
||||
);
|
||||
|
||||
authRoutes.get(
|
||||
'/me',
|
||||
asyncHandler(async (req, res) => {
|
||||
const rawToken =
|
||||
extractBearerToken(req.headers.authorization) ?? req.headers['x-api-key'] ?? null;
|
||||
|
||||
if (!rawToken) {
|
||||
throw new AppError('Missing authorization token', { status: 401, code: 'UNAUTHORIZED' });
|
||||
}
|
||||
|
||||
const validation = await validateSession(rawToken);
|
||||
const tenant = getTenant(validation.tenantId);
|
||||
const usage = getUsageSummary(validation.tenantId);
|
||||
|
||||
res.json({
|
||||
...validation,
|
||||
plan: tenant?.plan ?? validation.plan ?? 'free',
|
||||
usage,
|
||||
});
|
||||
}),
|
||||
);
|
||||
23
src/routes/hashRoutes.js
Normal file
23
src/routes/hashRoutes.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* @module routes/hashRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import { lookupHash } from '../services/timestampService.js';
|
||||
|
||||
export const hashRoutes = Router();
|
||||
|
||||
hashRoutes.get(
|
||||
'/:sha256',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { sha256 } = req.params;
|
||||
if (!sha256 || !/^[a-fA-F0-9]{64}$/.test(sha256)) {
|
||||
throw new AppError('sha256 must be 64 hex chars', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const includeAttached = req.query.includeAttached === 'true' || req.query.includeAttached === '1';
|
||||
const includeTree = req.query.includeTree === 'true' || req.query.includeTree === '1';
|
||||
const result = await lookupHash(req.auth, sha256, { includeAttached, includeTree });
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
40
src/routes/index.js
Normal file
40
src/routes/index.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @fileoverview Mount all /zapier routes.
|
||||
* @module routes/index
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { authRoutes } from './authRoutes.js';
|
||||
import { timestampRoutes } from './timestampRoutes.js';
|
||||
import { verifyRoutes } from './verifyRoutes.js';
|
||||
import { statusRoutes } from './statusRoutes.js';
|
||||
import { webhookRoutes } from './webhookRoutes.js';
|
||||
import { publicTenantRoutes, adminTenantRoutes } from './tenantRoutes.js';
|
||||
import { hashRoutes } from './hashRoutes.js';
|
||||
import { receiptRoutes } from './receiptRoutes.js';
|
||||
import { objectRoutes, publicShareRoutes } from './objectRoutes.js';
|
||||
import { authenticate } from '../middleware/authenticate.js';
|
||||
import { rateLimit } from '../middleware/rateLimit.js';
|
||||
|
||||
export const apiRoutes = Router();
|
||||
|
||||
// Public
|
||||
apiRoutes.use('/v1/auth', authRoutes);
|
||||
apiRoutes.use('/v1', publicTenantRoutes);
|
||||
apiRoutes.use('/v1/admin', adminTenantRoutes);
|
||||
apiRoutes.use('/v1/shares', publicShareRoutes);
|
||||
|
||||
// Protected
|
||||
const protectedRoutes = Router();
|
||||
protectedRoutes.use(authenticate);
|
||||
protectedRoutes.use(rateLimit);
|
||||
|
||||
protectedRoutes.use('/timestamp', timestampRoutes);
|
||||
protectedRoutes.use('/verify', verifyRoutes);
|
||||
protectedRoutes.use('/status', statusRoutes);
|
||||
protectedRoutes.use('/webhooks', webhookRoutes);
|
||||
protectedRoutes.use('/hashes', hashRoutes);
|
||||
protectedRoutes.use('/receipts', receiptRoutes);
|
||||
protectedRoutes.use('/objects', objectRoutes);
|
||||
|
||||
apiRoutes.use('/v1', protectedRoutes);
|
||||
125
src/routes/objectRoutes.js
Normal file
125
src/routes/objectRoutes.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* Encrypted LTS mock + share grants.
|
||||
* @module routes/objectRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import {
|
||||
putBlob,
|
||||
readBlobPlaintext,
|
||||
createShare,
|
||||
redeemShare,
|
||||
putTree,
|
||||
createTreeShare,
|
||||
} from '../store/blobs.js';
|
||||
import { createTimestamp } from '../services/timestampService.js';
|
||||
|
||||
export const objectRoutes = Router();
|
||||
|
||||
objectRoutes.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { filename, contentBase64, sha256, publicMetadata, privateMetadata, data } = req.body ?? {};
|
||||
if (!contentBase64 && !data) {
|
||||
throw new AppError('contentBase64 or data is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const bytes = Buffer.from(String(contentBase64 ?? data), contentBase64 ? 'base64' : 'utf8');
|
||||
const stamp = await createTimestamp(req.auth, {
|
||||
data: data ?? bytes.toString('utf8').slice(0, 1024),
|
||||
sha256,
|
||||
publicMetadata,
|
||||
privateMetadata,
|
||||
});
|
||||
const stored = putBlob({
|
||||
tenantId: req.auth.tenantId,
|
||||
sha256: stamp.sha256 ?? sha256 ?? null,
|
||||
filename,
|
||||
bytes,
|
||||
publicMetadata,
|
||||
privateMetadata,
|
||||
});
|
||||
res.status(201).json({ ...stored, jobId: stamp.jobId, existing: stamp.existing });
|
||||
}),
|
||||
);
|
||||
|
||||
objectRoutes.get(
|
||||
'/:id',
|
||||
asyncHandler(async (req, res) => {
|
||||
try {
|
||||
const rec = readBlobPlaintext(req.params.id, req.auth.tenantId);
|
||||
if (!rec) throw new AppError('Object not found', { status: 404, code: 'NOT_FOUND' });
|
||||
res.json({
|
||||
id: rec.id,
|
||||
filename: rec.filename,
|
||||
size: rec.size,
|
||||
sha256: rec.sha256,
|
||||
publicMetadata: rec.publicMetadata,
|
||||
privateMetadata: rec.privateMetadata,
|
||||
contentBase64: rec.bytes.toString('base64'),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'FORBIDDEN') {
|
||||
throw new AppError('Forbidden', { status: 403, code: 'FORBIDDEN' });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
objectRoutes.post(
|
||||
'/trees',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { prefix, files } = req.body ?? {};
|
||||
if (!Array.isArray(files) || files.length === 0) {
|
||||
throw new AppError('files array is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const tree = putTree({
|
||||
tenantId: req.auth.tenantId,
|
||||
prefix: prefix || '/',
|
||||
files: files.map((f) => ({
|
||||
path: String(f.path || f.filename || 'file.bin'),
|
||||
bytes: Buffer.from(String(f.contentBase64 || f.data || ''), f.contentBase64 ? 'base64' : 'utf8'),
|
||||
sha256: f.sha256,
|
||||
})),
|
||||
});
|
||||
res.status(201).json(tree);
|
||||
}),
|
||||
);
|
||||
|
||||
objectRoutes.post(
|
||||
'/trees/:id/share',
|
||||
asyncHandler(async (req, res) => {
|
||||
const share = createTreeShare({
|
||||
treeId: req.params.id,
|
||||
fromTenantId: req.auth.tenantId,
|
||||
label: req.body?.label,
|
||||
});
|
||||
if (!share) throw new AppError('Tree not found', { status: 404, code: 'NOT_FOUND' });
|
||||
res.status(201).json(share);
|
||||
}),
|
||||
);
|
||||
|
||||
objectRoutes.post(
|
||||
'/:id/share',
|
||||
asyncHandler(async (req, res) => {
|
||||
const share = createShare({
|
||||
blobId: req.params.id,
|
||||
fromTenantId: req.auth.tenantId,
|
||||
label: req.body?.label,
|
||||
});
|
||||
if (!share) throw new AppError('Object not found', { status: 404, code: 'NOT_FOUND' });
|
||||
res.status(201).json(share);
|
||||
}),
|
||||
);
|
||||
|
||||
export const publicShareRoutes = Router();
|
||||
|
||||
publicShareRoutes.get(
|
||||
'/:token',
|
||||
asyncHandler(async (req, res) => {
|
||||
const payload = redeemShare(req.params.token);
|
||||
if (!payload) throw new AppError('Share not found', { status: 404, code: 'NOT_FOUND' });
|
||||
res.json(payload);
|
||||
}),
|
||||
);
|
||||
45
src/routes/receiptRoutes.js
Normal file
45
src/routes/receiptRoutes.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Mock certified retrieval receipt (JSON). Not a payment invoice.
|
||||
* @module routes/receiptRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import { getJobStatus } from '../services/timestampService.js';
|
||||
import { buildReceiptPdf } from '../lib/receiptPdf.js';
|
||||
|
||||
export const receiptRoutes = Router();
|
||||
|
||||
receiptRoutes.get(
|
||||
'/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { jobId } = req.params;
|
||||
const format = String(req.query.format || 'json').toLowerCase();
|
||||
const status = await getJobStatus(req.auth, jobId);
|
||||
if (status.status !== 'completed') {
|
||||
throw new AppError('Receipt only for completed jobs', { status: 409, code: 'NOT_COMPLETE' });
|
||||
}
|
||||
const retrievedAt = new Date().toISOString();
|
||||
const receipt = {
|
||||
type: 'verae.retrieval-receipt',
|
||||
format: 'json',
|
||||
jobId,
|
||||
sha256: status.sha256 ?? status.metadata?.sha256,
|
||||
timestamp: status.completedAt ?? status.metadata?.timestamp,
|
||||
certificate: status.result,
|
||||
extraSeal: {
|
||||
event: 'document.retrieved',
|
||||
retrievedAt,
|
||||
tenantId: req.auth.tenantId,
|
||||
},
|
||||
};
|
||||
if (format === 'pdf') {
|
||||
const pdf = buildReceiptPdf({ ...receipt, format: 'pdf' });
|
||||
res.setHeader('content-type', 'application/pdf');
|
||||
res.setHeader('content-disposition', `attachment; filename="verae-receipt-${jobId}.pdf"`);
|
||||
res.send(pdf);
|
||||
return;
|
||||
}
|
||||
res.json(receipt);
|
||||
}),
|
||||
);
|
||||
51
src/routes/statusRoutes.js
Normal file
51
src/routes/statusRoutes.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* @module routes/statusRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import {
|
||||
getJobStatus,
|
||||
getBatchJobStatus,
|
||||
getJobVerification,
|
||||
} from '../services/timestampService.js';
|
||||
|
||||
export const statusRoutes = Router();
|
||||
|
||||
// Static/more-specific routes first
|
||||
statusRoutes.post(
|
||||
'/batch',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { jobIds } = req.body ?? {};
|
||||
if (!Array.isArray(jobIds) || jobIds.length === 0) {
|
||||
throw new AppError('jobIds array is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const result = await getBatchJobStatus(req.auth, { jobIds });
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
statusRoutes.get(
|
||||
'/:jobId/verification',
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await getJobVerification(req.auth, req.params.jobId);
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
statusRoutes.get(
|
||||
'/:jobId',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { jobId } = req.params;
|
||||
if (!jobId) {
|
||||
throw new AppError('jobId is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const result = await getJobStatus(req.auth, jobId);
|
||||
if (req.query.scope === 'public' && result.metadata) {
|
||||
const { publicMetadata, ...restMeta } = result.metadata;
|
||||
result.metadata = { ...restMeta, publicMetadata };
|
||||
delete result.privateMetadata;
|
||||
}
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
46
src/routes/tenantRoutes.js
Normal file
46
src/routes/tenantRoutes.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* @module routes/tenantRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import {
|
||||
selfServeSignup,
|
||||
provisionTenant,
|
||||
listProvisionedTenants,
|
||||
} from '../services/tenantService.js';
|
||||
import { config } from '../config.js';
|
||||
|
||||
export const publicTenantRoutes = Router();
|
||||
export const adminTenantRoutes = Router();
|
||||
|
||||
publicTenantRoutes.post(
|
||||
'/signup',
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await selfServeSignup(req.body ?? {});
|
||||
res.status(201).json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
adminTenantRoutes.use((req, _res, next) => {
|
||||
const secret = req.headers['x-admin-secret'];
|
||||
if (secret !== config.adminSecret) {
|
||||
return next(new AppError('Invalid admin secret', { status: 403, code: 'FORBIDDEN' }));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
adminTenantRoutes.post(
|
||||
'/tenants',
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await provisionTenant(req.body ?? {});
|
||||
res.status(201).json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
adminTenantRoutes.get(
|
||||
'/tenants',
|
||||
asyncHandler(async (_req, res) => {
|
||||
res.json({ tenants: listProvisionedTenants() });
|
||||
}),
|
||||
);
|
||||
61
src/routes/timestampRoutes.js
Normal file
61
src/routes/timestampRoutes.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @module routes/timestampRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import {
|
||||
createTimestamp,
|
||||
createTimestampAndWait,
|
||||
createBatchTimestamp,
|
||||
} from '../services/timestampService.js';
|
||||
|
||||
export const timestampRoutes = Router();
|
||||
|
||||
timestampRoutes.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { data, hashAlg, sha256, publicMetadata, privateMetadata } = req.body ?? {};
|
||||
if (!data && !sha256) {
|
||||
throw new AppError('data or sha256 is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const result = await createTimestamp(req.auth, {
|
||||
data,
|
||||
hashAlg,
|
||||
sha256,
|
||||
publicMetadata,
|
||||
privateMetadata,
|
||||
});
|
||||
res.status(202).json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
timestampRoutes.post(
|
||||
'/wait',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { data, hashAlg, sha256, publicMetadata, privateMetadata } = req.body ?? {};
|
||||
if (!data && !sha256) {
|
||||
throw new AppError('data or sha256 is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const result = await createTimestampAndWait(req.auth, {
|
||||
data,
|
||||
hashAlg,
|
||||
sha256,
|
||||
publicMetadata,
|
||||
privateMetadata,
|
||||
});
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
timestampRoutes.post(
|
||||
'/batch',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { items } = req.body ?? {};
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
throw new AppError('items array is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const result = await createBatchTimestamp(req.auth, { items });
|
||||
res.status(202).json(result);
|
||||
}),
|
||||
);
|
||||
36
src/routes/verifyRoutes.js
Normal file
36
src/routes/verifyRoutes.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* @module routes/verifyRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import { verifyTimestamp, verifyBatch } from '../services/verifyService.js';
|
||||
|
||||
export const verifyRoutes = Router();
|
||||
|
||||
verifyRoutes.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { certificate } = req.body ?? {};
|
||||
if (!certificate) {
|
||||
throw new AppError('certificate is required', { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
const result = await verifyTimestamp(req.auth, { certificate });
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
|
||||
verifyRoutes.post(
|
||||
'/batch',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { certificates } = req.body ?? {};
|
||||
if (!Array.isArray(certificates) || certificates.length === 0) {
|
||||
throw new AppError('certificates array is required', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
const result = await verifyBatch(req.auth, { certificates });
|
||||
res.json(result);
|
||||
}),
|
||||
);
|
||||
44
src/routes/webhookRoutes.js
Normal file
44
src/routes/webhookRoutes.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* @module routes/webhookRoutes
|
||||
*/
|
||||
|
||||
import { Router } from 'express';
|
||||
import { asyncHandler, AppError } from '../errors.js';
|
||||
import { subscribe, unsubscribe } from '../services/webhookService.js';
|
||||
import { listWebhooksForTenant } from '../store/webhooks.js';
|
||||
|
||||
export const webhookRoutes = Router();
|
||||
|
||||
webhookRoutes.post(
|
||||
'/subscribe',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { targetUrl, event = 'timestamp.completed' } = req.body ?? {};
|
||||
try {
|
||||
const hook = subscribe(req.auth, { targetUrl, event });
|
||||
res.status(201).json(hook);
|
||||
} catch (err) {
|
||||
throw new AppError(err.message, { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
webhookRoutes.delete(
|
||||
'/unsubscribe',
|
||||
asyncHandler(async (req, res) => {
|
||||
const { hookId, targetUrl } = req.body ?? req.query ?? {};
|
||||
try {
|
||||
const result = unsubscribe(req.auth, { hookId, targetUrl });
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
throw new AppError(err.message, { status: 404, code: 'NOT_FOUND' });
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
webhookRoutes.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const hooks = listWebhooksForTenant(req.auth.tenantId);
|
||||
res.json({ webhooks: hooks });
|
||||
}),
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue