diff --git a/package.json b/package.json index 23433e5..233b721 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "scripts": { "analyze": "tsdown --clean --no-fixed-extension --sourcemap source/index.ts && esbuild-analyzer dist/", "build": "tsdown --clean --sourcemap source/index.ts", - "dev": "tsx watch source/app.ts", + "dev": "tsx watch source/index.ts", "docs": "typedoc --hideGenerator --categorizeByGroup", "format": "prettier --write . && eslint --fix", "spellcheck": "cspell 'source/**' 'test/**' 'playground/**'", diff --git a/source/services/config.ts b/source/services/config.ts index 19bdd54..d13c079 100644 --- a/source/services/config.ts +++ b/source/services/config.ts @@ -32,7 +32,7 @@ const configSchema = z.object({ .object({ origin: z.string().default('*'), methods: z.array(z.string()).default([ 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS' ]), - allowedHeaders: z.array(z.string()).default([ 'Content-Type', 'cache-control', 'X-Timestamp', 'X-PublicKey', 'X-Signature' ]), + allowedHeaders: z.array(z.string()).default([ 'Content-Type', 'cache-control', 'X-Timestamp', 'X-Public-Key', 'X-Signature' ]), }) .partial() .prefault({}), @@ -48,6 +48,11 @@ const configSchema = z.object({ .int() .positive() .default(5 * 60 * 1000), + uniqueRequestCleanupIntervalMs: z.coerce + .number() + .int() + .positive() + .default(10 * 60 * 1000), }) .prefault({}), }); @@ -83,6 +88,7 @@ export class Config { }, auth: { timestampWindowMs: process.env.AUTH_TIMESTAMP_WINDOW_MS ? Number(process.env.AUTH_TIMESTAMP_WINDOW_MS) : undefined, + uniqueRequestCleanupIntervalMs: process.env.AUTH_UNIQUE_REQUEST_CLEANUP_INTERVAL_MS ? Number(process.env.AUTH_UNIQUE_REQUEST_CLEANUP_INTERVAL_MS) : undefined, }, }); } diff --git a/source/services/storage/migrations/001-resources.ts b/source/services/storage/migrations/001-resources.ts index 27dcf73..b143454 100644 --- a/source/services/storage/migrations/001-resources.ts +++ b/source/services/storage/migrations/001-resources.ts @@ -23,8 +23,19 @@ export const up = async (db: Kysely): Promise => { .addColumn('public_key', 'text', (col) => col.notNull()) .addColumn('blob', 'blob', (col) => col.notNull()) .addColumn('timestamp', 'integer', (col) => col.notNull().defaultTo(millisecondTime)) + .addColumn('signature', 'text', (col) => col.notNull()) .addPrimaryKeyConstraint('pk_resource_data', [ 'resource_id', 'public_key' ]) .execute(); + + // Table for authed requests + // We will store the signature and the timestamp of the request, and we will clear out rows that are older than our msTimeout for our auth + await db.schema + .createTable('authed_requests') + .ifNotExists() + .addColumn('signature', 'text', (col) => col.notNull()) + .addColumn('timestamp', 'integer', (col) => col.notNull().defaultTo(millisecondTime)) + .addPrimaryKeyConstraint('pk_authed_requests', [ 'signature' ]) + .execute(); }; /** @@ -34,5 +45,8 @@ export const up = async (db: Kysely): Promise => { */ export const down = async (db: Kysely): Promise => { await db.schema.dropTable('resource_data').ifExists() -.execute(); + .execute(); + + await db.schema.dropTable('authed_requests').ifExists() + .execute(); }; diff --git a/source/services/storage/tables.ts b/source/services/storage/tables.ts index d937c28..ee5039d 100644 --- a/source/services/storage/tables.ts +++ b/source/services/storage/tables.ts @@ -22,9 +22,22 @@ export interface ResourceDataTable { /** Millisecond timestamp of the last write. */ timestamp: Timestamp; + + /** Signature of the write. */ + signature: string; +} + +export interface AuthedRequestsTable { + + /** Signature of the request. */ + signature: string; + + /** Millisecond timestamp of the request. */ + timestamp: Timestamp; } /** Complete Kysely schema mapping for the sync server database. */ export interface DatabaseTables { resource_data: ResourceDataTable; + authed_requests: AuthedRequestsTable; } diff --git a/test/services/config.test.ts b/test/services/config.test.ts index 6100e28..eb68f46 100644 --- a/test/services/config.test.ts +++ b/test/services/config.test.ts @@ -18,7 +18,7 @@ const testConfigDefaultsTo1MiBRequestBodyLimit = (): void => { expect(config.server.host).toBe('0.0.0.0'); expect(config.server.cors.origin).toBe('*'); expect(config.server.cors.methods).toEqual([ 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS' ]); - expect(config.server.cors.allowedHeaders).toEqual([ 'Content-Type', 'cache-control', 'X-Timestamp', 'X-PublicKey', 'X-Signature' ]); + expect(config.server.cors.allowedHeaders).toEqual([ 'Content-Type', 'cache-control', 'X-Timestamp', 'X-Public-Key', 'X-Signature' ]); expect(config.auth.timestampWindowMs).toBe(300000); };