forta-mcp 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +225 -0
- package/README.md +18 -0
- package/index.js +49 -2
- package/package.json +1 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# AGENTS.md — forta-mcp
|
|
2
|
+
|
|
3
|
+
> `forta-mcp` is the **Model Context Protocol server for Forta**, the OAuth2 identity provider
|
|
4
|
+
> for `appleby.cloud`. It exposes the `forta-api` admin surface to Claude Code as **25 typed
|
|
5
|
+
> tools** — users, platforms, roles, grants, token introspection and API tokens. This file
|
|
6
|
+
> orients any agent/worker before touching code in this repo.
|
|
7
|
+
>
|
|
8
|
+
> **⚠️ Golden rule — keep this file current:** any change that adds, removes or retypes a tool,
|
|
9
|
+
> or drifts from `forta-api`'s routes, MUST update this AGENTS.md in the SAME change.
|
|
10
|
+
>
|
|
11
|
+
> **⚠️ Blast radius:** these tools can delete users, delete platforms and rotate client
|
|
12
|
+
> secrets. `forta_rotate_platform_credentials` takes a platform *down* until its deployed config
|
|
13
|
+
> is updated. Tool descriptions are the only warning an agent gets — keep them honest.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What this repo is
|
|
18
|
+
|
|
19
|
+
A single-file Node ESM program (`index.js`) speaking MCP over stdio, translating tool calls into
|
|
20
|
+
HTTP requests against `forta-api` at **`auth.appleby.cloud`**. Published to npm as `forta-mcp`,
|
|
21
|
+
consumed via `npx -y forta-mcp`.
|
|
22
|
+
|
|
23
|
+
It owns only the translation layer — tool names, argument schemas, descriptions, URL
|
|
24
|
+
construction. All behaviour comes from `forta-api`.
|
|
25
|
+
|
|
26
|
+
Its reason to exist: Forta is the identity provider for the whole ecosystem, so nearly every
|
|
27
|
+
auth bug starts there. `forta_list_grants` turns "why is this user getting a 403 from Keyring"
|
|
28
|
+
from a manual database dig into one call.
|
|
29
|
+
|
|
30
|
+
## Stack & dependencies
|
|
31
|
+
|
|
32
|
+
- **Node ≥18** (global `fetch`, `AbortSignal.timeout`). `"type": "module"`, top-level `await`.
|
|
33
|
+
- **`@modelcontextprotocol/sdk` ^1.29.0**, **`zod`** (transitive via the SDK — not declared).
|
|
34
|
+
- No build step, no tests, no linter. `node --check index.js` is the only static gate.
|
|
35
|
+
|
|
36
|
+
## Project structure
|
|
37
|
+
|
|
38
|
+
| Path | Role |
|
|
39
|
+
|------|------|
|
|
40
|
+
| `index.js` | `--setup` flow, config guard, `api()`/`text()`/`body()` helpers, all 25 tools, transport. |
|
|
41
|
+
| `package.json` | `bin.forta-mcp` → `index.js`. |
|
|
42
|
+
| `README.md` | User-facing setup + tool tables. |
|
|
43
|
+
| `AGENTS.md` | This file. |
|
|
44
|
+
|
|
45
|
+
## Running, building & testing
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install
|
|
49
|
+
node --check index.js
|
|
50
|
+
node index.js --setup # writes ~/.mcp.json
|
|
51
|
+
FORTA_API_URL=... FORTA_API_TOKEN=frt_... node index.js # run on stdio
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Smoke test without a client (the `sleep`s are required — requests otherwise race the handshake):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
{ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'
|
|
58
|
+
sleep 2
|
|
59
|
+
echo '{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
|
60
|
+
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
|
|
61
|
+
sleep 2; } | FORTA_API_URL=x FORTA_API_TOKEN=frt_x node index.js
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## How code is written here
|
|
65
|
+
|
|
66
|
+
- **Every tool follows one shape** — `server.tool(name, description, zodSchema, handler)`, and
|
|
67
|
+
the handler ends `return { content: text(res) }`.
|
|
68
|
+
- **Naming:** `forta_` prefix, then verb, then noun.
|
|
69
|
+
- **`api(method, path, params, body, opts)`** — `params` become query entries (undefined/null
|
|
70
|
+
dropped); `body` is JSON by default. `opts.form` switches to
|
|
71
|
+
`application/x-www-form-urlencoded`; `opts.noAuth` omits the `Authorization` header. **Both
|
|
72
|
+
exist solely for `/oauth/introspect`** — see below.
|
|
73
|
+
- **`body(obj)` strips `undefined`.** Always use it on PUT tools: `forta-api` treats a
|
|
74
|
+
present-but-null field as an explicit clear, so forwarding raw `{...fields}` would wipe every
|
|
75
|
+
field the caller omitted.
|
|
76
|
+
- **Descriptions carry the blast radius.** Anything destructive must say so and name the safer
|
|
77
|
+
alternative.
|
|
78
|
+
|
|
79
|
+
### Sensitive value masking
|
|
80
|
+
|
|
81
|
+
`api()` passes every decoded JSON response through **`sanitise()`** before returning it. This is
|
|
82
|
+
central, not per-tool, so a newly added tool is safe by default rather than by remembering.
|
|
83
|
+
|
|
84
|
+
`mask()` keeps a value's **first two characters** and appends a **fixed-width tail** —
|
|
85
|
+
`"supersecret"` → `"su**********"`. The prefix is what makes the mask useful rather than merely
|
|
86
|
+
safe: you can still tell one credential from another, or confirm a rotation actually changed a
|
|
87
|
+
value. The tail is fixed width so the mask does not disclose the real length. Values under
|
|
88
|
+
three characters are masked whole, so a two-character secret cannot round-trip as itself.
|
|
89
|
+
|
|
90
|
+
The same shape is implemented in all five `*-mcp` servers.
|
|
91
|
+
|
|
92
|
+
`SECRET_FIELDS` covers `client_secret`, `secret`, `password`, `token`, `api_token`,
|
|
93
|
+
`access_token`, `refresh_token`, `id_token`, `oauth_request_token`, `plaintext`, `private_key`
|
|
94
|
+
and `signing_key`, masked at **any nesting depth**, in objects and arrays alike.
|
|
95
|
+
|
|
96
|
+
This matters most on the three tools that mint a credential and return it exactly once:
|
|
97
|
+
`forta_create_platform` and `forta_rotate_platform_credentials` (a `client_secret`) and
|
|
98
|
+
`forta_create_api_token` (a plaintext `frt_` token). Those are keys to the whole SSO estate.
|
|
99
|
+
Introspection metadata — `active`, `sub`, `exp`, `token_type` — is deliberately left readable,
|
|
100
|
+
since that is the entire point of `forta_introspect_token`.
|
|
101
|
+
|
|
102
|
+
`FORTA_ALLOW_SECRET_VALUES=1` disables masking. It is off by default and should stay that way;
|
|
103
|
+
read a real credential from the Forta dashboard instead.
|
|
104
|
+
|
|
105
|
+
## Domain & architecture
|
|
106
|
+
|
|
107
|
+
**Auth.** One long-lived `frt_` API token in `FORTA_API_TOKEN`, sent as
|
|
108
|
+
`Authorization: Bearer` on every request. `/admin/*` additionally requires the owning user to
|
|
109
|
+
be a Forta **super-admin** — the token does not bypass that.
|
|
110
|
+
|
|
111
|
+
**Tool groups:**
|
|
112
|
+
|
|
113
|
+
| Group | Tools |
|
|
114
|
+
|-------|-------|
|
|
115
|
+
| Health & identity | `forta_health`, `forta_get_self` |
|
|
116
|
+
| Users | list, get, update, delete |
|
|
117
|
+
| Platforms | list, get, create, update, delete, rotate credentials. `create`/`update` carry **`backchannel_logout_uri`** — see the warning below |
|
|
118
|
+
| Roles | list, get, create, update, delete |
|
|
119
|
+
| Grants | list, create, revoke, check |
|
|
120
|
+
| Tokens | introspect, list/create/revoke API tokens |
|
|
121
|
+
|
|
122
|
+
### Setting `backchannel_logout_uri` — order matters
|
|
123
|
+
|
|
124
|
+
Forta POSTs a signed `logout_token` to this URL the instant a grant is revoked, so the consumer
|
|
125
|
+
ends the session immediately instead of waiting up to five minutes for its introspection poll.
|
|
126
|
+
|
|
127
|
+
⚠️ **Only set it once the platform actually serves the endpoint.** Until then every notification
|
|
128
|
+
404s, is retried six times on a backoff, and is marked `exhausted` — which looks in the logs like
|
|
129
|
+
a broken receiver rather than one that was never built. Deploy first, register second.
|
|
130
|
+
|
|
131
|
+
Pass `null` to clear it. Absent is the normal state for a platform that has not implemented the
|
|
132
|
+
receiver, and it is never an error — such a platform is simply skipped by the sender.
|
|
133
|
+
|
|
134
|
+
`https` only; the emitter refuses anything else at send time and logs
|
|
135
|
+
`FORTA_BACKCHANNEL_BAD_ENDPOINT`.
|
|
136
|
+
|
|
137
|
+
### Two behaviours that are easy to get wrong
|
|
138
|
+
|
|
139
|
+
**1. `/oauth/introspect` is a resource-server endpoint, not an admin one.** It requires the
|
|
140
|
+
*calling platform's* `client_id` and `client_secret`, reads **form-encoded** parameters, and
|
|
141
|
+
ignores the `Authorization` header entirely. It also only understands access JWTs — an opaque
|
|
142
|
+
`frt_` token reports `active: false`, which reads as "revoked" but means "wrong tool"
|
|
143
|
+
(`forta_list_api_tokens` is the right one). Introspection is scoped to the calling platform, so
|
|
144
|
+
a token valid elsewhere returns `active: false`.
|
|
145
|
+
|
|
146
|
+
The original implementation sent JSON with no credentials and **returned `invalid_client` on
|
|
147
|
+
every call** — it never worked. `opts.form` and `opts.noAuth` exist because of this.
|
|
148
|
+
|
|
149
|
+
**2. `/admin/grants` filters are mutually exclusive.** `HandleAdminListGrants` selects with a
|
|
150
|
+
`switch`, so passing both `platform_id` and `user_id` silently drops `user_id`. The tool
|
|
151
|
+
description says so; do not "fix" it by sending both.
|
|
152
|
+
|
|
153
|
+
## Ecosystem & related repos
|
|
154
|
+
|
|
155
|
+
| Repo | Relationship |
|
|
156
|
+
|------|--------------|
|
|
157
|
+
| [`forta-api`](https://github.com/aidenappl/forta-api) | The API this wraps. Its `main.go` is the source of truth for coverage. |
|
|
158
|
+
| [`go-forta`](https://github.com/aidenappl/go-forta) | v1.3.0+ accepts the `frt_` tokens this server uses. |
|
|
159
|
+
| [`keyring-mcp`](https://github.com/aidenappl/keyring-mcp) | **Uses the same `frt_` token** — Keyring has no admin credential of its own. |
|
|
160
|
+
| `lattice-mcp`, `monitor-mcp`, `openbucket-mcp` | Sibling MCPs; keep the single-file structure aligned. |
|
|
161
|
+
|
|
162
|
+
## Operations
|
|
163
|
+
|
|
164
|
+
- **Published to npm** as `forta-mcp` (public). `npx -y` resolves the latest, so **publishing
|
|
165
|
+
is deployment**.
|
|
166
|
+
- **Bump the version in TWO places before publishing** — `package.json` *and* the `McpServer({
|
|
167
|
+
version })` literal in `index.js`. npm refuses to republish an existing version, so forgetting
|
|
168
|
+
`package.json` fails loudly (`cannot publish over the previously published versions`), but
|
|
169
|
+
forgetting the one in `index.js` fails SILENTLY: the server then reports a version it is not,
|
|
170
|
+
and a client debugging "which build am I talking to" is misled by the only field that answers
|
|
171
|
+
that question.
|
|
172
|
+
- **Publishing requires 2FA via passkey** and must run from an interactive terminal — npm's web
|
|
173
|
+
auth flow needs a browser, and from a non-TTY subprocess it degrades to demanding an OTP a
|
|
174
|
+
passkey-only account cannot produce.
|
|
175
|
+
- **A running MCP server does not pick up a new npm version** — the client must restart.
|
|
176
|
+
- **Common failure modes:**
|
|
177
|
+
- *All tools `fetch failed`* — wrong `FORTA_API_URL`. The API is **`auth.appleby.cloud`**;
|
|
178
|
+
`forta.appleby.cloud` is the dashboard and returns 404 on `/healthcheck`.
|
|
179
|
+
- *All tools 401* — token revoked or expired.
|
|
180
|
+
- *`/admin` tools 403 but `forta_get_self` works* — the owning user is not super-admin.
|
|
181
|
+
|
|
182
|
+
## Rules & guardrails
|
|
183
|
+
|
|
184
|
+
- **Never hardcode a token, URL or hostname.**
|
|
185
|
+
- **Never log request or response bodies.** `forta_create_api_token` and
|
|
186
|
+
`forta_create_platform` return live secrets exactly once.
|
|
187
|
+
- **Never weaken `sanitise()`.** It must stay recursive, applied centrally in `api()`, and on by
|
|
188
|
+
default. If something genuinely needs a real value, the answer is
|
|
189
|
+
`FORTA_ALLOW_SECRET_VALUES=1` in that server's env, not an exemption in the code.
|
|
190
|
+
- **Never add a tool without reading its handler in `forta-api`.** Struct fields are not the
|
|
191
|
+
request contract — that assumption is exactly what shipped the broken introspect tool.
|
|
192
|
+
- **Never rename an existing tool.** Names are a public contract; add and deprecate instead.
|
|
193
|
+
- **`zod` is used but not declared** in `package.json`, resolving transitively through the MCP
|
|
194
|
+
SDK. Declaring it explicitly is the right fix, as a standalone change.
|
|
195
|
+
- Publishing is outward-facing and effectively irreversible (npm unpublish is restricted after
|
|
196
|
+
72 hours) — do not publish without explicit instruction.
|
|
197
|
+
|
|
198
|
+
## Verification — always before "done"
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
node --check index.js
|
|
202
|
+
grep -c 'server.tool(' index.js # expect 25
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Then the stdio handshake above, asserting the server starts with **no stderr**, the tool count
|
|
206
|
+
is right, and there are **no duplicate names** — `server.tool` accepts duplicates silently, the
|
|
207
|
+
last registration wins, and the earlier tool vanishes without an error.
|
|
208
|
+
|
|
209
|
+
For any tool added or changed, make **one real call against the live API**. Schema checks catch
|
|
210
|
+
typos; they do not catch wrong units, wrong enum values, wrong content types, or parameters the
|
|
211
|
+
handler ignores — every bug this repo has shipped was in that second category.
|
|
212
|
+
|
|
213
|
+
**Never report work complete on the strength of `tools/list` alone.**
|
|
214
|
+
|
|
215
|
+
## Keeping this file updated
|
|
216
|
+
|
|
217
|
+
Update this AGENTS.md in the same change when you:
|
|
218
|
+
- **Add/remove/rename a tool** → update the group table and the count in the header.
|
|
219
|
+
- **Change `api()` or its `opts`** → update *How code is written here*.
|
|
220
|
+
- **Discover a `forta-api` behaviour that contradicts a tool description** → fix both, and
|
|
221
|
+
record it under *Two behaviours that are easy to get wrong* if it is non-obvious.
|
|
222
|
+
- **Add/change a tool parameter** → bump the minor version in BOTH places (see *Operations*) in
|
|
223
|
+
the same change, so the commit that adds the capability is the commit that can be published.
|
|
224
|
+
- **Publish** → note the version and what changed.
|
|
225
|
+
- Also keep `README.md`'s tool tables in sync.
|
package/README.md
CHANGED
|
@@ -46,6 +46,24 @@ curl -X POST https://auth.appleby.cloud/admin/api-tokens \
|
|
|
46
46
|
|
|
47
47
|
Tokens are `frt_`-prefixed, owned by the creating user, and require that user to be a Forta super-admin for the `/admin` tools. The plaintext is returned once and stored only as a SHA-256 hash.
|
|
48
48
|
|
|
49
|
+
## Secret values are masked
|
|
50
|
+
|
|
51
|
+
Every response is passed through a masking step before it reaches the model. Anything that looks
|
|
52
|
+
like a credential keeps its **first two characters** and loses the rest to a fixed-width tail —
|
|
53
|
+
`supersecret` becomes `su**********`.
|
|
54
|
+
|
|
55
|
+
That is enough to tell two credentials apart, or to confirm a rotation actually changed
|
|
56
|
+
something, and not enough to use. The tail is a fixed width so the mask does not reveal the real
|
|
57
|
+
length.
|
|
58
|
+
|
|
59
|
+
This matters most on the tools that mint a credential and return it exactly once —
|
|
60
|
+
`forta_create_platform` and `forta_rotate_platform_credentials` (a `client_secret`) and
|
|
61
|
+
`forta_create_api_token` (a plaintext `frt_` token). Read the real value from the Forta
|
|
62
|
+
dashboard instead. Introspection metadata (`active`, `sub`, `exp`, `token_type`) is left
|
|
63
|
+
readable.
|
|
64
|
+
|
|
65
|
+
Set `FORTA_ALLOW_SECRET_VALUES=1` to turn masking off if you genuinely need a working value.
|
|
66
|
+
|
|
49
67
|
## Tools
|
|
50
68
|
|
|
51
69
|
### Health & identity
|
package/index.js
CHANGED
|
@@ -62,6 +62,51 @@ if (!API_URL || !API_TOKEN) {
|
|
|
62
62
|
process.exit(1);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// --- Sensitive value masking ---
|
|
66
|
+
//
|
|
67
|
+
// Several Forta endpoints return a live credential exactly once, at the moment
|
|
68
|
+
// it is minted: forta_create_platform and forta_rotate_platform_credentials
|
|
69
|
+
// return a client_secret, forta_create_api_token returns a plaintext frt_
|
|
70
|
+
// token. Those are the keys to the whole SSO estate, and anything that reaches
|
|
71
|
+
// a model's context is in a transcript forever.
|
|
72
|
+
//
|
|
73
|
+
// Sensitive values are masked to their first two characters plus a
|
|
74
|
+
// fixed-width tail: "frt_abc123" -> "fr**********". The prefix keeps a value
|
|
75
|
+
// identifiable and comparable while the fixed tail avoids disclosing length.
|
|
76
|
+
// Read the usable value from the Forta dashboard instead.
|
|
77
|
+
//
|
|
78
|
+
// Set FORTA_ALLOW_SECRET_VALUES=1 to pass values through unmasked.
|
|
79
|
+
|
|
80
|
+
const ALLOW_SECRETS = process.env.FORTA_ALLOW_SECRET_VALUES === "1";
|
|
81
|
+
|
|
82
|
+
function mask(value) {
|
|
83
|
+
if (typeof value !== "string" || value === "") return value;
|
|
84
|
+
// Under three characters there is no prefix worth keeping — a two-char
|
|
85
|
+
// secret would otherwise round-trip as itself.
|
|
86
|
+
if (value.length < 3) return "**********";
|
|
87
|
+
return value.slice(0, 2) + "**********";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Response fields that are a credential wherever they appear. `id_token` and
|
|
91
|
+
// `access_token` are included because the OAuth endpoints return real, usable
|
|
92
|
+
// session tokens.
|
|
93
|
+
const SECRET_FIELDS = new Set([
|
|
94
|
+
"client_secret", "secret", "password", "token", "api_token", "access_token",
|
|
95
|
+
"refresh_token", "id_token", "oauth_request_token", "plaintext",
|
|
96
|
+
"private_key", "signing_key",
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
// Walks a decoded response and masks every sensitive value in place.
|
|
100
|
+
function sanitise(node) {
|
|
101
|
+
if (ALLOW_SECRETS || node === null || typeof node !== "object") return node;
|
|
102
|
+
if (Array.isArray(node)) return node.map(sanitise);
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [k, v] of Object.entries(node)) {
|
|
105
|
+
out[k] = SECRET_FIELDS.has(k) && typeof v === "string" ? mask(v) : sanitise(v);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
// --- HTTP helper ---
|
|
66
111
|
|
|
67
112
|
async function api(method, path, params, body, opts_ = {}) {
|
|
@@ -91,7 +136,7 @@ async function api(method, path, params, body, opts_ = {}) {
|
|
|
91
136
|
const text = await res.text();
|
|
92
137
|
if (!text) return { success: res.ok, status: res.status };
|
|
93
138
|
try {
|
|
94
|
-
return JSON.parse(text);
|
|
139
|
+
return sanitise(JSON.parse(text));
|
|
95
140
|
} catch {
|
|
96
141
|
// /healthcheck and the OAuth endpoints return bare text, not the envelope.
|
|
97
142
|
return { success: res.ok, status: res.status, body: text };
|
|
@@ -113,7 +158,7 @@ function body(obj) {
|
|
|
113
158
|
|
|
114
159
|
const server = new McpServer({
|
|
115
160
|
name: "forta",
|
|
116
|
-
version: "1.
|
|
161
|
+
version: "1.2.0",
|
|
117
162
|
});
|
|
118
163
|
|
|
119
164
|
// --- Health & identity ---
|
|
@@ -188,6 +233,7 @@ server.tool("forta_create_platform", "Register a new OAuth platform. Returns the
|
|
|
188
233
|
description: z.string().optional().describe("Description"),
|
|
189
234
|
redirect_uris: z.array(z.string()).optional().describe("Allowed OAuth redirect URIs"),
|
|
190
235
|
logout_redirect_uris: z.array(z.string()).optional().describe("Allowed post-logout redirect URIs"),
|
|
236
|
+
backchannel_logout_uri: z.string().nullable().optional().describe("OIDC Back-Channel Logout 1.0 endpoint on the platform (https only). Forta POSTs a signed logout_token here the moment a grant is revoked, instead of the consumer waiting up to 5 minutes for its introspection poll. Pass null to clear. ⚠️ Only set this once the platform actually serves the endpoint — until then every notification 404s, retries 6 times and is marked exhausted"),
|
|
191
237
|
allowed_origins: z.array(z.string()).optional().describe("Allowed CORS origins"),
|
|
192
238
|
is_first_party: z.boolean().optional().describe("First-party app (skips the consent screen)"),
|
|
193
239
|
internal_resource: z.boolean().optional().describe("Internal resource, hidden from the app launcher"),
|
|
@@ -209,6 +255,7 @@ server.tool("forta_update_platform", "Update a platform's configuration. Only th
|
|
|
209
255
|
platform_type: z.enum(["WEB", "SPA", "SERVICE", "MOBILE"]).optional(),
|
|
210
256
|
redirect_uris: z.array(z.string()).optional(),
|
|
211
257
|
logout_redirect_uris: z.array(z.string()).optional(),
|
|
258
|
+
backchannel_logout_uri: z.string().nullable().optional().describe("OIDC Back-Channel Logout 1.0 endpoint on the platform (https only). Forta POSTs a signed logout_token here the moment a grant is revoked, instead of the consumer waiting up to 5 minutes for its introspection poll. Pass null to clear. ⚠️ Only set this once the platform actually serves the endpoint — until then every notification 404s, retries 6 times and is marked exhausted"),
|
|
212
259
|
allowed_origins: z.array(z.string()).optional(),
|
|
213
260
|
is_first_party: z.boolean().optional(),
|
|
214
261
|
internal_resource: z.boolean().optional(),
|