diff --git a/.vscode/launch.json b/.vscode/launch.json index 4e608c98c..7a17ead31 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -94,7 +94,7 @@ "--run", "--inspect-brk", "--no-file-parallelism", - "packages/payload/packages/xns/plugins/record/src/diviner/lib/spec/parseEstimatesFromArray.spec.ts" + "packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/Name.spec.ts" ], "sourceMaps": true, "resolveSourceMapLocations": [ diff --git a/cspell.json b/cspell.json index 06288577e..2af5e6911 100644 --- a/cspell.json +++ b/cspell.json @@ -26,6 +26,7 @@ "decentralnetworkservices", "deregistering", "deregisters", + "doejohn", "dotenv", "emittery", "etherchain", @@ -47,6 +48,7 @@ "interoperably", "IPFS", "IXSCAN", + "johndoe", "jsdom", "jsonpatch", "keccak", diff --git a/package.json b/package.json index c0c1d8fd3..55704053a 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,7 @@ "coverage": "yarn jest --coverage --forceExit", "deploy": "xy deploy", "lint-pkg": "npmPkgJsonLint .", - "test": "jest --no-cache --forceExit", - "test-esm": "node $(yarn jest --no-cache --forceExit)" + "test": "vitest run" }, "resolutions": { "axios": "^1", diff --git a/packages/payload/packages/xns/plugins/record/package.json b/packages/payload/packages/xns/plugins/record/package.json index 6e006e748..d1eb6ff49 100644 --- a/packages/payload/packages/xns/plugins/record/package.json +++ b/packages/payload/packages/xns/plugins/record/package.json @@ -33,6 +33,7 @@ "@xylabs/hex": "^4.0.9", "@xyo-network/boundwitness-model": "^3.1.4", "@xyo-network/boundwitness-validator": "^3.1.4", + "@xyo-network/diviner-hash-lease": "^3.1.4", "@xyo-network/module-model": "^3.1.4", "@xyo-network/payload-builder": "^3.1.4", "@xyo-network/payload-model": "^3.1.4", @@ -43,7 +44,6 @@ "@xylabs/tsconfig": "^4.0.7", "@xyo-network/account": "^3.1.4", "@xyo-network/boundwitness-builder": "^3.1.4", - "@xyo-network/diviner-hash-lease": "^3.1.4", "typescript": "^5.5.4" }, "publishConfig": { diff --git a/packages/payloadset/packages/xns/plugins/record/package.json b/packages/payloadset/packages/xns/plugins/record/package.json index 4df046d96..bcb5d32f6 100644 --- a/packages/payloadset/packages/xns/plugins/record/package.json +++ b/packages/payloadset/packages/xns/plugins/record/package.json @@ -29,9 +29,13 @@ "module": "dist/neutral/index.mjs", "types": "dist/neutral/index.d.ts", "dependencies": { + "@xylabs/assert": "^4.0.9", "@xylabs/exists": "^4.0.9", + "@xylabs/hex": "^4.0.9", + "@xylabs/promise": "^4.0.9", "@xyo-network/boundwitness-model": "^3.1.4", "@xyo-network/diviner-hash-lease": "^3.1.4", + "@xyo-network/module-model": "^3.1.5", "@xyo-network/payload-builder": "^3.1.4", "@xyo-network/payload-model": "^3.1.4", "@xyo-network/xns-record-payload-plugins": "workspace:^" diff --git a/packages/payloadset/packages/xns/plugins/record/src/estimate/index.ts b/packages/payloadset/packages/xns/plugins/record/src/estimate/index.ts new file mode 100644 index 000000000..ac9d58e65 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/estimate/index.ts @@ -0,0 +1 @@ +export * from './lib/index.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/lib/index.ts b/packages/payloadset/packages/xns/plugins/record/src/estimate/lib/index.ts similarity index 100% rename from packages/payloadset/packages/xns/plugins/record/src/lib/index.ts rename to packages/payloadset/packages/xns/plugins/record/src/estimate/lib/index.ts diff --git a/packages/payloadset/packages/xns/plugins/record/src/lib/parseDomainEstimates.ts b/packages/payloadset/packages/xns/plugins/record/src/estimate/lib/parseDomainEstimates.ts similarity index 100% rename from packages/payloadset/packages/xns/plugins/record/src/lib/parseDomainEstimates.ts rename to packages/payloadset/packages/xns/plugins/record/src/estimate/lib/parseDomainEstimates.ts diff --git a/packages/payloadset/packages/xns/plugins/record/src/lib/spec/__snapshots__/parseDomainEstimates.spec.ts.snap b/packages/payloadset/packages/xns/plugins/record/src/estimate/lib/spec/__snapshots__/parseDomainEstimates.spec.ts.snap similarity index 100% rename from packages/payloadset/packages/xns/plugins/record/src/lib/spec/__snapshots__/parseDomainEstimates.spec.ts.snap rename to packages/payloadset/packages/xns/plugins/record/src/estimate/lib/spec/__snapshots__/parseDomainEstimates.spec.ts.snap diff --git a/packages/payloadset/packages/xns/plugins/record/src/lib/spec/parseDomainEstimates.spec.ts b/packages/payloadset/packages/xns/plugins/record/src/estimate/lib/spec/parseDomainEstimates.spec.ts similarity index 100% rename from packages/payloadset/packages/xns/plugins/record/src/lib/spec/parseDomainEstimates.spec.ts rename to packages/payloadset/packages/xns/plugins/record/src/estimate/lib/spec/parseDomainEstimates.spec.ts diff --git a/packages/payloadset/packages/xns/plugins/record/src/lib/spec/tsconfig.json b/packages/payloadset/packages/xns/plugins/record/src/estimate/lib/spec/tsconfig.json similarity index 100% rename from packages/payloadset/packages/xns/plugins/record/src/lib/spec/tsconfig.json rename to packages/payloadset/packages/xns/plugins/record/src/estimate/lib/spec/tsconfig.json diff --git a/packages/payloadset/packages/xns/plugins/record/src/index.ts b/packages/payloadset/packages/xns/plugins/record/src/index.ts index ac9d58e65..96f376156 100644 --- a/packages/payloadset/packages/xns/plugins/record/src/index.ts +++ b/packages/payloadset/packages/xns/plugins/record/src/index.ts @@ -1 +1,2 @@ -export * from './lib/index.ts' +export * from './estimate/index.ts' +export * from './validation/index.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/index.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/index.ts new file mode 100644 index 000000000..cbf4b2a86 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/index.ts @@ -0,0 +1,2 @@ +export * from './name/index.ts' +export * from './validation/index.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/Name.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/name/Name.ts new file mode 100644 index 000000000..d8ee1c744 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/Name.ts @@ -0,0 +1,100 @@ +import { assertEx } from '@xylabs/assert' +import { isHash } from '@xylabs/hex' +import type { Promisable } from '@xylabs/promise' +import type { Payload } from '@xyo-network/payload-model' +import type { DomainFields, TopLevelDomain } from '@xyo-network/xns-record-payload-plugins' +import { DomainSchema } from '@xyo-network/xns-record-payload-plugins' + +import { MAX_DOMAIN_LENGTH, XnsNameSimpleValidators } from '../validation/index.ts' +import { removeDisallowedCharacters } from './lib/index.ts' +import type { ValidSourceTypes } from './types/index.ts' + +export class XnsNameHelper { + static ValidTLDs = ['.xyo'] as const + + private _xnsName: Payload + + private constructor(xnsName: Payload) { + this._xnsName = xnsName + } + + get domain() { + return assertEx(this.xnsName.domain, () => 'domain not found in payload') + } + + get name() { + return `${this.domain}.${this.tld}` + } + + get tld() { + return assertEx(this.xnsName.tld, () => 'tld not found in payload') + } + + get xnsName() { + return assertEx(this._xnsName, () => 'XnsNameHelper xnsName not set') + } + + /** + * Create an XnsNameHelper from a domain payload + * @param {Domain} domain + * @returns Promise + */ + static fromPayload(domain: Payload): Promisable { + return new XnsNameHelper(domain) + } + + /** + * Create an XnsNameHelper from a string + * @param {string} xnsName + * @returns Promise + */ + static fromString(xnsName: string): XnsNameHelper { + const parts = xnsName.split('.') + assertEx(parts.length === 2, () => 'Unable to parse xnsName') + + const domain = parts[0] + const tld = parts[1] as TopLevelDomain + return new XnsNameHelper({ + schema: DomainSchema, domain, tld, + }) + } + + /** + * Determine if a string is a valid XNS name or hash + * @param {string} source? + * @returns ValidSourceTypes + */ + static isPotentialXnsNameOrHash(source?: string): ValidSourceTypes { + if (isHash(source)) return 'hash' + const xnsName = XnsNameHelper.ValidTLDs.some(tld => source?.endsWith(tld)) ? source : null + return xnsName ? 'xnsName' : null + } + + static isValid(domain: Payload) { + return XnsNameSimpleValidators.every(validator => validator(domain)) + } + + /** + * Mask a string to be a valid XNS name + * @param {string} str + * @returns string + */ + static mask(str: string) { + // Check if the domain name is too long + if (str.length > MAX_DOMAIN_LENGTH) { + throw new Error(`Domain name too long: ${str.length} exceeds max length: ${MAX_DOMAIN_LENGTH}`) + } + + // convert to lowercase + const lowercaseXnsName = str.toLowerCase() + + // Remove everything except letters, numbers, and dashes + let formattedXnsName = lowercaseXnsName.replaceAll(/[^\dA-Za-z-]+$/g, '') + + // Remove leading and trailing dashes + formattedXnsName = formattedXnsName.replaceAll(/^-+|-+$/g, '') + + // Filter out disallowed characters. + return removeDisallowedCharacters(formattedXnsName) + } +} diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/index.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/name/index.ts new file mode 100644 index 000000000..df431fb1d --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/index.ts @@ -0,0 +1,2 @@ +export * from './Name.ts' +export * from './types/index.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/lib/index.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/name/lib/index.ts new file mode 100644 index 000000000..f15734e49 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/lib/index.ts @@ -0,0 +1 @@ +export * from './removeDisallowedCharacters.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/lib/removeDisallowedCharacters.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/name/lib/removeDisallowedCharacters.ts new file mode 100644 index 000000000..3b896599f --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/lib/removeDisallowedCharacters.ts @@ -0,0 +1,26 @@ +import { DisallowedModuleIdentifierCharacters } from '@xyo-network/module-model' + +/** + * A set of all the disallowed characters in module identifiers + */ +const DISALLOWED_CHARACTERS = new Set(Object.keys(DisallowedModuleIdentifierCharacters)) + +/** + * Iterates over a string removing disallowed characters + * @param xnsName The XNS name to remove disallowed characters from + * @returns The XNS name with disallowed characters removed + */ +export const removeDisallowedCharacters = (xnsName: string): string => { + // Create the initial result + let result = '' + // Iterate over each character in the XNS name + for (const char of xnsName) { + // If the character is not a disallowed character + if (!DISALLOWED_CHARACTERS.has(char)) { + // add it to the result + result += char + } + } + // Return the result which contains only allowed characters + return result +} diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/Name.spec.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/Name.spec.ts new file mode 100644 index 000000000..2a37572bb --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/Name.spec.ts @@ -0,0 +1,125 @@ +import type { Domain } from '@xyo-network/xns-record-payload-plugins' +import { DomainSchema } from '@xyo-network/xns-record-payload-plugins' + +import { MAX_DOMAIN_LENGTH } from '../../validation/index.ts' +import { XnsNameHelper } from '../Name.ts' + +describe('XnsNameHelper', () => { + const validDomain: Domain = { + schema: DomainSchema, domain: 'example', tld: 'xyo', + } + describe('domain getter', () => { + it('should return the domain if set', async () => { + const helper = await XnsNameHelper.fromPayload(validDomain) + expect(helper.domain).toBe('example') + }) + + it('should throw an error if domain is not set', async () => { + const domain: Domain = { + schema: DomainSchema, domain: '', tld: 'xyo', + } + const helper = await XnsNameHelper.fromPayload(domain) + expect(() => helper.domain).toThrow('domain not found in payload') + }) + }) + + describe('tld getter', () => { + it('should return the tld if set', async () => { + const helper = await XnsNameHelper.fromPayload(validDomain) + expect(helper.tld).toBe('xyo') + }) + + it('should throw an error if tld is not set', async () => { + const domain: Domain = { + schema: DomainSchema, domain: 'example', tld: '' as 'xyo', + } + const helper = await XnsNameHelper.fromPayload(domain) + expect(() => helper.tld).toThrow('tld not found in payload') + }) + }) + + describe('xnsName getter', () => { + it('should return the xnsName if set', async () => { + const helper = await XnsNameHelper.fromPayload(validDomain) + expect(helper.xnsName).toEqual(validDomain) + }) + + it('should throw an error if xnsName is not set', async () => { + const helper = await XnsNameHelper.fromPayload(undefined as unknown as Domain) + expect(() => helper.xnsName).toThrow('XnsNameHelper xnsName not set') + }) + }) + + describe('fromString', () => { + it('should create an instance from a valid xnsName string', () => { + const { domain, tld } = validDomain + const helper = XnsNameHelper.fromString(`${domain}.${tld}`) + expect(helper.domain).toBe(domain) + expect(helper.xnsName.domain).toBe(domain) + expect(helper.tld).toBe(tld) + expect(helper.xnsName.tld).toBe(tld) + }) + + it('should throw an error if xnsName string is invalid', () => { + expect(() => XnsNameHelper.fromString('invalid')).toThrow('Unable to parse xnsName') + }) + }) + + describe('isXnsNameOrHash', () => { + it('should return "xnsName" if the source ends with a valid TLD', () => { + expect(XnsNameHelper.isPotentialXnsNameOrHash('example.xyo')).toBe('xnsName') + }) + + it('should return "hash" if the source is a valid hash', () => { + expect(XnsNameHelper.isPotentialXnsNameOrHash('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2')).toBe('hash') + }) + + it('should return null if the source is neither a valid xnsName nor a hash', () => { + expect(XnsNameHelper.isPotentialXnsNameOrHash('invalid')).toBe(null) + }) + }) + + describe('isValid', () => { + it('should return true for valid xns names', () => { + expect(XnsNameHelper.isValid(validDomain)).toBe(true) + }) + + it('should return false for invalid xns names', () => { + const domain: Domain = { + schema: DomainSchema, domain: 'example-', tld: 'xyo', + } + expect(XnsNameHelper.isValid(domain)).toBe(false) + }) + }) + + describe('mask', () => { + const cases = [ + ['Example$123', 'example123'], + ['Example/123', 'example123'], + ['Example.123', 'example123'], + ['Example-123', 'example-123'], + ['Example 123', 'example123'], + ['Example_123', 'example123'], + ['-Example_123-', 'example123'], + ['-Example_123', 'example123'], + ['Example_123-', 'example123'], + ['--Example_123', 'example123'], + ['Example_123--', 'example123'], + ['--Example_123--', 'example123'], + ['- Example_123 -', 'example123'], + ] + + describe.each(cases)('mask(%s)', (input, expected) => { + it(`should return ${expected}`, () => { + expect(XnsNameHelper.mask(input)).toBe(expected) + }) + }) + + describe('With invalid input', () => { + it('should throw an error', () => { + expect(() => XnsNameHelper.mask('a'.repeat(MAX_DOMAIN_LENGTH + 1))) + .toThrow('Domain name too long: 129 exceeds max length: 128') + }) + }) + }) +}) diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/tsconfig.json b/packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/tsconfig.json new file mode 100644 index 000000000..e055e22d3 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/spec/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "sourceMap": true, + "inlineSources": true + }, + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/types/ValidSources.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/name/types/ValidSources.ts new file mode 100644 index 000000000..f969aff60 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/types/ValidSources.ts @@ -0,0 +1 @@ +export type ValidSourceTypes = 'xnsName' | 'hash' | null diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/name/types/index.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/name/types/index.ts new file mode 100644 index 000000000..eed3d7d98 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/name/types/index.ts @@ -0,0 +1 @@ +export * from './ValidSources.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/Constants.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/Constants.ts new file mode 100644 index 000000000..5cfb6509a --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/Constants.ts @@ -0,0 +1,2 @@ +export const MIN_DOMAIN_LENGTH = 3 +export const MAX_DOMAIN_LENGTH = 128 diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/index.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/index.ts new file mode 100644 index 000000000..804263de1 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/index.ts @@ -0,0 +1 @@ +export * from './validators.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/spec/tsconfig.json b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/spec/tsconfig.json new file mode 100644 index 000000000..e055e22d3 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/spec/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "sourceMap": true, + "inlineSources": true + }, + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/spec/validators.spec.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/spec/validators.spec.ts new file mode 100644 index 000000000..c1c2faf4e --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/spec/validators.spec.ts @@ -0,0 +1,81 @@ +import type { Domain } from '@xyo-network/xns-record-payload-plugins' +import { DomainSchema } from '@xyo-network/xns-record-payload-plugins' + +import { + getDomainReservedFragmentsValidator, getDomainReservedNamesValidator, getDomainReservedStringsValidator, +} from '../index.ts' + +const baseDomainFields: Domain = { + domain: '', + schema: DomainSchema, + tld: 'xyo', +} + +describe('XNS Name', () => { + describe('Factory Validators', () => { + const cases = [ + { + getValidator: getDomainReservedStringsValidator, + name: 'ReservedStringsValidator', + reservedList: ['foo'], + valid: ['bar'], + invalid: ['foo'], + }, + { + getValidator: getDomainReservedStringsValidator, + name: 'ReservedStringsValidator', + reservedList: [], + valid: ['bar', 'foobar'], + invalid: [''], + }, + { + getValidator: getDomainReservedFragmentsValidator, + name: 'ReservedFragmentsValidator', + reservedList: ['foo'], + valid: ['bar'], + invalid: ['foobar', 'bar-foo'], + }, + { + getValidator: getDomainReservedFragmentsValidator, + name: 'ReservedFragmentsValidator', + reservedList: [], + valid: ['foobar', 'bar-foo'], + invalid: [''], + }, + { + getValidator: getDomainReservedNamesValidator, + name: 'ReservedNamesValidator', + reservedList: ['john doe'], + valid: ['john1'], + invalid: ['johndoe', 'doejohn'], + }, + { + getValidator: getDomainReservedNamesValidator, + name: 'ReservedNamesValidator', + reservedList: [], + valid: ['john', 'doe'], + invalid: [''], + }, + ] + + describe.each(cases)('$name', ({ + getValidator, valid, invalid, reservedList, + }) => { + const validator = getValidator(reservedList) + describe('Valid', () => { + it.each(valid)('should return true for %s', (domain) => { + const payload: Domain = { ...baseDomainFields, domain } + expect(validator(payload)).toBe(true) + }) + }) + describe('Invalid', () => { + it.each(invalid)('should return false for %s', (domain) => { + if (domain) { + const payload: Domain = { ...baseDomainFields, domain } + expect(validator(payload)).toBe(false) + } + }) + }) + }) + }) +}) diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/validators.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/validators.ts new file mode 100644 index 000000000..542e79a8b --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/factory/validators.ts @@ -0,0 +1,47 @@ +import type { Payload, PayloadValidationFunction } from '@xyo-network/payload-model' +import type { DomainFields } from '@xyo-network/xns-record-payload-plugins' + +export const getDomainReservedStringsValidator = ( + reservedStrings: string[], +): PayloadValidationFunction> => { + return (payload: Payload) => { + const { domain } = payload + // Check if in one of the reserved name lists + if (reservedStrings.includes(domain)) { + console.log('Reserved name') + return false + } + return true + } +} + +export const getDomainReservedFragmentsValidator = ( + reservedFragments: string[], +): PayloadValidationFunction> => { + return (payload: Payload) => { + const { domain } = payload + // Check if any of our fragments are in the name + for (const reserved of reservedFragments) { + if (domain.includes(reserved)) { + console.log('Reserved name fragment') + return false + } + } + return true + } +} + +export const getDomainReservedNamesValidator = (reservedNames: string[]): PayloadValidationFunction> => { + return (payload: Payload) => { + const { domain } = payload + // Check if any of our fragments are in the name + for (const reserved of reservedNames) { + const parts = reserved.split(' ') + if (domain === [parts[1], parts[0]].join('') || domain === [parts[0], parts[1]].join('')) { + console.log('Reserved name') + return false + } + } + return true + } +} diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/index.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/index.ts new file mode 100644 index 000000000..2313653f3 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/index.ts @@ -0,0 +1,3 @@ +export * from './Constants.ts' +export * from './factory/index.ts' +export * from './validators/index.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/index.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/index.ts new file mode 100644 index 000000000..804263de1 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/index.ts @@ -0,0 +1 @@ +export * from './validators.ts' diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/spec/tsconfig.json b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/spec/tsconfig.json new file mode 100644 index 000000000..e055e22d3 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/spec/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "sourceMap": true, + "inlineSources": true + }, + "extends": "@xylabs/tsconfig-jest" +} \ No newline at end of file diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/spec/validators.spec.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/spec/validators.spec.ts new file mode 100644 index 000000000..f731f0e99 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/spec/validators.spec.ts @@ -0,0 +1,152 @@ +import type { Payload } from '@xyo-network/payload-model' +import { type Domain, DomainSchema } from '@xyo-network/xns-record-payload-plugins' + +import { MAX_DOMAIN_LENGTH } from '../../Constants.ts' +import { + domainCasingValidator, + domainModuleNameValidator, + domainTldValidator, + getDomainAllowedHyphensValidator, + getDomainLengthValidator, +} from '../index.ts' + +const baseDomainFields: Payload = { + domain: '', + tld: 'xyo', + schema: DomainSchema, +} + +describe('XNS Name', () => { + describe('Validators', () => { + const cases = [ + { + name: 'domainCasingValidator', + validator: domainCasingValidator, + valid: ['example'], + invalid: ['Example'], + }, + { + name: 'domainModuleNameValidator', + validator: domainModuleNameValidator, + valid: ['valid-domain'], + invalid: ['invalid_domain'], + }, + ] + + describe.each(cases)('$name', ({ + validator, valid, invalid, + }) => { + describe('Valid', () => { + it.each(valid)('should return true for %s', (domain) => { + const payload: Domain = { ...baseDomainFields, domain } + expect(validator(payload)).toBe(true) + }) + }) + + describe('Invalid', () => { + it.each(invalid)('should return false for %s', (domain) => { + const payload: Domain = { ...baseDomainFields, domain } + expect(validator(payload)).toBe(false) + }) + }) + }) + + const casesTld = [ + { + name: 'domainTldValidator', + valid: ['xyo'], + invalid: ['com', 'Xyo'], + }, + ] + + describe.each(casesTld)('$name', ({ valid, invalid }) => { + describe('Valid', () => { + it.each(valid)('should return true for %s', (tld) => { + const payload: Domain = { ...baseDomainFields, tld: tld as unknown as 'xyo' } + expect(domainTldValidator(payload)).toBe(true) + }) + }) + + describe('Invalid', () => { + it.each(invalid)('should return false for %s', (tld) => { + const payload: Domain = { ...baseDomainFields, tld: tld as unknown as 'xyo' } + expect(domainTldValidator(payload)).toBe(false) + }) + }) + }) + + const casesLength = [ + { + name: 'getDomainLengthValidator', + valid: ['abc', 'abcd'], + invalid: ['', 'a', 'a'.repeat(MAX_DOMAIN_LENGTH + 1)], + }, + ] + + describe.each(casesLength)('$name', ({ valid, invalid }) => { + describe('Valid', () => { + it.each(valid)('should return true for %s', (domain) => { + const payload: Domain = { ...baseDomainFields, domain } + expect(getDomainLengthValidator()(payload)).toBe(true) + }) + }) + + describe('Invalid', () => { + it.each(invalid)('should return false for %s', (domain) => { + const payload: Domain = { ...baseDomainFields, domain } + expect(getDomainLengthValidator()(payload)).toBe(false) + }) + }) + }) + + const casesHyphens = [ + { + name: 'getDomainAllowedHyphensValidator', + options: {}, + valid: ['example'], + invalid: ['example-', '-example', '-example-'], + }, + { + name: 'getDomainAllowedHyphensValidator', + options: { start: true }, + valid: ['example', '-example'], + invalid: ['example-', '-example-'], + }, + { + name: 'getDomainAllowedHyphensValidator', + options: { end: true }, + valid: ['example', 'example-'], + invalid: ['-example', '-example-'], + }, + { + name: 'getDomainAllowedHyphensValidator', + options: { start: true, end: true }, + valid: ['example', '-example', 'example-', '-example-'], + invalid: [''], + }, + ] + + describe.each(casesHyphens)('$name with $options', ({ + options, valid, invalid, + }) => { + const validator = getDomainAllowedHyphensValidator(options) + describe('Valid', () => { + it.each(valid)('should return true for %s', (domain) => { + const payload: Domain = { ...baseDomainFields, domain } + expect(validator(payload)).toBe(true) + }) + }) + + describe('Invalid', () => { + it.each(invalid)('should return false for %s', (domain) => { + if (domain) { + const payload: Domain = { ...baseDomainFields, domain } + expect(validator(payload)).toBe(false) + } else { + return true + } + }) + }) + }) + }) +}) diff --git a/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/validators.ts b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/validators.ts new file mode 100644 index 000000000..3756d3166 --- /dev/null +++ b/packages/payloadset/packages/xns/plugins/record/src/validation/validation/validators/validators.ts @@ -0,0 +1,93 @@ +import { isModuleName } from '@xyo-network/module-model' +import type { Payload, PayloadValidationFunction } from '@xyo-network/payload-model' +import type { DomainFields } from '@xyo-network/xns-record-payload-plugins' + +import { MAX_DOMAIN_LENGTH, MIN_DOMAIN_LENGTH } from '../Constants.ts' + +export const domainCasingValidator: PayloadValidationFunction> = ( + payload: Payload, +) => { + const { domain } = payload + // Check if all lowercase + if (domain.toLowerCase() !== domain) { + console.log('name must be lowercase') + return false + } + return true +} + +export const domainModuleNameValidator: PayloadValidationFunction> = ( + payload: Payload, +) => { + const { domain } = payload + + // check if domain is a valid name + if (!isModuleName(domain)) { + console.log(`Domain is not a valid module name: ${domain}`) + return false + } + + return true +} + +export const domainTldValidator: PayloadValidationFunction> = ( + payload: Payload, +) => { + const { tld } = payload + // Check if all lowercase + if (tld.toLowerCase() !== tld) { + console.log('TLD must be lowercase') + return false + } + // Check if supported TLDs + if (tld !== 'xyo') { + console.log('Only XYO TLD currently supported') + return false + } + return true +} + +export const getDomainLengthValidator = ( + minNameLength = MIN_DOMAIN_LENGTH, + maxLength = MAX_DOMAIN_LENGTH, +): PayloadValidationFunction> => { + return (payload: Payload) => { + const { domain } = payload + // Check if min length + if (domain.length < minNameLength) { + console.log(`name must be at least ${minNameLength} characters`) + return false + } + if (domain.length > maxLength) { + console.log(`name must be at least ${maxLength} characters`) + return false + } + return true + } +} + +export const getDomainAllowedHyphensValidator = ( + options?: { end?: boolean; start?: boolean }, +): PayloadValidationFunction> => { + return (payload: Payload) => { + const { domain } = payload + const { start, end } = options ?? {} + if (!start && domain.startsWith('-')) { + console.log('name cannot start with hyphen') + return false + } + if (!end && domain.endsWith('-')) { + console.log('name cannot end with hyphen') + return false + } + return true + } +} + +export const XnsNameSimpleValidators = [ + domainCasingValidator, + domainModuleNameValidator, + domainTldValidator, + getDomainLengthValidator(), + getDomainAllowedHyphensValidator(), +] diff --git a/yarn.lock b/yarn.lock index b9d01475c..e73e7b043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3807,7 +3807,7 @@ __metadata: languageName: node linkType: hard -"@xylabs/arraybuffer@npm:^4.0.5": +"@xylabs/arraybuffer@npm:^4.0.5, @xylabs/arraybuffer@npm:^4.0.9": version: 4.0.9 resolution: "@xylabs/arraybuffer@npm:4.0.9" dependencies: @@ -3985,7 +3985,7 @@ __metadata: languageName: node linkType: hard -"@xylabs/retry@npm:^4.0.5": +"@xylabs/retry@npm:^4.0.5, @xylabs/retry@npm:^4.0.9": version: 4.0.9 resolution: "@xylabs/retry@npm:4.0.9" dependencies: @@ -4250,6 +4250,23 @@ __metadata: languageName: node linkType: hard +"@xyo-network/account-model@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/account-model@npm:3.1.5" + dependencies: + "@xylabs/hex": "npm:^4.0.9" + "@xylabs/lodash": "npm:^4.0.9" + "@xyo-network/key-model": "npm:^3.1.5" + "@xyo-network/previous-hash-store-model": "npm:^3.1.5" + peerDependencies: + ethers: ^6 + peerDependenciesMeta: + ethers: + optional: true + checksum: 10/95fd21376a622685f2e3c43e7ea3dd09d0e79c01e48233fc22c67bc789942838783a57779c78464c35a491cac899b55562d50d3f6fa832e6d3c00dec8575a2b0 + languageName: node + linkType: hard + "@xyo-network/account@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/account@npm:3.1.4" @@ -4506,6 +4523,17 @@ __metadata: languageName: node linkType: hard +"@xyo-network/boundwitness-model@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/boundwitness-model@npm:3.1.5" + dependencies: + "@xylabs/hex": "npm:^4.0.9" + "@xylabs/object": "npm:^4.0.9" + "@xyo-network/payload-model": "npm:^3.1.5" + checksum: 10/8638dafbff2213429b3d9cd492b329602b688d805c5ff6f280c4d51448f181109c2d7fd84ed7e23d3a3081291b0bdaf95c4649c5d94fd186f261f3ebd9b559b6 + languageName: node + linkType: hard + "@xyo-network/boundwitness-validator@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/boundwitness-validator@npm:3.1.4" @@ -5212,6 +5240,19 @@ __metadata: languageName: node linkType: hard +"@xyo-network/data@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/data@npm:3.1.5" + dependencies: + "@scure/base": "npm:^1.1.7" + "@xylabs/arraybuffer": "npm:^4.0.9" + "@xylabs/assert": "npm:^4.0.9" + "@xylabs/hex": "npm:^4.0.9" + ethers: "npm:6.13.2" + checksum: 10/d32e4a0698e37c33838bdbd42bfb6c1c69bd635aa6ebd9e749b2e840e845cde777b4b91f3975819ddefa49df3ac4f4ca34ef95bdf10ee837e5002a577134a170 + languageName: node + linkType: hard + "@xyo-network/diviner-abstract@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/diviner-abstract@npm:3.1.4" @@ -6489,6 +6530,15 @@ __metadata: languageName: node linkType: hard +"@xyo-network/key-model@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/key-model@npm:3.1.5" + dependencies: + "@xyo-network/data": "npm:^3.1.5" + checksum: 10/898dfea1f364340b3bf26e48c24517f2d9d847777097ef466b31c46d5cb33c4f1e997c2e1020f83b1ae65a3f4e8da30dff7974fd80aca5108907c933342f0b8b + languageName: node + linkType: hard + "@xyo-network/location-certainty-payload-plugin@workspace:^, @xyo-network/location-certainty-payload-plugin@workspace:packages/payload/packages/location-certainty": version: 0.0.0-use.local resolution: "@xyo-network/location-certainty-payload-plugin@workspace:packages/payload/packages/location-certainty" @@ -6566,6 +6616,16 @@ __metadata: languageName: node linkType: hard +"@xyo-network/manifest-model@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/manifest-model@npm:3.1.5" + dependencies: + "@xylabs/hex": "npm:^4.0.9" + "@xyo-network/payload-model": "npm:^3.1.5" + checksum: 10/6e26b4dc2a0be004cc86387e4b6e665d78903c5b624e83f680f7c7b98833cca1865cbe4b172739174bcf8d916054b5019bb4489331255e65f63995c8d05bf1ee + languageName: node + linkType: hard + "@xyo-network/manifest-wrapper@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/manifest-wrapper@npm:3.1.4" @@ -6641,6 +6701,19 @@ __metadata: languageName: node linkType: hard +"@xyo-network/module-events@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/module-events@npm:3.1.5" + dependencies: + "@xylabs/assert": "npm:^4.0.9" + "@xylabs/error": "npm:^4.0.9" + "@xylabs/forget": "npm:^4.0.9" + "@xylabs/object": "npm:^4.0.9" + "@xylabs/promise": "npm:^4.0.9" + checksum: 10/3339f9f36b3979c404bf2ab69544174c4069f316263a695dc9428836d637050c63e6dc1e22580023e94c1969f566b644a3e5192abeb1211d33d252c4b43e201c + languageName: node + linkType: hard + "@xyo-network/module-factory-locator@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/module-factory-locator@npm:3.1.4" @@ -6715,6 +6788,27 @@ __metadata: languageName: node linkType: hard +"@xyo-network/module-model@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/module-model@npm:3.1.5" + dependencies: + "@xylabs/assert": "npm:^4.0.9" + "@xylabs/exists": "npm:^4.0.9" + "@xylabs/hex": "npm:^4.0.9" + "@xylabs/lodash": "npm:^4.0.9" + "@xylabs/logger": "npm:^4.0.9" + "@xylabs/object": "npm:^4.0.9" + "@xylabs/promise": "npm:^4.0.9" + "@xylabs/retry": "npm:^4.0.9" + "@xyo-network/account-model": "npm:^3.1.5" + "@xyo-network/boundwitness-model": "npm:^3.1.5" + "@xyo-network/manifest-model": "npm:^3.1.5" + "@xyo-network/module-events": "npm:^3.1.5" + "@xyo-network/payload-model": "npm:^3.1.5" + checksum: 10/43c2f4409eb016d98e91062ce2e90e436e8673eabb638e56356e4ba1581f06f02983828eff825e0caf1ac68bb0d78075124f751681e37507001c562d2e155534 + languageName: node + linkType: hard + "@xyo-network/module-resolver@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/module-resolver@npm:3.1.4" @@ -6923,6 +7017,16 @@ __metadata: languageName: node linkType: hard +"@xyo-network/payload-model@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/payload-model@npm:3.1.5" + dependencies: + "@xylabs/hex": "npm:^4.0.9" + "@xylabs/object": "npm:^4.0.9" + checksum: 10/fe534cba9e0e53388912ac255be9192a280c14715296d2cd5eb892f16a391a2f7f2805a68f4ffb4259d80f0b5da23d6501957142b35648f84b61ef529606bcce + languageName: node + linkType: hard + "@xyo-network/payload-plugin@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/payload-plugin@npm:3.1.4" @@ -7183,6 +7287,15 @@ __metadata: languageName: node linkType: hard +"@xyo-network/previous-hash-store-model@npm:^3.1.5": + version: 3.1.5 + resolution: "@xyo-network/previous-hash-store-model@npm:3.1.5" + dependencies: + "@xylabs/hex": "npm:^4.0.9" + checksum: 10/23d3f65c76303a02f762862bb90893c7745da3fdd6f3553538fde83ed539563ab015a5aa7799da8b0d94e123acfd978dd3d9f8cbb327493aa4732005f2401d23 + languageName: node + linkType: hard + "@xyo-network/previous-hash-store-storage@npm:^3.1.4": version: 3.1.4 resolution: "@xyo-network/previous-hash-store-storage@npm:3.1.4" @@ -7812,11 +7925,15 @@ __metadata: version: 0.0.0-use.local resolution: "@xyo-network/xns-record-payloadset-plugins@workspace:packages/payloadset/packages/xns/plugins/record" dependencies: + "@xylabs/assert": "npm:^4.0.9" "@xylabs/exists": "npm:^4.0.9" + "@xylabs/hex": "npm:^4.0.9" + "@xylabs/promise": "npm:^4.0.9" "@xylabs/ts-scripts-yarn3": "npm:^4.0.7" "@xylabs/tsconfig": "npm:^4.0.7" "@xyo-network/boundwitness-model": "npm:^3.1.4" "@xyo-network/diviner-hash-lease": "npm:^3.1.4" + "@xyo-network/module-model": "npm:^3.1.5" "@xyo-network/payload-builder": "npm:^3.1.4" "@xyo-network/payload-model": "npm:^3.1.4" "@xyo-network/xns-record-payload-plugins": "workspace:^"