From 6557b04f4ef894b71e3f0501ac04e25079676b98 Mon Sep 17 00:00:00 2001 From: Less Date: Thu, 23 Apr 2026 14:09:18 +0700 Subject: [PATCH 1/2] refactor: always allow alias for all spaces Drop the space.voting.aliased opt-in so every space accepts vote/proposal/update-proposal/delete-proposal signed by a valid alias of the author. Promote flag-proposal to EVM as well (it was already allowed on Starknet). See snapshot-labs/workflow#793. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/helpers/alias.ts | 3 ++- src/ingestor.ts | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/helpers/alias.ts b/src/helpers/alias.ts index a32a19eb..6c1513f0 100644 --- a/src/helpers/alias.ts +++ b/src/helpers/alias.ts @@ -20,7 +20,8 @@ const OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS = [ 'vote-string', 'proposal', 'update-proposal', - 'delete-proposal' + 'delete-proposal', + 'flag-proposal' ] as const; // These types can be executed with a Starknet alias diff --git a/src/ingestor.ts b/src/ingestor.ts index 6842f3d6..9ae9cbf6 100644 --- a/src/ingestor.ts +++ b/src/ingestor.ts @@ -91,7 +91,6 @@ export default async function ingestor(req) { return Promise.reject('Invalid space id'); } - let aliased = false; if (!['settings', 'alias', 'profile', 'delete-space'].includes(type)) { if (!message.space) return Promise.reject('unknown space'); @@ -99,13 +98,12 @@ export default async function ingestor(req) { const space = await getSpace(message.space, false, message.network); if (!space) return Promise.reject('unknown space'); network = space.network; - if (space.voting?.aliased) aliased = true; } catch (e: any) { return Promise.reject(e.message); } } - await verifyAlias(type, body, aliased); + await verifyAlias(type, body, true); // Check if signature is valid try { From 04198c68b85e5fb370e1921cfbbba49694931398 Mon Sep 17 00:00:00 2001 From: Less Date: Thu, 23 Apr 2026 14:17:59 +0700 Subject: [PATCH 2/2] refactor: collapse alias type buckets into a single list Now that every space is aliased, the optional and Starknet-specific buckets in alias.ts are redundant. Merge them into a single TYPES_EXECUTABLE_BY_ALIAS list and drop the optionalAlias param, isStarknetAddress, getAllowedTypes, and the memoization cache. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/helpers/alias.ts | 81 +++++---------------------------- src/ingestor.ts | 2 +- test/unit/helpers/alias.test.ts | 53 ++++++--------------- 3 files changed, 25 insertions(+), 111 deletions(-) diff --git a/src/helpers/alias.ts b/src/helpers/alias.ts index 6c1513f0..91eb16c3 100644 --- a/src/helpers/alias.ts +++ b/src/helpers/alias.ts @@ -1,20 +1,14 @@ -import { uniq } from 'lodash'; import db from './mysql'; const DEFAULT_ALIAS_EXPIRY_DAYS = 30; -// These types can always be executed with an alias const TYPES_EXECUTABLE_BY_ALIAS = [ 'follow', 'unfollow', 'subscribe', 'unsubscribe', 'profile', - 'statement' -] as const; - -// These types can be executed with an alias only when enabled in the space settings -const OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS = [ + 'statement', 'vote', 'vote-array', 'vote-string', @@ -24,53 +18,28 @@ const OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS = [ 'flag-proposal' ] as const; -// These types can be executed with a Starknet alias -const TYPES_EXECUTABLE_BY_STARKNET_ALIAS = [ - 'flag-proposal', - ...OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS -] as const; - -// Memoization cache for getAllowedTypes -const allowedTypesCache = new Map(); - -// Types -type ExecutableType = - | (typeof TYPES_EXECUTABLE_BY_ALIAS)[number] - | (typeof OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS)[number] - | (typeof TYPES_EXECUTABLE_BY_STARKNET_ALIAS)[number]; - -/** - * Checks if an alias relationship exists and is not expired - * @param address - The original address - * @param alias - The alias address to check - * @param expiryDays - Number of days after which alias expires (default: 30) - * @returns Promise - True if valid alias exists - */ export async function isExistingAlias( address: string, alias: string, expiryDays = DEFAULT_ALIAS_EXPIRY_DAYS ): Promise { - const query = `SELECT 1 - FROM aliases - WHERE address = ? AND alias = ? - AND created > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY)) - LIMIT 1`; - - const results = await db.queryAsync(query, [address, alias, expiryDays]); + const results = await db.queryAsync( + `SELECT 1 + FROM aliases + WHERE address = ? AND alias = ? + AND created > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL ? DAY)) + LIMIT 1`, + [address, alias, expiryDays] + ); return results.length > 0; } -export async function verifyAlias(type: string, body: any, optionalAlias = false): Promise { +export async function verifyAlias(type: string, body: any): Promise { const { message } = body.data; if (body.address === message.from) return; - if ( - !getAllowedTypes(optionalAlias, isStarknetAddress(message.from)).includes( - type as ExecutableType - ) - ) { + if (!TYPES_EXECUTABLE_BY_ALIAS.includes(type as any)) { return Promise.reject(`alias not allowed for the type: ${type}`); } @@ -78,31 +47,3 @@ export async function verifyAlias(type: string, body: any, optionalAlias = false return Promise.reject('wrong alias'); } } - -// Loose checking here, as we're looking for this address in the database later, -// which will always be a formatted starknet address if valid. -export function isStarknetAddress(address: string): boolean { - return /^0x[0-9a-fA-F]{64}$/.test(address); -} - -export function getAllowedTypes(withAlias: boolean, forStarknet: boolean): ExecutableType[] { - const cacheKey = `${withAlias}-${forStarknet}`; - - if (allowedTypesCache.has(cacheKey)) { - return allowedTypesCache.get(cacheKey)!; - } - - const types: ExecutableType[] = [...TYPES_EXECUTABLE_BY_ALIAS]; - - if (withAlias) { - types.push(...OPTIONAL_TYPES_EXECUTABLE_BY_ALIAS); - } - - if (forStarknet) { - types.push(...TYPES_EXECUTABLE_BY_STARKNET_ALIAS); - } - - const result = uniq(types); - allowedTypesCache.set(cacheKey, result); - return result; -} diff --git a/src/ingestor.ts b/src/ingestor.ts index 9ae9cbf6..c600ba1d 100644 --- a/src/ingestor.ts +++ b/src/ingestor.ts @@ -103,7 +103,7 @@ export default async function ingestor(req) { } } - await verifyAlias(type, body, true); + await verifyAlias(type, body); // Check if signature is valid try { diff --git a/test/unit/helpers/alias.test.ts b/test/unit/helpers/alias.test.ts index 7d9e0ca4..8263b8d4 100644 --- a/test/unit/helpers/alias.test.ts +++ b/test/unit/helpers/alias.test.ts @@ -1,45 +1,18 @@ -import { getAllowedTypes, isStarknetAddress } from '../../../src/helpers/alias'; +import { verifyAlias } from '../../../src/helpers/alias'; -describe('Alias', () => { - describe('isStarknetAddress()', () => { - it('should return true for a starknet address', () => { - expect( - isStarknetAddress('0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef') - ).toBe(true); - }); - it('should return false for a non-starknet address', () => { - expect(isStarknetAddress('0x91FD2c8d24767db4Ece7069AA27832ffaf8590f3')).toBe(false); - }); - it('should return false for an invalid address', () => { - expect(isStarknetAddress('')).toBe(false); - expect(isStarknetAddress('test')).toBe(false); - }); +jest.mock('../../../src/helpers/mysql', () => ({ + __esModule: true, + default: { queryAsync: jest.fn() } +})); + +describe('verifyAlias()', () => { + it('resolves when the sender is the creator', async () => { + const body = { address: '0xabc', data: { message: { from: '0xabc' } } }; + await expect(verifyAlias('vote', body)).resolves.toBeUndefined(); }); - describe('getAllowedTypes()', () => { - it('should return the correct types when both withAlias and forStarknet are false', () => { - const result = getAllowedTypes(false, false); - expect(result).toContain('profile'); - expect(result).not.toContain('vote'); - expect(result).not.toContain('update-proposal'); - }); - it('should return the correct types when withAlias is true and forStarknet is false', () => { - const result = getAllowedTypes(true, false); - expect(result).toContain('profile'); - expect(result).toContain('vote'); - expect(result).toContain('update-proposal'); - }); - it('should return the correct types when withAlias is false and forStarknet is true', () => { - const result = getAllowedTypes(false, true); - expect(result).toContain('profile'); - expect(result).toContain('vote'); - expect(result).toContain('update-proposal'); - }); - it('should return the correct types when both withAlias and forStarknet are true', () => { - const result = getAllowedTypes(true, true); - expect(result).toContain('profile'); - expect(result).toContain('vote'); - expect(result).toContain('update-proposal'); - }); + it('rejects when the type is not alias-executable', async () => { + const body = { address: '0xaaa', data: { message: { from: '0xbbb' } } }; + await expect(verifyAlias('settings', body)).rejects.toMatch('alias not allowed'); }); });