diff --git a/docs/openapi.yaml b/docs/openapi.yaml index c7db34e..6f16aa7 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -6,6 +6,12 @@ info: description: | AR.IO ArNS Resolver components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: apiToken + description: ADMIN_API_KEY set in your .env file. schemas: ArweaveAddress: type: string @@ -67,6 +73,47 @@ paths: application/json: schema: '$ref': '#/components/schemas/Info' + '/ar-io/resolver/admin/evaluate': + post: + security: + - bearerAuth: [] + responses: + '200': + description: |- + 200 response + content: + application/json: + schema: + type: object + properties: + message: { type: string } + '202': + description: |- + 202 response + content: + application/json: + schema: + type: object + properties: + message: { type: string } + '401': + description: |- + 401 response + content: + application/json: + schema: + type: object + properties: + message: { type: string } + '500': + description: |- + 500 response + content: + application/json: + schema: + type: object + properties: + error: { type: string } '/ar-io/resolver/records/{name}': head: parameters: diff --git a/src/config.ts b/src/config.ts index daf431a..ce6dce8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,6 +21,8 @@ import * as env from './lib/env.js'; dotenv.config(); +export const ADMIN_API_KEY = env.varOrRandom('ADMIN_API_KEY'); + export const EVALUATION_INTERVAL_MS = +env.varOrDefault( 'EVALUATION_INTERVAL_MS', `${1000 * 60 * 15}`, // 15 mins by default diff --git a/src/lib/env.ts b/src/lib/env.ts index 7b1c3ef..6964390 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -15,6 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import crypto from 'node:crypto'; + +import log from '../log.js'; export function varOrDefault(envVarName: string, defaultValue: string): string { const value = process.env[envVarName]; @@ -25,3 +28,13 @@ export function varOrUndefined(envVarName: string): string | undefined { const value = process.env[envVarName]; return value !== undefined && value.trim() !== '' ? value : undefined; } + +export function varOrRandom(envVarName: string): string { + const value = process.env[envVarName]; + if (value === undefined) { + const value = crypto.randomBytes(32).toString('base64url'); + log.info(`${envVarName} not provided, generated random value: ${value}`); + return value; + } + return value; +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..ba8d79c --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,34 @@ +/** + * AR.IO ArNS Resolver + * Copyright (C) 2023 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { NextFunction, Request, Response } from 'express'; + +import * as config from './config.js'; + +export const adminMiddleware = ( + req: Request, + res: Response, + next: NextFunction, +) => { + if (req.headers['authorization'] !== `Bearer ${config.ADMIN_API_KEY}`) { + res.status(401).send({ + message: 'Unauthorized', + }); + return; + } + next(); +}; diff --git a/src/server.ts b/src/server.ts index bf6b6c6..24b2701 100644 --- a/src/server.ts +++ b/src/server.ts @@ -24,7 +24,13 @@ import YAML from 'yaml'; import * as config from './config.js'; import log from './log.js'; -import { cache, getLastEvaluatedTimestamp } from './system.js'; +import { adminMiddleware } from './middleware.js'; +import { + cache, + evaluateArNSNames, + getLastEvaluatedTimestamp, + isEvaluationInProgress, +} from './system.js'; import { ArNSResolvedData } from './types.js'; // HTTP server @@ -34,7 +40,7 @@ export const app = express(); app.use( cors({ origin: '*', - methods: ['GET', 'HEAD'], + methods: ['GET', 'HEAD', 'POST'], }), ); @@ -86,6 +92,23 @@ app.get('/ar-io/resolver/info', (_req, res) => { }); }); +app.post('/ar-io/resolver/admin/evaluate', adminMiddleware, (_req, res) => { + // TODO: we could support post request to trigger evaluation for specific names rather than re-evaluate everything + if (isEvaluationInProgress()) { + res.status(202).send({ + message: 'Evaluation in progress', + }); + } else { + log.info('Evaluation triggered by request', { + processId: config.IO_PROCESS_ID, + }); + evaluateArNSNames(); // don't await + res.status(200).send({ + message: 'Evaluation triggered', + }); + } +}); + app.head('/ar-io/resolver/records/:name', async (req, res) => { try { log.debug('Checking cache for record', { name: req.params.name }); diff --git a/src/system.ts b/src/system.ts index f2421f0..03e3fa7 100644 --- a/src/system.ts +++ b/src/system.ts @@ -33,6 +33,7 @@ import { ArNSResolvedData } from './types.js'; let lastEvaluationTimestamp: number | undefined; let evaluationInProgress = false; export const getLastEvaluatedTimestamp = () => lastEvaluationTimestamp; +export const isEvaluationInProgress = () => evaluationInProgress; export const contract: AoIORead = IO.init({ processId: config.IO_PROCESS_ID, }); @@ -75,7 +76,7 @@ export async function evaluateArNSNames() { ); log.debug('Identified unique process ids assigned to records:', { - recordCount: Object.keys(apexRecords).length, + apexRecordCount: Object.keys(apexRecords).length, processCount: processIds.size, }); @@ -120,6 +121,7 @@ export async function evaluateArNSNames() { log.info('Retrieved unique process ids assigned to records:', { processCount: Object.keys(processRecordMap).length, + apexRecordCount: Object.keys(apexRecords).length, }); // filter out any records associated with an invalid contract @@ -127,6 +129,8 @@ export async function evaluateArNSNames() { ([_, record]) => record.processId in processRecordMap, ); + let successfulEvaluationCount = 0; + const insertPromises = []; // now go through all the record names and assign them to the resolved tx ids @@ -175,10 +179,17 @@ export async function evaluateArNSNames() { } // use pLimit to prevent overwhelming cache await Promise.all( - insertPromises.map((promise) => parallelLimit(() => promise)), + insertPromises.map((promise) => + parallelLimit(() => promise.then(() => successfulEvaluationCount++)), + ), ); - log.info('Successfully evaluated arns names', { + log.info('Finished evaluating arns names', { durationMs: Date.now() - startTime, + apexRecordCount: Object.keys(apexRecords).length, + evaluatedRecordCount: successfulEvaluationCount, + evaluatedProcessCount: Object.keys(processRecordMap).length, + failedProcessCount: + processIds.size - Object.keys(processRecordMap).length, }); lastEvaluationTimestamp = Date.now(); } catch (err: any) {