Improved XRPC error protocol ()

* Add lexicon doc

* Update error-handling spec

* Implement new error-behaviors in xrpc and xrpc-server packages

* Update lexicon and lex-cli packages to add xrpc error behaviors

* Generate new API and test an error behavior
This commit is contained in:
Paul Frazee 2022-09-28 11:55:43 -05:00 committed by GitHub
parent 31bd54e69c
commit a21417ba91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 1032 additions and 415 deletions

@ -133,44 +133,7 @@ To fetch a schema, a request must be sent to the xrpc [`getSchema`](../xrpc.md#g
### Schema structure
Record schemas are encoded in JSON and adhere to the following interface:
```typescript
interface RecordSchema {
adx: 1
id: string
revision?: number // a versioning counter
description?: string
record: JSONSchema
}
```
Here is an example schema:
```json
{
"adx": 1,
"id": "com.example.post",
"record": {
"type": "object",
"required": ["text", "createdAt"],
"properties": {
"text": {"type": "string", "maxLength": 256},
"createdAt": {"type": "string", "format": "date-time"}
}
}
}
```
And here is a record using this example schema:
```json
{
"$type": "com.example.post",
"text": "Hello, world!",
"createdAt": "2022-09-15T16:37:17.131Z"
}
```
Record schemas are encoded in JSON using [Lexicon Schema Documents](../lexicon.md).
### Reserved field names

111
docs/specs/lexicon.md Normal file

@ -0,0 +1,111 @@
# Lexicon Schema Documents
Lexicon is a schemas document format used to define [XRPC](./xrpc.md) methods and [ATP Repository](./adx/repo.md) record types. Every Lexicon schema is written in JSON and follows the interface specified below. The schemas are identified using [NSIDs](./nsid.md) which are then used to identify the methods or record types they describe.
## Interface
```typescript
interface LexiconDoc {
lexicon: 1
id: string // an NSID
type: 'query' | 'procedure' | 'record'
revision?: number
description?: string
}
interface RecordLexiconDoc extends LexiconDoc {
record: JSONSchema
}
interface XrpcLexiconDoc extends LexiconDoc {
parameters?: Record<string, XrpcParameter>
input?: XrpcBody
output?: XrpcBody
errors?: XrpcError[]
}
interface XrpcParameter {
type: 'string' | 'number' | 'integer' | 'boolean'
description?: string
default?: string | number | boolean
required?: boolean
minLength?: number
maxLength?: number
minimum?: number
maximum?: number
}
interface XrpcBody {
encoding: string|string[]
schema: JSONSchema
}
interface XrpcError {
name: string
description?: string
}
```
## Examples
### XRPC Method
```json
{
"lexicon": 1,
"id": "todo.adx.createAccount",
"type": "procedure",
"description": "Create an account.",
"parameters": {},
"input": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["email", "username", "password"],
"properties": {
"email": {"type": "string"},
"username": {"type": "string"},
"inviteCode": {"type": "string"},
"password": {"type": "string"}
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["jwt", "name", "did"],
"properties": {
"jwt": { "type": "string" },
"name": {"type": "string"},
"did": {"type": "string"}
}
}
},
"errors": [
{"name": "InvalidEmail"},
{"name": "InvalidUsername"},
{"name": "InvalidPassword"},
{"name": "InvalidInviteCode"},
{"name": "UsernameTaken"},
]
}
```
### ATP Record Type
```json
{
"lexicon": 1,
"id": "todo.social.repost",
"type": "record",
"record": {
"type": "object",
"required": ["subject", "createdAt"],
"properties": {
"subject": {"type": "string"},
"createdAt": {"type": "string", "format": "date-time"}
}
}
}
```

@ -73,82 +73,7 @@ net.users.bob.ping
#### Method schemas
Method schemas are encoded in JSON and adhere to the following interface:
```typescript
interface MethodSchema {
xrpc: 1
id: string
type: 'query' | 'procedure'
description?: string
parameters?: Record<string, MethodParam> // a map of param names to their definitions
input?: MethodBody
output?: MethodBody
}
interface MethodParam {
type: 'string' | 'number' | 'integer' | 'boolean'
description?: string
default?: string | number | boolean
required?: boolean
minLength?: number // string only
maxLength?: number // string only
minimum?: number // number and integer only
maximum?: number // number and integer only
}
interface MethodBody {
encoding: string | string[] // must be a valid mimetype
schema?: JSONSchema // json only
}
```
An example query-method schema:
```json
{
"xrpc": 1,
"id": "io.social.getFeed",
"type": "query",
"description": "Fetch the user's latest feed.",
"parameters": {
"limit": {"type": "integer", "minimum": 1, "maximum": 50},
"cursor": {"type": "string"},
"reverse": {"type": "boolean", "default": true}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["entries", "totalCount"],
"properties": {
"entries": {
"type": "array",
"items": {
"type": "object",
"description": "Entry items will vary and are not constrained at the method level"
}
},
"totalCount": {"type": "number"}
}
}
}
}
```
An example procedure-method schema:
```json
{
"xrpc": 1,
"id": "io.social.setProfilePicture",
"type": "procedure",
"description": "Set the user's avatar.",
"input": {
"encoding": ["image/png", "image/jpg"],
}
}
```
Method schemas are encoded in JSON using [Lexicon Schema Documents](./lexicon.md).
#### Schema distribution
@ -211,10 +136,7 @@ The request has succeeded. Expectations:
#### `400` Invalid request
The request is invalid and was not processed. Expecations:
- `Content-Type` header must be `application/json`.
- Response body must match the [InvalidRequest](#invalidrequest) schema.
The request is invalid and was not processed.
#### `401` Authentication required
@ -244,10 +166,7 @@ The client has sent too many requests. Rate-limits are decided by each server. E
#### `500` Internal server error
The server reached an unexpected condition during processing. Expecations:
- `Content-Type` header must be `application/json`.
- Response body must match the [InternalError](#internalerror) schema.
The server reached an unexpected condition during processing.
#### `501` Method not implemented
@ -255,10 +174,7 @@ The server does not implement the requested method.
#### `502` A request to upstream failed
The execution of the procedure depends on a call to another server which has failed. Expecations:
- `Content-Type` header must be `application/json`.
- Response body must match the [UpstreamError](#upstreamerror) schema.
The execution of the procedure depends on a call to another server which has failed.
#### `503` Not enough resources
@ -266,10 +182,7 @@ The server is under heavy load and can't complete the request.
#### `504` A request to upstream timed out
The execution of the procedure depends on a call to another server which timed out. Expecations:
- `Content-Type` header must be `application/json`.
- Response body must match the [UpstreamError](#upstreamerror) schema.
The execution of the procedure depends on a call to another server which timed out.
#### Remaining codes
@ -285,36 +198,15 @@ Any response code not explicitly enumerated should be handled as follows:
TODO
### Response schemas
### Custom error codes and descriptions
The following schemas are used within the XRPC protocol.
#### `InvalidRequest`
In non-200 (error) responses, services may respond with a JSON body which matches the following schema:
```typescript
interface InvalidRequest {
error: true
type: 'InvalidRequest'
message: string
interface XrpcErrorDescription {
error?: string
message?: string
}
```
#### `InternalError`
```typescript
interface InternalError {
error: true
type: 'InternalError'
message: string
}
```
#### `UpstreamError`
```typescript
interface UpstreamError {
error: true
type: 'UpstreamError'
message: string
}
```
The `error` field of the response body should map to an error name defined in the method's [Lexicon schema](./lexicon.md). This enables more specific error-handling by client software. This is especially advised on 400, 500, and 502 responses where further information will be useful.

@ -40,6 +40,40 @@ import * as TodoSocialPost from './types/todo/social/post'
import * as TodoSocialProfile from './types/todo/social/profile'
import * as TodoSocialRepost from './types/todo/social/repost'
export * as TodoAdxCreateAccount from './types/todo/adx/createAccount'
export * as TodoAdxCreateSession from './types/todo/adx/createSession'
export * as TodoAdxDeleteAccount from './types/todo/adx/deleteAccount'
export * as TodoAdxDeleteSession from './types/todo/adx/deleteSession'
export * as TodoAdxGetAccount from './types/todo/adx/getAccount'
export * as TodoAdxGetAccountsConfig from './types/todo/adx/getAccountsConfig'
export * as TodoAdxGetSession from './types/todo/adx/getSession'
export * as TodoAdxRepoBatchWrite from './types/todo/adx/repoBatchWrite'
export * as TodoAdxRepoCreateRecord from './types/todo/adx/repoCreateRecord'
export * as TodoAdxRepoDeleteRecord from './types/todo/adx/repoDeleteRecord'
export * as TodoAdxRepoDescribe from './types/todo/adx/repoDescribe'
export * as TodoAdxRepoGetRecord from './types/todo/adx/repoGetRecord'
export * as TodoAdxRepoListRecords from './types/todo/adx/repoListRecords'
export * as TodoAdxRepoPutRecord from './types/todo/adx/repoPutRecord'
export * as TodoAdxResolveName from './types/todo/adx/resolveName'
export * as TodoAdxSyncGetRepo from './types/todo/adx/syncGetRepo'
export * as TodoAdxSyncGetRoot from './types/todo/adx/syncGetRoot'
export * as TodoAdxSyncUpdateRepo from './types/todo/adx/syncUpdateRepo'
export * as TodoSocialBadge from './types/todo/social/badge'
export * as TodoSocialFollow from './types/todo/social/follow'
export * as TodoSocialGetFeed from './types/todo/social/getFeed'
export * as TodoSocialGetLikedBy from './types/todo/social/getLikedBy'
export * as TodoSocialGetNotifications from './types/todo/social/getNotifications'
export * as TodoSocialGetPostThread from './types/todo/social/getPostThread'
export * as TodoSocialGetProfile from './types/todo/social/getProfile'
export * as TodoSocialGetRepostedBy from './types/todo/social/getRepostedBy'
export * as TodoSocialGetUserFollowers from './types/todo/social/getUserFollowers'
export * as TodoSocialGetUserFollows from './types/todo/social/getUserFollows'
export * as TodoSocialLike from './types/todo/social/like'
export * as TodoSocialMediaEmbed from './types/todo/social/mediaEmbed'
export * as TodoSocialPost from './types/todo/social/post'
export * as TodoSocialProfile from './types/todo/social/profile'
export * as TodoSocialRepost from './types/todo/social/repost'
export class Client {
xrpc: XrpcClient = new XrpcClient()
@ -95,7 +129,11 @@ export class AdxNS {
data?: TodoAdxCreateAccount.InputSchema,
opts?: TodoAdxCreateAccount.CallOptions
): Promise<TodoAdxCreateAccount.Response> {
return this._service.xrpc.call('todo.adx.createAccount', params, data, opts)
return this._service.xrpc
.call('todo.adx.createAccount', params, data, opts)
.catch((e) => {
throw TodoAdxCreateAccount.toKnownErr(e)
})
}
createSession(
@ -103,7 +141,11 @@ export class AdxNS {
data?: TodoAdxCreateSession.InputSchema,
opts?: TodoAdxCreateSession.CallOptions
): Promise<TodoAdxCreateSession.Response> {
return this._service.xrpc.call('todo.adx.createSession', params, data, opts)
return this._service.xrpc
.call('todo.adx.createSession', params, data, opts)
.catch((e) => {
throw TodoAdxCreateSession.toKnownErr(e)
})
}
deleteAccount(
@ -111,7 +153,11 @@ export class AdxNS {
data?: TodoAdxDeleteAccount.InputSchema,
opts?: TodoAdxDeleteAccount.CallOptions
): Promise<TodoAdxDeleteAccount.Response> {
return this._service.xrpc.call('todo.adx.deleteAccount', params, data, opts)
return this._service.xrpc
.call('todo.adx.deleteAccount', params, data, opts)
.catch((e) => {
throw TodoAdxDeleteAccount.toKnownErr(e)
})
}
deleteSession(
@ -119,7 +165,11 @@ export class AdxNS {
data?: TodoAdxDeleteSession.InputSchema,
opts?: TodoAdxDeleteSession.CallOptions
): Promise<TodoAdxDeleteSession.Response> {
return this._service.xrpc.call('todo.adx.deleteSession', params, data, opts)
return this._service.xrpc
.call('todo.adx.deleteSession', params, data, opts)
.catch((e) => {
throw TodoAdxDeleteSession.toKnownErr(e)
})
}
getAccount(
@ -127,7 +177,11 @@ export class AdxNS {
data?: TodoAdxGetAccount.InputSchema,
opts?: TodoAdxGetAccount.CallOptions
): Promise<TodoAdxGetAccount.Response> {
return this._service.xrpc.call('todo.adx.getAccount', params, data, opts)
return this._service.xrpc
.call('todo.adx.getAccount', params, data, opts)
.catch((e) => {
throw TodoAdxGetAccount.toKnownErr(e)
})
}
getAccountsConfig(
@ -135,12 +189,11 @@ export class AdxNS {
data?: TodoAdxGetAccountsConfig.InputSchema,
opts?: TodoAdxGetAccountsConfig.CallOptions
): Promise<TodoAdxGetAccountsConfig.Response> {
return this._service.xrpc.call(
'todo.adx.getAccountsConfig',
params,
data,
opts
)
return this._service.xrpc
.call('todo.adx.getAccountsConfig', params, data, opts)
.catch((e) => {
throw TodoAdxGetAccountsConfig.toKnownErr(e)
})
}
getSession(
@ -148,7 +201,11 @@ export class AdxNS {
data?: TodoAdxGetSession.InputSchema,
opts?: TodoAdxGetSession.CallOptions
): Promise<TodoAdxGetSession.Response> {
return this._service.xrpc.call('todo.adx.getSession', params, data, opts)
return this._service.xrpc
.call('todo.adx.getSession', params, data, opts)
.catch((e) => {
throw TodoAdxGetSession.toKnownErr(e)
})
}
repoBatchWrite(
@ -156,12 +213,11 @@ export class AdxNS {
data?: TodoAdxRepoBatchWrite.InputSchema,
opts?: TodoAdxRepoBatchWrite.CallOptions
): Promise<TodoAdxRepoBatchWrite.Response> {
return this._service.xrpc.call(
'todo.adx.repoBatchWrite',
params,
data,
opts
)
return this._service.xrpc
.call('todo.adx.repoBatchWrite', params, data, opts)
.catch((e) => {
throw TodoAdxRepoBatchWrite.toKnownErr(e)
})
}
repoCreateRecord(
@ -169,12 +225,11 @@ export class AdxNS {
data?: TodoAdxRepoCreateRecord.InputSchema,
opts?: TodoAdxRepoCreateRecord.CallOptions
): Promise<TodoAdxRepoCreateRecord.Response> {
return this._service.xrpc.call(
'todo.adx.repoCreateRecord',
params,
data,
opts
)
return this._service.xrpc
.call('todo.adx.repoCreateRecord', params, data, opts)
.catch((e) => {
throw TodoAdxRepoCreateRecord.toKnownErr(e)
})
}
repoDeleteRecord(
@ -182,12 +237,11 @@ export class AdxNS {
data?: TodoAdxRepoDeleteRecord.InputSchema,
opts?: TodoAdxRepoDeleteRecord.CallOptions
): Promise<TodoAdxRepoDeleteRecord.Response> {
return this._service.xrpc.call(
'todo.adx.repoDeleteRecord',
params,
data,
opts
)
return this._service.xrpc
.call('todo.adx.repoDeleteRecord', params, data, opts)
.catch((e) => {
throw TodoAdxRepoDeleteRecord.toKnownErr(e)
})
}
repoDescribe(
@ -195,7 +249,11 @@ export class AdxNS {
data?: TodoAdxRepoDescribe.InputSchema,
opts?: TodoAdxRepoDescribe.CallOptions
): Promise<TodoAdxRepoDescribe.Response> {
return this._service.xrpc.call('todo.adx.repoDescribe', params, data, opts)
return this._service.xrpc
.call('todo.adx.repoDescribe', params, data, opts)
.catch((e) => {
throw TodoAdxRepoDescribe.toKnownErr(e)
})
}
repoGetRecord(
@ -203,7 +261,11 @@ export class AdxNS {
data?: TodoAdxRepoGetRecord.InputSchema,
opts?: TodoAdxRepoGetRecord.CallOptions
): Promise<TodoAdxRepoGetRecord.Response> {
return this._service.xrpc.call('todo.adx.repoGetRecord', params, data, opts)
return this._service.xrpc
.call('todo.adx.repoGetRecord', params, data, opts)
.catch((e) => {
throw TodoAdxRepoGetRecord.toKnownErr(e)
})
}
repoListRecords(
@ -211,12 +273,11 @@ export class AdxNS {
data?: TodoAdxRepoListRecords.InputSchema,
opts?: TodoAdxRepoListRecords.CallOptions
): Promise<TodoAdxRepoListRecords.Response> {
return this._service.xrpc.call(
'todo.adx.repoListRecords',
params,
data,
opts
)
return this._service.xrpc
.call('todo.adx.repoListRecords', params, data, opts)
.catch((e) => {
throw TodoAdxRepoListRecords.toKnownErr(e)
})
}
repoPutRecord(
@ -224,7 +285,11 @@ export class AdxNS {
data?: TodoAdxRepoPutRecord.InputSchema,
opts?: TodoAdxRepoPutRecord.CallOptions
): Promise<TodoAdxRepoPutRecord.Response> {
return this._service.xrpc.call('todo.adx.repoPutRecord', params, data, opts)
return this._service.xrpc
.call('todo.adx.repoPutRecord', params, data, opts)
.catch((e) => {
throw TodoAdxRepoPutRecord.toKnownErr(e)
})
}
resolveName(
@ -232,7 +297,11 @@ export class AdxNS {
data?: TodoAdxResolveName.InputSchema,
opts?: TodoAdxResolveName.CallOptions
): Promise<TodoAdxResolveName.Response> {
return this._service.xrpc.call('todo.adx.resolveName', params, data, opts)
return this._service.xrpc
.call('todo.adx.resolveName', params, data, opts)
.catch((e) => {
throw TodoAdxResolveName.toKnownErr(e)
})
}
syncGetRepo(
@ -240,7 +309,11 @@ export class AdxNS {
data?: TodoAdxSyncGetRepo.InputSchema,
opts?: TodoAdxSyncGetRepo.CallOptions
): Promise<TodoAdxSyncGetRepo.Response> {
return this._service.xrpc.call('todo.adx.syncGetRepo', params, data, opts)
return this._service.xrpc
.call('todo.adx.syncGetRepo', params, data, opts)
.catch((e) => {
throw TodoAdxSyncGetRepo.toKnownErr(e)
})
}
syncGetRoot(
@ -248,7 +321,11 @@ export class AdxNS {
data?: TodoAdxSyncGetRoot.InputSchema,
opts?: TodoAdxSyncGetRoot.CallOptions
): Promise<TodoAdxSyncGetRoot.Response> {
return this._service.xrpc.call('todo.adx.syncGetRoot', params, data, opts)
return this._service.xrpc
.call('todo.adx.syncGetRoot', params, data, opts)
.catch((e) => {
throw TodoAdxSyncGetRoot.toKnownErr(e)
})
}
syncUpdateRepo(
@ -256,12 +333,11 @@ export class AdxNS {
data?: TodoAdxSyncUpdateRepo.InputSchema,
opts?: TodoAdxSyncUpdateRepo.CallOptions
): Promise<TodoAdxSyncUpdateRepo.Response> {
return this._service.xrpc.call(
'todo.adx.syncUpdateRepo',
params,
data,
opts
)
return this._service.xrpc
.call('todo.adx.syncUpdateRepo', params, data, opts)
.catch((e) => {
throw TodoAdxSyncUpdateRepo.toKnownErr(e)
})
}
}
@ -291,7 +367,11 @@ export class SocialNS {
data?: TodoSocialGetFeed.InputSchema,
opts?: TodoSocialGetFeed.CallOptions
): Promise<TodoSocialGetFeed.Response> {
return this._service.xrpc.call('todo.social.getFeed', params, data, opts)
return this._service.xrpc
.call('todo.social.getFeed', params, data, opts)
.catch((e) => {
throw TodoSocialGetFeed.toKnownErr(e)
})
}
getLikedBy(
@ -299,7 +379,11 @@ export class SocialNS {
data?: TodoSocialGetLikedBy.InputSchema,
opts?: TodoSocialGetLikedBy.CallOptions
): Promise<TodoSocialGetLikedBy.Response> {
return this._service.xrpc.call('todo.social.getLikedBy', params, data, opts)
return this._service.xrpc
.call('todo.social.getLikedBy', params, data, opts)
.catch((e) => {
throw TodoSocialGetLikedBy.toKnownErr(e)
})
}
getNotifications(
@ -307,12 +391,11 @@ export class SocialNS {
data?: TodoSocialGetNotifications.InputSchema,
opts?: TodoSocialGetNotifications.CallOptions
): Promise<TodoSocialGetNotifications.Response> {
return this._service.xrpc.call(
'todo.social.getNotifications',
params,
data,
opts
)
return this._service.xrpc
.call('todo.social.getNotifications', params, data, opts)
.catch((e) => {
throw TodoSocialGetNotifications.toKnownErr(e)
})
}
getPostThread(
@ -320,12 +403,11 @@ export class SocialNS {
data?: TodoSocialGetPostThread.InputSchema,
opts?: TodoSocialGetPostThread.CallOptions
): Promise<TodoSocialGetPostThread.Response> {
return this._service.xrpc.call(
'todo.social.getPostThread',
params,
data,
opts
)
return this._service.xrpc
.call('todo.social.getPostThread', params, data, opts)
.catch((e) => {
throw TodoSocialGetPostThread.toKnownErr(e)
})
}
getProfile(
@ -333,7 +415,11 @@ export class SocialNS {
data?: TodoSocialGetProfile.InputSchema,
opts?: TodoSocialGetProfile.CallOptions
): Promise<TodoSocialGetProfile.Response> {
return this._service.xrpc.call('todo.social.getProfile', params, data, opts)
return this._service.xrpc
.call('todo.social.getProfile', params, data, opts)
.catch((e) => {
throw TodoSocialGetProfile.toKnownErr(e)
})
}
getRepostedBy(
@ -341,12 +427,11 @@ export class SocialNS {
data?: TodoSocialGetRepostedBy.InputSchema,
opts?: TodoSocialGetRepostedBy.CallOptions
): Promise<TodoSocialGetRepostedBy.Response> {
return this._service.xrpc.call(
'todo.social.getRepostedBy',
params,
data,
opts
)
return this._service.xrpc
.call('todo.social.getRepostedBy', params, data, opts)
.catch((e) => {
throw TodoSocialGetRepostedBy.toKnownErr(e)
})
}
getUserFollowers(
@ -354,12 +439,11 @@ export class SocialNS {
data?: TodoSocialGetUserFollowers.InputSchema,
opts?: TodoSocialGetUserFollowers.CallOptions
): Promise<TodoSocialGetUserFollowers.Response> {
return this._service.xrpc.call(
'todo.social.getUserFollowers',
params,
data,
opts
)
return this._service.xrpc
.call('todo.social.getUserFollowers', params, data, opts)
.catch((e) => {
throw TodoSocialGetUserFollowers.toKnownErr(e)
})
}
getUserFollows(
@ -367,12 +451,11 @@ export class SocialNS {
data?: TodoSocialGetUserFollows.InputSchema,
opts?: TodoSocialGetUserFollows.CallOptions
): Promise<TodoSocialGetUserFollows.Response> {
return this._service.xrpc.call(
'todo.social.getUserFollows',
params,
data,
opts
)
return this._service.xrpc
.call('todo.social.getUserFollows', params, data, opts)
.catch((e) => {
throw TodoSocialGetUserFollows.toKnownErr(e)
})
}
}

@ -40,6 +40,17 @@ export const methodSchemas: MethodSchema[] = [
},
},
},
errors: [
{
name: 'InvalidUsername',
},
{
name: 'InvalidPassword',
},
{
name: 'UsernameNotAvailable',
},
],
},
{
lexicon: 1,

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {}
@ -22,7 +22,34 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export class InvalidUsernameError extends XRPCError {
constructor(src: XRPCError) {
super(src.status, src.error, src.message)
}
}
export class InvalidPasswordError extends XRPCError {
constructor(src: XRPCError) {
super(src.status, src.error, src.message)
}
}
export class UsernameNotAvailableError extends XRPCError {
constructor(src: XRPCError) {
super(src.status, src.error, src.message)
}
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
if (e.error === 'InvalidUsername') return new InvalidUsernameError(e)
if (e.error === 'InvalidPassword') return new InvalidPasswordError(e)
if (e.error === 'UsernameNotAvailable')
return new UsernameNotAvailableError(e)
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {}
@ -23,7 +23,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {}
@ -20,7 +20,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {}
@ -20,7 +20,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {}
@ -20,7 +20,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {}
@ -18,7 +18,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {}
@ -18,7 +18,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
did: string;
@ -40,7 +40,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
did: string;
@ -24,7 +24,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
did: string;
@ -17,6 +17,11 @@ export type InputSchema = undefined
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
nameOrDid: string;
@ -23,7 +23,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
nameOrDid: string;
@ -22,7 +22,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
nameOrDid: string;
@ -27,7 +27,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
did: string;
@ -25,7 +25,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
name?: string;
@ -19,7 +19,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
did: string;
@ -16,7 +16,12 @@ export type InputSchema = undefined
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: Uint8Array;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
did: string;
@ -19,7 +19,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
did: string;
@ -16,6 +16,11 @@ export type InputSchema = string | Uint8Array
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
author?: string;
@ -56,7 +56,12 @@ export interface UnknownEmbed {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
uri: string;
@ -28,7 +28,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
limit?: number;
@ -31,7 +31,12 @@ export interface Notification {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
uri: string;
@ -56,7 +56,12 @@ export interface UnknownEmbed {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
user: string;
@ -42,7 +42,12 @@ export interface Badge {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
uri: string;
@ -28,7 +28,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
user: string;
@ -32,7 +32,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -1,7 +1,7 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import { Headers } from '@adxp/xrpc'
import { Headers, XRPCError } from '@adxp/xrpc'
export interface QueryParams {
user: string;
@ -32,7 +32,12 @@ export interface OutputSchema {
export interface Response {
success: boolean;
error: boolean;
headers: Headers;
data: OutputSchema;
}
export function toKnownErr(e: any) {
if (e instanceof XRPCError) {
}
return e
}

@ -53,13 +53,15 @@ const indexTs = (project: Project, schemas: Schema[], nsidTree: NsidNS[]) =>
.addImportDeclaration({ moduleSpecifier: './schemas' })
.addNamedImports([{ name: 'methodSchemas' }, { name: 'recordSchemas' }])
// generate type imports
// generate type imports and re-exports
for (const schema of schemas) {
const moduleSpecifier = `./types/${schema.id.split('.').join('/')}`
file
.addImportDeclaration({
moduleSpecifier: `./types/${schema.id.split('.').join('/')}`,
})
.addImportDeclaration({ moduleSpecifier })
.setNamespaceImport(toTitleCase(schema.id))
file
.addExportDeclaration({ moduleSpecifier })
.setNamespaceExport(toTitleCase(schema.id))
}
//= export class Client {...}
@ -255,7 +257,13 @@ function genNamespaceCls(file: SourceFile, ns: NsidNS) {
type: `${moduleName}.CallOptions`,
})
method.setBodyText(
`return this._service.xrpc.call('${schema.id}', params, data, opts)`,
[
`return this._service.xrpc`,
` .call('${schema.id}', params, data, opts)`,
` .catch((e) => {`,
` throw ${moduleName}.toKnownErr(e)`,
` })`,
].join('\n'),
)
}
@ -409,13 +417,11 @@ function genRecordCls(file: SourceFile, schema: RecordSchema) {
const methodSchemaTs = (project, schema: MethodSchema) =>
gen(project, `/types/${schema.id.split('.').join('/')}.ts`, async (file) => {
//= import {Headers} from '@adxp/xrpc'
//= import {Headers, XRPCError} from '@adxp/xrpc'
const xrpcImport = file.addImportDeclaration({
moduleSpecifier: '@adxp/xrpc',
})
xrpcImport.addNamedImport({
name: 'Headers',
})
xrpcImport.addNamedImports([{ name: 'Headers' }, { name: 'XRPCError' }])
//= export interface QueryParams {...}
const qp = file.addInterface({
@ -493,7 +499,6 @@ const methodSchemaTs = (project, schema: MethodSchema) =>
isExported: true,
})
res.addProperty({ name: 'success', type: 'boolean' })
res.addProperty({ name: 'error', type: 'boolean' })
res.addProperty({ name: 'headers', type: 'Headers' })
if (schema.output?.schema) {
if (Array.isArray(schema.output.encoding)) {
@ -504,6 +509,41 @@ const methodSchemaTs = (project, schema: MethodSchema) =>
} else if (schema.output?.encoding) {
res.addProperty({ name: 'data', type: 'Uint8Array' })
}
// export class {errcode}Error {...}
const customErrors: { name: string; cls: string }[] = []
for (const error of schema.errors || []) {
let name = toTitleCase(error.name)
if (!name.endsWith('Error')) name += 'Error'
const errCls = file.addClass({
name,
extends: 'XRPCError',
isExported: true,
})
errCls
.addConstructor({
parameters: [{ name: 'src', type: 'XRPCError' }],
})
.setBodyText(`super(src.status, src.error, src.message)`)
customErrors.push({ name: error.name, cls: name })
}
// export function toKnownErr(err: any) {...}
const toKnownErrFn = file.addFunction({
name: 'toKnownErr',
isExported: true,
})
toKnownErrFn.addParameter({ name: 'e', type: 'any' })
toKnownErrFn.setBodyText(
[
`if (e instanceof XRPCError) {`,
...customErrors.map(
(err) => `if (e.error === '${err.name}') return new ${err.cls}(e)`,
),
`}`,
`return e`,
].join('\n'),
)
})
const recordSchemaTs = (project, schema: RecordSchema) =>

@ -243,43 +243,62 @@ const methodSchemaTs = (project, schema: MethodSchema) =>
)
}
// export interface HandlerOutput {...}
// export interface HandlerSuccess {...}
let hasHandlerSuccess = false
if (schema.output?.schema || schema.output?.encoding) {
const handlerOutput = file.addInterface({
name: 'HandlerOutput',
hasHandlerSuccess = true
const handlerSuccess = file.addInterface({
name: 'HandlerSuccess',
isExported: true,
})
if (Array.isArray(schema.output.encoding)) {
handlerOutput.addProperty({
handlerSuccess.addProperty({
name: 'encoding',
type: schema.output.encoding.map((v) => `'${v}'`).join(' | '),
})
} else if (typeof schema.output.encoding === 'string') {
handlerOutput.addProperty({
handlerSuccess.addProperty({
name: 'encoding',
type: `'${schema.output.encoding}'`,
})
}
if (schema.output?.schema) {
if (Array.isArray(schema.output.encoding)) {
handlerOutput.addProperty({
handlerSuccess.addProperty({
name: 'body',
type: 'OutputSchema | Uint8Array',
})
} else {
handlerOutput.addProperty({ name: 'body', type: 'OutputSchema' })
handlerSuccess.addProperty({ name: 'body', type: 'OutputSchema' })
}
} else if (schema.output?.encoding) {
handlerOutput.addProperty({ name: 'body', type: 'Uint8Array' })
handlerSuccess.addProperty({ name: 'body', type: 'Uint8Array' })
}
} else {
file.addTypeAlias({
isExported: true,
name: 'HandlerOutput',
type: 'void',
}
// export interface HandlerError {...}
const handlerError = file.addInterface({
name: 'HandlerError',
isExported: true,
})
handlerError.addProperties([
{ name: 'status', type: 'number' },
{ name: 'message?', type: 'string' },
])
if (schema.errors?.length) {
handlerError.addProperty({
name: 'error?',
type: schema.errors.map((err) => `'${err.name}'`).join(' | '),
})
}
// export type HandlerOutput = ...
file.addTypeAlias({
isExported: true,
name: 'HandlerOutput',
type: `HandlerError | ${hasHandlerSuccess ? 'HandlerSuccess' : 'void'}`,
})
//= export interface OutputSchema {...}
if (schema.output?.schema) {
file.insertText(

@ -43,6 +43,12 @@ export const methodSchemaParam = z.object({
})
export type MethodSchemaParam = z.infer<typeof methodSchemaParam>
export const methodSchemaError = z.object({
name: z.string(),
description: z.string().optional(),
})
export type MethodSchemaError = z.infer<typeof methodSchemaError>
export const methodSchema = z.object({
lexicon: z.literal(1),
id: z.string(),
@ -51,6 +57,7 @@ export const methodSchema = z.object({
parameters: z.record(methodSchemaParam).optional(),
input: methodSchemaBody.optional(),
output: methodSchemaBody.optional(),
errors: methodSchemaError.array().optional(),
})
export type MethodSchema = z.infer<typeof methodSchema>

@ -1,4 +1,5 @@
import { Server } from '../../../lexicon'
import * as CreateAccount from '../../../lexicon/types/todo/adx/createAccount'
import { InvalidRequestError } from '@adxp/xrpc-server'
import * as util from '../../../util'
import { Repo } from '@adxp/repo'
@ -33,14 +34,17 @@ export default function (server: Server) {
const cfg = util.getConfig(res)
if (username.startsWith('did:')) {
throw new InvalidRequestError(
'Cannot register a username that starts with `did:`',
)
return {
status: 400,
error: 'InvalidUsername',
message: 'Cannot register a username that starts with `did:`',
}
}
if (!did.startsWith('did:')) {
throw new InvalidRequestError(
'Cannot register a did that does not start with `did:`',
)
return {
status: 400,
message: 'Cannot register a did that does not start with `did:`',
}
}
let isTestUser = false

@ -40,6 +40,17 @@ export const methodSchemas: MethodSchema[] = [
},
},
},
errors: [
{
name: 'InvalidUsername',
},
{
name: 'InvalidPassword',
},
{
name: 'UsernameNotAvailable',
},
],
},
{
lexicon: 1,

@ -16,11 +16,19 @@ export interface InputSchema {
password: string;
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
error?: 'InvalidUsername' | 'InvalidPassword' | 'UsernameNotAvailable';
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
jwt: string;
}

@ -15,11 +15,18 @@ export interface InputSchema {
password: string;
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
jwt: string;
name: string;

@ -11,11 +11,18 @@ export interface InputSchema {
[k: string]: unknown;
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: '';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
[k: string]: unknown;
}

@ -11,11 +11,18 @@ export interface InputSchema {
[k: string]: unknown;
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: '';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
[k: string]: unknown;
}

@ -11,11 +11,18 @@ export interface InputSchema {
[k: string]: unknown;
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: '';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
[k: string]: unknown;
}

@ -7,11 +7,18 @@ export interface QueryParams {}
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
inviteCodeRequired?: boolean;
availableUserDomains: string[];

@ -7,11 +7,18 @@ export interface QueryParams {}
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
name: string;
did: string;

@ -34,11 +34,18 @@ export interface InputSchema {
)[];
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
[k: string]: unknown;
}

@ -18,11 +18,18 @@ export interface InputSchema {
[k: string]: unknown;
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
uri: string;
}

@ -10,7 +10,13 @@ export interface QueryParams {
}
export type HandlerInput = undefined
export type HandlerOutput = void
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | void
export type Handler = (
params: QueryParams,
input: HandlerInput,

@ -9,11 +9,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
name: string;
did: string;

@ -11,11 +11,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
uri: string;
value: {};

@ -14,11 +14,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
records: {
uri: string,

@ -19,11 +19,18 @@ export interface InputSchema {
[k: string]: unknown;
}
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
uri: string;
}

@ -9,11 +9,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
did: string;
}

@ -10,11 +10,17 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/cbor';
body: Uint8Array;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export type Handler = (
params: QueryParams,
input: HandlerInput,

@ -9,11 +9,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
root: string;
}

@ -12,7 +12,12 @@ export interface HandlerInput {
body: Uint8Array;
}
export type HandlerOutput = void
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | void
export type Handler = (
params: QueryParams,
input: HandlerInput,

@ -11,11 +11,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
feed: FeedItem[];
}

@ -11,11 +11,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
uri: string;
likedBy: {

@ -10,11 +10,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
notifications: Notification[];
}

@ -10,11 +10,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
thread: Post;
}

@ -9,11 +9,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
did: string;
name: string;

@ -11,11 +11,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
uri: string;
repostedBy: {

@ -11,11 +11,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
subject: {
did: string,

@ -11,11 +11,18 @@ export interface QueryParams {
export type HandlerInput = undefined
export interface HandlerOutput {
export interface HandlerSuccess {
encoding: 'application/json';
body: OutputSchema;
}
export interface HandlerError {
status: number;
message?: string;
}
export type HandlerOutput = HandlerError | HandlerSuccess
export interface OutputSchema {
subject: {
did: string,

@ -1,4 +1,7 @@
import AdxApi, { ServiceClient as AdxServiceClient } from '@adxp/api'
import AdxApi, {
ServiceClient as AdxServiceClient,
TodoAdxCreateAccount,
} from '@adxp/api'
import * as util from './_util'
const username = 'alice.test'
@ -33,6 +36,24 @@ describe('auth', () => {
expect(typeof res.data.jwt).toBe('string')
})
it('fails on invalid usernames', async () => {
try {
await client.todo.adx.createAccount(
{},
{
username: 'did:bad-username.test',
did: 'bad.test',
password: 'asdf',
},
)
throw new Error('Didnt throw')
} catch (e: any) {
expect(
e instanceof TodoAdxCreateAccount.InvalidUsernameError,
).toBeTruthy()
}
})
it('fails on authenticated requests', async () => {
await expect(client.todo.adx.getSession({})).rejects.toThrow()
})

@ -1,7 +1,16 @@
import express from 'express'
import { ValidateFunction } from 'ajv'
import { MethodSchema, methodSchema, isValidMethodSchema } from '@adxp/lexicon'
import { XRPCHandler, XRPCError, InvalidRequestError } from './types'
import {
XRPCHandler,
XRPCError,
InvalidRequestError,
HandlerOutput,
HandlerSuccess,
handlerSuccess,
HandlerError,
handlerError,
} from './types'
import {
ajv,
validateReqParams,
@ -113,37 +122,43 @@ export class Server {
// run the handler
const outputUnvalidated = await handler(params, input, req, res)
// validate response
const output = validateOutput(
schema,
outputUnvalidated,
this.outputValidators.get(schema.id),
)
if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) {
// validate response
const output = validateOutput(
schema,
outputUnvalidated,
this.outputValidators.get(schema.id),
)
// send response
if (
output?.encoding === 'application/json' ||
output?.encoding === 'json'
) {
res.status(200).json(output.body)
} else if (output) {
res.header('Content-Type', output.encoding)
res
.status(200)
.send(
output.body instanceof Uint8Array
? Buffer.from(output.body)
: output.body,
)
} else {
res.status(200).end()
// send response
if (
output?.encoding === 'application/json' ||
output?.encoding === 'json'
) {
res.status(200).json(output.body)
} else if (output) {
res.header('Content-Type', output.encoding)
res
.status(200)
.send(
output.body instanceof Uint8Array
? Buffer.from(output.body)
: output.body,
)
} else {
res.status(200).end()
}
} else if (isHandlerError(outputUnvalidated)) {
return res.status(outputUnvalidated.status).json({
error: outputUnvalidated.error,
message: outputUnvalidated.message,
})
}
} catch (e: any) {
if (e instanceof XRPCError) {
res.status(e.type).json({
error: true,
type: e.typeStr,
message: e.message || e.typeStr,
error: e.customErrorName,
message: e.errorMessage || e.typeStr,
})
} else {
console.error(
@ -151,11 +166,17 @@ export class Server {
)
console.error(e)
res.status(500).json({
error: true,
type: 'InternalError',
message: 'Unexpected internal server error',
})
}
}
}
}
function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess {
return handlerSuccess.safeParse(v).success
}
function isHandlerError(v: HandlerOutput): v is HandlerError {
return handlerError.safeParse(v).success
}

@ -10,11 +10,20 @@ export const handlerInput = zod.object({
})
export type HandlerInput = zod.infer<typeof handlerInput>
export const handlerOutput = zod.object({
export const handlerSuccess = zod.object({
encoding: zod.string(),
body: zod.any(),
})
export type HandlerOutput = zod.infer<typeof handlerOutput>
export type HandlerSuccess = zod.infer<typeof handlerSuccess>
export const handlerError = zod.object({
status: zod.number(),
error: zod.string().optional(),
message: zod.string().optional(),
})
export type HandlerError = zod.infer<typeof handlerError>
export type HandlerOutput = HandlerSuccess | HandlerError
export type XRPCHandler = (
params: Params,
@ -24,8 +33,12 @@ export type XRPCHandler = (
) => Promise<HandlerOutput> | HandlerOutput | undefined
export class XRPCError extends Error {
constructor(public type: ResponseType, message?: string) {
super(message)
constructor(
public type: ResponseType,
public errorMessage?: string,
public customErrorName?: string,
) {
super(errorMessage)
}
get typeStr() {
@ -34,43 +47,43 @@ export class XRPCError extends Error {
}
export class InvalidRequestError extends XRPCError {
constructor(message?: string) {
super(ResponseType.InvalidRequest, message)
constructor(errorMessage?: string, customErrorName?: string) {
super(ResponseType.InvalidRequest, errorMessage, customErrorName)
}
}
export class AuthRequiredError extends XRPCError {
constructor(message?: string) {
super(ResponseType.AuthRequired, message)
constructor(errorMessage?: string, customErrorName?: string) {
super(ResponseType.AuthRequired, errorMessage, customErrorName)
}
}
export class ForbiddenError extends XRPCError {
constructor(message?: string) {
super(ResponseType.Forbidden, message)
constructor(errorMessage?: string, customErrorName?: string) {
super(ResponseType.Forbidden, errorMessage, customErrorName)
}
}
export class InternalServerError extends XRPCError {
constructor(message?: string) {
super(ResponseType.InternalServerError, message)
constructor(errorMessage?: string, customErrorName?: string) {
super(ResponseType.InternalServerError, errorMessage, customErrorName)
}
}
export class UpstreamFailureError extends XRPCError {
constructor(message?: string) {
super(ResponseType.UpstreamFailure, message)
constructor(errorMessage?: string, customErrorName?: string) {
super(ResponseType.UpstreamFailure, errorMessage, customErrorName)
}
}
export class NotEnoughResoucesError extends XRPCError {
constructor(message?: string) {
super(ResponseType.NotEnoughResouces, message)
constructor(errorMessage?: string, customErrorName?: string) {
super(ResponseType.NotEnoughResouces, errorMessage, customErrorName)
}
}
export class UpstreamTimeoutError extends XRPCError {
constructor(message?: string) {
super(ResponseType.UpstreamTimeout, message)
constructor(errorMessage?: string, customErrorName?: string) {
super(ResponseType.UpstreamTimeout, errorMessage, customErrorName)
}
}

@ -6,8 +6,8 @@ import addFormats from 'ajv-formats'
import {
Params,
HandlerInput,
HandlerOutput,
handlerOutput,
HandlerSuccess,
handlerSuccess,
InvalidRequestError,
InternalServerError,
} from './types'
@ -144,12 +144,12 @@ export function validateInput(
export function validateOutput(
schema: MethodSchema,
output: HandlerOutput | undefined,
output: HandlerSuccess | undefined,
jsonValidator?: ValidateFunction,
): HandlerOutput | undefined {
): HandlerSuccess | undefined {
// initial validation
if (output) {
handlerOutput.parse(output)
handlerSuccess.parse(output)
}
// response expectation

@ -0,0 +1,74 @@
import * as http from 'http'
import { createServer, closeServer } from './_util'
import * as xrpcServer from '../src'
import xrpc, { XRPCError } from '@adxp/xrpc'
const SCHEMAS = [
{
lexicon: 1,
id: 'io.example.error',
type: 'query',
parameters: {
which: { type: 'string', default: 'foo' },
},
errors: [{ name: 'Foo' }, { name: 'Bar' }],
},
]
describe('Procedures', () => {
let s: http.Server
const server = xrpcServer.createServer(SCHEMAS)
server.method('io.example.error', (params: xrpcServer.Params) => {
if (params.which === 'foo') {
throw new xrpcServer.InvalidRequestError('It was this one!', 'Foo')
} else if (params.which === 'bar') {
return { status: 400, error: 'Bar', message: 'It was that one!' }
} else {
return { status: 400 }
}
})
const client = xrpc.service(`http://localhost:8893`)
xrpc.addSchemas(SCHEMAS)
beforeAll(async () => {
s = await createServer(8893, server)
})
afterAll(async () => {
await closeServer(s)
})
it('serves requests', async () => {
try {
await client.call('io.example.error', {
which: 'foo',
})
throw new Error('Didnt throw')
} catch (e: any) {
expect(e instanceof XRPCError).toBeTruthy()
expect(e.success).toBeFalsy()
expect(e.error).toBe('Foo')
expect(e.message).toBe('It was this one!')
}
try {
await client.call('io.example.error', {
which: 'bar',
})
throw new Error('Didnt throw')
} catch (e: any) {
expect(e instanceof XRPCError).toBeTruthy()
expect(e.success).toBeFalsy()
expect(e.error).toBe('Bar')
expect(e.message).toBe('It was that one!')
}
try {
await client.call('io.example.error', {
which: 'other',
})
throw new Error('Didnt throw')
} catch (e: any) {
expect(e instanceof XRPCError).toBeTruthy()
expect(e.success).toBeFalsy()
expect(e.error).toBe('InvalidRequest')
expect(e.message).toBe('Invalid Request')
}
})
})

@ -99,7 +99,6 @@ describe('Procedures', () => {
message: 'hello world',
})
expect(res1.success).toBeTruthy()
expect(res1.error).toBeFalsy()
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
expect(res1.data).toBe('hello world')
@ -107,7 +106,6 @@ describe('Procedures', () => {
encoding: 'text/plain',
})
expect(res2.success).toBeTruthy()
expect(res2.error).toBeFalsy()
expect(res2.headers['content-type']).toBe('text/plain; charset=utf-8')
expect(res2.data).toBe('hello world')
@ -118,7 +116,6 @@ describe('Procedures', () => {
{ encoding: 'application/octet-stream' },
)
expect(res3.success).toBeTruthy()
expect(res3.error).toBeFalsy()
expect(res3.headers['content-type']).toBe('application/octet-stream')
expect(new TextDecoder().decode(res3.data)).toBe('hello world')
@ -128,7 +125,6 @@ describe('Procedures', () => {
{ message: 'hello world' },
)
expect(res4.success).toBeTruthy()
expect(res4.error).toBeFalsy()
expect(res4.headers['content-type']).toBe('application/json; charset=utf-8')
expect(res4.data?.message).toBe('hello world')
})

@ -67,7 +67,6 @@ describe('Queries', () => {
message: 'hello world',
})
expect(res1.success).toBeTruthy()
expect(res1.error).toBeFalsy()
expect(res1.headers['content-type']).toBe('text/plain; charset=utf-8')
expect(res1.data).toBe('hello world')
@ -75,7 +74,6 @@ describe('Queries', () => {
message: 'hello world',
})
expect(res2.success).toBeTruthy()
expect(res2.error).toBeFalsy()
expect(res2.headers['content-type']).toBe('application/octet-stream')
expect(new TextDecoder().decode(res2.data)).toBe('hello world')
@ -83,7 +81,6 @@ describe('Queries', () => {
message: 'hello world',
})
expect(res3.success).toBeTruthy()
expect(res3.error).toBeFalsy()
expect(res3.headers['content-type']).toBe('application/json; charset=utf-8')
expect(res3.data?.message).toBe('hello world')
})

@ -113,7 +113,7 @@ export class ServiceClient {
return new XRPCResponse(res.body, res.headers)
} else {
if (res.body && isErrorResponseBody(res.body)) {
throw new XRPCError(resCode, res.body.message)
throw new XRPCError(resCode, res.body.error, res.body.message)
} else {
throw new XRPCError(resCode)
}

@ -22,6 +22,7 @@ export type FetchHandler = (
) => Promise<FetchHandlerResponse>
export const errorResponseBody = z.object({
error: z.string().optional(),
message: z.string().optional(),
})
export type ErrorResponseBody = z.infer<typeof errorResponseBody>
@ -43,6 +44,22 @@ export enum ResponseType {
UpstreamTimeout = 504,
}
export const ResponseTypeNames = {
[ResponseType.InvalidResponse]: 'InvalidResponse',
[ResponseType.Success]: 'Success',
[ResponseType.InvalidRequest]: 'InvalidRequest',
[ResponseType.AuthRequired]: 'AuthenticationRequired',
[ResponseType.Forbidden]: 'Forbidden',
[ResponseType.XRPCNotSupported]: 'XRPCNotSupported',
[ResponseType.PayloadTooLarge]: 'PayloadTooLarge',
[ResponseType.RateLimitExceeded]: 'RateLimitExceeded',
[ResponseType.InternalServerError]: 'InternalServerError',
[ResponseType.MethodNotImplemented]: 'MethodNotImplemented',
[ResponseType.UpstreamFailure]: 'UpstreamFailure',
[ResponseType.NotEnoughResouces]: 'NotEnoughResouces',
[ResponseType.UpstreamTimeout]: 'UpstreamTimeout',
}
export const ResponseTypeStrings = {
[ResponseType.InvalidResponse]: 'Invalid Response',
[ResponseType.Success]: 'Success',
@ -61,20 +78,21 @@ export const ResponseTypeStrings = {
export class XRPCResponse {
success = true
error = false
constructor(public data: any, public headers: Headers) {}
}
export class XRPCError extends Error {
success = false
error = true
constructor(public code: ResponseType, message?: string) {
super(
message
? `${ResponseTypeStrings[code]}: ${message}`
: ResponseTypeStrings[code],
)
constructor(
public status: ResponseType,
public error?: string,
message?: string,
) {
super(message || error || ResponseTypeStrings[status])
if (!this.error) {
this.error = ResponseTypeNames[status]
}
}
}

@ -25,5 +25,10 @@
"jwt": { "type": "string" }
}
}
}
},
"errors": [
{"name": "InvalidUsername"},
{"name": "InvalidPassword"},
{"name": "UsernameNotAvailable"}
]
}