Network Permissions
@monospace/cli 0.2 and @monospace/extension-kit 0.3. Overview
A data connector extension reaches only the hosts it was granted. Every other network request from its code fails. The connector declares the hosts it needs as net: permissions in its entry's engine.permissions.
The grant belongs to each data source: the connector configured in a workspace (a container with its own members, roles and data sources). Whoever creates the data source approves the grant, and the data source stores it.
For a data connector, the required set comes from two places:
| Source | Declares | Use for |
|---|---|---|
engine.permissions in the extension config | Hosts every data source needs | A fixed external data source, such as net:api.artic.edu |
The connector's preflight | Hosts that depend on the data source's configuration | A configurable base URL or tenant host |
The instance (your running Monospace deployment) requires the grant to match the union of both sets exactly. A grant missing a host is refused, and so is a grant with an extra host.
Each redirect is checked against the grant too. When a granted host redirects to a host or port outside the grant, fetch fails at that hop, so grant every host in a redirect chain your connector follows.
Write a Permission
Each permission is an object in the entry's engine.permissions array, with a permission string, a reason, and no other fields:
{
"permission": "net:api.artic.edu",
"reason": "Read artworks and artists from the Art Institute of Chicago API"
}
| Field | Rules |
|---|---|
permission | net: followed by a target. net is the only capability. NET: and other prefixes are rejected |
reason | Required. Must contain a non-whitespace character. Shown to whoever approves the grant |
Target Syntax
A target names a host, an IPv4 address or subnet, or a bracketed IPv6 address, with an optional port from 1 to 65535, never a URL. A grant can't limit a connector to certain URL paths or HTTP methods: a granted host is reachable with any request. Monospace supports the forms marked accepted below, and no others. The rejected rows are common mistakes, which monospace extension build and Engine's manifest check refuse. Permissions that preflight answers skip that check and go through the runtime's parser only, which accepts some of them, such as a unix: socket path:
| Target | Accepted | Notes |
|---|---|---|
net:api.example.com | Yes | Any port |
net:api.example.com:443 | Yes | Port 443 only |
net:*.example.com | Yes | example.com itself and every subdomain |
net:127.0.0.1, net:127.0.0.1:8080 | Yes | IPv4, with or without a port |
net:10.0.0.0/8 | Yes | An IPv4 subnet |
net:[::1], net:[::1]:443 | Yes | IPv6 in brackets |
net:https://example.com | No | URLs are rejected |
net:unix:/tmp/app.sock | No | Unix sockets and paths are rejected |
net:example.com:0 | No | Port 0 |
net:example.com: | No | Empty port |
net:example.com:notaport | No | Non-numeric port |
net:::1 | No | IPv6 without brackets |
net: | No | Empty target |
The instance rejects an entire extension whose manifest contains a target it can't parse.
Normalization and Deduplication
The instance normalizes each target before comparing it:
- Hostnames are lowercased:
net:API.Example.comandnet:api.example.comare one permission. - IPv6 addresses are shortened:
net:[0:0:0:0:0:0:0:1]:443becomesnet:[::1]:443.
A target listed twice keeps its first reason. When the manifest and preflight name the same target, the manifest's reason wins. Reasons are never compared: a grant matches when it names the same targets, whatever reasons it carries.
Targets match as written after normalization, not by the access they cover. net:api.example.com doesn't stand in for net:api.example.com:443, and net:*.example.com doesn't stand in for net:api.example.com.
Derive Permissions in preflight
preflight receives the data source's configuration and returns the extra permissions that configuration needs. The instance calls it before setup, on every start of the data source.
| Rule | Result when broken |
|---|---|
| Return synchronously | A returned promise fails with preflight must answer synchronously |
Return { permissions: [...] } in the same shape as the manifest | An unparseable target or blank reason deactivates the data source until the extension is reloaded |
| Touch nothing the runtime denies | A permission denial in preflight (Deno's NotCapable, e.g., from reading an environment variable) deactivates the data source until the extension is reloaded |
A deactivated data source refuses every request that reaches its connector without starting the connector again. After malformed permissions it answers 503 "Data source is deactivated". After a permission denial, or a grant that doesn't match (see Change Permissions), it answers 422 "Invalid extension permissions". A result the cache already holds is still served. Reloading the extension, on restart or through MONOSPACE_EXTENSIONS__AUTO_RELOAD, lets the next request try again.
Deactivation belongs to the data source. A connection test or schema preview starts a fresh connector from the configuration it's sent, so a failed attempt doesn't need a reload before the next one.
Derive hosts from the configuration only. preflight is stateless: don't make requests, open connections or build clients in it. When the configuration it reads is invalid, throw InvalidConfiguration; that stops the data source until the extension is reloaded, as it does from setup.
A connector without preflight requires only its manifest permissions. See Data Connector API for the preflight signature.
Approve Permissions
When you create a data source, you approve its network permissions. Creating, testing or introspecting a data source sends a permissions array with the request. When it doesn't match the required set, the instance answers 422 with code 7001 and the complete required set in meta.permissions. Resend the same request with permissions replaced by that set.
The connector file loads under the permissions you send, before Engine compares them. A request at module scope to a host your grant lacks fails the start before Engine can answer 7001, so keep network calls out of module scope. See Data Source Lifecycle.
The three endpoints share this handshake, and each requires the dataSource:create entitlement in the workspace; a matching grant doesn't replace it. Their bodies and answers differ: testing and introspecting take the configuration without apiName, a passing test answers 204 with no body, and introspection answers with the schema in data.schema, next to a data.hash.
| Operation | Method | Path |
|---|---|---|
| Create a data source | POST | /api/{workspace}/sources/data |
| Test a connection | POST | /api/{workspace}/sources/data/test |
| Introspect without creating | POST | /api/{workspace}/sources/data/introspect |
Send the First Request
This request tries to create a data source with no grant. The SDK has no method for data sources, so call the endpoint directly:
curl -X POST https://example.monospace.io/api/blog/sources/data \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiName": "artic",
"provider": "example/artic-connector",
"config": {}
}'
const response = await fetch('https://example.monospace.io/api/blog/sources/data', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
apiName: 'artic',
provider: 'example/artic-connector',
config: {},
}),
});
provider is the entry id of an installed data connector. See Install and Manage Extensions to install one first. config is the data source's configuration, passed to preflight and setup unchanged. Both config and permissions default to empty when omitted.
Each permissions item takes the manifest shape: a permission and a non-blank reason. A malformed item fails the request without the 7001 handshake.
Read the Refusal
The instance refuses the request and lists the set to approve:
{
"message": "The connector requires permissions this data source was not granted. Approve them and retry.",
"code": "7001",
"meta": {
"permissions": [
{ "permission": "net:api.artic.edu", "reason": "Read artworks and artists from the Art Institute of Chicago API" }
]
}
}
meta.permissions is the whole required set, not the difference from what you sent. Nothing is created and the connector's setup doesn't run. The connector's module and preflight have already run, with the grant you sent, to compute the set.
Resend with the Grant
Review the hosts and reasons in meta.permissions. To approve them, copy the set into the request as permissions:
curl -X POST https://example.monospace.io/api/blog/sources/data \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"apiName": "artic",
"provider": "example/artic-connector",
"config": {},
"permissions": [
{ "permission": "net:api.artic.edu", "reason": "Read artworks and artists from the Art Institute of Chicago API" }
]
}'
const response = await fetch('https://example.monospace.io/api/blog/sources/data', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
apiName: 'artic',
provider: 'example/artic-connector',
config: {},
permissions: [
{ permission: 'net:api.artic.edu', reason: 'Read artworks and artists from the Art Institute of Chicago API' },
],
}),
});
With the grant matching, the instance runs the connector's testConnection, creates the data source, and returns its id:
{ "data": { "id": "d7c8f0e2-9b3a-4c1e-8f2a-1a2b3c4d5e6f" } }
Studio runs the same handshake. It sends the manifest permissions first, and when they match the required set, no dialog appears.
When the instance refuses them because the required set has hosts they lack, for example because preflight adds a host, Studio shows the required set with each reason. It resends once you approve. A refusal caused only by extra hosts opens no dialog; Studio reports the error.
GET /api/{workspace}/connectors/data lists every installed data connector with its manifest permissions, so you can review them before creating a data source. It requires the extension:read entitlement in the workspace.Handle a Denied Request
A request to a host outside the grant never leaves the instance. fetch rejects with Deno's NotCapable error inside your connector. Engine looks for that error along the cause chain of whatever your connector throws. When you wrap the rejection in a kit error class, pass it as the cause, as HTTP Through fetch shows.
| Where it's thrown | What the caller sees |
|---|---|
testConnection or query, uncaught or kept as the cause | 422 "Invalid extension permissions" |
setup, uncaught or kept as the cause | The data source is deactivated until the extension is reloaded, and answers 422 "Invalid extension permissions" |
Wrapped in another error class without cause | That class's error. The permission denial is lost. A ConnectionFailed wrapper reads as 500 "Failed to connect to the database" |
A connection test of the Quickstart connector, changed to call www.artic.edu instead of the granted api.artic.edu, answers 422. The connector wraps the rejection in ConnectionFailed with the rejection as its cause, so the innermost message is the connector's own:
{
"message": "Failed to connect to data source with the provided configuration",
"source": {
"message": "Invalid extension permissions",
"source": {
"message": "the extension returned an error: The Art Institute API did not answer."
}
}
}
A connector that lets the rejection through uncaught shows Deno's message there instead, which names the host: Requires net access to "www.artic.edu:443", run again with the --allow-net flag. The --allow-net hint comes from Deno; it isn't an option you can set. When the host is wrong, fix the URL your connector calls. When the connector needs the host, add it to the manifest or preflight, then recreate the data source.
Change Permissions
A data source's grant is fixed when you create it. There is no endpoint to update a data source's grant or configuration. Reintrospecting runs the data source's existing connector, with its original configuration and grant.
When an extension update changes the required set, existing data sources stop serving:
- The instance reloads the extension, on restart or through
MONOSPACE_EXTENSIONS__AUTO_RELOAD. - Each data source compares its stored grant with the new required set on its next start.
- A mismatch deactivates the data source. Queries that reach the connector fail with
422"Invalid extension permissions", code7001, and the full required set inmeta.permissions.
This applies to removed hosts as well as added ones. To recover, delete the data source and create it again with the new set. Reverting the extension to the previous set reactivates the data source. A connection test with the new set doesn't change the data source's stored grant. See Recreate a Data Source.
See Also
- Data Connector API: where
engine.permissionslives, and thepreflightandsetupsignatures - How Data Connectors Run: where the permission check sits in the data source lifecycle
- Manage Data Sources: update and recreate data sources after an extension update
- Runtime Limits: what else the runtime allows and refuses