Skip to content

Commit

Permalink
helpers for rkey and tid syntax; validate rkey at record creation time (
Browse files Browse the repository at this point in the history
bluesky-social#1738)

* syntax: fix jest config displayName

* syntax: TID validation

* syntax: add recordkey validation

* pds: verify rkey syntax at record creation time

---------

Co-authored-by: dholms <[email protected]>
  • Loading branch information
bnewbold and dholms authored Dec 1, 2023
1 parent c17971a commit 3be9c74
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 2 deletions.
15 changes: 15 additions & 0 deletions interop-test-files/syntax/tid_syntax_invalid.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

# not base32
3jzfcijpj2z21
0000000000000

# too long/short
3jzfcijpj2z2aa
3jzfcijpj2z2

# old dashes syntax not actually supported (TTTT-TTT-TTTT-CC)
3jzf-cij-pj2z-2a

# high bit can't be high
zzzzzzzzzzzzz
kjzfcijpj2z2a
6 changes: 6 additions & 0 deletions interop-test-files/syntax/tid_syntax_valid.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 13 digits
# 234567abcdefghijklmnopqrstuvwxyz

3jzfcijpj2z2a
7777777777777
3zzzzzzzzzzzz
4 changes: 4 additions & 0 deletions packages/pds/src/api/com/atproto/repo/createRecord.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CID } from 'multiformats/cid'
import { InvalidRequestError, AuthRequiredError } from '@atproto/xrpc-server'
import { InvalidRecordKeyError } from '@atproto/syntax'
import { prepareCreate } from '../../../../repo'
import { Server } from '../../../../lexicon'
import {
Expand Down Expand Up @@ -59,6 +60,9 @@ export default function (server: Server, ctx: AppContext) {
if (err instanceof InvalidRecordError) {
throw new InvalidRequestError(err.message)
}
if (err instanceof InvalidRecordKeyError) {
throw new InvalidRequestError(err.message)
}
throw err
}

Expand Down
8 changes: 7 additions & 1 deletion packages/pds/src/repo/prepare.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { CID } from 'multiformats/cid'
import { AtUri, ensureValidDatetime } from '@atproto/syntax'
import {
AtUri,
ensureValidRecordKey,
ensureValidDatetime,
} from '@atproto/syntax'
import { MINUTE, TID, dataToCborBlock } from '@atproto/common'
import {
LexiconDefNotFoundError,
Expand Down Expand Up @@ -188,6 +192,8 @@ export const prepareCreate = async (opts: {
}

const rkey = opts.rkey || nextRkey.toString()
// @TODO: validate against Lexicon record 'key' type, not just overall recordkey syntax
ensureValidRecordKey(rkey)
assertNoExplicitSlurs(rkey, record)
return {
action: WriteOpAction.Create,
Expand Down
16 changes: 16 additions & 0 deletions packages/pds/tests/crud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,22 @@ describe('crud operations', () => {
)
})

it('requires valid rkey', async () => {
await expect(
aliceAgent.api.com.atproto.repo.createRecord({
repo: alice.did,
collection: 'app.bsky.feed.generator',
record: {
$type: 'app.bsky.feed.generator',
did: 'did:web:dummy.example.com',
displayName: 'dummy',
createdAt: new Date().toISOString(),
},
rkey: '..',
}),
).rejects.toThrow('record key can not be "." or ".."')
})

it('validates the record on write', async () => {
await expect(
aliceAgent.api.com.atproto.repo.createRecord({
Expand Down
2 changes: 1 addition & 1 deletion packages/syntax/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ const base = require('../../jest.config.base.js')

module.exports = {
...base,
displayName: 'Identifier',
displayName: 'Syntax',
}
2 changes: 2 additions & 0 deletions packages/syntax/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ export * from './handle'
export * from './did'
export * from './nsid'
export * from './aturi'
export * from './tid'
export * from './recordkey'
export * from './datetime'
26 changes: 26 additions & 0 deletions packages/syntax/src/recordkey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const ensureValidRecordKey = (rkey: string): void => {
if (rkey.length > 512 || rkey.length < 1) {
throw new InvalidRecordKeyError('record key must be 1 to 512 characters')
}
// simple regex to enforce most constraints via just regex and length.
if (!/^[a-zA-Z0-9_~.-]{1,512}$/.test(rkey)) {
throw new InvalidRecordKeyError('record key syntax not valid (regex)')
}
if (rkey == '.' || rkey == '..')
throw new InvalidRecordKeyError('record key can not be "." or ".."')
}

export const isValidRecordKey = (rkey: string): boolean => {
try {
ensureValidRecordKey(rkey)
} catch (err) {
if (err instanceof InvalidRecordKeyError) {
return false
}
throw err
}

return true
}

export class InvalidRecordKeyError extends Error {}
24 changes: 24 additions & 0 deletions packages/syntax/src/tid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const ensureValidTid = (tid: string): void => {
if (tid.length != 13) {
throw new InvalidTidError('TID must be 13 characters')
}
// simple regex to enforce most constraints via just regex and length.
if (!/^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$/.test(tid)) {
throw new InvalidTidError('TID syntax not valid (regex)')
}
}

export const isValidTid = (tid: string): boolean => {
try {
ensureValidTid(tid)
} catch (err) {
if (err instanceof InvalidTidError) {
return false
}
throw err
}

return true
}

export class InvalidTidError extends Error {}
1 change: 1 addition & 0 deletions packages/syntax/tests/interop-files/tid_syntax_invalid.txt
1 change: 1 addition & 0 deletions packages/syntax/tests/interop-files/tid_syntax_valid.txt
42 changes: 42 additions & 0 deletions packages/syntax/tests/recordkey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ensureValidRecordKey, InvalidRecordKeyError } from '../src'
import * as readline from 'readline'
import * as fs from 'fs'

describe('recordkey validation', () => {
const expectValid = (r: string) => {
ensureValidRecordKey(r)
}
const expectInvalid = (r: string) => {
expect(() => ensureValidRecordKey(r)).toThrow(InvalidRecordKeyError)
}

it('conforms to interop valid recordkey', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/recordkey_syntax_valid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length == 0) {
return
}
expectValid(line)
})
})

it('conforms to interop invalid recordkeys', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/recordkey_syntax_invalid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length == 0) {
return
}
expectInvalid(line)
})
})
})
42 changes: 42 additions & 0 deletions packages/syntax/tests/tid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ensureValidTid, InvalidTidError } from '../src'
import * as readline from 'readline'
import * as fs from 'fs'

describe('tid validation', () => {
const expectValid = (t: string) => {
ensureValidTid(t)
}
const expectInvalid = (t: string) => {
expect(() => ensureValidTid(t)).toThrow(InvalidTidError)
}

it('conforms to interop valid tid', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/tid_syntax_valid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length == 0) {
return
}
expectValid(line)
})
})

it('conforms to interop invalid tids', () => {
const lineReader = readline.createInterface({
input: fs.createReadStream(
`${__dirname}/interop-files/tid_syntax_invalid.txt`,
),
terminal: false,
})
lineReader.on('line', (line) => {
if (line.startsWith('#') || line.length == 0) {
return
}
expectInvalid(line)
})
})
})

0 comments on commit 3be9c74

Please sign in to comment.