Matthieu Sieben f9dc9aa4c9
Permission set (#4108)
* 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>
2025-08-29 12:19:19 +02:00

199 lines
5.5 KiB
TypeScript

import * as fs from 'node:fs'
import {
InvalidNsidError,
NSID,
ensureValidNsid,
isValidNsid,
parseNsid,
validateNsid,
validateNsidRegex,
} from '../src'
describe('NSID parsing & creation', () => {
it('parses valid NSIDs', () => {
expect(NSID.parse('com.example.foo').authority).toBe('example.com')
expect(NSID.parse('com.example.foo').name).toBe('foo')
expect(NSID.parse('com.example.foo').toString()).toBe('com.example.foo')
expect(NSID.parse('com.long-thing1.cool.fooBarBaz').authority).toBe(
'cool.long-thing1.com',
)
expect(NSID.parse('com.long-thing1.cool.fooBarBaz').name).toBe('fooBarBaz')
expect(NSID.parse('com.long-thing1.cool.fooBarBaz').toString()).toBe(
'com.long-thing1.cool.fooBarBaz',
)
})
it('creates valid NSIDs', () => {
expect(NSID.create('example.com', 'foo').authority).toBe('example.com')
expect(NSID.create('example.com', 'foo').name).toBe('foo')
expect(NSID.create('example.com', 'foo').toString()).toBe('com.example.foo')
expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').authority).toBe(
'cool.long-thing1.com',
)
expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').name).toBe(
'fooBarBaz',
)
expect(NSID.create('cool.long-thing1.com', 'fooBarBaz').toString()).toBe(
'com.long-thing1.cool.fooBarBaz',
)
})
})
describe('NSID validation', () => {
const expectValid = (h: string) => {
expect(isValidNsid(h)).toBe(true)
ensureValidNsid(h)
expect(parseNsid(h)).toEqual(h.split('.'))
expect(validateNsidRegex(h)).toMatchObject({
success: true,
value: expect.any(String),
})
expect(validateNsid(h)).toMatchObject({
success: true,
value: expect.any(Array),
})
}
const expectInvalid = (h: string) => {
expect(isValidNsid(h)).toBe(false)
expect(() => parseNsid(h)).toThrow(InvalidNsidError)
expect(() => ensureValidNsid(h)).toThrow(InvalidNsidError)
expect(validateNsidRegex(h)).toMatchObject({
success: false,
message: expect.any(String),
})
expect(validateNsid(h)).toMatchObject({
success: false,
message: expect.any(String),
})
}
it('enforces spec details', () => {
expectValid('com.example.foo')
const longNsid = 'com.' + 'o'.repeat(63) + '.foo'
expectValid(longNsid)
const tooLongNsid = 'com.' + 'o'.repeat(64) + '.foo'
expectInvalid(tooLongNsid)
const longEnd = 'com.example.' + 'o'.repeat(63)
expectValid(longEnd)
const tooLongEnd = 'com.example.' + 'o'.repeat(64)
expectInvalid(tooLongEnd)
const longOverall = 'com.' + 'middle.'.repeat(40) + 'foo'
expect(longOverall.length).toBe(287)
expectValid(longOverall)
const tooLongOverall = 'com.' + 'middle.'.repeat(50) + 'foo'
expect(tooLongOverall.length).toBe(357)
expectInvalid(tooLongOverall)
})
describe('valid NSIDs', () => {
for (const validNsid of [
'com.example.foo',
'o'.repeat(63) + '.foo.bar',
'com.' + 'o'.repeat(63) + '.foo',
'com.example.' + 'o'.repeat(63),
'com.' + 'middle.'.repeat(40) + 'foo',
'a-0.b-1.c',
'a.0.c',
'a.b.c',
'a0.b1.c3',
'a0.b1.cc',
'a01.thing.record',
'cn.8.lex.stuff',
'com.example.f00',
'com.example.fooBar',
'm.xn--masekowski-d0b.pl',
'net.users.bob.ping',
'one.2.three',
'one.two.three',
'one.two.three.four-and.FiVe',
'onion.2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
'onion.expyuzz4wqqyqhjn.spec.getThing',
'onion.g2zyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.lex.deleteThing',
'org.4chan.lex.getThing',
'test.12345.record',
'xn--fiqs8s.xn--fiqa61au8b7zsevnm8ak20mc4a87e.record.two',
]) {
it(validNsid, () => {
expectValid(validNsid)
})
}
})
describe('invalid NSIDs', () => {
for (const invalidNsid of [
'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',
]) {
it(invalidNsid, () => {
expect(validateNsid(invalidNsid)).toMatchObject({
success: false,
message: expect.any(String),
})
})
}
})
describe('conforms to interop valid NSIDs', () => {
for (const line of fs
.readFileSync(`${__dirname}/interop-files/nsid_syntax_valid.txt`)
.toString()
.split('\n')) {
if (line.startsWith('#') || line.length === 0) {
continue
}
it(line, () => {
expectValid(line)
})
}
})
describe('conforms to interop invalid NSIDs', () => {
for (const line of fs
.readFileSync(`${__dirname}/interop-files/nsid_syntax_invalid.txt`)
.toString()
.split('\n')) {
if (line.startsWith('#') || line.length === 0) {
continue
}
it(line, () => {
expectInvalid(line)
})
}
})
})