Skip to content

Commit

Permalink
Make lexicon validation strict (bluesky-social#1088)
Browse files Browse the repository at this point in the history
This makes the whole type of bugs like bluesky-social#1080 impossible,
Also fixes `app.bsky.feed.defs` in the bsky package as it was now throwing errors

Co-authored-by: Daniel Holmgren <[email protected]>
  • Loading branch information
Matrix89 and dholms authored May 26, 2023
1 parent 41330ca commit 18c9924
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 116 deletions.
2 changes: 1 addition & 1 deletion packages/bsky/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4403,7 +4403,7 @@ export const schemaDict = {
properties: {
repost: {
type: 'string',
ref: 'at-uri',
format: 'at-uri',
},
},
},
Expand Down
271 changes: 156 additions & 115 deletions packages/lexicon/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@ import { requiredPropertiesRefinement } from './util'
// primitives
// =

export const lexBoolean = z.object({
type: z.literal('boolean'),
description: z.string().optional(),
default: z.boolean().optional(),
const: z.boolean().optional(),
})
export const lexBoolean = z
.object({
type: z.literal('boolean'),
description: z.string().optional(),
default: z.boolean().optional(),
const: z.boolean().optional(),
})
.strict()
export type LexBoolean = z.infer<typeof lexBoolean>

export const lexInteger = z.object({
type: z.literal('integer'),
description: z.string().optional(),
default: z.number().int().optional(),
minimum: z.number().int().optional(),
maximum: z.number().int().optional(),
enum: z.number().int().array().optional(),
const: z.number().int().optional(),
})
export const lexInteger = z
.object({
type: z.literal('integer'),
description: z.string().optional(),
default: z.number().int().optional(),
minimum: z.number().int().optional(),
maximum: z.number().int().optional(),
enum: z.number().int().array().optional(),
const: z.number().int().optional(),
})
.strict()
export type LexInteger = z.infer<typeof lexInteger>

export const lexStringFormat = z.enum([
Expand All @@ -36,25 +40,29 @@ export const lexStringFormat = z.enum([
])
export type LexStringFormat = z.infer<typeof lexStringFormat>

export const lexString = z.object({
type: z.literal('string'),
format: lexStringFormat.optional(),
description: z.string().optional(),
default: z.string().optional(),
minLength: z.number().int().optional(),
maxLength: z.number().int().optional(),
minGraphemes: z.number().int().optional(),
maxGraphemes: z.number().int().optional(),
enum: z.string().array().optional(),
const: z.string().optional(),
knownValues: z.string().array().optional(),
})
export const lexString = z
.object({
type: z.literal('string'),
format: lexStringFormat.optional(),
description: z.string().optional(),
default: z.string().optional(),
minLength: z.number().int().optional(),
maxLength: z.number().int().optional(),
minGraphemes: z.number().int().optional(),
maxGraphemes: z.number().int().optional(),
enum: z.string().array().optional(),
const: z.string().optional(),
knownValues: z.string().array().optional(),
})
.strict()
export type LexString = z.infer<typeof lexString>

export const lexUnknown = z.object({
type: z.literal('unknown'),
description: z.string().optional(),
})
export const lexUnknown = z
.object({
type: z.literal('unknown'),
description: z.string().optional(),
})
.strict()
export type LexUnknown = z.infer<typeof lexUnknown>

export const lexPrimitive = z.discriminatedUnion('type', [
Expand All @@ -68,18 +76,22 @@ export type LexPrimitive = z.infer<typeof lexPrimitive>
// ipld types
// =

export const lexBytes = z.object({
type: z.literal('bytes'),
description: z.string().optional(),
maxLength: z.number().optional(),
minLength: z.number().optional(),
})
export const lexBytes = z
.object({
type: z.literal('bytes'),
description: z.string().optional(),
maxLength: z.number().optional(),
minLength: z.number().optional(),
})
.strict()
export type LexBytes = z.infer<typeof lexBytes>

export const lexCidLink = z.object({
type: z.literal('cid-link'),
description: z.string().optional(),
})
export const lexCidLink = z
.object({
type: z.literal('cid-link'),
description: z.string().optional(),
})
.strict()
export type LexCidLink = z.infer<typeof lexCidLink>

export const lexIpldType = z.discriminatedUnion('type', [lexBytes, lexCidLink])
Expand All @@ -88,19 +100,23 @@ export type LexIpldType = z.infer<typeof lexIpldType>
// references
// =

export const lexRef = z.object({
type: z.literal('ref'),
description: z.string().optional(),
ref: z.string(),
})
export const lexRef = z
.object({
type: z.literal('ref'),
description: z.string().optional(),
ref: z.string(),
})
.strict()
export type LexRef = z.infer<typeof lexRef>

export const lexRefUnion = z.object({
type: z.literal('union'),
description: z.string().optional(),
refs: z.string().array(),
closed: z.boolean().optional(),
})
export const lexRefUnion = z
.object({
type: z.literal('union'),
description: z.string().optional(),
refs: z.string().array(),
closed: z.boolean().optional(),
})
.strict()
export type LexRefUnion = z.infer<typeof lexRefUnion>

export const lexRefVariant = z.discriminatedUnion('type', [lexRef, lexRefUnion])
Expand All @@ -109,37 +125,45 @@ export type LexRefVariant = z.infer<typeof lexRefVariant>
// blobs
// =

export const lexBlob = z.object({
type: z.literal('blob'),
description: z.string().optional(),
accept: z.string().array().optional(),
maxSize: z.number().optional(),
})
export const lexBlob = z
.object({
type: z.literal('blob'),
description: z.string().optional(),
accept: z.string().array().optional(),
maxSize: z.number().optional(),
})
.strict()
export type LexBlob = z.infer<typeof lexBlob>

// complex types
// =

export const lexArray = z.object({
type: z.literal('array'),
description: z.string().optional(),
items: z.union([lexPrimitive, lexIpldType, lexBlob, lexRefVariant]),
minLength: z.number().int().optional(),
maxLength: z.number().int().optional(),
})
export const lexArray = z
.object({
type: z.literal('array'),
description: z.string().optional(),
items: z.union([lexPrimitive, lexIpldType, lexBlob, lexRefVariant]),
minLength: z.number().int().optional(),
maxLength: z.number().int().optional(),
})
.strict()
export type LexArray = z.infer<typeof lexArray>

export const lexPrimitiveArray = lexArray.merge(
z.object({
items: lexPrimitive,
}),
z
.object({
items: lexPrimitive,
})
.strict(),
)
export type LexPrimitiveArray = z.infer<typeof lexPrimitiveArray>

export const lexToken = z.object({
type: z.literal('token'),
description: z.string().optional(),
})
export const lexToken = z
.object({
type: z.literal('token'),
description: z.string().optional(),
})
.strict()
export type LexToken = z.infer<typeof lexToken>

export const lexObject = z
Expand All @@ -154,6 +178,7 @@ export const lexObject = z
)
.optional(),
})
.strict()
.superRefine(requiredPropertiesRefinement)
export type LexObject = z.infer<typeof lexObject>

Expand All @@ -167,68 +192,83 @@ export const lexXrpcParameters = z
required: z.string().array().optional(),
properties: z.record(z.union([lexPrimitive, lexPrimitiveArray])),
})
.strict()
.superRefine(requiredPropertiesRefinement)
export type LexXrpcParameters = z.infer<typeof lexXrpcParameters>

export const lexXrpcBody = z.object({
description: z.string().optional(),
encoding: z.string(),
schema: z.union([lexRefVariant, lexObject]).optional(),
})
export const lexXrpcBody = z
.object({
description: z.string().optional(),
encoding: z.string(),
schema: z.union([lexRefVariant, lexObject]).optional(),
})
.strict()
export type LexXrpcBody = z.infer<typeof lexXrpcBody>

export const lexXrpcSubscriptionMessage = z.object({
description: z.string().optional(),
schema: z.union([lexRefVariant, lexObject]).optional(),
})
export const lexXrpcSubscriptionMessage = z
.object({
description: z.string().optional(),
schema: z.union([lexRefVariant, lexObject]).optional(),
})
.strict()
export type LexXrpcSubscriptionMessage = z.infer<
typeof lexXrpcSubscriptionMessage
>

export const lexXrpcError = z.object({
name: z.string(),
description: z.string().optional(),
})
export const lexXrpcError = z
.object({
name: z.string(),
description: z.string().optional(),
})
.strict()
export type LexXrpcError = z.infer<typeof lexXrpcError>

export const lexXrpcQuery = z.object({
type: z.literal('query'),
description: z.string().optional(),
parameters: lexXrpcParameters.optional(),
output: lexXrpcBody.optional(),
errors: lexXrpcError.array().optional(),
})
export const lexXrpcQuery = z
.object({
type: z.literal('query'),
description: z.string().optional(),
parameters: lexXrpcParameters.optional(),
output: lexXrpcBody.optional(),
errors: lexXrpcError.array().optional(),
})
.strict()
export type LexXrpcQuery = z.infer<typeof lexXrpcQuery>

export const lexXrpcProcedure = z.object({
type: z.literal('procedure'),
description: z.string().optional(),
parameters: lexXrpcParameters.optional(),
input: lexXrpcBody.optional(),
output: lexXrpcBody.optional(),
errors: lexXrpcError.array().optional(),
})
export const lexXrpcProcedure = z
.object({
type: z.literal('procedure'),
description: z.string().optional(),
parameters: lexXrpcParameters.optional(),
input: lexXrpcBody.optional(),
output: lexXrpcBody.optional(),
errors: lexXrpcError.array().optional(),
})
.strict()
export type LexXrpcProcedure = z.infer<typeof lexXrpcProcedure>

export const lexXrpcSubscription = z.object({
type: z.literal('subscription'),
description: z.string().optional(),
parameters: lexXrpcParameters.optional(),
message: lexXrpcSubscriptionMessage.optional(),
infos: lexXrpcError.array().optional(),
errors: lexXrpcError.array().optional(),
})
export const lexXrpcSubscription = z
.object({
type: z.literal('subscription'),
description: z.string().optional(),
parameters: lexXrpcParameters.optional(),
message: lexXrpcSubscriptionMessage.optional(),
infos: lexXrpcError.array().optional(),
errors: lexXrpcError.array().optional(),
})
.strict()
export type LexXrpcSubscription = z.infer<typeof lexXrpcSubscription>

// database
// =

export const lexRecord = z.object({
type: z.literal('record'),
description: z.string().optional(),
key: z.string().optional(),
record: lexObject,
})
export const lexRecord = z
.object({
type: z.literal('record'),
description: z.string().optional(),
key: z.string().optional(),
record: lexObject,
})
.strict()
export type LexRecord = z.infer<typeof lexRecord>

// core
Expand Down Expand Up @@ -331,6 +371,7 @@ export const lexiconDoc = z
description: z.string().optional(),
defs: z.record(lexUserType),
})
.strict()
.superRefine((doc, ctx) => {
for (const defId in doc.defs) {
const def = doc.defs[defId]
Expand Down
16 changes: 16 additions & 0 deletions packages/lexicon/tests/general.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ describe('General validation', () => {
lexiconDoc.parse(schema)
}).toThrow('Required field \\"foo\\" not defined')
})
it('fails when unknown fields are present', () => {
const schema = {
lexicon: 1,
id: 'com.example.unknownFields',
defs: {
test: {
type: 'object',
foo: 3,
},
},
}

expect(() => {
lexiconDoc.parse(schema)
}).toThrow("Unrecognized key(s) in object: 'foo'")
})
it('fails lexicon parsing when uri is invalid', () => {
const schema = {
lexicon: 1,
Expand Down

0 comments on commit 18c9924

Please sign in to comment.