Go middleware for monetizing your API on a per-request basis with Bitcoin and Lightning ⚡️

ln-paywall

GoDoc Build Status Go Report Card GitHub Releases

Go middleware for monetizing your API on a per-request basis with Bitcoin and Lightning ⚡️

Middlewares for:

A client package exists as well to make consuming LN-paywalled APIs extremely easy (you just use it like the standard Go http.Client and the payment handling is done in the background).

An API gateway is on the roadmap as well, which you can use to monetize your API that's written in any language, not just in Go.

Contents

Purpose

Until the rise of cryptocurrencies, if you wanted to monetize your API (set up a paywall), you had to:

  1. Use a centralized service (like PayPal)
    • Can shut you down any time
    • High fees
    • Your API users need an account
    • Can be hacked
  2. Keep track of your API users (keep accounts and their API keys in some database)
    • Privacy concerns
    • Data breaches / leaks
  3. Charge for a bunch of requests, like 10.000 at a time, because real per-request payments weren't possible

With cryptocurrencies in general some of those problems were solved, but with long confirmation times and high per-transaction fees a real per-request billing was still not feasable.

But then came the Lightning Network, an implementation of routed payment channels, which enables real near-instant microtransactions with extremely low fees, which cryptocurrencies have long promised, but never delivered. It's a second layer on top of existing cryptocurrencies like Bitcoin that scales far beyond the limitations of the underlying blockchain.

ln-paywall makes it easy to set up an API paywall for payments over the Lightning Network.

How it works

With ln-paywall you can simply use one of the provided middlewares in your Go web service to have your web service do two things:

  1. The first request gets rejected with the 402 Payment Required HTTP status, a Content-Type: application/vnd.lightning.bolt11 header and a Lightning (BOLT-11-conforming) invoice in the body
  2. The second request must contain a X-Preimage header with the preimage of the paid Lightning invoice (hex encoded). The middleware checks if 1) the invoice was paid and 2) not already used for a previous request. If both preconditions are met, it continues to the next middleware or final request handler.

Prerequisites

There are currently two prerequisites:

  1. A running Lightning Network node. The middleware connects to the node for example to create invoices for a request. The ln package currently provides factory functions for the following LN implementations:
    • lnd
      • Requires the node to listen to gRPC connections
      • If you don't run it locally, it needs to listen to connections from external machines (so for example on 0.0.0.0 instead of localhost) and has the TLS certificate configured to include the external IP address of the node.
    • c-lightning with Lightning Charge
      • Run for example with Docker: docker run -d -u `id -u` -v `pwd`/data:/data -p 9112:9112 -e API_TOKEN=secret shesek/lightning-charge
      • Vanilla c-lightning (without Lightning Charge) won't be supported as long as c-lightning's RPC API only works via Unix socket and cannot be used as a remote server, because this is not a good fit for potentially multiple web service instances elastically scaled across a cluster of host machines
    • eclair (not implemented yet - PRs Welcome )
    • Roll your own!
      • Just implement the simple wall.LNClient interface (only two methods!)
  2. A supported storage mechanism. It's used to cache preimages that have been used as a payment for an API call, so that a user can't do multiple requests with the same preimage of a settled Lightning payment. The wall package currently provides factory functions for the following storages:
    • A simple Go map
      • The fastest option, but 1) can't be used across horizontally scaled service instances and 2) doesn't persist data, so when you restart your server, users can re-use old preimages
    • bbolt - a fork of Bolt maintained by CoreOS
      • Very fast, doesn't require any remote or local TCP connections and persists the data, but can't be used across horizontally scaled service instances because it's file-based. Production-ready for single-instance web services though.
    • Redis
      • Although the slowest of these options, still fast and most suited for popular web services: Requires a remote or local TCP connection and some administration, but allows data persistency and can even be used with a horizontally scaled web service
      • Run for example with Docker: docker run -d -p 6379:6379 redis
        • Note: In production you should use a configuration with password (check out bitnami/redis which makes that easy)!
    • groupcache (not implemented yet - PRs Welcome )
    • Roll your own!
      • Just implement the simple wall.StorageClient interface (only two methods!)

Usage

GoDoc

Get the package with go get -u github.com/philippgille/ln-paywall/....

We strongly encourage you to use vendoring, because as long as ln-paywall is version 0.x, breaking changes may be introduced in new versions, including changes to the package name / import path. The project adheres to Semantic Versioning and all notable changes to this project are documented in RELEASES.md.

Middleware

The best way to see how to use ln-paywall is by example. In the below examples we create a web service that responds to requests to /ping with "pong", using Gin as the web framework.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/philippgille/ln-paywall/ln"
	"github.com/philippgille/ln-paywall/storage"
	"github.com/philippgille/ln-paywall/wall"
)

func main() {
	r := gin.Default()

	// Configure middleware
	invoiceOptions := wall.DefaultInvoiceOptions // Price: 1 Satoshi; Memo: "API call"
	lndOptions := ln.DefaultLNDoptions           // Address: "localhost:10009", CertFile: "tls.cert", MacaroonFile: "invoice.macaroon"
	storageClient := storage.NewGoMap()          // Local in-memory cache
	lnClient, err := ln.NewLNDclient(lndOptions)
	if err != nil {
		panic(err)
	}
	// Use middleware
	r.Use(wall.NewGinMiddleware(invoiceOptions, lnClient, storageClient))

	r.GET("/ping", func(c *gin.Context) {
		c.String(http.StatusOK, "pong")
	})

	r.Run() // Listen and serve on 0.0.0.0:8080
}

This is just the most basic example. See the list of examples below for examples with other web frameworks / routers / just the stdlib, as well as for a more complex and useful example.

List of examples

Follow the links to the example code files.

Simple examples to show the use for the different web frameworks / routers / just the stdlib:

More complex and useful example:

Client

package main

import (
	"fmt"
	"io/ioutil"

	"github.com/philippgille/ln-paywall/ln"
	"github.com/philippgille/ln-paywall/pay"
)

func main() {
	// Set up client
	lndOptions := ln.LNDoptions{ // Default address: "localhost:10009", CertFile: "tls.cert"
		MacaroonFile: "admin.macaroon", // admin.macaroon is required for making payments
	}
	lnClient, err := ln.NewLNDclient(lndOptions)
	if err != nil {
		panic(err)
	}
	client := pay.NewClient(nil, lnClient) // Uses http.DefaultClient if no http.Client is passed

	// Send request to an ln-paywalled API
	res, err := client.Get("http://localhost:8080/ping")
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	// Print response body
	resBody, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(resBody))
}

You can also view this example here.

Related Projects

Owner
Philipp Gillé
Software engineer, progressive, cosmopolitan, globalist, futurist, technology maximalist - interested in #technology #innovation #blockchain #bitcoin #lightning
Philipp Gillé
Comments
  • Some logs don't contain a timestamp

    Some logs don't contain a timestamp

    Example:

    [GIN-debug] Listening and serving HTTP on :8080
    Creating invoice for a new API request
    2018/09/02 22:45:30 Sending invoice in response: [...]
    [GIN] 2018/09/02 - 22:45:30 | 402 |    279.2542ms |       127.0.0.1 | GET      /qr?data=testtext
    

    So it's Creating invoice for a new API request, but also Checking invoice for hash [...] and potentially others.

  • A client can cheat if the backend has multiple endpoints with different prices

    A client can cheat if the backend has multiple endpoints with different prices

    Reproduction

    1. Create backend with 2 endpoints, "a" and "b"
      • "a" is 1 Satoshi, "b" is 10 Satoshis
    2. Client sends initial request to each
    3. Client pays invoice for "a"
    4. Client sends final request to "b", but with the preimage of the payment for "a"
    5. => Request works although the client didn't pay the related invoice

    Problem

    The middleware doesn't track which invoice was created for which endpoint, it doesn't know about the endpoints at all. It just checks if the preimage was already used as payment proof and then checks the LN node for the invoice's existence and settlement.

    Possible solution

    1. When the initial request arrives we're in the correct middleware instance, so we cache the URL path along with the preimage or its hash
    2. When the final request arrives we don't just check if it's valid (not used before, invoice settled), but also if the current request URL path is the same we previously cached. (Lookup via the preimage or its hash)

    Note 1: It's not enough to have an in-memory cache of just the preimage or its hash per middleware instance, because in case of a horizontally scaled web service the caches wouldn't work properly anymore. It's probably best to use the existing storage client implementations, so the web service developer can choose for example Redis when he wants to scale horizontally.

  • Add package for client-side functionality

    Add package for client-side functionality

    Currently when a developer uses an API that's paywalled with ln-paywall (or ElementProject's paypercall and potentially other projects in the future), he has to take care of:

    • Sending two physical requests for one logical one
    • Handling the 402 response
    • Establishing a connection to his Lightning Network node by himself and calling its RPC / HTTP API

    All by himself.

    This can be easily wrapped in a way so that the developer only has to create one instance of a client, similar to the net/http.Client, create a single net/http.Request and then call client.Do(...). Just as with the net/http.Client. All the payment handling can happen automatically in the background.

    The first goal should be a minimal working version. More advaned features should be added in the future with separate issues created for each feature.

    Minimal working version:

    • Connects to and works with lnd
    • No convenience methods (Get(...), Post(...)), just Do(...)
      • Are they exposed by the embedded HTTP client? Can they be hidden in that case?
    • No configuration like "only pay if the amount in the invoice is <= x BTC", just always pay the invoice

    Future features:

    • Support other LN node implementations
    • Make payments configurable, e.g. "only pay if the amount in the invoice is <= x BTC"
    • ...
  • Add reporting of code coverage

    Add reporting of code coverage

    1. Add code coverage analysis, for example: go test -v -race -coverprofile="coverage.txt" -covermode=atomic ./...
    2. Add reporting to service like Codecov or Coveralls
    3. Add badge to README

    See:

    • https://blog.golang.org/cover
    • https://github.com/codecov/example-go
  • Add groupcache as storage implementation

    Add groupcache as storage implementation

    Groupcache is a distributed cache, but unlike Redis, it doesn't need an extra server - it connects to its peers directly. While skimming info about it I read that you can't update or delete entries, because it's just meant as the most simple cache, but that's currently sufficient for ln-paywall's needs.

    https://github.com/golang/groupcache

  • Clarify preimage format / encoding

    Clarify preimage format / encoding

    Currently the value in the "x-preimage" header is required to be Base64, because that's the format that's used in listed invoices when executing lncli listinvoices. Not only for preimages, but also for their hashes.

    But on the other hand the LightningClient's method LookupInvoice(...) expects a hex-encoded payment hash.

    That's why in our package, when receiving a request with a correct x-preimage header, we need to:

    1. Base64 Decode the preimage
    2. Sha256 the previous value
    3. Hex the previous value
    4. Send it to lnd

    So:

    1. Figure out which format is best for using in the header (Plain preimage, Base64 or hex)
    2. Change our implementation in case it's not Base64
    3. Document which format is expected
  • Improve performance by reusing the gRPC connection

    Improve performance by reusing the gRPC connection

    Currently a new gRPC connection to lnd is created for every request.

    It should be possible to create the gRPC connection only once when one of the middlewares is created, for example when NewHandlerFuncMiddleware(...) is called, but before the handler func is returned.

    func createHandlerFunc(invoiceOptions InvoiceOptions, lndOptions LNDoptions, storageClient StorageClient, next http.HandlerFunc) func(w http.ResponseWriter, r *http.Request) {
    	// <---- create the gRPC connection here and reuse it within the below func
    	return func(w http.ResponseWriter, r *http.Request) {
    

    Care must be taken regarding connections that are aborted/lost/..., for example due to a network error. Is it possible to reconnect with the existing connection object/ref?

  • Refactor middlewares

    Refactor middlewares

    Currently the middleware factory functions contain a lot of duplicated code. There should be only one web framework-independent function that contains the main middleware logic, and then web framework-specific implementations that handle the details of the used web frameworks.

    This PR contains that, including a fix for the middleware for Echo.

  • Bad error responses when using the Echo middleware

    Bad error responses when using the Echo middleware

    • The invoice response contains a 200 OK status code instead of 402 Payment Required
    • All error responses wrap the error message in JSON instead of just using the error message as text, as the other middlewares do. It should be the same for all middlewares, and it should be the text directly.
  • Performance decreases when using Lightning Charge and the amount of invoices increases

    Performance decreases when using Lightning Charge and the amount of invoices increases

    As mentioned in the v0.5.0 release notes, the ln.charge implementation of the wall.LNclient interface currently becomes slower when the amount of invoices in the Lightning Charge server increases.

    The reason is that ln-paywall currently uses the payment hash (a.k.a. preimage hash) of an invoice as ID, which works fine with lnd (and would also work with eclair). But Lightning Charge uses its own randomly generated IDs, so instead of fetching the specific ID we need, we fetch all IDs and filter out the one we're looking for by comparing the preimage hashes.

    The solution is to add the implementation specific ID as field to the wall.invoiceMetaData so it can be stored, and when a client sends the final request we can still use the preimage hash to fetch the metadata from the storage, but then use the ID within that data to make the lookup on Lightning Charge.

    This leads to the ID and the preimage hash fields being the same when using lnd. We could omit one of the fields and just use one of them, but this could also lead to confusion later.

  • Fix a client can cheat with using the wrong preimage for a request

    Fix a client can cheat with using the wrong preimage for a request

    • Previously a client just had to send any preimage that corresponds to a settled invoice in the LN node, without the invoice having to originate from the middleware, or from the same endpoint as used in the current request. So he could pay cheap invoices and use expensive endpoints for example.
    • The fix required storing metadata in the DB, e.g. the HTTP method and URL path, while being backward compatible and not send those data encrypted as token in the first response, requiring the same token and decrypting and using the values during the second request
    • Both modes are interesting and have their pros and cons, so evaluate supporting both in the future
    • Storing the metadata required changing the wall.StorageClient interface and its implementations in the storage package

    Closes #16

  • Add license scan report and status

    Add license scan report and status

    Your FOSSA integration was successful! Attached in this PR is a badge and license report to track scan status in your README.

    Below are docs for integrating FOSSA license checks into your CI:

  • Make client fully compatible with standard http.Client

    Make client fully compatible with standard http.Client

    Currently pay.Client only implements a subset of the http.Client's methods. I think it should be fully compatible though.

    Check out gentlemen for creating HTTP clients with middleware.

  • Store gobs as alternative to JSON

    Store gobs as alternative to JSON

    Currently the storage implementations all marshal and unmarshal to/from JSON. For example, a wall.invoiceMetaData object is first marshalled to JSON and then the resulting string is stored as value in Redis.

    JSON is nice because when problems occur (e.g. a customer says his HTTP client sent the correct X-Preimage header but the web service responded with an error "Corresponding invoice not found" or something similar), you can just check the storage and have a look at the stored data, easily seeing what's in there without having to decode anything. This might be harder for the Go Map (it's only visible / only exists within the running web service) and the bbolt DB (file is locked while in use), but easier for Redis and future storages like etcd and Consul, where even some web dashboards for exploring the storage contents exist.

    The downside of JSON though is that it's verbose. A binary format can be much more compact and thus require less storage.

    In Go, there are "gobs". See:

    • https://godoc.org/encoding/gob
    • https://blog.golang.org/gobs-of-data

    Before implementing this, we should first benchmark if it makes sense at all for the limited types of data we store (currently only wall.invoiceMetaData). A 50% reduction in storage would be nice, or maybe 30% is enough?

    Also be aware that when storing gobs, other programming languages can't deal with the data! For example when a customer uses one lnd instance, but two web services with one using ln-paywall and the other using a compatible Java middleware, and he wants to prevent web service clients from cheating by reusing preimages. Cheating example: Send first request to the Go web service, pay the invoice, send final request with preimage to Go web service, then send another request immediately with the same preimage to the Java web service. If both web services would have separate storage mechanisms the client could cheat that way. So a common storage must be used, like Redis for example. But when the stored value in Redis is a gob, the Java middleware can't read it. So:

    1. Using gobs instead of JSON must be optional, off by default
    2. This downside must be mentioned in the comments for the respective option (probably in the storage implementation's specific options object)
  • Make the middleware compatible with paypercall clients

    Make the middleware compatible with paypercall clients

    As mentioned in https://github.com/philippgille/ln-paywall/issues/16#issuecomment-423765859, ln-paywall stores invoice metadata in the storage, so it doesn't require the X-Token that paypercall requires. The token in paypercall is used to not only include the invoice ID, but also store the metadata (HTTP method, URL path) in the token, send it to the client, then the client has to include it in the final request so the middleware can read the metadata from the token again and verify it matches the method + path of the current request, so the client can't cheat.

    So while ln-paywall doesn't require it, it would be nice to be compatible with paypercall clients. Although I'm not aware of any client-side libraries (other than our own ln-paywall client, which will gain paypercall compatibility with #23), this doesn't mean no client-side code exists (could be just plain HTTP requests), and also it would be nice to offer this compatibility anyway.

    So: After #25 is implemented, after which the middleware will accept not only the preimage of a payment, but also the invoice ID, this invoice ID can just be sent as X-Token in the response to the initial request.

    I don't think this should be the default though. So make it configurable with some new wall.MiddlewareOptions.

  • Delete old metadata from the storage

    Delete old metadata from the storage

    Currently the invoice metadata is stored in the DB forever.

    This isn't necessary from a company's point of view, because 1) the invoices are already stored in the LN node and 2) companies that need to keep invoices around for a couple of years need additional data anyway, so the metadata in the ln-paywall storage aren't of use here.

    And it's also not necessary from an implementation point of view (since #24), because 1) LN invoices expire after some time (can be configured when creating the invoice), so when an invoice isn't paid until its expiry, the metadata is of no use anymore, and 2) due to the final request now only working when the DB has a metadata entry about the corresponding initial invoice request, cheating with reusing preimages that aren't in the storage doesn't work.

    The two cases in detail:

    • Invoice request is sent, metadata is created. The invoice expires after 1 hour (default in the LN protocol standard, see BOLT 11.
      • If the client doesn't pay within 1 hour, we can throw away the metadata anyway, because the client is technically not able to pay the invoice later (due to invoice expiry).
      • If the client does pay within 1 hour, we should give him some time to send the final request, but that shouldn't be unlimited. Considering the programmatic nature of HTTP requests, a shorter expiry might make sense (invoice payment is often done by humans, with wallet software which might require some channel syncing first etc.), but 1 hour might be fine as well. This needs to be configurable.
    • Invoice request is sent, metadata created, final request sent, metadata marked as "used", so everything is done. Until #24 the preimage needed to be kept around in storage, because otherwise a client could just use any preimage that corresponds to an invoice in the LN node and the middleware would have accepted it, but now, if a request contains a preimage, which is not in the storage, it's rejected immediately! BUT the error message might confuse clients that accidentally reuse a preimage in a second request. So for usability reasons the metadata should be kept around for a bit as well. This expiry should not start from the invoice creation, but the use (the final request).

    So:

    • Unused metadata should be kept for 1 hour (configurable) after invoice creation, to give the client time to send the final request after payment
    • Used metadata should be kept for 1 hour (configurable) after usage, to give the client proper error messages when reusing a preimage
  • Accept invoice ID in request header as alternative to preimage

    Accept invoice ID in request header as alternative to preimage

    So far the middleware required the preimage as payment proof in the second request.

    But since PR #24 invoice metadata is stored in the DB, with the invoice ID being used as the key, so now we could also just accept an invoice ID and look up the status in the DB or check the settlement on the LN node.

    This would make it much easier to implement clients that don't have access to the preimage. For example: Let's say the API is used on a website, but without automated payments via an LN node operated by the people running the website, and instead the website shows the user a QR code so he can pay for the API usage. Up until now the website had to ask for the preimage. This is an extra step in itself, but even worse, if the user paid via mobile wallet, but browsed the website via desktop PC, he now had to get the preimage from his smartphone to the PC just to proof that he made the payment. When the invoice ID is enough, the user doesn't have to input anything. The website can decode the LN invoice and take the invoice ID, show the user the QR code and a button "next"/"continue" or "paid", and then the website can just send the request with the invoice ID.

    One example of such a website is https://lightning.ws, where the deployed APIs have a "Try out" section where the user has to do exactly the steps mentioned above.

Fault injection library in Go using standard http middleware

Fault The fault package provides go http middleware that makes it easy to inject faults into your service. Use the fault package to reject incoming re

Dec 25, 2022
Dead simple rate limit middleware for Go.

Limiter Dead simple rate limit middleware for Go. Simple API "Store" approach for backend Redis support (but not tied too) Middlewares: HTTP, FastHTTP

Jan 7, 2023
Simple middleware to rate-limit HTTP requests.

Tollbooth This is a generic middleware to rate-limit HTTP requests. NOTE 1: This library is considered finished. NOTE 2: Major version changes are bac

Jan 4, 2023
A Golang Middleware to handle X-Forwarded-For Header

X-Forwarded-For middleware fo Go Package xff is a net/http middleware/handler to parse Forwarded HTTP Extension in Golang. Example usage Install xff:

Nov 3, 2022
Stash is a locally hosted web-based app written in Go which organizes and serves your porn.

Stash is a locally hosted web-based app written in Go which organizes and serves your porn.

Jan 2, 2023
Go (golang) library for creating and consuming HTTP Server-Timing headers
Go (golang) library for creating and consuming HTTP Server-Timing headers

HTTP Server-Timing for Go This is a library including middleware for using HTTP Server-Timing with Go. This header allows a server to send timing info

Dec 8, 2022
Redcon is a custom Redis server framework for Go that is fast and simple to use.
Redcon is a custom Redis server framework for Go that is fast and simple to use.

Redcon is a custom Redis server framework for Go that is fast and simple to use. The reason for this library it to give an efficient server front-end for the BuntDB and Tile38 projects.

Dec 28, 2022
A simple and lightweight encrypted password manager written in Go.
A simple and lightweight encrypted password manager written in Go.

A simple and lightweight encrypted password manager written in Go.

Jun 16, 2022
Remark42 is a self-hosted, lightweight, and simple comment engine
Remark42 is a self-hosted, lightweight, and simple comment engine

Remark42 is a self-hosted, lightweight, and simple (yet functional) comment engine, which doesn't spy on users. It can be embedded into blogs, articles or any other place where readers add comments.

Dec 28, 2022
👄 The most accurate natural language detection library in the Go ecosystem, suitable for long and short text alike
👄 The most accurate natural language detection library in the Go ecosystem, suitable for long and short text alike

Its task is simple: It tells you which language some provided textual data is written in. This is very useful as a preprocessing step for linguistic data in natural language processing applications such as text classification and spell checking. Other use cases, for instance, might include routing e-mails to the right geographically located customer service department, based on the e-mails' languages.

Dec 29, 2022
Enforcing per team quota (sum of used resources across all their namespaces) and delegating the per namespace quota to users.

Quota Operator Enforcing per team quota (sum of used resources across all their namespaces) and delegating the per namespace quota to users. Instructi

Nov 9, 2022
Go implementation of a vanity attempt to generate Bitcoin private keys and subsequently checking whether the corresponding Bitcoin address has a non-zero balance.

vanity-BTC-miner Go implementation of a vanity attempt to generate Bitcoin private keys and subsequently checking whether the corresponding Bitcoin ad

Jun 3, 2022
A proof-of-concept Bitcoin client that treats Bitcoin with the contempt it deserves

Democracy A proof-of-concept Bitcoin client that treats Bitcoin with the contempt it deserves. Bitcoin offers no inbuilt democracy. One can either vot

Jul 19, 2022
This is the basis of the bot for viewing information on the crp.is exchange in a telegram.
This is the basis of the bot for viewing information on the crp.is exchange in a telegram.

crye_go_bot An example of a telegram bot for working with the crp.is exchange Usage Create config.json to test bot operation Later Test the bot by cre

Jan 15, 2022
⚡ 🖥️ 👾 Host your own Lightning Address on LND

⚡ ??️ ?? Host your own Lightning Address on LND Lighting Wallets like BlueWallet, Blixt and many more allow us to send sats to Lighting Addresses like

Dec 22, 2022
Goget will send a http request, and show the request time, status, response, and save response to a file

Goget will send a http request, and show the request time, status, response, and save response to a file

Feb 9, 2022
Header Block is a middleware plugin for Traefik to block request and response headers which regex matched by their name and/or value

Header Block is a middleware plugin for Traefik to block request and response headers which regex matched by their name and/or value Conf

May 24, 2022
Request: a HTTP request library for Go with interfaces and mocks for unit tests

Requester Request is a HTTP request library for Go with interfaces and mocks for

Jan 10, 2022
Validate Golang request data with simple rules. Highly inspired by Laravel's request validation.
Validate Golang request data with simple rules. Highly inspired by Laravel's request validation.

Validate golang request data with simple rules. Highly inspired by Laravel's request validation. Installation Install the package using $ go get githu

Dec 29, 2022
github-actions-merger is github actions that merges pull request with commit message including pull request labels.

github-actions-merger github-actions-merger is github actions that merges pull request with commit message including pull request labels. Usage Write

Dec 7, 2022