@everfur/cli 0.2.1 → 0.4.1

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { hostnameOf, listen, close, writeJson, serveStatelessMcp } from './chunk-FC23PLWG.js';
3
- import { createHostedTokenVerifier, tokenBucketOf, isAccessTokenShaped, buildServer, MCP_SCOPE, tenantContextOf, sendTestUserRef, hostedVerifyUserRef, REVOKED_AUTHORIZATION_REMEDY, SYNTHETIC_USER_REF_PREFIX } from './chunk-XMIERMOW.js';
4
- import { BASE_URL_ENV_VAR, resolveEndpoint, fetchTransport, systemClock, createApiClient, withPrefix, ephemeralCredential, validateContractText, isJsonObject, describeFailure } from './chunk-KQICUK54.js';
2
+ import { hostnameOf, listen, close, writeJson, serveStatelessMcp } from './chunk-A2QEBO5U.js';
3
+ import { createHostedTokenVerifier, tokenBucketOf, isAccessTokenShaped, buildServer, MCP_SCOPE, tenantContextOf, sendTestUserRef, hostedVerifyUserRef, REVOKED_AUTHORIZATION_REMEDY, SYNTHETIC_USER_REF_PREFIX } from './chunk-QCXZJW5C.js';
4
+ import { BASE_URL_ENV_VAR, resolveEndpoint, fetchTransport, systemClock, createApiClient, withPrefix, ephemeralCredential, validateContractText, isJsonObject, describeFailure } from './chunk-EBVP6FAD.js';
5
5
  import { createServer } from 'http';
6
6
  import { InvalidTokenError, TooManyRequestsError, TemporarilyUnavailableError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
7
7
  import { isIP } from 'net';
@@ -12,8 +12,9 @@ var PORT_ENV_VAR = "EVERFUR_MCP_HTTP_PORT";
12
12
  var LEGACY_PORT_ENV_VAR = "MCP_PORT";
13
13
  var ALLOWED_HOSTS_ENV_VAR = "MCP_ALLOWED_HOSTS";
14
14
  var ALLOWED_ORIGINS_ENV_VAR = "MCP_ALLOWED_ORIGINS";
15
- var DEFAULT_ALLOWED_ORIGINS = Object.freeze(["https://claude.ai", "https://claude.com"]);
15
+ var DEFAULT_ALLOWED_ORIGINS = Object.freeze(["https://claude.ai", "https://claude.com", "https://chatgpt.com"]);
16
16
  var TRUSTED_PROXY_HOPS_ENV_VAR = "MCP_TRUSTED_PROXY_HOPS";
17
+ var OPENAI_APPS_CHALLENGE_TOKEN_ENV_VAR = "OPENAI_APPS_CHALLENGE_TOKEN";
17
18
  var MAX_TRUSTED_PROXY_HOPS = 8;
18
19
  var DEFAULT_HOSTED_PORT = 3100;
19
20
  var DEFAULT_MCP_PATH = "/mcp";
@@ -106,6 +107,14 @@ function parseAllowedOrigins(raw) {
106
107
  return origin;
107
108
  });
108
109
  }
110
+ function parseOpenAiAppsChallengeToken(raw) {
111
+ const value = (raw ?? "").trim();
112
+ if (value.length === 0) return null;
113
+ if (!/^[\x21-\x7e]+$/.test(value)) {
114
+ throw new Error(`${OPENAI_APPS_CHALLENGE_TOKEN_ENV_VAR} must be one token of printable ASCII, with no space or line break inside it.`);
115
+ }
116
+ return value;
117
+ }
109
118
  function loadHostedMcpConfig(env) {
110
119
  if ((env[BASE_URL_ENV_VAR] ?? "").trim().length === 0) {
111
120
  throw new Error(`${BASE_URL_ENV_VAR} is required: the Everfur API this server verifies bearers against.`);
@@ -126,7 +135,8 @@ function loadHostedMcpConfig(env) {
126
135
  port: parsePort(env),
127
136
  allowedHosts: /* @__PURE__ */ new Set([publicHostname, ...parseAllowedHosts(env[ALLOWED_HOSTS_ENV_VAR])]),
128
137
  trustedProxyHops: parseTrustedProxyHops(env[TRUSTED_PROXY_HOPS_ENV_VAR]),
129
- allowedOrigins: /* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...parseAllowedOrigins(env[ALLOWED_ORIGINS_ENV_VAR])])
138
+ allowedOrigins: /* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...parseAllowedOrigins(env[ALLOWED_ORIGINS_ENV_VAR])]),
139
+ openaiAppsChallengeToken: parseOpenAiAppsChallengeToken(env[OPENAI_APPS_CHALLENGE_TOKEN_ENV_VAR])
130
140
  });
131
141
  }
132
142
  function resourceMetadataUrl(config) {
@@ -265,15 +275,35 @@ The integration tools:
265
275
  - everfur_plan_integration produces an integration plan, or a refusal that names each missing fact (identity mode,
266
276
  capabilities, backend language, environment) rather than a plan built on a guess. An integration built on a guessed
267
277
  identity mode is wrong in a way that only shows up in production. Omitted, the environment is the connected tenant's.
278
+ A plan comes back as a small summary with a plan_id and a number of pages: the goal, the detected facts, each step's
279
+ id, title, one-line intent and page, and the checklist. The steps in full, with their commands and the complete file
280
+ bodies, come from further everfur_plan_integration calls with the same arguments plus plan_id and page, for page 1
281
+ to pages; page 1 also carries the detection, the warnings and the checklist in full, and the principles. (With
282
+ steps, step ids, instead of page, those steps come a page at a time and remaining_steps names the rest.) Each call
283
+ recomputes the plan from the project_details it is given, and the plan's own commands and files change the project,
284
+ so every page is fetched before any command runs or any file is written. The same arguments
285
+ and contract always produce the same plan, byte for byte. A refusal for missing facts gives each fact the developer
286
+ answers a fixed question and its closed list of options, so the same refusal always asks the same thing. A ready
287
+ plan carries a checklist: the finished integration as a fixed list of requirements, each with the check that proves
288
+ it. The theme is matched to the host app's own brand colours, read from its code (a Tailwind config, the CSS that
289
+ declares its theme, or a React Native theme file) and passed as theme_source; when none is given, the plan's
290
+ match-theme step names where those colours are looked for, and no colour is chosen for the host. The tenant's console
291
+ branding is the other side of the theme: it fills every field the theme leaves unset and wins for a field the tenant
292
+ locked in the partner console.
268
293
  - everfur_verify_integration exercises the real wire with a short-lived session the Everfur API mints for a synthetic
269
- test user of a sandbox tenant (its entitlements report livemode false). On a live tenant the signed session step is
270
- reported GATED; no session can be supplied by hand. identity_mode anonymous verifies the anonymous path instead: the
294
+ test user of a sandbox tenant (its entitlements report livemode false). On a live tenant with a paired sandbox the
295
+ session is minted on that sandbox and the call uses its publishable key; on a live tenant with no pair the signed
296
+ session step is reported GATED; no session can be supplied by hand. identity_mode anonymous verifies the anonymous path instead: the
271
297
  tenant's publishable key and a device session id derived for a synthetic test user, with nothing minted. It is the
272
- read-only check a live tenant takes, GET requests only.
298
+ read-only check a live tenant takes, GET requests only. For a web page pass origin with embedding beside it: an
299
+ in-page page calls the API from its own origin, so the CORS checks apply to it, and with embedding in-page the
300
+ frame's own checks (the embed fence and the loader) are reported N/A.
273
301
  - everfur_diagnose separates the four byte-identical causes of an empty entitlement verdict.
274
302
  - everfur_explain_error looks an error code up in the contract's registry, case sensitive, and refuses a code the
275
303
  registry does not list, with near misses. Three naming families are live at once and one code is the default for
276
- four different HTTP statuses, so the registry, not the shape of a code's string, says whether a retry can help.
304
+ four different HTTP statuses, so the registry, not the shape of a code's string, says whether a retry can help. It
305
+ also answers, offline, the error.code the tools on this server return (VERIFICATION_FAILED, PLAN_CHANGED and the
306
+ rest).
277
307
  - everfur_send_test_message sends a real message to a sandbox tenant only, and streams the real answer.
278
308
 
279
309
  The tenant tools read the connected tenant's own state and change nothing:
@@ -296,6 +326,13 @@ diagnose-integration lay out an order to call the tools in.`;
296
326
 
297
327
  // src/mcp/hosted/tenant-env.ts
298
328
  var VERIFY_SESSIONS_PATH = "/mcp/verify-sessions";
329
+ var MINTING_TOOLS = /* @__PURE__ */ new Set(["everfur_verify_integration", "everfur_send_test_message"]);
330
+ function hostedCallMode(context, tool, args) {
331
+ if (!context.tenant.livemode || context.test === null) return "connected";
332
+ if (!MINTING_TOOLS.has(tool)) return "connected";
333
+ if (tool === "everfur_verify_integration" && args["identity_mode"] === "anonymous") return "connected";
334
+ return "test";
335
+ }
299
336
  function toolApiBaseUrl(context, fallback) {
300
337
  const resolved = resolveEndpoint(null, { [BASE_URL_ENV_VAR]: context.apiBaseUrl });
301
338
  return resolved.ok ? resolved.endpoint.baseUrl : fallback;
@@ -334,7 +371,9 @@ async function mintVerifySession(options) {
334
371
  sessionToken: null,
335
372
  outcome: {
336
373
  kind: "gated",
337
- reason: "this tenant is live: the Everfur API mints verify sessions for sandbox tenants only",
374
+ // The live-tenant wording applies to an UNPAIRED live tenant only: a paired one is minted on its sandbox, and is
375
+ // refused only when that sandbox is no longer an active partner sandbox (EFBackend #1629).
376
+ reason: options.paired === true ? "this tenant is live and its paired sandbox is no longer an active sandbox: the Everfur API mints verify sessions on a sandbox tenant only" : "this tenant is live: the Everfur API mints verify sessions for sandbox tenants only",
338
377
  requestId: result.requestId
339
378
  }
340
379
  };
@@ -391,7 +430,8 @@ async function mintVerifySession(options) {
391
430
  };
392
431
  }
393
432
  function synthesizeTenantEnv(options) {
394
- const publishableKey = options.context.publishableKey;
433
+ const test = options.mode === "test" ? options.context.test : null;
434
+ const publishableKey = test === null ? options.context.publishableKey : test.publishableKey;
395
435
  return Object.freeze({
396
436
  [BASE_URL_ENV_VAR]: toolApiBaseUrl(options.context, options.apiBaseUrl),
397
437
  ...publishableKey === null ? {} : { EVERFUR_PARTNER_KEY: publishableKey }
@@ -610,6 +650,7 @@ function createWellKnownMirror(options) {
610
650
 
611
651
  // src/mcp/hosted/http.ts
612
652
  var HEALTH_PATH = "/healthz";
653
+ var OPENAI_APPS_CHALLENGE_PATH = "/.well-known/openai-apps-challenge";
613
654
  var MAX_BODY_BYTES = 1024 * 1024;
614
655
  var DRAIN_CEILING_BYTES = 16 * 1024 * 1024;
615
656
  var RETRY_AFTER_SECONDS = 5;
@@ -720,12 +761,23 @@ function createHostedMcpRequestListener(options) {
720
761
  const userRef = call.tool === "everfur_send_test_message" && typeof idempotencyKey === "string" && idempotencyKey.length > 0 ? sendTestUserRef(idempotencyKey) : hostedVerifyUserRef(typeof label === "string" ? label : void 0);
721
762
  const bearer = call.authInfo.token;
722
763
  const onUnauthorized = () => verifier.forget(bearer);
764
+ const mode = hostedCallMode(tenant, call.tool, call.args);
765
+ const paired = tenant.tenant.livemode && tenant.test !== null;
723
766
  return {
724
- env: synthesizeTenantEnv({ context: tenant, apiBaseUrl: config.apiBaseUrl }),
767
+ env: synthesizeTenantEnv({ context: tenant, apiBaseUrl: config.apiBaseUrl, mode }),
725
768
  credentialSource: "hosted",
726
- hostedMint: createHostedMint({ bearer, apiBaseUrl: config.apiBaseUrl, userRef, transport, clock, onUnauthorized }),
769
+ hostedMint: createHostedMint({ bearer, apiBaseUrl: config.apiBaseUrl, userRef, transport, clock, onUnauthorized, paired }),
727
770
  syntheticUserRef: userRef,
728
- tenantReads: createHostedTenantReads({ bearer, apiBaseUrl: config.apiBaseUrl, transport, clock, onUnauthorized })
771
+ tenantReads: createHostedTenantReads({ bearer, apiBaseUrl: config.apiBaseUrl, transport, clock, onUnauthorized }),
772
+ tenantMode: { livemode: tenant.tenant.livemode, paired },
773
+ connectedTenant: {
774
+ slug: tenant.tenant.slug,
775
+ livemode: tenant.tenant.livemode,
776
+ pairedSandboxSlug: tenant.test === null ? null : tenant.test.slug,
777
+ // The prefix alone, which names the tenant mode; the key itself never leaves the context.
778
+ publishableKeyPrefix: ["pk_test_", "pk_live_"].find((prefix) => tenant.publishableKey?.startsWith(prefix) === true) ?? null,
779
+ apiBaseUrl: tenant.apiBaseUrl
780
+ }
729
781
  };
730
782
  };
731
783
  return async (req, res) => {
@@ -738,6 +790,21 @@ function createHostedMcpRequestListener(options) {
738
790
  writeJson(res, 200, { status: "ok" });
739
791
  return;
740
792
  }
793
+ const challengeToken = config.openaiAppsChallengeToken;
794
+ if (challengeToken !== null && url.pathname === OPENAI_APPS_CHALLENGE_PATH) {
795
+ const refused2 = refusalFor(req, config, ["GET", "HEAD"]);
796
+ if (refused2 !== null) {
797
+ writeJson(res, refused2.status, { error: refused2.message }, refused2.headers);
798
+ return;
799
+ }
800
+ res.writeHead(200, {
801
+ "content-type": "text/plain; charset=utf-8",
802
+ "content-length": String(Buffer.byteLength(challengeToken)),
803
+ "cache-control": "no-store"
804
+ });
805
+ res.end(req.method === "HEAD" ? void 0 : challengeToken);
806
+ return;
807
+ }
741
808
  const document = metadataPaths.get(url.pathname);
742
809
  if (document !== void 0) {
743
810
  if (req.method === "OPTIONS") {
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { loadHostedMcpConfig, startHostedMcpServer } from './chunk-SJUEHHU6.js';
3
- import './chunk-FC23PLWG.js';
4
- import './chunk-XMIERMOW.js';
5
- import { CLI_VERSION } from './chunk-KQICUK54.js';
2
+ import { loadHostedMcpConfig, startHostedMcpServer } from './chunk-YRGSY4R2.js';
3
+ import './chunk-A2QEBO5U.js';
4
+ import './chunk-QCXZJW5C.js';
5
+ import { CLI_VERSION } from './chunk-EBVP6FAD.js';
6
6
 
7
7
  // src/bin/everfur-mcp-hosted.ts
8
8
  async function main() {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { startRemoteMcpServer, loadRemoteMcpConfig } from './chunk-FC23PLWG.js';
3
- import './chunk-XMIERMOW.js';
4
- import { CLI_VERSION } from './chunk-KQICUK54.js';
2
+ import { startRemoteMcpServer, loadRemoteMcpConfig } from './chunk-A2QEBO5U.js';
3
+ import './chunk-QCXZJW5C.js';
4
+ import { CLI_VERSION } from './chunk-EBVP6FAD.js';
5
5
 
6
6
  // src/bin/everfur-mcp-http.ts
7
7
  async function main() {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runStdioServer } from './chunk-XMIERMOW.js';
3
- import { CLI_VERSION } from './chunk-KQICUK54.js';
2
+ import { runStdioServer } from './chunk-QCXZJW5C.js';
3
+ import { CLI_VERSION } from './chunk-EBVP6FAD.js';
4
4
 
5
5
  // src/bin/everfur-mcp.ts
6
6
  if (process.argv.slice(2).includes("--version")) {
package/dist/everfur.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-4UB26D67.js';
3
- import { ExitCode, systemClock, fetchTransport, CLI_VERSION, nodeFileSystem, nodeContractCache } from './chunk-KQICUK54.js';
2
+ import { runCli } from './chunk-2MTX6YWO.js';
3
+ import { ExitCode, systemClock, fetchTransport, CLI_VERSION, nodeFileSystem, nodeContractCache } from './chunk-EBVP6FAD.js';
4
4
 
5
5
  // src/bin/everfur.ts
6
6
  async function main() {