@vantageos/mcp-boilerplate 0.2.1 → 0.2.3
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +90 -0
- package/README.fr.md +70 -2
- package/README.md +64 -2
- package/dist/envKnobs.d.ts +14 -0
- package/dist/envKnobs.js +54 -0
- package/dist/http.js +4 -20
- package/dist/httpApp.js +17 -0
- package/dist/locales/en.json +5 -1
- package/dist/locales/fr.json +5 -1
- package/dist/nodeAdapter.d.ts +20 -0
- package/dist/nodeAdapter.js +71 -0
- package/dist/oauth/authorizationServer.d.ts +21 -1
- package/dist/oauth/authorizationServer.js +130 -12
- package/dist/oauth/clerk.d.ts +5 -1
- package/dist/oauth/clerk.js +29 -11
- package/dist/oauth/clientAssertion.d.ts +28 -0
- package/dist/oauth/clientAssertion.js +106 -0
- package/dist/oauth/clientRegistry.d.ts +28 -1
- package/dist/oauth/clientRegistry.js +139 -43
- package/dist/oauth/codeStore.d.ts +19 -1
- package/dist/oauth/codeStore.js +28 -3
- package/dist/oauth/picker.d.ts +6 -1
- package/dist/oauth/picker.js +8 -2
- package/dist/oauth/settings.d.ts +15 -0
- package/dist/oauth/settings.js +50 -3
- package/dist/ui/uiResource.d.ts +1 -1
- package/dist/ui/uiResource.js +1 -1
- package/dist/ui-bundle/widget.html +9 -49
- package/docs/oauth-stores.md +12 -1
- package/examples/oauth.env.example +10 -0
- package/package.json +5 -6
- package/plugin.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vantage-mcp-boilerplate",
|
|
3
3
|
"displayName": "VantageOS MCP Boilerplate",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"description": "Clonable MCP server template: SDK v2 on protocol 2026-07-28 (stdio + stateless Streamable HTTP), its own OAuth 2.1 authorization server (Clerk person + organisation), MCP Apps UI, FR/EN i18n and a declarative eval harness. Scaffold, extend, publish.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ElPi Corp / Laurent Perello"
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,96 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.3] - 2026-10-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `/authorize/callback` resolves the person's Clerk session with Clerk's own handshake
|
|
15
|
+
(`@vantageos/cloud-identity` `^0.19.0`, `resolveClerkSessionFromRequest`, built on
|
|
16
|
+
`@clerk/backend` `authenticateRequest()`). A return from Clerk's hosted sign-in with no session
|
|
17
|
+
on this host used to answer `403 session-required`; it now redirects through the handshake and
|
|
18
|
+
continues. An explicit Bearer token and injected Clerk collaborators keep their path. The
|
|
19
|
+
publishable key is `CLERK_PUBLISHABLE_KEY` or derived from `CLERK_DOMAIN`.
|
|
20
|
+
- README section "Sign-in with ChatGPT and Claude" (EN + FR): the production instance on the
|
|
21
|
+
server's own registrable domain, the three URLs including `AUTHORIZE_SIGN_IN_URL`, why no
|
|
22
|
+
redirect URL is registered, development-instance limits, how the session arrives, what is and
|
|
23
|
+
is not yet measured.
|
|
24
|
+
- The handshake redirect carries `Cache-Control: no-store`, and a forged or stale
|
|
25
|
+
`__clerk_handshake` continues signed out instead of answering 500.
|
|
26
|
+
- Boot warning (EN + FR) when the Clerk instance is a development one (`sk_test_` key or an
|
|
27
|
+
`accounts.dev` domain) and `PUBLIC_BASE_URL` is not localhost: it names the Home URL
|
|
28
|
+
requirement. A warning, never a refusal.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- The Node adapter emitted only the last `Set-Cookie` of a response. A response with N cookies
|
|
33
|
+
now yields N (Clerk's handshake return sets five).
|
|
34
|
+
- The deny route (`POST /authorize/org` with `approved=false`) forwards `consentToken`;
|
|
35
|
+
cloud-identity 0.18 and later verify it on a denial too.
|
|
36
|
+
|
|
37
|
+
- CIMD: a Client ID Metadata Document that declares `token_endpoint_auth_method` other than
|
|
38
|
+
`none` (for example ChatGPT's `private_key_jwt`) is now accepted when its
|
|
39
|
+
`token_endpoint_auth_methods_supported` includes `none`. A document with neither is still
|
|
40
|
+
refused. All other checks (client_id equality, https, SSRF guard, size cap, redirect_uris)
|
|
41
|
+
are unchanged. `/authorize` no longer answers `unknown-client` for ChatGPT.
|
|
42
|
+
- `/token` verifies a `private_key_jwt` client assertion instead of ignoring it: keys from the
|
|
43
|
+
CIMD document's `jwks_uri` (same SSRF-guarded fetch), RS256 only, `iss` = `sub` = `client_id`,
|
|
44
|
+
`aud` = the token endpoint or the issuer, `exp` required (30 s skew), `jti` required and
|
|
45
|
+
accepted once (bounded in-memory set, fail closed when full). Any failure answers 401
|
|
46
|
+
`invalid_client`. A request without an assertion keeps the public-client path (`client_id` +
|
|
47
|
+
PKCE). AS metadata advertises `["none","private_key_jwt"]` and RS256.
|
|
48
|
+
|
|
49
|
+
## [0.2.2] - 2026-10-10
|
|
50
|
+
|
|
51
|
+
### Security
|
|
52
|
+
|
|
53
|
+
- Consent page shows the ORIGIN the authorization code is sent to and the requested scope, and
|
|
54
|
+
flags a Dynamic-Client-Registration client as unverified (FR + EN).
|
|
55
|
+
- `/register` rate limit keys on the socket address, or on the `X-Forwarded-For` entry the
|
|
56
|
+
N-th trusted proxy appended (`MCP_TRUSTED_PROXY_COUNT`, default 0); a client-supplied first
|
|
57
|
+
entry no longer bypasses it.
|
|
58
|
+
- In-memory client registry expires registrations (30 days) and evicts the oldest at the cap
|
|
59
|
+
instead of refusing forever; in-memory code store drops expired codes and prunes its used set.
|
|
60
|
+
- `NODE_ENV=production` refuses the in-memory stores unless both durable adapters are injected
|
|
61
|
+
or `MCP_ALLOW_IN_MEMORY_STORES=1` is set (explicit, logged opt-in).
|
|
62
|
+
- The Node entry caps the request body (`MCP_MAX_BODY_BYTES`, default 1 MiB): `413` before any
|
|
63
|
+
handler runs, counted while the body streams in.
|
|
64
|
+
- `/mcp` enforces the `mcp:full` scope on the own-authorization-server tier (`403
|
|
65
|
+
insufficient_scope`).
|
|
66
|
+
- Clerk Backend calls time out (5 s); organisation memberships are paginated past 100 and fail
|
|
67
|
+
loudly rather than truncate past 5 000.
|
|
68
|
+
- `http:` issuer, sign-in and callback URLs are refused outside loopback.
|
|
69
|
+
- A consent denial answers the OAuth error redirect (`error=access_denied`, `state`, `iss`)
|
|
70
|
+
instead of a `403` JSON body; the redirect target is read only from a state the flow verified.
|
|
71
|
+
- `/register` sends no CORS headers unless the origin is listed in `MCP_REGISTER_CORS_ORIGINS`.
|
|
72
|
+
- SSRF guard also blocks the IPv4 ranges embedded in IPv4-translated (`::ffff:0:0/96`) and
|
|
73
|
+
6to4 (`2002::/16`) addresses, and refuses Teredo (`2001::/32`).
|
|
74
|
+
|
|
75
|
+
- A SET but invalid `MCP_MAX_BODY_BYTES`, `MCP_TRUSTED_PROXY_COUNT`, `MCP_REGISTER_CORS_ORIGINS`
|
|
76
|
+
or `MCP_ALLOW_IN_MEMORY_STORES` refuses to start, naming the variable and what was expected
|
|
77
|
+
(the value is never echoed); unset or empty keeps the default. No silent fallback.
|
|
78
|
+
- SSRF guard reads the IPv4 embedded in ISATAP addresses (interface id `0000:5efe` /
|
|
79
|
+
`0200:5efe`, any prefix) and refuses it when that IPv4 is in a blocked range.
|
|
80
|
+
|
|
81
|
+
- `@vantageos/cloud-identity` ^0.17.0: the consent page reads `OrgPickerModel.redirectOrigin` and a
|
|
82
|
+
denial is `resumeAuthorize({ denied: true })` (a verified redirect-to-client, `iss` appended in
|
|
83
|
+
one place). The local authorize-state parser (`src/oauth/stateBlob.ts`) is removed.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- `createInMemoryClientRegistry` takes an options object (`maxClients`, `ttlSeconds`, `now`)
|
|
88
|
+
instead of a bare number; `createInMemoryCodeStore` takes `{ now, maxEntries }` and exposes
|
|
89
|
+
`stats()`; `renderOrgPicker` takes a third `ConsentContext` argument (`redirectUri`,
|
|
90
|
+
`unverified`).
|
|
91
|
+
- `tests/toolRecordComplete.test.ts` asserts the four annotations are declared booleans, not
|
|
92
|
+
fixed values (a fork's write tool is rightly `readOnlyHint: false`).
|
|
93
|
+
|
|
94
|
+
### Dependencies
|
|
95
|
+
|
|
96
|
+
- `hono` 4.13.13, `vitest` 5, `vite` 8. `vite-plugin-singlefile` is removed (its
|
|
97
|
+
`micromatch` -> `braces` chain has an advisory with no patched release); `vite.config.ts`
|
|
98
|
+
inlines the bundle itself. `npm audit`: 0 critical, 0 high.
|
|
99
|
+
|
|
10
100
|
## [0.2.1] - 2026-10-10
|
|
11
101
|
|
|
12
102
|
### Fixed
|
package/README.fr.md
CHANGED
|
@@ -122,11 +122,79 @@ interfaces avec adaptateurs en mémoire pour les tests et le développement
|
|
|
122
122
|
mono-processus. Utilisez des adaptateurs durables en production — contrat dans
|
|
123
123
|
[`docs/oauth-stores.md`](docs/oauth-stores.md).
|
|
124
124
|
|
|
125
|
+
**Garde de production** : avec `NODE_ENV=production`, le serveur refuse de démarrer
|
|
126
|
+
sur les stores en mémoire (codes et clients enregistrés perdus au redémarrage, non
|
|
127
|
+
partagés entre instances) sauf si les deux adaptateurs durables sont injectés ou si
|
|
128
|
+
`MCP_ALLOW_IN_MEMORY_STORES=1` est défini — un choix explicite et journalisé pour un
|
|
129
|
+
déploiement mono-instance. Derrière un reverse proxy, réglez `MCP_TRUSTED_PROXY_COUNT`
|
|
130
|
+
sur le nombre de proxys de confiance (défaut `0`) : la limite de débit de `/register`
|
|
131
|
+
se fonde alors sur l'entrée `X-Forwarded-For` ajoutée par le proxy, jamais sur la
|
|
132
|
+
première, fournie par le client ; avec `0`, elle se fonde sur l'adresse du socket et
|
|
133
|
+
ignore l'en-tête.
|
|
134
|
+
|
|
135
|
+
**Limites de requête et CORS** : Une variable d'exploitation définie mais invalide (par exemple `MCP_MAX_BODY_BYTES=abc`) arrête le serveur au démarrage avec le nom de la variable et la forme attendue, jamais un défaut silencieux ; non définie ou vide, le défaut s'applique. l'entrée Node lit au plus `MCP_MAX_BODY_BYTES`
|
|
136
|
+
(défaut 1 Mio) du corps d'une requête et répond `413` avant tout handler.
|
|
137
|
+
`/register` n'envoie aucun en-tête CORS sauf si l'origine du navigateur figure dans
|
|
138
|
+
`MCP_REGISTER_CORS_ORIGINS` (séparées par des virgules). `/mcp` exige le scope
|
|
139
|
+
`mcp:full` sur le palier serveur d'autorisation propre (`403 insufficient_scope`
|
|
140
|
+
sinon). La page de consentement affiche l'origine vers laquelle le code est envoyé et
|
|
141
|
+
le scope demandé, signale comme non vérifié un client issu de l'enregistrement
|
|
142
|
+
dynamique, et un refus redirige vers le client avec `error=access_denied` (plus
|
|
143
|
+
`state` et `iss`). Les URL `http:` d'émetteur, de connexion et de rappel sont refusées
|
|
144
|
+
hors boucle locale.
|
|
145
|
+
|
|
125
146
|
**Non inclus** : jetons de rafraîchissement (les jetons d'accès durent une heure),
|
|
126
147
|
clés d'API par organisation pour les clients hors ChatGPT (ce code n'a jamais eu ce
|
|
127
148
|
chemin ; c'est un manque, pas une régression), URI de redirection à joker, et liste
|
|
128
149
|
de révocation des jetons.
|
|
129
150
|
|
|
151
|
+
### Connexion avec ChatGPT et Claude
|
|
152
|
+
|
|
153
|
+
ChatGPT et Claude envoient la personne sur le `/authorize` de ce serveur, qui l'envoie vers la
|
|
154
|
+
page de connexion hébergée par Clerk puis la ramène sur `/authorize/callback`. Pour que cela
|
|
155
|
+
fonctionne sur un serveur que vous hébergez, dans cet ordre :
|
|
156
|
+
|
|
157
|
+
1. **Utilisez une instance Clerk de PRODUCTION sur le domaine enregistrable du serveur.** Un
|
|
158
|
+
serveur sur `mcp.example.com` prend le domaine Clerk `example.com` : Clerk sert alors
|
|
159
|
+
`clerk.example.com` et `accounts.example.com`. Tout autre domaine relève d'une configuration
|
|
160
|
+
satellite, non mesurée. Ajoutez les CNAME DNS listés par le tableau de bord Clerk. Créez votre propre client OAuth Google
|
|
161
|
+
avec l'URI de redirection autorisée `https://clerk.<domaine>/v1/oauth_callback`, renseignez
|
|
162
|
+
son identifiant et son secret dans le tableau de bord Clerk, et publiez l'écran de
|
|
163
|
+
consentement Google avec une page d'accueil d'application et une politique de
|
|
164
|
+
confidentialité (un écran non publié n'admet que les utilisateurs de test).
|
|
165
|
+
2. **Réglez les trois URL.** `PUBLIC_BASE_URL` = `https://<serveur>` ; `AUTHORIZE_CALLBACK_URL`
|
|
166
|
+
= `https://<serveur>/authorize/callback` ; `AUTHORIZE_SIGN_IN_URL` = la page de connexion
|
|
167
|
+
hébergée de l'instance : `https://accounts.<domaine>/sign-in` en production,
|
|
168
|
+
`https://<instance>.accounts.dev/sign-in` en développement (l'URL de l'Account Portal,
|
|
169
|
+
affichée dans le tableau de bord Clerk sous **Account Portal**). Ce parcours n'a aucune
|
|
170
|
+
« URL de redirection autorisée » à remplir dans le tableau de bord : la page hébergée ne
|
|
171
|
+
renvoie le navigateur que vers un hôte du domaine de l'instance (production, selon la
|
|
172
|
+
documentation Clerk, pas encore mesuré) ou vers `localhost` / la Home URL (développement,
|
|
173
|
+
mesuré). C'est pourquoi l'étape 1 met le serveur et l'instance sur un même domaine.
|
|
174
|
+
*Mesuré* : la liste `redirect_urls` de l'API Backend n'est pas consultée par les pages
|
|
175
|
+
hébergées.
|
|
176
|
+
3. **Une instance de développement sert uniquement en local ou en test.** Sa page de connexion
|
|
177
|
+
hébergée ne redirige que vers `localhost` ou vers l'URL d'accueil (Home URL) de l'instance.
|
|
178
|
+
Pour un test distant, réglez l'origine de développement / Home URL de l'instance de
|
|
179
|
+
développement sur l'origine du serveur dans le tableau de bord Clerk, faites le test, puis
|
|
180
|
+
rétablissez-la.
|
|
181
|
+
4. **Comment la session arrive au serveur.** Après la connexion, le navigateur revient sur
|
|
182
|
+
`/authorize/callback` sans session sur votre hôte. Le serveur la résout avec la poignée de
|
|
183
|
+
main (handshake) propre à Clerk (`@vantageos/cloud-identity` 0.19.0, fondé sur
|
|
184
|
+
`authenticateRequest()` de `@clerk/backend`) : il redirige le navigateur par le point
|
|
185
|
+
d'entrée handshake de Clerk, et Clerk pose `__session` sur votre hôte. Il faut
|
|
186
|
+
`CLERK_SECRET_KEY` et la clé publiable : `CLERK_PUBLISHABLE_KEY`, ou, à défaut, dérivée de
|
|
187
|
+
`CLERK_DOMAIN` sous la forme `pk_test_` / `pk_live_` (selon la clé secrète) +
|
|
188
|
+
base64(`<hôte de l'API Frontend>$`). *Mesuré* : une instance de développement avec un serveur
|
|
189
|
+
distant fonctionne dès que sa Home URL pointe sur le serveur. *Pas encore mesuré* : une
|
|
190
|
+
instance de production sur un domaine enregistrable différent de celui du serveur.
|
|
191
|
+
5. **Avertissement au démarrage.** Quand l'instance configurée est de développement (clé
|
|
192
|
+
`sk_test_`, ou `CLERK_DOMAIN` sous `accounts.dev`) et que `PUBLIC_BASE_URL` n'est pas
|
|
193
|
+
localhost, le serveur écrit un avertissement au démarrage, en anglais et en français, qui
|
|
194
|
+
nomme l'exigence de Home URL. C'est un avertissement, jamais un refus : il signifie « une
|
|
195
|
+
connexion distante ne marchera que si la Home URL pointe sur ce serveur, ou si vous passez à
|
|
196
|
+
une instance de production ». Il ne contient ni clé ni secret.
|
|
197
|
+
|
|
130
198
|
### Multi-tenance
|
|
131
199
|
|
|
132
200
|
`list_items` filtre les lignes par leur `orgId` stocké face à l'organisation
|
|
@@ -217,11 +285,11 @@ de `npm publish`.
|
|
|
217
285
|
qui ne négocient jamais l'extension.
|
|
218
286
|
|
|
219
287
|
Le widget (`src/ui/`) réutilise une famille `MosaicCard` vendorisée et est empaqueté
|
|
220
|
-
en un seul fichier HTML autonome (`vite
|
|
288
|
+
en un seul fichier HTML autonome (un plugin d'inlining local dans `vite.config.ts`,
|
|
221
289
|
`dist/ui-bundle/widget.html`) par `npm run build:ui`, sur un chemin qui n'entre
|
|
222
290
|
jamais en collision avec `dist/ui/` de tsc.
|
|
223
291
|
|
|
224
|
-
**Couche d'identité** : `@vantageos/cloud-identity` (^0.
|
|
292
|
+
**Couche d'identité** : `@vantageos/cloud-identity` (^0.19.0) est le seul paquet
|
|
225
293
|
d'identité. Son point d'entrée resource-server (`verifyMcpAccessToken`,
|
|
226
294
|
`buildProtectedResourceMetadata`) vérifie le jeton Bearer sur `/mcp` ; `jose` ne sert
|
|
227
295
|
plus qu'à signer les jetons d'accès du serveur d'autorisation. Un jeu de clés
|
package/README.md
CHANGED
|
@@ -118,10 +118,72 @@ log (names, never values). There is no auto-approve path.
|
|
|
118
118
|
with in-memory adapters for tests and single-process development. Use durable
|
|
119
119
|
adapters in production — contract in [`docs/oauth-stores.md`](docs/oauth-stores.md).
|
|
120
120
|
|
|
121
|
+
**Production guard**: with `NODE_ENV=production` the server refuses to start on
|
|
122
|
+
the in-memory stores (codes and registered clients are lost on restart and not
|
|
123
|
+
shared between instances) unless both durable adapters are injected or
|
|
124
|
+
`MCP_ALLOW_IN_MEMORY_STORES=1` is set — an explicit, logged opt-in for a
|
|
125
|
+
single-instance deployment. Behind a reverse proxy set `MCP_TRUSTED_PROXY_COUNT`
|
|
126
|
+
to the number of trusted proxies (default `0`): the `/register` rate limit then
|
|
127
|
+
keys on the proxy-appended `X-Forwarded-For` entry, never on the client-supplied
|
|
128
|
+
first one; with `0` it keys on the socket address and ignores the header.
|
|
129
|
+
|
|
130
|
+
**Request limits and CORS**: An operator variable that is set but invalid (for example `MCP_MAX_BODY_BYTES=abc`) stops the server at startup with the variable's name and the expected form, never a silent default; unset or empty keeps the default. the Node entry reads at most `MCP_MAX_BODY_BYTES`
|
|
131
|
+
(default 1 MiB) of a request body and answers `413` before any handler runs.
|
|
132
|
+
`/register` sends no CORS headers unless the browser origin is listed in
|
|
133
|
+
`MCP_REGISTER_CORS_ORIGINS` (comma-separated). `/mcp` requires the `mcp:full`
|
|
134
|
+
scope on the own-authorization-server tier (`403 insufficient_scope` otherwise).
|
|
135
|
+
The consent page shows the origin the code is sent to and the requested scope,
|
|
136
|
+
flags a Dynamic-Client-Registration client as unverified, and a denial redirects
|
|
137
|
+
to the client with `error=access_denied` (plus `state` and `iss`). `http:` issuer,
|
|
138
|
+
sign-in and callback URLs are refused outside loopback.
|
|
139
|
+
|
|
121
140
|
**Not included**: refresh tokens (access tokens last one hour), per-organisation
|
|
122
141
|
API keys for non-ChatGPT clients (this code never had that path; it is a gap, not
|
|
123
142
|
a regression), wildcard redirect URIs, and a token revocation list.
|
|
124
143
|
|
|
144
|
+
### Sign-in with ChatGPT and Claude
|
|
145
|
+
|
|
146
|
+
ChatGPT and Claude send the person to this server's `/authorize`, which sends them to
|
|
147
|
+
Clerk's hosted sign-in and takes them back to `/authorize/callback`. To make that work on a
|
|
148
|
+
server you host, in this order:
|
|
149
|
+
|
|
150
|
+
1. **Use a Clerk PRODUCTION instance on the server's own registrable domain.** A server at
|
|
151
|
+
`mcp.example.com` takes the Clerk domain `example.com`, so Clerk serves `clerk.example.com`
|
|
152
|
+
and `accounts.example.com`. Any other domain is a satellite setup and is not measured. Add
|
|
153
|
+
the DNS CNAMEs the Clerk dashboard lists. Create your own Google OAuth client with the authorized redirect URI
|
|
154
|
+
`https://clerk.<domain>/v1/oauth_callback`, put its id and secret in the Clerk dashboard, and
|
|
155
|
+
publish the Google consent screen with an application home page and a privacy policy (an
|
|
156
|
+
unpublished consent screen only lets test users in).
|
|
157
|
+
2. **Set the three URLs.** `PUBLIC_BASE_URL` = `https://<server>`; `AUTHORIZE_CALLBACK_URL` =
|
|
158
|
+
`https://<server>/authorize/callback`; `AUTHORIZE_SIGN_IN_URL` = the instance's hosted
|
|
159
|
+
sign-in page: `https://accounts.<domain>/sign-in` on production,
|
|
160
|
+
`https://<instance>.accounts.dev/sign-in` on development (the Account Portal URL, shown in
|
|
161
|
+
the Clerk dashboard under **Account Portal**). There is no "allowed redirect URL" to fill in
|
|
162
|
+
the dashboard for this flow: the hosted page sends the browser back only to a host under
|
|
163
|
+
the instance's own domain (production, per Clerk's documentation, not yet measured) or to
|
|
164
|
+
`localhost` / the Home URL (development, measured). This is why step 1 puts the server and
|
|
165
|
+
the instance on one domain. *Measured*: the Backend API `redirect_urls` list is not
|
|
166
|
+
consulted by the hosted pages.
|
|
167
|
+
3. **A development instance is for local or test use only.** Its hosted sign-in page redirects
|
|
168
|
+
only to `localhost` or to the instance Home URL. For a remote test, set the development
|
|
169
|
+
instance's development origin / Home URL to the server origin in the Clerk dashboard, run
|
|
170
|
+
the test, and revert it afterwards.
|
|
171
|
+
4. **How the session reaches the server.** After sign-in the browser comes back to
|
|
172
|
+
`/authorize/callback` without a session on your host. The server resolves it with Clerk's
|
|
173
|
+
own handshake (`@vantageos/cloud-identity` 0.19.0, built on `@clerk/backend`
|
|
174
|
+
`authenticateRequest()`): it redirects the browser through Clerk's handshake endpoint and
|
|
175
|
+
Clerk sets `__session` on your host. It needs `CLERK_SECRET_KEY` and the publishable key:
|
|
176
|
+
`CLERK_PUBLISHABLE_KEY`, or, when unset, derived from `CLERK_DOMAIN` as
|
|
177
|
+
`pk_test_` / `pk_live_` (following the secret key) + base64(`<frontend API host>$`).
|
|
178
|
+
*Measured*: a development instance with a remote server works once its Home URL points at
|
|
179
|
+
the server. *Not yet measured*: a production instance on a registrable domain different
|
|
180
|
+
from the server's.
|
|
181
|
+
5. **Boot warning.** When the configured instance is a development one (an `sk_test_` key, or
|
|
182
|
+
a `CLERK_DOMAIN` under `accounts.dev`) and `PUBLIC_BASE_URL` is not localhost, the server
|
|
183
|
+
logs one warning at boot, in English and French, naming the Home URL requirement. It is a
|
|
184
|
+
warning, never a refusal: it means "a remote sign-in will only work if the Home URL points
|
|
185
|
+
at this server, or if you move to a production instance". It carries no key or secret.
|
|
186
|
+
|
|
125
187
|
### Multi-tenancy
|
|
126
188
|
|
|
127
189
|
`list_items` filters rows by their stored `orgId` against the caller's verified
|
|
@@ -211,11 +273,11 @@ Publication is proven at the registry's `dist-tag`, never by the exit code of
|
|
|
211
273
|
negotiate the extension.
|
|
212
274
|
|
|
213
275
|
The widget (`src/ui/`) reuses a vendored `MosaicCard` family and is bundled to
|
|
214
|
-
one self-contained HTML file (`vite
|
|
276
|
+
one self-contained HTML file (an in-repo inlining plugin in `vite.config.ts`,
|
|
215
277
|
`dist/ui-bundle/widget.html`) by `npm run build:ui`, on a path that never
|
|
216
278
|
collides with tsc's `dist/ui/`.
|
|
217
279
|
|
|
218
|
-
**Identity layer**: `@vantageos/cloud-identity` (^0.
|
|
280
|
+
**Identity layer**: `@vantageos/cloud-identity` (^0.19.0) is the only identity
|
|
219
281
|
package. Its resource-server entry point (`verifyMcpAccessToken`,
|
|
220
282
|
`buildProtectedResourceMetadata`) verifies the Bearer token on `/mcp`; `jose` is
|
|
221
283
|
kept only to mint the authorization server's access tokens. An unreachable key set
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator knobs read from the environment. UNSET (or empty) means the default; a value that is
|
|
3
|
+
* SET and invalid throws at startup, naming the variable and what was expected. The value is
|
|
4
|
+
* never echoed (it may be a secret pasted into the wrong variable) and never replaced by the
|
|
5
|
+
* default: a silent fallback would run the server on a setting its operator did not choose.
|
|
6
|
+
*/
|
|
7
|
+
/** The raw value, or undefined when the variable is unset or empty. */
|
|
8
|
+
export declare function rawKnob(env: NodeJS.ProcessEnv, name: string): string | undefined;
|
|
9
|
+
/** A base-10 integer within [min, max]; undefined when unset. */
|
|
10
|
+
export declare function integerKnob(env: NodeJS.ProcessEnv, name: string, min: number, max?: number): number | undefined;
|
|
11
|
+
/** Exactly "0" or "1"; undefined when unset. */
|
|
12
|
+
export declare function flagKnob(env: NodeJS.ProcessEnv, name: string): boolean | undefined;
|
|
13
|
+
/** A comma-separated list of origins (`scheme://host[:port]`, http or https, no path). */
|
|
14
|
+
export declare function originListKnob(env: NodeJS.ProcessEnv, name: string): string[];
|
package/dist/envKnobs.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator knobs read from the environment. UNSET (or empty) means the default; a value that is
|
|
3
|
+
* SET and invalid throws at startup, naming the variable and what was expected. The value is
|
|
4
|
+
* never echoed (it may be a secret pasted into the wrong variable) and never replaced by the
|
|
5
|
+
* default: a silent fallback would run the server on a setting its operator did not choose.
|
|
6
|
+
*/
|
|
7
|
+
/** The raw value, or undefined when the variable is unset or empty. */
|
|
8
|
+
export function rawKnob(env, name) {
|
|
9
|
+
const raw = env[name];
|
|
10
|
+
return raw === undefined || raw === "" ? undefined : raw;
|
|
11
|
+
}
|
|
12
|
+
/** A base-10 integer within [min, max]; undefined when unset. */
|
|
13
|
+
export function integerKnob(env, name, min, max = Number.MAX_SAFE_INTEGER) {
|
|
14
|
+
const raw = rawKnob(env, name);
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const n = /^\d+$/.test(raw) ? Number(raw) : Number.NaN;
|
|
18
|
+
if (!Number.isSafeInteger(n) || n < min || n > max) {
|
|
19
|
+
const range = max === Number.MAX_SAFE_INTEGER ? `at least ${min}` : `from ${min} to ${max}`;
|
|
20
|
+
throw new Error(`${name} is set but invalid: expected ${min > 0 ? "a positive" : "a non-negative"} integer ${range} (value not shown)`);
|
|
21
|
+
}
|
|
22
|
+
return n;
|
|
23
|
+
}
|
|
24
|
+
/** Exactly "0" or "1"; undefined when unset. */
|
|
25
|
+
export function flagKnob(env, name) {
|
|
26
|
+
const raw = rawKnob(env, name);
|
|
27
|
+
if (raw === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (raw !== "0" && raw !== "1") {
|
|
30
|
+
throw new Error(`${name} is set but invalid: expected "0" or "1" (value not shown)`);
|
|
31
|
+
}
|
|
32
|
+
return raw === "1";
|
|
33
|
+
}
|
|
34
|
+
/** A comma-separated list of origins (`scheme://host[:port]`, http or https, no path). */
|
|
35
|
+
export function originListKnob(env, name) {
|
|
36
|
+
const raw = rawKnob(env, name);
|
|
37
|
+
if (raw === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
const origins = raw.split(",").map((entry) => entry.trim());
|
|
40
|
+
for (const entry of origins) {
|
|
41
|
+
let ok = false;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(entry);
|
|
44
|
+
ok = (url.protocol === "https:" || url.protocol === "http:") && url.origin === entry;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
ok = false;
|
|
48
|
+
}
|
|
49
|
+
if (!ok) {
|
|
50
|
+
throw new Error(`${name} is set but invalid: expected a comma-separated list of origins like https://app.example.com (scheme, host, optional port; no path, no "*") (value not shown)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return origins;
|
|
54
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadAuthConfig, loadAuthMode } from "./config.js";
|
|
2
2
|
import { buildHttpApp } from "./httpApp.js";
|
|
3
|
+
import { createNodeListener, maxBodyBytesFrom } from "./nodeAdapter.js";
|
|
3
4
|
const authMode = loadAuthMode();
|
|
4
5
|
const uiDomain = process.env.MCP_UI_DOMAIN || undefined;
|
|
5
6
|
// OAuth tier: MCP_OAUTH_ISSUER + MCP_OAUTH_AUDIENCE select an EXTERNAL issuer (resource server
|
|
@@ -13,26 +14,9 @@ const app = buildHttpApp(authMode === "oauth"
|
|
|
13
14
|
: { authMode, authorizationServer: {}, uiDomain }
|
|
14
15
|
: { authMode, uiDomain });
|
|
15
16
|
const port = Number(process.env.PORT ?? 8787);
|
|
16
|
-
// Hono's fetch handler adapted onto Node's http server
|
|
17
|
-
//
|
|
17
|
+
// Hono's fetch handler adapted onto Node's http server, with the request-body cap
|
|
18
|
+
// (MCP_MAX_BODY_BYTES, default 1 MiB) enforced while the body streams in: src/nodeAdapter.ts.
|
|
18
19
|
const { createServer } = await import("node:http");
|
|
19
|
-
createServer(
|
|
20
|
-
const url = `http://${req.headers.host ?? "localhost"}${req.url}`;
|
|
21
|
-
const chunks = [];
|
|
22
|
-
for await (const chunk of req)
|
|
23
|
-
chunks.push(chunk);
|
|
24
|
-
const body = req.method === "GET" || req.method === "HEAD" ? undefined : Buffer.concat(chunks);
|
|
25
|
-
const headers = new Headers();
|
|
26
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
27
|
-
if (typeof value === "string")
|
|
28
|
-
headers.set(key, value);
|
|
29
|
-
}
|
|
30
|
-
const request = new Request(url, { method: req.method, headers, body });
|
|
31
|
-
const response = await app.fetch(request);
|
|
32
|
-
res.statusCode = response.status;
|
|
33
|
-
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
34
|
-
const buf = Buffer.from(await response.arrayBuffer());
|
|
35
|
-
res.end(buf);
|
|
36
|
-
}).listen(port, () => {
|
|
20
|
+
createServer(createNodeListener(app, { maxBodyBytes: maxBodyBytesFrom(process.env) })).listen(port, () => {
|
|
37
21
|
console.log(`mcp-boilerplate HTTP transport listening on :${port} (authMode: ${authMode})`);
|
|
38
22
|
});
|
package/dist/httpApp.js
CHANGED
|
@@ -23,11 +23,13 @@
|
|
|
23
23
|
* trusted (mirrors stdio's trust model). The DEFAULT when `buildHttpApp()` gets no deps.
|
|
24
24
|
*/
|
|
25
25
|
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
26
|
+
import { unauthorizedChallenge } from "@vantageos/cloud-identity";
|
|
26
27
|
import { Hono } from "hono";
|
|
27
28
|
import { authenticateRequest, protectedResourceMetadataDocument } from "./auth.js";
|
|
28
29
|
import { detectLocale, t } from "./i18n.js";
|
|
29
30
|
import { createBoilerplateMcpServer } from "./mcpServerFactory.js";
|
|
30
31
|
import { createAuthorizationServer, } from "./oauth/authorizationServer.js";
|
|
32
|
+
import { MCP_SCOPE } from "./oauth/settings.js";
|
|
31
33
|
import { resolveTenantFromJwtPayload } from "./tenancy.js";
|
|
32
34
|
const PRM_PATHS = [
|
|
33
35
|
"/.well-known/oauth-protected-resource",
|
|
@@ -82,6 +84,21 @@ export function buildHttpApp(deps = {}) {
|
|
|
82
84
|
c.header("WWW-Authenticate", result.headers["WWW-Authenticate"]);
|
|
83
85
|
return c.json({ error: t(locale, "auth.unauthorized") }, result.status);
|
|
84
86
|
}
|
|
87
|
+
// A token must carry the scope this resource is protected by. Only the own-authorization-
|
|
88
|
+
// server tier knows its scope vocabulary; an external issuer's scopes are its own.
|
|
89
|
+
if (deps.authorizationServer) {
|
|
90
|
+
const granted = String(result.session.claims.scope ?? "").split(/\s+/);
|
|
91
|
+
if (!granted.includes(MCP_SCOPE)) {
|
|
92
|
+
const challenge = unauthorizedChallenge({
|
|
93
|
+
realm: rs.auth.realm,
|
|
94
|
+
resourceMetadataUrl: rs.auth.resourceMetadataUrl,
|
|
95
|
+
error: "insufficient_scope",
|
|
96
|
+
scope: MCP_SCOPE,
|
|
97
|
+
});
|
|
98
|
+
c.header("WWW-Authenticate", challenge.headers["WWW-Authenticate"]);
|
|
99
|
+
return c.json({ error: "insufficient_scope" }, 403);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
85
102
|
// Tenant resolution (tenancy.ts) is attempted here but is NOT request-fatal:
|
|
86
103
|
// `initialize`/`tools/list` and any tenant-agnostic tool (e.g. `ping`) must keep working
|
|
87
104
|
// for a verified caller with no organization on their token. Tenant-scoped tools enforce
|
package/dist/locales/en.json
CHANGED
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
"oauth": {
|
|
13
13
|
"pickerTitle": "Connect {client}",
|
|
14
14
|
"pickerIntro": "{client} asks to act on your behalf. Choose the organisation it may reach, then approve or deny.",
|
|
15
|
+
"pickerDestination": "After approval you are sent to:",
|
|
16
|
+
"pickerScope": "Requested access:",
|
|
17
|
+
"pickerUnverified": "Unverified client: it registered itself automatically and nobody has vouched for it. Approve only if you recognise the address above.",
|
|
15
18
|
"pickerOrganisation": "Organisation",
|
|
16
19
|
"approve": "Approve",
|
|
17
20
|
"deny": "Deny",
|
|
18
21
|
"denied": "The request was denied; you can close this window.",
|
|
19
|
-
"unavailable": "Authorization is not configured on this server."
|
|
22
|
+
"unavailable": "Authorization is not configured on this server.",
|
|
23
|
+
"devInstanceRemote": "Development Clerk instance on a remote server: its hosted sign-in page redirects only to localhost or to the instance Home URL. Set the development instance's Home URL (development origin) to this server's origin for a remote test and revert it afterwards, or use a Clerk production instance (see README, \"Sign-in with ChatGPT and Claude\")."
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
}
|
package/dist/locales/fr.json
CHANGED
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
"oauth": {
|
|
13
13
|
"pickerTitle": "Connecter {client}",
|
|
14
14
|
"pickerIntro": "{client} demande à agir en votre nom. Choisissez l'organisation à laquelle il peut accéder, puis approuvez ou refusez.",
|
|
15
|
+
"pickerDestination": "Redirection après validation vers :",
|
|
16
|
+
"pickerScope": "Accès demandé :",
|
|
17
|
+
"pickerUnverified": "Client non vérifié : il s'est enregistré automatiquement et personne ne s'en porte garant. N'approuvez que si vous reconnaissez l'adresse ci-dessus.",
|
|
15
18
|
"pickerOrganisation": "Organisation",
|
|
16
19
|
"approve": "Approuver",
|
|
17
20
|
"deny": "Refuser",
|
|
18
21
|
"denied": "La demande a été refusée ; vous pouvez fermer cette fenêtre.",
|
|
19
|
-
"unavailable": "L'autorisation n'est pas configurée sur ce serveur."
|
|
22
|
+
"unavailable": "L'autorisation n'est pas configurée sur ce serveur.",
|
|
23
|
+
"devInstanceRemote": "Instance Clerk de développement sur un serveur distant : sa page de connexion hébergée ne redirige que vers localhost ou vers l'URL d'accueil (Home URL) de l'instance. Pour un test distant, réglez l'URL d'accueil (origine de développement) de l'instance sur l'origine de ce serveur, puis rétablissez-la ; sinon utilisez une instance Clerk de production (voir README, « Connexion avec ChatGPT et Claude »)."
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono's fetch handler adapted onto Node's `http` server (no framework beyond hono + node:http).
|
|
3
|
+
*
|
|
4
|
+
* The request body is read here, so the cap lives here: bytes are counted as they stream in and
|
|
5
|
+
* the read stops the moment the cap is crossed — an oversized body is answered 413 before any
|
|
6
|
+
* handler runs and is never held whole in memory.
|
|
7
|
+
*/
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
+
/** 1 MiB: far above any MCP JSON-RPC request or OAuth form this server accepts. */
|
|
10
|
+
export declare const DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
11
|
+
/** `MCP_MAX_BODY_BYTES`, a positive integer. Unset = default; set and invalid throws. */
|
|
12
|
+
export declare function maxBodyBytesFrom(env: NodeJS.ProcessEnv): number;
|
|
13
|
+
export interface FetchApp {
|
|
14
|
+
fetch(request: Request, bindings?: {
|
|
15
|
+
remoteAddress?: string;
|
|
16
|
+
}): Response | Promise<Response>;
|
|
17
|
+
}
|
|
18
|
+
export declare function createNodeListener(app: FetchApp, options?: {
|
|
19
|
+
maxBodyBytes?: number;
|
|
20
|
+
}): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono's fetch handler adapted onto Node's `http` server (no framework beyond hono + node:http).
|
|
3
|
+
*
|
|
4
|
+
* The request body is read here, so the cap lives here: bytes are counted as they stream in and
|
|
5
|
+
* the read stops the moment the cap is crossed — an oversized body is answered 413 before any
|
|
6
|
+
* handler runs and is never held whole in memory.
|
|
7
|
+
*/
|
|
8
|
+
import { integerKnob } from "./envKnobs.js";
|
|
9
|
+
/** 1 MiB: far above any MCP JSON-RPC request or OAuth form this server accepts. */
|
|
10
|
+
export const DEFAULT_MAX_BODY_BYTES = 1_048_576;
|
|
11
|
+
/** `MCP_MAX_BODY_BYTES`, a positive integer. Unset = default; set and invalid throws. */
|
|
12
|
+
export function maxBodyBytesFrom(env) {
|
|
13
|
+
return integerKnob(env, "MCP_MAX_BODY_BYTES", 1) ?? DEFAULT_MAX_BODY_BYTES;
|
|
14
|
+
}
|
|
15
|
+
function tooLarge(res, maxBodyBytes) {
|
|
16
|
+
res.writeHead(413, { "content-type": "application/json", connection: "close" });
|
|
17
|
+
res.end(JSON.stringify({
|
|
18
|
+
error: "payload_too_large",
|
|
19
|
+
error_description: `request body exceeds ${maxBodyBytes} bytes`,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
export function createNodeListener(app, options = {}) {
|
|
23
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
24
|
+
return async (req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const hasBody = req.method !== "GET" && req.method !== "HEAD";
|
|
27
|
+
let body;
|
|
28
|
+
if (hasBody) {
|
|
29
|
+
const declared = Number(req.headers["content-length"]);
|
|
30
|
+
if (Number.isFinite(declared) && declared > maxBodyBytes)
|
|
31
|
+
return tooLarge(res, maxBodyBytes);
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let received = 0;
|
|
34
|
+
for await (const chunk of req) {
|
|
35
|
+
const piece = chunk;
|
|
36
|
+
received += piece.byteLength;
|
|
37
|
+
if (received > maxBodyBytes)
|
|
38
|
+
return tooLarge(res, maxBodyBytes);
|
|
39
|
+
chunks.push(piece);
|
|
40
|
+
}
|
|
41
|
+
body = new Uint8Array(Buffer.concat(chunks));
|
|
42
|
+
}
|
|
43
|
+
const headers = new Headers();
|
|
44
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
45
|
+
if (typeof value === "string")
|
|
46
|
+
headers.set(key, value);
|
|
47
|
+
}
|
|
48
|
+
const url = `http://${req.headers.host ?? "localhost"}${req.url}`;
|
|
49
|
+
const request = new Request(url, { method: req.method, headers, body });
|
|
50
|
+
const response = await app.fetch(request, { remoteAddress: req.socket.remoteAddress });
|
|
51
|
+
res.statusCode = response.status;
|
|
52
|
+
// `forEach` hands back Set-Cookie folded into one value, and setHeader overwrites: several
|
|
53
|
+
// cookies (Clerk's handshake sets five) would collapse to the last. Emit them as an array.
|
|
54
|
+
const cookies = response.headers.getSetCookie();
|
|
55
|
+
response.headers.forEach((value, key) => {
|
|
56
|
+
if (key.toLowerCase() !== "set-cookie")
|
|
57
|
+
res.setHeader(key, value);
|
|
58
|
+
});
|
|
59
|
+
if (cookies.length > 0)
|
|
60
|
+
res.setHeader("Set-Cookie", cookies);
|
|
61
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error("[http] request failed:", err instanceof Error ? err.message : err);
|
|
65
|
+
if (!res.headersSent) {
|
|
66
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
67
|
+
}
|
|
68
|
+
res.end(JSON.stringify({ error: "server_error" }));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* No refresh tokens are issued; access tokens live one hour.
|
|
22
22
|
*/
|
|
23
23
|
import { type AuthorizationCodeStore } from "@vantageos/cloud-identity";
|
|
24
|
-
import type { Hono } from "hono";
|
|
24
|
+
import type { Context, Hono } from "hono";
|
|
25
25
|
import type { AuthConfig } from "../auth.js";
|
|
26
26
|
import { type ClerkCollaborators } from "./clerk.js";
|
|
27
27
|
import { type CimdOptions, type ClientRegistry } from "./clientRegistry.js";
|
|
@@ -52,5 +52,25 @@ export interface AuthorizationServer {
|
|
|
52
52
|
/** The resource-server (`/mcp` gate) configuration derived from the same settings. */
|
|
53
53
|
resourceServer(): Promise<ResourceServerConfig>;
|
|
54
54
|
}
|
|
55
|
+
/** Bindings the Node entry (src/http.ts) hands to `app.fetch(request, bindings)`. */
|
|
56
|
+
export interface NodeBindings {
|
|
57
|
+
/** Address of the TCP peer, from the socket. */
|
|
58
|
+
remoteAddress?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The rate-limit key of a request: who is really on the other end.
|
|
62
|
+
*
|
|
63
|
+
* `X-Forwarded-For` is client-controlled up to the point a trusted proxy appends to it. With
|
|
64
|
+
* N trusted proxies the real client is the N-th entry from the END (each proxy appended the
|
|
65
|
+
* peer it saw); anything left of it was supplied by the client and is ignored. With none
|
|
66
|
+
* configured the header is not read at all and the socket address decides.
|
|
67
|
+
*/
|
|
68
|
+
export declare function clientKeyOf(c: Context, trustedProxyCount: number): string;
|
|
69
|
+
/**
|
|
70
|
+
* The instance publishable key: `CLERK_PUBLISHABLE_KEY` when set, else derived from the Frontend
|
|
71
|
+
* API host in `CLERK_DOMAIN` as `pk_<test|live>_` + base64(host + "$"), the format Clerk documents
|
|
72
|
+
* (the prefix follows the secret key's `sk_live_` / `sk_test_`).
|
|
73
|
+
*/
|
|
74
|
+
export declare function publishableKeyFor(env: Record<string, string | undefined>, clerkIssuer: string): string;
|
|
55
75
|
export declare function createAuthorizationServer(deps?: AuthorizationServerDeps): AuthorizationServer;
|
|
56
76
|
export {};
|