UNPKG

did-uri

Version:

Decentralized Identity (DID) URI parser and formatter

481 lines (400 loc) 12.5 kB
const qs = require('qs') const DIGIT = array(10, (i) => 0x30 + i) const ALPHA = concat( array(26, (i) => 0x61 + i), // a-z array(26, (i) => 0x41 + i), // A-Z ) function code(ch) { return String(ch).charCodeAt(0) } function array(size, map) { return Array(size).fill(0).map((_, i) => map(i)) } function concat() { return Array.prototype.concat.apply([], arguments) } function inRange(value, start, end) { return value >= start && value <= end } function toLower(ch) { return String(ch).toLowerCase() } // specific-idstring = idstring *( ":" idstring ) // idstring = 1*idchar // idchar = ALPHA / DIGIT / "." / "-" / "_" function isValidIdentifierCharacter(ch) { if (!ch) { return false } return ( 0x5f == code(ch) || // '_' 0x2d == code(ch) || // '-' 0x2e == code(ch) || // '.' 0x3a == code(ch) || // ':' inRange(code(ch), 0x30, 0x39) || // 0-9 (DIGIT) inRange(code(ch), 0x61, 0x7a) || // a-z (ALPHA) inRange(code(ch), 0x41, 0x5a) // A-Z (ALPHA) ) } // method = 1*methodchar // methodchar = %x61-7A / DIGIT function isValidMethodCharacter(ch) { if (!ch) { return false } return ( inRange(code(ch), 0x30, 0x39) || inRange(code(ch), 0x61, 0x7a) ) } // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // pct-encoded = "%" HEXDIG HEXDIG // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" // / "*" / "+" / "," / ";" / "=" function isValidPathCharacter(ch) { if (!ch) { return false } const sub = ['!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '='].map(code) return concat(ALPHA, DIGIT, sub, code(':'), code('@')).indexOf(code(ch)) > -1 } // query = *( pchar / "/" / "?" ) // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // pct-encoded = "%" HEXDIG HEXDIG // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" // / "*" / "+" / "," / ";" / "=" function isValidQueryCharacter(ch) { if (!ch) { return false } const gen = [':', '/', '?', '#', '[', ']', '@'].map(code) const sub = ['!', '$', '&', "'", '(', ')', '*', '+', ',', ';', '='].map(code) const pct = ['%', '-', '_', '~'].map(code) return concat(ALPHA, DIGIT, gen, sub, pct, code('.')).indexOf(code(ch)) > -1 } // fragment = *( pchar / "/" / "?" ) // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" // pct-encoded = "%" HEXDIG HEXDIG // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" // / "*" / "+" / "," / ";" / "=" function isValidFragmentCharacter(ch) { return isValidQueryCharacter(ch) } // param = param-name [ "=" param-value ] // param-name = 1*param-char // param-value = *param-char // param-char = ALPHA / DIGIT / "." / "-" / "_" / ":" / pct-encoded function isValidParamCharacter(ch) { if (!ch) { return false } return ( 0x2e == code(ch) || // '.' 0x2d == code(ch) || // '-' 0x5f == code(ch) || // '_' 0x3a == code(ch) || // ':' inRange(code(ch), 0x30, 0x39) || // 0-9 (DIGIT) inRange(code(ch), 0x61, 0x7a) || // a-z (ALPHA) inRange(code(ch), 0x41, 0x5a) || // A-Z (ALPHA) -1 !== ['%', '-', '_', '~'].map(code).indexOf(ch) // pct-encoded ) } /** * The `DID` represents an object class similar to the * `URL` class exported by the `url` module. This object is * serializable. */ class DID { static from(uri) { return new this(format(uri)) } constructor(uri, base) { if (base && 'string' == typeof base) { if ('/' != uri[0]) { if ('/' != base.slice(-1)) { uri = '/' + uri } } uri = base + uri } this.reference = null this.did = null this.method = null this.identifier = null this.path = null this.fragment = null this.query = null this.param = null this.parameters = null this.queryParameters = null this.fragmentParameters = null Object.seal(this) Object.assign(this, parse(uri)) this.parameters = parseParam(this.param) this.queryParameters = Object.seal(Object.freeze(qs.parse(this.query))) this.fragmentParameters = Object.seal(Object.freeze(qs.parse(this.fragment))) function parseParam(param) { if (!param) { return {} } const result = {} const params = param.split(';').filter(Boolean).map((p) => p.split('=')) const values = params.map((tuple) => tuple[1]) const keys = params.map((tuple) => tuple[0]) for (let i = 0; i< params.length; ++i) { const raw = undefined === values[i] ? true : values[i] const key = keys[i] const parts = key.split('.') const depth = parts.length let value = raw if ('string' === typeof value) { try { value = JSON.parse(value) } catch (err) { value = raw } } if (depth > 1) { let scope = result const p = parts.pop() for (const k of parts) { scope[k] = scope[k] || {} scope = scope[k] } scope[p] = value } else { result[key] = value } } return Object.freeze(Object.seal(result)) } } toString() { return format(this) } toJSON() { return format(this) } valueOf() { return format(this) } } function parse(uri) { if (uri && 'object' == typeof uri && false == Array.isArray(uri)) { uri = format(uri) } if ('string' != typeof uri) { throw new TypeError("did.parse: Expecting uri to be a string.") } uri = normalize(uri) const ctx = { // https://w3c-ccg.github.io/did-spec/#the-generic-did-scheme reference: null, // did [ "/" path ] [ "#" fragment ] ] did: null, // "did:" method ":" identifier method: null, // *(%x61-7A / DIGIT) identifier: null, // *([:] ALPHA / DIGIT / "." / "-") // https://w3c-ccg.github.io/did-spec/#did-paths path: null, // [ "/" path ] // https://w3c-ccg.github.io/did-spec/#did-fragments fragment: null, // [ "#" fragment ] // https://tools.ietf.org/html/rfc3986 query: null, // https://w3c-ccg.github.io/did-spec/#method-specific-did-parameter-names param: null, } const len = uri.length let i = 0 while (peek()) { if (null == ctx.reference) { ctx.reference = '' // parse protocol first to determine if URI is valid to continue parsing for (let j = 0; j < 4; ++j) { ctx.reference += toLower(peek()) next() } // ensure correct protocol is used if ('did:' != ctx.reference) { throw new SyntaxError(`Invalid protocol (${ctx.reference}) specified.`) } else { ctx.reference += uri.slice(4) } } if (null == ctx.method) { ctx.method = '' while (':' != peek()) { if (isValidMethodCharacter(peek())) { ctx.method += next() } else { throw new SyntaxError(`Invalid method (${ctx.method || peek()}) specified.`) } } if (':' != next()) { // UNREACHABLE throw new SyntaxError(`Unexpected token '${peek()}'.`) } } if (null == ctx.identifier) { ctx.identifier = '' // break on EOL and path character prefix const breaks = [ '/', '?', '#', ';' ] while (null != peek() && -1 == breaks.indexOf(peek())) { if (isValidIdentifierCharacter(peek())) { ctx.identifier += next() } else { throw new SyntaxError(`Invalid character (${peek()}) in "idstring".`) } } } if (null == ctx.did) { if (ctx.method && ctx.identifier) { ctx.did = `did:${ctx.method}:${ctx.identifier}` } } if (null == ctx.param) { ctx.param = '' if (';' == peek()) { ctx.param += next() if (false == isValidParamCharacter(peek())) { throw new SyntaxError(`Invalid character (${peek()}) in "param".`) } // break on EOL, query search prefix, and hash fragment prefix const breaks = [ '/', '?', '#' ] while (null != peek() && -1 == breaks.indexOf(peek())) { if (';' == peek() || '=' == peek() || isValidParamCharacter(peek())) { ctx.param += next() } else { throw new SyntaxError(`Invalid character (${peek()}) in "param".`) } } } } if (null == ctx.path) { ctx.path = '' if ('/' == peek()) { ctx.path += next() // ensure next character is a valid path character before // proceeding to parse if ( false == isValidPathCharacter(peek()) && false == isValidQueryCharacter(peek()) && false == isValidFragmentCharacter(peek()) ) { throw new SyntaxError(`Invalid character (${peek()}) in "path".`) } // break on EOL, query search prefix, and hash fragment prefix while (null != peek() && '?' != peek() && '#' != peek()) { if ('/' == peek() || isValidPathCharacter(peek())) { ctx.path += next() } else { throw new SyntaxError(`Invalid character (${peek()}) in "path".`) } } } } if (null == ctx.query) { ctx.query = '' if ('?' == peek()) { // ensure next character is a valid path character before // proceeding to parse if (false == isValidQueryCharacter(peek(1))) { throw new SyntaxError(`Invalid character (${peek(1)}) in "query".`) } else { next() } while (null != peek() && '#' != peek()) { if (isValidQueryCharacter(peek())) { ctx.query += next() } else { throw new SyntaxError(`Invalid character (${peek()}) in "query".`) } } } } if (null == ctx.fragment) { ctx.fragment = '' if ('#' == peek()) { // ensure next character is a valid path character before // proceeding to parse if (false == isValidFragmentCharacter(peek(1))) { throw new SyntaxError(`Invalid character (${peek(1)}) in "fragment".`) } else { next() } while (null != peek() && '#' != peek()) { if (isValidFragmentCharacter(peek())) { ctx.fragment += next() } else { throw new SyntaxError(`Invalid character (${peek()}) in "fragment".`) } } } } next() } return ctx function next() { return uri[i++] } function peek(direction = 0) { return uri[i + direction] } } function normalize(uri) { if (uri && 'object' == typeof uri && false == Array.isArray(uri)) { uri = format(uri) } if ('string' != typeof uri) { throw new TypeError("did.normalize: Expecting uri to be a string.") } // remove dead padding uri = uri.trim() // remove newlines uri = uri.replace(/\n|\r/g, '') // normalize' did://' uri = uri.replace(/^did:\/\//g, 'did:') // prepend `did:?` if (false == /^did:/.test(uri)) { if (/^:/.test(uri)) { uri = `did${uri}` } else { uri = `did:${uri}` } } return uri } function format(obj) { if ('string' == typeof obj) { obj = parse(obj) } else if (!obj || 'object' != typeof obj || Array.isArray(obj)) { throw new TypeError("did.format: Expecting object.") } else if (!obj.method && !obj.identifier && (!obj.did || 'string' != typeof obj.did)) { throw new TypeError( "did.format: Expecting 'did' string in object or 'method' and 'identifier' to be defined." ) } let uri = '' if (obj.did) { uri += obj.did } else if (obj.method && obj.identifier) { uri += `did:${obj.method}:${obj.identifier}` } if (obj.param && 'string' == typeof obj.param) { uri += obj.param } if (obj.path && 'string' == typeof obj.path) { uri += obj.path } if (obj.query && 'string' == typeof obj.query) { uri += '?' + obj.query } if (obj.fragment && 'string' == typeof obj.fragment) { uri += '#' + obj.fragment } parse(uri) return uri } module.exports = Object.assign(DID, { normalize, format, parse, DID, })