0.5.3 • Published 9 months ago

@noreajs/oauth-v2-provider-me v0.5.3

Weekly downloads
424
License
MIT
Repository
github
Last release
9 months ago

Oauth v2 Provider ME (MongoDB + Express)

When you develop your APIs, you need to secure the resources they will offer. The Oauth 2 framework offers a safe and secure way to achieve this.

This package is an OAuth 2.0 Authorization Server with mongoose, Express and EJS.

While developing app using MEAN (MongoDB + Express+ Angular + Node.js), MERN (MongoDB + Express+ React.js + Node.js) or globally ME*N stack you can use this package to host a Oauth 2 server.

Table of Contents

TOC

Implemented specifications & Features

Installation

Installation command

npm  install @noreajs/oauth-v2-provider-me --save

The package already content it's types definition.

Configuration

Initialization

The provider is initialize with a simple function.

Initialization function definition

Oauth.init(app: Application, initContext: IOauthContext): void

The IOauthContext is an object with some properties useful for the provider configuration.

PropertyTypeOptionalDescription
providerNamestringfalseOauth v2 provider name. This name is going to be used as cookie name.
secretKeystringfalseOauth v2 provider secret key
jwtAlgorithm"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"trueJwt encrypt algorithm
authenticationLogicFunctionfalseFunction which take username and password as parameters and authenticate related user. Response can be an object of type IEndUserAuthData or undefined
supportedOpenIdStandardClaimsFunctionfalseFunction that return claims to be included in id_token. Response can be an object of type JwtTokenReservedClaimsType or undefined
subLookupFunctiontrueLookup the token owner and make his data available in Express response within the locals property or express Response
securityMiddlewaresarraytrueMiddlewares to be applied to Clients management routes and Scopes management routes
tokenType"Bearer"trueToken type will be always Bearer
authorizationCodeLifeTimeobjecttrueAuthorization code lifetime in seconds
accessTokenExpiresInobjecttrueAccess Token Expiration Times
refreshTokenExpiresInobjecttrueRefresh Token Expiration Times

Oauth context default values:

  • jwtAlgorithm: "HS512"
  • securityMiddlewares: []
  • tokenType: "Bearer"
  • authorizationCodeLifeTime: 60 * 5 // 5 minutes
  • accessTokenExpiresIn
{
    confidential: {
        internal: 60 * 60 * 24, // 24h
        external: 60 * 60 * 12, // 12h
    },
    public: {
        internal: 60 * 60 * 2, // 2h
        external: 60 * 60, // 1h
    }
}
  • refreshTokenExpiresIn
{
    confidential: {
        internal: 60 * 60 * 24 * 30 * 12, // 1 year
        external: 60 * 60 * 24 * 30, // 30 days
    },
    public: {
        internal: 60 * 60 * 24 * 30, // 30 days
        external: 60 * 60 * 24 * 7, // 1 week
    }
}

Initialization with common Node.js + Express example

import express from "express";
import { Oauth, IEndUserAuthData, JwtTokenReservedClaimsType } from "@noreajs/oauth-v2-provider-me";

const app = express();

Oauth.init(app, {
    providerName: "Your App Name",
    secretKey: "66a5ddac054bfe9389e82de--your-secret-key--a7488756a00ca334a1468015da8",
    authenticationLogic: async function (username: string, password: string) {
      // Your authentication logic here
    },
    supportedOpenIdStandardClaims: async function (userId: string) {
      // Return supported Open ID standard claims
    },
    subLookup: async (sub: string) => {
      // returns the user who has an identifier equal to sub
    },
    securityMiddlewares: [
      // Oauth.authorize() - Add this middleware only on production mode
    ],
});

// start the app
app.listen(3000, function () {
    console.log('Example Oauth 2 server listening on port 3000!')
})

Session

This package uses Express session for session management during authentication operations. You can initialize Express session session in two ways.

Before Oauth initialization

import express from "express";
import session from "express-session";

const app = express();

// inject session
app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true,
    cookie: { secure: true }
}))

// initialize oauth now

During Oauth initialization

import express from "express";
import { Oauth } from "@noreajs/oauth-v2-provider-me";

const app = express();

// initialize Oauth v2 provider
Oauth.init(app, {
    providerName: "Your App Name",
    // ...some options
}, {
    sessionOptions: {
        resave: false,
        saveUninitialized: true,
        cookie: { secure: true }
	}
});

Session Store Implementation

Express-session middleware stores session data on the server; it only saves the session ID in the cookie itself, but not the session data. By default, it uses memory storage and is not designed for a production environment. In production, you will need to configure a scalable session store.

MongoDB session store example - MongoDBStore

import express from "express";
import session from "express-session";
import mongodbSession from "connect-mongodb-session";

const MongoDBStore = mongodbSession(session);

const app = express();

// inject session
app.use(session({
    secret: 'keyboard cat',
    // ... some options
    // mongoDB store session initialization
    store: new MongoDBStore({
        uri: 'mongodb://localhost:27017/connect_mongodb_session_test',
        collection: 'mySessions'
    })
}))

// initialize oauth now

See the list of other compatible session stores

Manage scopes

To make your API more secure, Each route should be associated with one or more scopes.

Some endpoints are already provided with the package to manage scopes:

HTTP MethodRouteDescription
GET/oauth/v2/scopesGet all scopes
GET/oauth/v2/scopes/:idGet scope by ID
POST/oauth/v2/scopesCreate a new scope
PUT/oauth/v2/scopes/:idEdit a scope
DELETE/oauth/v2/scopes/:idDelete a scope

Scope properties

Property NameTypeOptionalDescription
namestringfalseName of the scope. String without space.
descriptionstringtrueDescription of the scope
parentObjectIdtrueTo better organize the scopes, some can have parents.

Scope creation's body request example

{
    "name": "edit:user",
    "description": "Edit a user account"
}

Manage clients

Developers building applications that need to interact with your application's API will need to register their application with yours by creating a "client".

Client endpoints

Some endpoints are already provided with the package to manage clients:

HTTP MethodRouteDescription
GET/oauth/v2/clientsGet all clients
GET/oauth/v2/clients/:idGet client by ID
POST/oauth/v2/clientsCreate a new client
PUT/oauth/v2/clients/:idEdit a client
DELETE/oauth/v2/clients/:idDelete a client

Client properties

To respect Oauth 2 specifications some properties are needed for the client.

Property NameTypeOptionalDescription
clientIdstringGeneratedClient ID
namestringfalseName of the application
domainestringtrueDomaine name of the application
logostringtrueLink of the application logo
descriptionstringtrueDescription of the application
secretKeystringgeneratedSecret key of the client. It is only generated when the clientType value is confidential.
internalbooleanfalseSet internal value to true for First-party applications and false for Third-party applications
grantsarray of values in implicit, client_credentials, password, authorization_code and refresh_tokenAutomatically filled based on data providesAllowed grants depends on whether the client is confidential or public, internal or external.
redirectURIsarray of URIfalseAfter a user successfully authorizes an application, the server will redirect the user back to the application with either an authorization code or access token in the URL
clientProfileweb, user-agent-based or nativefalseweb for web application, user-agent-based for user-agent based application, and native for native desktop or mobile application.
clientTypeconfidential or publicAutomatically filled based on clientProfile valueA confidential client is a client who guarantees the confidentiality of credentials (Web application with a secure backend). A public client cannot hold credentials securely (native desktop or mobile application, user-agent-based application such as a single page app).
programmingLanguagestringtrueLanguage used to develop the application
scopestringfalseScope requested by the application (i.e. "read:users list:users add:users")

Other client properties:

  • legalTermsAcceptedAt (OPTIONAL): if some legal terms need to be accepted before consuming your API.
  • revokedAt (OPTIONAL): filled when the client is revoked

Revoke a client

To revoke a client, use the edit endpoint and send data as follow:

{
    // ... other fields
    revoke: true // you can also end false in other to cancel revokation
}

Client types detailed

OAuth defines two client types, based on their ability to authenticate securely with the authorization server.

  • confidential

  • public

    • Browser-based application: Most of SPA application based on Web browser JavaScript frameworks and libraries such as:
    • Native application: software program that is developed for use on a particular platform or device
      • Mobile applications: Android, IOS and Windows phone
      • Desktop application: Linux, windows, Mac OS

Client example

Client creation's request body example

{
    name: "Cake Shop",
    internal: false,
    redirectURIs: ["https://www.cakeshop.com/auth/callback"],
    clientProfile: "web",
    scope: "read:users read:cake add:cakes" // "*" is allowed only for internal client
}

Authorization Grants

Depending on the type of customers who want to access your API, there are appropriate types of authentication.

Authorization Code Grant

The authorization code grant type is the most commonly used because it is optimized for server-side applications, where source code is not publicly exposed, and Client Secret confidentiality can be maintained. This is a redirection-based flow, which means that the application must be capable of interacting with the user-agent (i.e. the user’s web browser) and receiving API authorization codes that are routed through the user-agent.

Creating The Client

Targeted applications:

  • Public and confidential web frontend application - web app or browser-based app
  • Native frontend application - mobile or desktop app

Request body example:

{
    name: "App Name",
    internal: true,
    redirectURIs: ["https://www.app_name.com/auth/callback"],
    clientProfile: "web",
    scope: "*"
}

Requesting Tokens

Once a client has been created, developers may use their client ID and secret to request an authorization code and access token from your application.

  1. Get authorization codes
  • HTTP Method: GET

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/authorize

  • Query parameters:

{
    client_id: "client-id",
    redirect_uri: "http://example.com/callback",
    response_type: "code",
    scope: "", // OPTIONAL
    state: "" // OPTIONAL but highly recommended
}

Note: client_id and client_secret can be sent via Basic authorization header and not in the request body.

Authorization: Basic {BASE64URL-ENCODE(client_id:client_secret)}

After sending this request, the client will be redirect to an authentication page. Once the end-user authenticated, he will be redirected to the provided redirect_uri with the authorization code.

The given authorization code will be used to request access token in the next step.

  1. Converting Authorization Codes To Access Tokens
  • HTTP Method: POST

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/token

  • Query body:

{
    grant_type: "authorization_code", 
    client_id: "client-id",
    client_secret: "client-secret", // required only for confidential client
    redirect_uri: "http://example.com/callback",
    code: "code" // code previously received
}

Note: client_id and client_secret can be sent via Basic authorization header and not in the request body.

Authorization: Basic {BASE64URL-ENCODE(client_id:client_secret)}

Try with Postman (You can also try with other rest API client)

  • Configure a single request
    • Create a new request
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Authorization Code as Grant Type value
    • Fill the rest of the form with the data of the client that you created before
  • Configure a folder
    • Right click on the folder and Click on Edit
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Authorization Code as Grant Type value
    • Fill the rest of the form with the data of the client that you created before

Authorization Code Grant with PKCE

The Authorization Code grant with "Proof Key for Code Exchange" (PKCE) is a secure way to authenticate public client. You use it when there is not guarantee that the client client can store secret key confidentially.

This grant is based on a "code verifier" and a "code challenge".

Code Verifier & Code Challenge

As this authorization grant does not provide a client secret, developers will need to generate a combination of a code verifier and a code challenge in order to request a token.

The code verifier should be a random string of between 43 and 128 characters containing letters, numbers and "-", ".", "*", "~", as defined in the RFC 7636 specification.

The code challenge should be a BASE64URL-ENCODE encoded string with URL and filename-safe characters. The trailing '=' characters should be removed and no line breaks, whitespace, or other additional characters should be present.

Creating The Client

Targeted applications:

  • Public web frontend application - web app or browser-based app
  • Native frontend application - mobile or desktop app

Request body example:

{
    name: "App Name",
    internal: false,
    redirectURIs: ["https://www.app_name.com/auth/callback"],
    clientProfile: "user-agent-based",
    scope: "read:users list:users"
}

Requesting Tokens

Once a client has been created, developers may use their client ID and secret to request an authorization code and access token from your application.

  1. Get authorization codes
  • HTTP Method: GET

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/authorize

  • Query parameters:

{
    client_id: "client-id",
    redirect_uri: "http://example.com/callback",
    response_type: "code",
	code_challenge: "generated-code-challenge", // REQUIRED.  Code challenge.
    code_challenge_method: "S256", // OPTIONAL, defaults to "plain" if not present in the request.  Code verifier transformation method is "S256" or "plain".
    scope: "", // OPTIONAL
    state: "" // OPTIONAL but highly recommended
}

After sending this request, the client will be redirect to an authentication page. Once the end-user authenticated, he will be redirected to the provided redirect_uri with the authorization code.

The given authorization code will be used to request access token in the next step.

  1. Converting Authorization Codes To Access Tokens
  • HTTP Method: POST

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/token

  • Query body:

{
    grant_type: "authorization_code",
    client_id: "client-id",
    redirect_uri: "http://example.com/callback",
    code_verifier: "codeVerifier",
    code: "code" // code previously received
}

Try with Postman (You can also try with other rest API client)

  • Configure a single request
    • Create a new request
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Authorization Code (With PKCE) as Grant Type value
    • Fill the rest of the form with the data of the client that you created before
  • Configure a folder
    • Right click on the folder and Click on Edit
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Authorization Code (With PKCE) as Grant Type value
    • Fill the rest of the form with the data of the client that you created before

Password Grant

The password grant allows confidential application to obtain an access token using an e-mail address / username and password. This allows you to issue access tokens securely to your first-party clients without requiring your users to go through the entire authorization code redirect flow.

Targeted clients:

This grant type should only be enabled on the authorization server if other flows are not viable. Also, it should only be used if first-party applications (e.g. : applications in your organization).

Creating A Password Grant Client

This grant is recommended for internal (First-party applications) applications:

  • Public or confidential web frontend application - web app or browser-based app
  • Native frontend application - mobile or desktop app

Request body example:

{
    name: "App Name",
    internal: true, // must be true for password grant
    redirectURIs: ["https://www.app_name.com/auth/callback"],
    clientProfile: "native",
    scope: "*"
}

Requesting Tokens

Once a client has been created, developers may use their client ID and secret to request an access token from your application.

The consuming application should send client ID, secret key, username and password to your application's /oauth/v2/token endpoint.

Request tokens

  • HTTP Method: POST

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/token

  • Query body:

{
    grant_type: "password",
    client_id: "client-id",
    client_secret: "client-secret",
    username: "john.conor@sky.net",
    password: "my-password",
    scope: "" // OPTIONAL
}

Note: client_id and client_secret can be sent via Basic authorization header and not in the request body.

Authorization: Basic {BASE64URL-ENCODE(client_id:client_secret)}

Try with Postman (You can also try with other rest API client)

  • Configure a single request
    • Create a new request
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Password Credentials as Grant Type value
    • Fill the rest of the form with the data of the client that you created before
  • Configure a folder
    • Right click on the folder and Click on Edit
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Password Credentials as Grant Type value
    • Fill the rest of the form with the data of the client that you created before

Implicit Grant

The implicit grant is similar to the authorization code grant; however, the token is returned to the client without exchanging an authorization code. This grant is most commonly used for JavaScript or mobile applications where the client credentials can't be securely stored.

The implicit grant type is used for mobile apps and web applications (i.e. applications that run in a web browser), where the client secret confidentiality is not guaranteed. The implicit grant type is also a redirection-based flow but the access token is given to the user-agent to forward to the application, so it may be exposed to the user and other applications on the user’s device.

Creating An Implicit Grant Client

Targeted applications:

  • Public web frontend application - browser-based app
  • Native frontend application - mobile or desktop app

Request body example:

{
    name: "App Name",
    internal: true,
    redirectURIs: ["https://www.app_name.com/auth/callback"],
    clientProfile: "user-agent-based",
    scope: "read:users list:users edit:users"
}

Request tokens

  • HTTP Method: POST

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/token

  • Query body:

{
    client_id: "client-id",
    redirect_uri: "http://example.com/callback",
    response_type: "token",
    scope: "", // OPTIONAL
    state: "state" // OPTIONAL but highly recommended
}

Try with Postman (You can also try with other rest API client)

  • Configure a single request
    • Create a new request
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token and fill the form with the client data
    • Select Implicit as Grant Type value
    • Fill the rest of the form with the data of the client that you created before
  • Configure a folder
    • Right click on the folder and Click on Edit
    • Select Authorization tab
    • Select Implicit as Grant Type value
    • Fill the rest of the form with the data of the client that you created before

Client Credentials Grant

The client credentials grant is suitable for machine-to-machine authentication. Use it if you need for example two or more servers of your organization to communicate together.

The client credentials grant type provides an application a way to access its own service. Server to server communication in the same organization.

Creating An Client Credentials Grant Client

Targeted applications:

  • Confidential web application - frontend or backend

Request body example:

{
    name: "App Name",
    internal: true, // must be true for client credentials grant
    redirectURIs: ["https://www.app_name.com/auth/callback"],
    clientProfile: "web", // must be web for client credentials grant
    scope: "*"
}

Request token

  • HTTP Method: POST

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/token

  • Query body:

{
    grant_type: "client_credentials",
    client_id: "client-id",
    client_secret: "client-secret",
    scope: "client-requested-scope" // OPTIONAL
}

Note: client_id and client_secret can be sent via Basic authorization header and not in the request body.

Authorization: Basic {BASE64URL-ENCODE(client_id:client_secret)}

Try with Postman (You can also try with other rest API client)

  • Configure a single request
    • Create a new request
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Client Credentials as Grant Type value
    • Fill the rest of the form with the data of the client that you created before
  • Configure a folder
    • Right click on the folder and Click on Edit
    • Select Authorization tab
    • Select Oauth 2.0 within the Type
    • Click on Get New Access Token
    • Select Client Credentials as Grant Type value
    • Fill the rest of the form with the data of the client that you created before

Refreshing Tokens

Token generated with some grants as Password Credentials Grant and Authorization Code Grant, come with a refresh token that the user can use to get a new token as follow.

Refresh token

  • HTTP Method: POST

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/token

  • Query body:

{
    grant_type: "refresh_token",
    refresh_token: "the-refresh-token",
    client_id: "client-id",
    client_secret: "client-secret",
    scope: "client-requested-scope" // OPTIONAL
}

Note: client_id and client_secret can be sent via Basic authorization header and not in the request body.

Authorization: Basic {BASE64URL-ENCODE(client_id:client_secret)}

Revoke Token

Refresh token and Access token can be revoked, In case you would like to disconnect a user.

Refresh token

  • HTTP Method: POST

  • Endpoint: {YOUR_API_BASE_URL}/oauth/v2/revoke

  • Query body:

{
    token_type_hint?: "refresh_token", // or "access_token";
  	token: "the-token",
  	client_id: "client-id", // OPTIONAL
  	client_secret: "client-secret" // OPTIONAL
}

Note: client_id and client_secret can be sent via Basic authorization header and not in the request body.

Authorization: Basic {BASE64URL-ENCODE(client_id:client_secret)}

Purging Tokens and Authorization codes

This package provide some endpoints to purge revoked or expired tokens and authorization codes.

TargetHTTP MethodEndpoint
TokensDELETE/oauth/v2/purge/token
Authorization CodesDELETE/oauth/v2/purge/code
Tokens & Authorization CodesDELETE/oauth/v2/purge

By default, all expired or revoked tokens or codes are purged, but you may want to delete only revoked tokens or only expired tokens. To do this, you can pass the type parameter to your request, which can respectively take the values revoked or expired.

Example:

  • DELETE: {YOUR_API_BASE_URL}/oauth/v2/purge/token?type=revoked

    Delete all revoked tokens

  • DELETE: {YOUR_API_BASE_URL}/oauth/v2/purge/code

    Delete both revoked and expired authorization codes

Protecting Routes

Via Middleware.

You can secure your routes by adding the middleware Oauth.authorize(). It is a static method of the Oauth class provided by the package.

Method definition:

Oauth.authorize(scope?: string | undefined): (req: Request, res: Response, next: NextFunction) => Promise<Response<any> | undefined>

Import Oauth

import { Oauth } from "@noreajs/oauth-v2-provider-me";

// app is an express application or express router
app.route('/account/update').put([
    // ... other middleware
    Oauth.authorize(), // oauth middleware. It must always be before the protected resource
    // ... other middleware
    authController.update // protected resource
]);

Verify a token manually

In some use cases, you can recover the access token on your own. There is a method that allows you to verify a token: Oauth.verifyToken.

Example

import { Oauth } from "@noreajs/oauth-v2-provider-me";

// Token example
const accessToken = "euiaoejsjflsdfhoiuezioueiz.ieaoufisdfosdfusdfksdlkfjdkfjs.skdjflksdfjls";

Oauth.verifyToken(accessToken, (userId, lookupData) => {
	// userId : current user id
    // lookupData: current user data if the lookup method has been defined while initializing Oauth. 
    // lookupData is undefined for client_credentials grant
    next();
}, (reason: string, authError: boolean) => {
    // reason: is the description of the error
    if (authError) {
        // Authorization error
    } else {
        // internal error (e.g. Oauth 2 Server has not been initialized yet)
    }
}, scope)

Method prototype (Typescript)

Oauth.verifyToken(token: string, success: (userId: string, lookupData?: any) => Promise<void> | void, error: (reason: string, authError: boolean) => Promise<void> | void, scope?: string | undefined): Promise<void>

Secure Oauth 2 endpoints

While initializing the provider, there is a property called securityMiddlewares. Once your app if fully functional and ready for production you can secure Oauth 2 endpoints (Client management endpoints, purge endpoints).

securityMiddlewares initialization example

{
    // ... other initialization properties
    securityMiddlewares: [
        // other middlewares
        Oauth.authorize('create:clients list:clients purge:tokens purge:codes'),
        // other middlewares
    ],
    // ... other initialization properties
}

Checking Scopes

The scope(s) required for a resource can be passed via the Oauth.authorize method as follow:

app.route('/account/update').put([
    Oauth.authorize('edit:profile'),
    authController.update
]);

Note : Many scopes can be transmitted by separating them with a space.

Mongoose Models

The Mongoose models used by the package are accessible. You can use them as you wish.

Model NameCollection NameDescription
OauthAccessTokenoauth_access_tokensManage access tokens
OauthAuthCodeoauth_auth_codesManage authorization codes
OauthClientoauth_clientsManage clients
OauthRefreshTokenoauth_refresh_tokensManage refresh tokens
OauthScopeoauth_scopesManage scopes

You can import these models as follows:

import { /* model_name*/ } from "@noreajs/oauth-v2-provider-me"

Consuming Your API With JavaScript (axios)

0.5.3

9 months ago

0.5.0

1 year ago

0.5.2

1 year ago

0.5.1

1 year ago

0.4.2

1 year ago

0.3.0-1

2 years ago

0.3.0-0

2 years ago

0.3.0-2

2 years ago

0.3.0

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.2.7

2 years ago

0.2.6

2 years ago

0.2.8

2 years ago

0.2.5

2 years ago

0.2.4-4

2 years ago

0.2.4

2 years ago

0.2.4-3

3 years ago

0.2.4-2

3 years ago

0.2.4-1

3 years ago

0.2.4-0

3 years ago

0.2.3

3 years ago

0.2.3-3

3 years ago

0.2.3-2

3 years ago

0.2.3-1

3 years ago

0.2.3-0

3 years ago

0.2.1

3 years ago

0.2.0

3 years ago

0.1.8

3 years ago

0.1.7

3 years ago

0.1.9

3 years ago

0.2.2

3 years ago

0.1.6

3 years ago

0.1.5

3 years ago

0.1.2

3 years ago

0.1.4

3 years ago

0.1.3

3 years ago

0.1.1

3 years ago

0.1.0

3 years ago

0.0.9

3 years ago

0.0.8

3 years ago

0.0.8-1

4 years ago

0.0.8-0

4 years ago

0.0.7-10

4 years ago

0.0.7-9

4 years ago

0.0.7-8

4 years ago

0.0.7-7

4 years ago

0.0.7-6

4 years ago

0.0.7-5

4 years ago

0.0.7-4

4 years ago

0.0.7-3

4 years ago

0.0.7-2

4 years ago

0.0.7-1

4 years ago

0.0.7-0

4 years ago

0.0.6

4 years ago

0.0.6-0

4 years ago

0.0.5

4 years ago

0.0.4

4 years ago

0.0.4-2

4 years ago

0.0.4-0

4 years ago

0.0.4-1

4 years ago

0.0.3

4 years ago

0.0.2

4 years ago

0.0.1

4 years ago