Initial import of verae-middleware from zapier monorepo
This commit is contained in:
commit
90713dcc78
70 changed files with 6773 additions and 0 deletions
153
src/services/authService.js
Normal file
153
src/services/authService.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* @fileoverview Auth bridge: API keys / sessions → Verae JWT context.
|
||||
* @module services/authService
|
||||
*/
|
||||
|
||||
import { veraeClient } from '../clients/veraeClient.js';
|
||||
import { getTenantByApiKey, getTenant, upsertTenant } from '../store/tenants.js';
|
||||
import { issueSessionToken, parseSessionToken, isApiKey } from '../lib/tokens.js';
|
||||
import { AppError } from '../errors.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('auth');
|
||||
|
||||
/**
|
||||
* Login with Verae username/password and issue middleware session token.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {string} params.username
|
||||
* @param {string} params.password
|
||||
* @param {object} [params.tenant]
|
||||
* @returns {Promise<{ accessToken: string, expiresAt: string, tenant: object, user: object }>}
|
||||
*/
|
||||
export async function loginWithCredentials({ username, password, tenant }) {
|
||||
log.debug('loginWithCredentials', { username, tenantId: tenant?.id });
|
||||
|
||||
const verae = await veraeClient.login({ username, password });
|
||||
|
||||
if (tenant?.veraeUsername && tenant.veraeUsername !== username) {
|
||||
throw new AppError('Credentials do not match tenant account', {
|
||||
status: 403,
|
||||
code: 'FORBIDDEN',
|
||||
});
|
||||
}
|
||||
|
||||
const tenantId = tenant?.id ?? `user:${verae.user.username}`;
|
||||
const veraeUserId = verae.user?.id;
|
||||
if (tenant && veraeUserId && tenant.veraeUserId !== veraeUserId) {
|
||||
upsertTenant({ ...tenant, veraeUserId });
|
||||
}
|
||||
const accessToken = issueSessionToken({
|
||||
tenantId,
|
||||
veraeToken: verae.token,
|
||||
expiresAt: verae.expiresAt,
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
expiresAt: verae.expiresAt,
|
||||
tenant: tenant
|
||||
? { id: tenant.id, name: tenant.name, plan: tenant.plan, veraeUserId: veraeUserId || tenant.veraeUserId }
|
||||
: { id: tenantId, name: verae.user.username, plan: 'free', veraeUserId },
|
||||
user: verae.user,
|
||||
veraeUserId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve API key to tenant and login with stored Verae credentials.
|
||||
* @param {string} apiKey
|
||||
* @returns {Promise<Awaited<ReturnType<typeof loginWithCredentials>>>}
|
||||
*/
|
||||
export async function loginWithApiKey(apiKey) {
|
||||
const tenant = getTenantByApiKey(apiKey);
|
||||
if (!tenant) {
|
||||
log.debug('invalid api key');
|
||||
throw new AppError('Invalid API key', { status: 401, code: 'UNAUTHORIZED' });
|
||||
}
|
||||
|
||||
if (!tenant.veraeUsername || !tenant.veraePassword) {
|
||||
throw new AppError('Tenant is missing Verae credentials', {
|
||||
status: 500,
|
||||
code: 'TENANT_MISCONFIGURED',
|
||||
});
|
||||
}
|
||||
|
||||
log.debug('loginWithApiKey', { tenantId: tenant.id, plan: tenant.plan });
|
||||
return loginWithCredentials({
|
||||
username: tenant.veraeUsername,
|
||||
password: tenant.veraePassword,
|
||||
tenant,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve bearer credential into request auth context.
|
||||
*
|
||||
* @param {string|null} rawToken
|
||||
* @returns {Promise<{ tenantId: string, tenant: object, veraeToken: string, authMethod: string }>}
|
||||
*/
|
||||
export async function resolveAuthContext(rawToken) {
|
||||
if (!rawToken) {
|
||||
throw new AppError('Missing authorization token', { status: 401, code: 'UNAUTHORIZED' });
|
||||
}
|
||||
|
||||
if (isApiKey(rawToken)) {
|
||||
const session = await loginWithApiKey(rawToken);
|
||||
const parsed = parseSessionToken(session.accessToken);
|
||||
const tenant = getTenant(session.tenant.id) ?? session.tenant;
|
||||
return {
|
||||
tenantId: session.tenant.id,
|
||||
tenant: {
|
||||
id: session.tenant.id,
|
||||
name: session.tenant.name,
|
||||
plan: session.tenant.plan,
|
||||
veraeUserId: session.veraeUserId || tenant?.veraeUserId,
|
||||
},
|
||||
veraeToken: parsed.veraeToken,
|
||||
veraeUserId: session.veraeUserId || tenant?.veraeUserId,
|
||||
authMethod: 'api_key',
|
||||
fullTenant: tenant,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseSessionToken(rawToken);
|
||||
if (!parsed?.veraeToken) {
|
||||
throw new AppError('Invalid or expired session token', { status: 401, code: 'UNAUTHORIZED' });
|
||||
}
|
||||
|
||||
if (parsed.expiresAt && Date.parse(parsed.expiresAt) < Date.now()) {
|
||||
throw new AppError('Session token expired', { status: 401, code: 'TOKEN_EXPIRED' });
|
||||
}
|
||||
|
||||
const tenant = getTenant(parsed.tenantId);
|
||||
log.debug('session auth', { tenantId: parsed.tenantId });
|
||||
return {
|
||||
tenantId: parsed.tenantId,
|
||||
tenant: tenant
|
||||
? { id: tenant.id, name: tenant.name, plan: tenant.plan, veraeUserId: tenant.veraeUserId }
|
||||
: { id: parsed.tenantId, plan: 'free' },
|
||||
veraeToken: parsed.veraeToken,
|
||||
veraeUserId: tenant?.veraeUserId,
|
||||
authMethod: 'session',
|
||||
fullTenant: tenant,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate auth and optionally ping Verae /auth/validate.
|
||||
* @param {string} rawToken
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
export async function validateSession(rawToken) {
|
||||
const context = await resolveAuthContext(rawToken);
|
||||
const validation = await veraeClient.validate(context.veraeToken);
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
tenantId: context.tenantId,
|
||||
plan: context.tenant.plan,
|
||||
authMethod: context.authMethod,
|
||||
user: validation,
|
||||
};
|
||||
}
|
||||
160
src/services/entitlementService.js
Normal file
160
src/services/entitlementService.js
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* @fileoverview Plan quotas and usage recording.
|
||||
* @module services/entitlementService
|
||||
*/
|
||||
|
||||
import { config } from '../config.js';
|
||||
import { AppError } from '../errors.js';
|
||||
import { getTenant, resolveLimits } from '../store/tenants.js';
|
||||
import { getUsage, incrementUsage } from '../store/usage.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('billing');
|
||||
|
||||
function isUnlimited(value) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
function quotaExceeded({ limit, used, allowOverage }) {
|
||||
if (isUnlimited(limit)) return false;
|
||||
if (used < limit) return false;
|
||||
return !allowOverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure tenant may perform an action under plan limits.
|
||||
*
|
||||
* @param {string} tenantId
|
||||
* @param {'timestamp'|'verify'|'batch_timestamp'} action
|
||||
* @param {{ amount?: number }} [options]
|
||||
* @returns {{ tenant: object, limits: object, usage: object }}
|
||||
*/
|
||||
export function checkEntitlement(tenantId, action, { amount = 1 } = {}) {
|
||||
const tenant = getTenant(tenantId);
|
||||
if (!tenant) {
|
||||
throw new AppError('Unknown tenant', { status: 401, code: 'UNAUTHORIZED' });
|
||||
}
|
||||
|
||||
const limits = resolveLimits(tenant);
|
||||
const usage = getUsage(tenantId);
|
||||
|
||||
log.debug('checkEntitlement', {
|
||||
tenantId,
|
||||
action,
|
||||
plan: tenant.plan,
|
||||
amount,
|
||||
timestampsUsed: usage.timestamps,
|
||||
timestampsLimit: limits.timestamps,
|
||||
});
|
||||
|
||||
if (action === 'batch_timestamp') {
|
||||
if (!limits.batch) {
|
||||
throw new AppError('Batch timestamps require a paid plan', {
|
||||
status: 403,
|
||||
code: 'PLAN_UPGRADE_REQUIRED',
|
||||
details: { upgradeUrl: config.upgradeUrl },
|
||||
});
|
||||
}
|
||||
if (!isUnlimited(limits.batchMaxItems) && amount > limits.batchMaxItems) {
|
||||
throw new AppError(`Batch size exceeds plan limit of ${limits.batchMaxItems}`, {
|
||||
status: 403,
|
||||
code: 'BATCH_LIMIT_EXCEEDED',
|
||||
details: { upgradeUrl: config.upgradeUrl, limit: limits.batchMaxItems },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'timestamp') {
|
||||
if (
|
||||
quotaExceeded({
|
||||
limit: limits.timestamps,
|
||||
used: usage.timestamps,
|
||||
allowOverage: limits.allowOverage,
|
||||
})
|
||||
) {
|
||||
const err = new AppError('Monthly timestamp quota exceeded', {
|
||||
status: 402,
|
||||
code: 'QUOTA_EXCEEDED',
|
||||
details: {
|
||||
limit: limits.timestamps,
|
||||
used: usage.timestamps,
|
||||
period: 'monthly',
|
||||
upgradeUrl: config.upgradeUrl,
|
||||
},
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'verify') {
|
||||
if (
|
||||
quotaExceeded({
|
||||
limit: limits.verifications,
|
||||
used: usage.verifications,
|
||||
allowOverage: limits.allowOverage,
|
||||
})
|
||||
) {
|
||||
throw new AppError('Monthly verification quota exceeded', {
|
||||
status: 402,
|
||||
code: 'QUOTA_EXCEEDED',
|
||||
details: {
|
||||
limit: limits.verifications,
|
||||
used: usage.verifications,
|
||||
period: 'monthly',
|
||||
upgradeUrl: config.upgradeUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { tenant, limits, usage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Record usage after a successful action.
|
||||
*
|
||||
* @param {string} tenantId
|
||||
* @param {string} action
|
||||
* @param {{ amount?: number }} [options]
|
||||
* @returns {void}
|
||||
*/
|
||||
export function recordUsage(tenantId, action, { amount = 1 } = {}) {
|
||||
const tenant = getTenant(tenantId);
|
||||
if (!tenant) return;
|
||||
|
||||
const limits = resolveLimits(tenant);
|
||||
const usage = getUsage(tenantId);
|
||||
|
||||
if (action === 'timestamp') {
|
||||
if (
|
||||
!isUnlimited(limits.timestamps) &&
|
||||
usage.timestamps >= limits.timestamps &&
|
||||
limits.allowOverage
|
||||
) {
|
||||
incrementUsage(tenantId, 'overage.timestamps', amount);
|
||||
}
|
||||
incrementUsage(tenantId, 'timestamps', amount);
|
||||
}
|
||||
|
||||
if (action === 'verify') {
|
||||
if (
|
||||
!isUnlimited(limits.verifications) &&
|
||||
usage.verifications >= limits.verifications &&
|
||||
limits.allowOverage
|
||||
) {
|
||||
incrementUsage(tenantId, 'overage.verifications', amount);
|
||||
}
|
||||
incrementUsage(tenantId, 'verifications', amount);
|
||||
}
|
||||
|
||||
if (action === 'status') {
|
||||
incrementUsage(tenantId, 'statusChecks', amount);
|
||||
}
|
||||
|
||||
if (action === 'batch_timestamp') {
|
||||
incrementUsage(tenantId, 'batchTimestamps', amount);
|
||||
incrementUsage(tenantId, 'timestamps', amount);
|
||||
}
|
||||
|
||||
log.debug('recordUsage', { tenantId, action, amount });
|
||||
}
|
||||
149
src/services/tenantService.js
Normal file
149
src/services/tenantService.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* @fileoverview Self-serve signup and admin tenant provisioning.
|
||||
* @module services/tenantService
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { createTenant, getTenant, listTenants } from '../store/tenants.js';
|
||||
import { veraeClient } from '../clients/veraeClient.js';
|
||||
import { AppError } from '../errors.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('auth');
|
||||
const ALLOWED_PLANS = new Set(['free', 'starter', 'pro', 'enterprise']);
|
||||
|
||||
function slugify(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 48);
|
||||
}
|
||||
|
||||
async function validateVeraeCredentials(username, password) {
|
||||
try {
|
||||
await veraeClient.login({ username, password });
|
||||
return true;
|
||||
} catch (err) {
|
||||
throw new AppError('Invalid Verae credentials', {
|
||||
status: 400,
|
||||
code: 'INVALID_VERAE_CREDENTIALS',
|
||||
details: { message: err.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public free-tier signup.
|
||||
* @param {{ email: string, name: string, veraeUsername: string, veraePassword: string }} params
|
||||
*/
|
||||
export async function selfServeSignup({ email, name, veraeUsername, veraePassword }) {
|
||||
if (!email || !name || !veraeUsername || !veraePassword) {
|
||||
throw new AppError('email, name, veraeUsername, and veraePassword are required', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
const bound = await veraeClient.bindUser({ username: veraeUsername, password: veraePassword });
|
||||
|
||||
const id = `tenant-${slugify(email)}-${randomUUID().slice(0, 8)}`;
|
||||
const { tenant, apiKey } = createTenant({
|
||||
id,
|
||||
name,
|
||||
plan: 'free',
|
||||
veraeUsername,
|
||||
veraePassword,
|
||||
veraeUserId: bound.veraeUserId,
|
||||
contract: null,
|
||||
metadata: { email, audience: 'self-serve', createdVia: 'signup' },
|
||||
});
|
||||
|
||||
log.info('self-serve signup', { tenantId: tenant.id, veraeUserId: bound.veraeUserId });
|
||||
|
||||
return {
|
||||
tenant: {
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
plan: tenant.plan,
|
||||
email,
|
||||
veraeUserId: bound.veraeUserId,
|
||||
},
|
||||
apiKey,
|
||||
zapierSetup: {
|
||||
authType: 'custom',
|
||||
field: 'api_key',
|
||||
middlewareUrl: '/zapier/v1/auth/me',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin provision (internal / enterprise).
|
||||
*/
|
||||
export async function provisionTenant({
|
||||
id,
|
||||
name,
|
||||
plan,
|
||||
veraeUsername,
|
||||
veraePassword,
|
||||
contract = null,
|
||||
metadata = {},
|
||||
audience = 'admin',
|
||||
}) {
|
||||
if (!name || !plan || !veraeUsername || !veraePassword) {
|
||||
throw new AppError('name, plan, veraeUsername, and veraePassword are required', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
if (!ALLOWED_PLANS.has(plan)) {
|
||||
throw new AppError(`Invalid plan: ${plan}`, { status: 400, code: 'VALIDATION_ERROR' });
|
||||
}
|
||||
|
||||
if (plan === 'enterprise' && !contract) {
|
||||
throw new AppError('enterprise tenants require a contract object', {
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
});
|
||||
}
|
||||
|
||||
const bound = await veraeClient.bindUser({ username: veraeUsername, password: veraePassword });
|
||||
|
||||
const tenantId = id ?? `tenant-${slugify(name)}-${randomUUID().slice(0, 8)}`;
|
||||
if (getTenant(tenantId)) {
|
||||
throw new AppError('Tenant already exists', { status: 409, code: 'CONFLICT' });
|
||||
}
|
||||
|
||||
const { tenant, apiKey } = createTenant({
|
||||
id: tenantId,
|
||||
name,
|
||||
plan,
|
||||
veraeUsername,
|
||||
veraePassword,
|
||||
veraeUserId: bound.veraeUserId,
|
||||
contract,
|
||||
metadata: { ...metadata, audience, createdVia: 'provision' },
|
||||
});
|
||||
|
||||
log.info('tenant provisioned', { tenantId: tenant.id, plan, audience, veraeUserId: bound.veraeUserId });
|
||||
return {
|
||||
tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan, veraeUserId: bound.veraeUserId },
|
||||
apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function listProvisionedTenants() {
|
||||
return listTenants().map((tenant) => ({
|
||||
id: tenant.id,
|
||||
name: tenant.name,
|
||||
plan: tenant.plan,
|
||||
audience: tenant.metadata?.audience ?? 'unknown',
|
||||
veraeUserId: tenant.veraeUserId,
|
||||
createdAt: tenant.createdAt,
|
||||
}));
|
||||
}
|
||||
170
src/services/timestampService.js
Normal file
170
src/services/timestampService.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* @fileoverview Timestamp create/status operations with billing + job enqueue.
|
||||
* @module services/timestampService
|
||||
*/
|
||||
|
||||
import { config } from '../config.js';
|
||||
import { veraeClient } from '../clients/veraeClient.js';
|
||||
import { enqueueJob } from '../store/jobWatchers.js';
|
||||
import { issueTokenRef } from '../store/tokenRefs.js';
|
||||
import { checkEntitlement, recordUsage } from './entitlementService.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
import { getTraceId } from '../debug/trace-context.js';
|
||||
|
||||
const log = createDebugger('jobs');
|
||||
|
||||
/**
|
||||
* Enqueue async job watch via NATS or in-process store.
|
||||
* @param {object} ctx
|
||||
* @param {string} jobId
|
||||
*/
|
||||
async function enqueueWatchForJob(ctx, jobId) {
|
||||
const traceId = getTraceId() ?? undefined;
|
||||
|
||||
if (config.natsEnabled) {
|
||||
const { enqueueWatch } = await import('../nats/publishers.js');
|
||||
await enqueueWatch({
|
||||
tenantId: ctx.tenantId,
|
||||
jobId,
|
||||
tokenRef: issueTokenRef(ctx.tenantId),
|
||||
veraeUserId: ctx.veraeUserId || ctx.tenant?.veraeUserId,
|
||||
maxAttempts: config.jobPollMaxAttempts,
|
||||
intervalMs: config.jobPollIntervalMs,
|
||||
traceId,
|
||||
});
|
||||
log.debug('watch enqueued on NATS', { jobId, tenantId: ctx.tenantId });
|
||||
return;
|
||||
}
|
||||
|
||||
enqueueJob({
|
||||
tenantId: ctx.tenantId,
|
||||
jobId,
|
||||
veraeToken: ctx.veraeToken,
|
||||
traceId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx - Auth context with tenantId, veraeToken
|
||||
* @param {{ data?: string, hashAlg?: string, sha256?: string, publicMetadata?: object, privateMetadata?: object }} body
|
||||
* @returns {Promise<{ jobId: string, sha256?: string, existing?: boolean }>}
|
||||
*/
|
||||
export async function createTimestamp(ctx, body) {
|
||||
checkEntitlement(ctx.tenantId, 'timestamp');
|
||||
const result = await veraeClient.createTimestamp(ctx.veraeToken, body);
|
||||
recordUsage(ctx.tenantId, 'timestamp');
|
||||
|
||||
await enqueueWatchForJob(ctx, result.jobId);
|
||||
|
||||
log.debug('timestamp created', { jobId: result.jobId, tenantId: ctx.tenantId });
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {{ data: string, hashAlg?: string }} body
|
||||
* @returns {Promise<object>} StatusResponse
|
||||
*/
|
||||
export async function createTimestampAndWait(ctx, body) {
|
||||
const created = await createTimestamp(ctx, body);
|
||||
|
||||
if (config.natsEnabled) {
|
||||
const { waitForJobEvent } = await import('../nats/wait.js');
|
||||
const event = await waitForJobEvent(created.jobId, config.waitTimeoutMs);
|
||||
if (event?.status) {
|
||||
recordUsage(ctx.tenantId, 'status');
|
||||
log.debug('wait via NATS event', {
|
||||
jobId: created.jobId,
|
||||
event: event.event,
|
||||
traceId: event.traceId,
|
||||
});
|
||||
return event.status;
|
||||
}
|
||||
const status = await veraeClient.getStatus(ctx.veraeToken, created.jobId);
|
||||
recordUsage(ctx.tenantId, 'status');
|
||||
if (status.status === 'pending') {
|
||||
return { id: created.jobId, jobId: created.jobId, status: 'pending' };
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await veraeClient.waitForJob(ctx.veraeToken, created.jobId, {
|
||||
maxAttempts: config.jobPollMaxAttempts,
|
||||
intervalMs: config.jobPollIntervalMs,
|
||||
});
|
||||
recordUsage(ctx.tenantId, 'status');
|
||||
return status;
|
||||
} catch {
|
||||
const status = await veraeClient.getStatus(ctx.veraeToken, created.jobId);
|
||||
recordUsage(ctx.tenantId, 'status');
|
||||
return { id: created.jobId, jobId: created.jobId, status: status.status ?? 'pending' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {{ items: Array<{ data: string, hashAlg?: string }> }} body
|
||||
*/
|
||||
export async function createBatchTimestamp(ctx, body) {
|
||||
const itemCount = body.items?.length ?? 0;
|
||||
checkEntitlement(ctx.tenantId, 'batch_timestamp', { amount: itemCount });
|
||||
const result = await veraeClient.createBatchTimestamp(ctx.veraeToken, body);
|
||||
recordUsage(ctx.tenantId, 'batch_timestamp', { amount: itemCount });
|
||||
|
||||
for (const jobId of result.jobIds ?? []) {
|
||||
await enqueueWatchForJob(ctx, jobId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {string} jobId
|
||||
*/
|
||||
export async function getJobStatus(ctx, jobId) {
|
||||
const status = await veraeClient.getStatus(ctx.veraeToken, jobId);
|
||||
recordUsage(ctx.tenantId, 'status');
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {{ jobIds: string[] }} body
|
||||
*/
|
||||
export async function getBatchJobStatus(ctx, body) {
|
||||
const status = await veraeClient.getBatchStatus(ctx.veraeToken, body);
|
||||
recordUsage(ctx.tenantId, 'status', { amount: body.jobIds?.length ?? 1 });
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {string} jobId
|
||||
*/
|
||||
export async function getJobVerification(ctx, jobId) {
|
||||
const status = await veraeClient.getJobVerification(ctx.veraeToken, jobId);
|
||||
recordUsage(ctx.tenantId, 'status');
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {string} sha256
|
||||
* @param {{ includeAttached?: boolean, includeTree?: boolean }} [opts]
|
||||
*/
|
||||
export async function lookupHash(ctx, sha256, opts = {}) {
|
||||
const result = await veraeClient.lookupHash(ctx.veraeToken, sha256);
|
||||
recordUsage(ctx.tenantId, 'status');
|
||||
if (!opts.includeAttached && !opts.includeTree) return result;
|
||||
return {
|
||||
...result,
|
||||
includeAttached: Boolean(opts.includeAttached),
|
||||
includeTree: Boolean(opts.includeTree),
|
||||
note:
|
||||
opts.includeTree && !result?.exists
|
||||
? 'Chain miss: middleware should broadcast verae.archive.query kinds=["tree"] (wired in simulator; live aggregator hook TBD).'
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
34
src/services/verifyService.js
Normal file
34
src/services/verifyService.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @fileoverview Certificate verification with usage metering.
|
||||
* @module services/verifyService
|
||||
*/
|
||||
|
||||
import { veraeClient } from '../clients/veraeClient.js';
|
||||
import { checkEntitlement, recordUsage } from './entitlementService.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('jobs');
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {{ certificate: string }} body
|
||||
*/
|
||||
export async function verifyTimestamp(ctx, body) {
|
||||
checkEntitlement(ctx.tenantId, 'verify');
|
||||
const result = await veraeClient.verify(ctx.veraeToken, body);
|
||||
recordUsage(ctx.tenantId, 'verify');
|
||||
log.debug('verify result', { tenantId: ctx.tenantId, valid: result.valid });
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {{ certificates: string[] }} body
|
||||
*/
|
||||
export async function verifyBatch(ctx, body) {
|
||||
const amount = body.certificates?.length ?? 1;
|
||||
checkEntitlement(ctx.tenantId, 'verify', { amount });
|
||||
const result = await veraeClient.verifyBatch(ctx.veraeToken, body);
|
||||
recordUsage(ctx.tenantId, 'verify', { amount });
|
||||
return result;
|
||||
}
|
||||
70
src/services/webhookService.js
Normal file
70
src/services/webhookService.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* @fileoverview REST Hook subscribe/unsubscribe and HTTP delivery.
|
||||
* @module services/webhookService
|
||||
*/
|
||||
|
||||
import { createWebhook, deleteWebhook } from '../store/webhooks.js';
|
||||
import { createDebugger } from '../debug/logger.js';
|
||||
|
||||
const log = createDebugger('webhooks');
|
||||
|
||||
const ALLOWED_EVENTS = new Set(['timestamp.completed', 'timestamp.failed']);
|
||||
|
||||
/**
|
||||
* @param {object} ctx - auth context
|
||||
* @param {{ targetUrl: string, event: string }} params
|
||||
*/
|
||||
export function subscribe(ctx, { targetUrl, event }) {
|
||||
if (!targetUrl) {
|
||||
throw new Error('targetUrl is required');
|
||||
}
|
||||
if (!ALLOWED_EVENTS.has(event)) {
|
||||
throw new Error(`Unsupported event: ${event}`);
|
||||
}
|
||||
|
||||
return createWebhook({
|
||||
tenantId: ctx.tenantId,
|
||||
targetUrl,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* @param {{ hookId?: string, targetUrl?: string }} params
|
||||
*/
|
||||
export function unsubscribe(ctx, { hookId, targetUrl }) {
|
||||
const removed = deleteWebhook({
|
||||
tenantId: ctx.tenantId,
|
||||
hookId,
|
||||
targetUrl,
|
||||
});
|
||||
|
||||
if (!removed) {
|
||||
throw new Error('Webhook subscription not found');
|
||||
}
|
||||
|
||||
return { removed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* POST payload to Zapier target URL.
|
||||
* @param {string} targetUrl
|
||||
* @param {object} payload
|
||||
* @returns {Promise<{ ok: boolean, status: number }>}
|
||||
*/
|
||||
export async function deliverWebhook(targetUrl, payload) {
|
||||
log.debug('deliverWebhook', { targetUrl, event: payload?.event });
|
||||
|
||||
const response = await fetch(targetUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Verae-Zapier-Middleware/1.0',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
log.debug('deliverWebhook result', { status: response.status, ok: response.ok });
|
||||
return { ok: response.ok, status: response.status };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue