/** * @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 { 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, // Prefer re-login in worker; include token for MVP simplicity when mock veraeToken: ctx.veraeToken, 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} 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, }; }