# Developing Webassembly extensions

Synapse supports Webassembly (WASM) extensions using the [Extism framework](https://extism.org/). While it’s possible to develop extensions without any framework, it’s recommended to use one of the supported [Extism plugin developments kits](https://extism.org/docs/concepts/pdk) as they provide helpers for sending JSON back and forth, reading configuration and other useful functions.

|     |     |
| --- | --- |
|  | WASM extensions should be reactor modules that are long-lived. Note that Synapse pools multiple instances of a single extension in order to handle parallel requests (WASM memory is not thread-safe). This is an important consideration to keep in mind when creating long-lived resources such as database connections as there will be multiple independent copies of each WASM module alive at the same time on a single Synapse instance. |

## Common functionality

### Lifecycle management

If the WASM module exports a `cerbosInit` function, Synapse invokes it to initialize the instance. Note that Synapse maintains an internal pool of instances to reduce load times and handle multi-threading so this function will be invoked multiple times (i.e. it’s not a singleton). It’s useful for allocating long-lived, per-instance, reusable resources such as database connections.

When Synapse is gracefully shut down, the `cerbosDeinit` function is invoked on all instances of the module if it’s exported. It can be used to perform cleanup tasks such as closing database connections acquired during `cerbosInit`.

Both `cerbosInit` and `cerbosDeinit` take no arguments and return an `i32` where `0` denotes success and any non-zero value denotes failure. Custom configuration set in the Synapse configuration file under the extension’s `configuration` field is accessible via the Extism PDK’s config helpers (`pdk.GetConfig` in Go, `Config.get` in JavaScript/TypeScript).

The following snippet demonstrates how these functions might be implemented in a Go WASM module.

```go
var sqlite *sqlx.DB

//go:wasmexport cerbosInit
func cerbosInit() int32 {
    connectionStr, exists := pdk.GetConfig("connectionString")
    if !exists {
        pdk.SetError(errors.New("connectionString configuration is required"))
        return 1
    }

db, err = sqlx.Connect("sqlite3", connectionStr)
	if err != nil {
		pdk.SetError(err)
		return 1
	}

sqlite = db
	return 0
}

//go:wasmexport cerbosDeinit
func cerbosDeinit() int32 {
    if err := sqlite.Close(); err != nil {
        pdk.SetError(err)
        return 1
    }

return 0
}
```

### Logging

Log messages emitted by the Extism PDK are routed to Synapse’s logger and tagged with the configured extension name. Messages below the Synapse log level set via `--log.level` are suppressed. Use the PDK’s log helpers from inside any export to surface diagnostic output through the host:

- Go: `pdk.Log(pdk.LogDebug, "msg")` (levels: `LogTrace`, `LogDebug`, `LogInfo`, `LogWarn`, `LogError`).

- JavaScript/TypeScript: `console.debug("msg")` (also `console.trace`, `console.log`/`info`, `console.warn`, `console.error`).

### Host functions

Synapse exposes the following host functions that can be called by any extension. They are all exported under the module name `extism:host/user`.

`cacheDelete`

Delete a value from the cache.

Inputs

- `key`: Pointer to a string

Outputs

- An integer value. Non-zero value indicates failure.

`cacheGet`

Get a value from the cache.

Inputs

- `key`: Pointer to a string

Outputs

- Pointer to a byte array containing the value.

`cacheSet`

Save a value to cache.

Inputs

- `key`: Pointer to a string

- `value`: Pointer to a byte array

- `duration`: Integer value defining cache duration in milliseconds

Outputs

- An integer value. Non-zero value indicates failure.

`cacheSetIfNotExists`

Save a value to cache if it doesn’t already exist.

Inputs

- `key`: Pointer to a string

- `value`: Pointer to a byte array

- `duration`: Integer value defining cache duration in milliseconds

Outputs

- An integer value. Non-zero value indicates failure.

`checkResources`

Perform a Cerbos `CheckResources` call.

Inputs

- `request`: Pointer to a byte array containing the JSON-encoded `CheckResources` request

Outputs

- Pointer to a byte array containing the JSON-encoded `CheckResources` response

`dataSourceLookup`

Lookup information from a data source.

Inputs

- `request`: Pointer to a byte array containing the JSON-encoded lookup request. See [Data sources](https://docs.cerbos.dev/synapse/latest/extensions/data-sources).

Outputs

- Pointer to a byte array containing the JSON-encoded lookup response. See [Data sources](https://docs.cerbos.dev/synapse/latest/extensions/data-sources).

`planResources`

Perform a Cerbos `PlanResources` call.

Inputs

- `request`: Pointer to a byte array containing the JSON-encoded `PlanResources` request

Outputs

- Pointer to a byte array containing the JSON-encoded `PlanResources` response

The following snippet illustrates how to access host functions from a Go WASM module.

```go
//go:wasmimport extism:host/user cacheSet
func cacheSet(uint64, uint64, uint32) int32

func writeToCache(key string value []byte) {
	keyMem := pdk.AllocateString(key)
	defer keyMem.Free()
	valueMem := pdk.AllocateBytes(value)
	defer valueMem.Free()

result := cacheSet(keyMem.Offset(), valueMem.Offset(), 10000)
	if result != 0 {
        // handle error
	}
}
```

## Data source extension

A data source extension must export a function named `lookup` that accepts a pointer to a JSON-encoded lookup request and returns a pointer to a JSON-encoded lookup result.

Lookup request

```json
{
    "dataSource": "myDataSource", (1)
    "query": "SELECT department FROM employees WHERE id = :employee_id", (2)
    "queryParameters": { (3)
        "employee_id": "simon"
    },
    "cacheOptions": { (4)
        "cacheKey": "simon", (5)
        "cacheExpiry": "300s", (6)
        "ifNotExists": true (7)
    }
}
```

|     |     |
| --- | --- |
| **1** | Name of the data source to query |
| **2** | Query to execute on the data source. This is specific to the data source. The value can be any valid JSON value, including complex objects and arrays. |
| **3** | Optional query parameters |
| **4** | Optional cache options for caching the result of the lookup |
| **5** | Cache key for this lookup |
| **6** | Optional duration for caching the result of the lookup |
| **7** | Optional flag to prevent overwriting an existing cache key |

Only the `dataSource` and `query` fields are required. The `query` field can be any valid JSON value, including complex nested objects. The API contract for the `query` field is specific to each data source implementation as it depends on the logic that each data source encapsulates.

The response from the data source only contains the single field `result`. It could contain any valid JSON value including objects and arrays. The format of the result depends on the data source.

Lookup response

```json
{
    "result": [\
      {"id": 1, "first_name": "Daffy"},\
      {"id": 2, "first_name": "Elmer"}\
    ]
}
```

The following snippet demonstrates a very simple implementation of a data source that echoes the query back to the caller.

```go

type LookupRequest struct {
	DataSource string          `json:"dataSource"`
	Query      json.RawMessage `json:"query"`
}

type LookupResponse struct {
	Result json.RawMessage `json:"result"`
}

//go:wasmexport lookup
func lookup() int32 {
	var req LookupRequest
	if err := pdk.InputJSON(&req); err != nil {
		pdk.SetError(err)
		return 1
	}

resp := LookupResponse{Result: req.Query}
	if err := pdk.OutputJSON(resp); err != nil {
		pdk.SetError(err)
		return 1
	}

return 0
}
```

## Envoy extension

An Envoy extension must export a function named `envoyCheck` that accepts a JSON-encoded [Envoy check request](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckRequest) and returns one of the following responses.

### Direct response

Return a complete [Envoy check response](https://buf.build/envoyproxy/envoy/docs/main:envoy.service.auth.v3#envoy.service.auth.v3.CheckResponse) to return back to the caller.

```json
{
    "envoyCheckResponse": {
        "status": {"code": 7},
        "deniedResponse": {
            "body": "Go away"
        }
    }
}
```

### Cerbos mapping

Return a JSON object that contains a Cerbos `CheckRequest` and the Envoy check response to send based on the Cerbos response. Synapse sends the Cerbos request to the configured PDP (applying any configured proxy extensions) and responds with the appropriate Envoy response based on whether Cerbos returns ALLOW or DENY.

```json
{
    "cerbosMapping": {
        "checkRequest": { (1)
            "principal": {
                "id": "daffy_duck",
                "roles": ["duck"]
            },
            "resources": [{\
                "actions": ["GET"],\
                "resource": {\
                    "id": "http",\
                    "kind": "request",\
                    "attr": {\
                        "path": "/foo"\
                    },\
                }\
            }]
        },
        "allowResponse": { (2)
            "status": {"code": 0}
        },
        "denyResponse": { (3)
            "status": {"code": 7},
            "deniedResponse": {
                "body": "Go away"
            }
        }
    }
}
```

### Cerbos check request

Return a Cerbos `CheckRequest` to send to the PDP (applying any configured proxy extensions). In this mode, the extension must implement a second function named `envoyMapCerbosResponse` to map the `CheckResources` response to an Envoy response. Use this mode when you need to write complex mapping logic to convert a Cerbos response to an Envoy response.

```json
{
    "cerbosCheckRequest": {
        "principal": {
            "id": "daffy_duck",
            "roles": ["duck"]
        },
        "resources": [{\
            "actions": ["GET"],\
            "resource": {\
                "id": "http",\
                "kind": "request",\
                "attr": {\
                    "path": "/foo"\
                },\
            }\
        }]
    }
}
```

Synapse will send the above request to the PDP and invoke the extension a second time by calling the `envoyMapCerbosResponse` function with a pointer to the following JSON-encoded content.

```json
{
    "envoyRequest": { ... }, (1)
    "cerbosRequest": { ... }, (2)
    "cerbosResponse": { ... } (3)
}
```

### Proxy extension

A proxy extension must export at least one of the following functions. (It’s legal to implement more than one.)

`augmentAuthzenEvaluationBatchRequest`

Modify an AuthZEN AccessEvaluations request

`augmentAuthzenEvaluationBatchResponse`

Modify an AuthZEN AccessEvaluations response

`augmentAuthzenEvaluationRequest`

Modify an AuthZEN AccessEvaluation request

`augmentAuthzenEvaluationResponse`

Modify an AuthZEN AccessEvaluation response

`augmentCheckRequest`

Modify a CheckResources request

`augmentCheckResponse`

Modify a CheckResources response

`augmentPlanRequest`

Modify a PlanResources request

`augmentPlanResponse`

Modify a PlanResources response

Each function is invoked with a pointer to a JSON-encoded `AccessEvaluation`/CheckResources`/`PlanResources` request or response. It must return a pointer to a JSON-encoded value of the same type. Refer to [https://docs.cerbos.dev/cerbos/latest/api/#_request_and_response_formats](https://docs.cerbos.dev/cerbos/latest/api/#_request_and_response_formats) for details about the request and response types.
