* Export constants and type assertion utilities * Add permission set support to oauth provider * improve permission set parsing * Rename `PermissionSet` to `ScopePermissions` * Improve performance of NSID validation * Add support for `permission-set` in lexicon document * Validate NSID syntax using `@atproto/syntax` * Export all types used in public interfaces (from `lexicon-resolver`) * Small performance improvement * Rework scope parsing utilities to work with Lexicon defined permissions * file rename * fixup! Rework scope parsing utilities to work with Lexicon defined permissions * removed outdated comment * removed outdated comment * fix comment typo * Improve `SimpleStore` api * permission-set NSID auth scopes * Remove dev dependency on dev-env * fix build script * pnpm-lock * Improve fetch-node unicast protection * Explicitly set the `redirect: "follow"` `fetch()` option * Add delay when building oauth-provider-ui in watch mode * Remove external dependencies from auth-scopes * Add customizable lexicon authority to pds (for dev purposes) * fix pds migration * update permission-set icon * Add support for `include:` syntax in scopes * tidy * Renaming of "resource" concept to better reflect the fact that not all oauth scope values are about resources * changeset * ui improvmeents * i18n * ui imporvements * add `AtprotoAudience` type * Enforce proper formatting of audience (atproto supported did + fragment part) * tidy * tidy * tidy * fix ci ? * ci fix ? * tidy ? * Apply consistent outline around focusable items * Use `inheritAud: true` to control `aud` inheritance * Update packages/oauth/oauth-provider/src/lexicon/lexicon-manager.ts Co-authored-by: devin ivy <devinivy@gmail.com> * Review comments * Add `nsid` property to `LexiconResolutionError` * improve nsid validation * i18n * Improve oauth scope parsing * Simplify lex scope parsing * tidy * docs * tidy * ci * Code simplification * tidy * improve type safety * improve deps graph * naming * Improve tests and package structure * Improve error when resolving a non permission-set * improve nsid parsing perfs * benchmark * Refactor ozone and lexicon into using a common service profile mechanism * improve perfs * ci fix (?) * tidy * Allow storage of valid lexicons in lexicon store * Improve handling of lexicon resolution failures * review comment * Test both regexp and non regexp based nsid validation * properly detect presence of port number in https did:web * Re-enable logging of `safeFetch` requests * tidy --------- Co-authored-by: devin ivy <devinivy@gmail.com>
209 lines
5.2 KiB
JavaScript
209 lines
5.2 KiB
JavaScript
/* eslint-env node, commonjs */
|
|
|
|
const { validateNsid, validateNsidRegex } = require('.')
|
|
|
|
// $ node benchmark.js
|
|
// valid NSIDs {
|
|
// parsed: 181.56524884700775,
|
|
// regexp: 77.61082607507706,
|
|
// optimized: 60.183539509773254
|
|
// }
|
|
// invalid NSIDs {
|
|
// parsed: 128.7685609459877,
|
|
// regexp: 108.75775015354156,
|
|
// optimized: 53.196488440036774
|
|
// }
|
|
|
|
bench('valid NSIDs', true, [
|
|
'com.example.foo',
|
|
'o'.repeat(63) + '.foo.bar',
|
|
'com.' + 'o'.repeat(63) + '.foo',
|
|
'com.example.' + 'o'.repeat(63),
|
|
'com.' + 'middle.'.repeat(40) + 'foo',
|
|
'com.example.fooBar',
|
|
'net.users.bob.ping',
|
|
'a.b.c',
|
|
'm.xn--masekowski-d0b.pl',
|
|
'one.two.three',
|
|
'one.two.three.four-and.FiVe',
|
|
'one.2.three',
|
|
'a-0.b-1.c',
|
|
'a0.b1.cc',
|
|
'cn.8.lex.stuff',
|
|
'test.12345.record',
|
|
'a01.thing.record',
|
|
'a.0.c',
|
|
'xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two',
|
|
'a0.b1.c3',
|
|
'com.example.f00',
|
|
'onion.expyuzz4wqqyqhjn.spec.getThing',
|
|
'onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
|
|
'org.4chan.lex.getThing',
|
|
'cn.8.lex.stuff',
|
|
'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
|
|
'a.'.repeat(158) + 'a',
|
|
])
|
|
|
|
bench('invalid NSIDs', false, [
|
|
'a.'.repeat(158) + '9',
|
|
'a.'.repeat(154) + 'a😅.9',
|
|
'o'.repeat(64) + '.foo.bar',
|
|
'com.' + 'o'.repeat(64) + '.foo',
|
|
'com.example.' + 'o'.repeat(64),
|
|
'com.' + 'middle.'.repeat(50) + 'foo',
|
|
'com.example.foo.*',
|
|
'com.example.foo.blah*',
|
|
'com.example.foo.*blah',
|
|
'com.exa💩ple.thing',
|
|
'a-0.b-1.c-3',
|
|
'a-0.b-1.c-o',
|
|
'1.0.0.127.record',
|
|
'0two.example.foo',
|
|
'example.com',
|
|
'com.example',
|
|
'a.',
|
|
'.one.two.three',
|
|
'one.two.three ',
|
|
'one.two..three',
|
|
'one .two.three',
|
|
' one.two.three',
|
|
'com.atproto.feed.p@st',
|
|
'com.atproto.feed.p_st',
|
|
'com.atproto.feed.p*st',
|
|
'com.atproto.feed.po#t',
|
|
'com.atproto.feed.p!ot',
|
|
'com.example-.foo',
|
|
'com.-example.foo',
|
|
'com.example.0foo',
|
|
'com.example.f-o',
|
|
])
|
|
|
|
function bench(name, expectedResult, cases) {
|
|
const validators = {
|
|
parsed: (nsid) => validateNsid(nsid).success,
|
|
regexp: (nsid) => validateNsidRegex(nsid).success,
|
|
optimized: (nsid) => validateNsidOptimized(nsid).success,
|
|
}
|
|
|
|
const times = Object.fromEntries(Object.keys(validators).map((k) => [k, 0]))
|
|
|
|
for (let i = 0; i < 1000; i++) {
|
|
for (const [name, fn] of Object.entries(validators)) {
|
|
const start = performance.now()
|
|
for (let j = 0; j < 20; j++) {
|
|
for (const value of cases) {
|
|
if (fn(value) !== expectedResult) {
|
|
throw new Error(`Validator ${name} gave wrong result`)
|
|
}
|
|
}
|
|
}
|
|
times[name] += performance.now() - start
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
name,
|
|
Object.fromEntries(
|
|
Object.entries(times).map(([k, v]) => [k, `${v.toFixed(2)} ms`]),
|
|
),
|
|
)
|
|
}
|
|
|
|
/** @param value {string} */
|
|
function validateNsidOptimized(value) {
|
|
const { length } = value
|
|
if (length > 253 + 1 + 63) {
|
|
return { success: false, message: 'NSID is too long (317 chars max)' }
|
|
}
|
|
|
|
let partCount = 1
|
|
let partStart = 0
|
|
let partHasLeadingDigit = false
|
|
let partHasHyphen = false
|
|
|
|
let charCode
|
|
for (let i = 0; i < length; i++) {
|
|
charCode = value.charCodeAt(i)
|
|
|
|
// Hot path: check frequent chars first
|
|
if (
|
|
(charCode >= 97 && charCode <= 122) /* a-z */ ||
|
|
(charCode >= 65 && charCode <= 90) /* A-Z */
|
|
) {
|
|
// All good
|
|
} else if (charCode >= 48 && charCode <= 57 /* 0-9 */) {
|
|
if (i === 0) {
|
|
return {
|
|
success: false,
|
|
message: 'NSID first part may not start with a digit',
|
|
}
|
|
}
|
|
|
|
// All good
|
|
|
|
if (i === partStart) {
|
|
partHasLeadingDigit = true
|
|
}
|
|
} else if (charCode === 45 /* - */) {
|
|
if (i === partStart) {
|
|
return {
|
|
success: false,
|
|
message: 'NSID part can not start with hyphen',
|
|
}
|
|
}
|
|
if (i === length - 1 || value.charCodeAt(i + 1) === 46 /* . */) {
|
|
return { success: false, message: 'NSID part can not end with hyphen' }
|
|
}
|
|
|
|
// All good
|
|
|
|
partHasHyphen = true
|
|
} else if (charCode === 46 /* . */) {
|
|
// Check prev part size
|
|
if (i === partStart) {
|
|
return { success: false, message: 'NSID parts can not be empty' }
|
|
}
|
|
if (i - partStart > 63) {
|
|
return { success: false, message: 'NSID part too long (max 63 chars)' }
|
|
}
|
|
|
|
// All good
|
|
|
|
partCount++
|
|
partStart = i + 1
|
|
partHasHyphen = false
|
|
partHasLeadingDigit = false
|
|
} else {
|
|
return {
|
|
success: false,
|
|
message:
|
|
'Disallowed characters in NSID (ASCII letters, digits, dashes, periods only)',
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check last part size
|
|
if (length === partStart) {
|
|
return { success: false, message: 'NSID parts can not be empty' }
|
|
}
|
|
if (length - partStart > 63) {
|
|
return { success: false, message: 'NSID part too long (max 63 chars)' }
|
|
}
|
|
|
|
// Check last part chars
|
|
if (partHasHyphen || partHasLeadingDigit) {
|
|
return {
|
|
success: false,
|
|
message:
|
|
'NSID name part must be only letters and digits (and no leading digit)',
|
|
}
|
|
}
|
|
|
|
// Check part count
|
|
if (partCount < 3) {
|
|
return { success: false, message: 'NSID needs at least three parts' }
|
|
}
|
|
|
|
return { success: true, value }
|
|
}
|