diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1b6a454 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,92 @@ +# AGENTS.md — aio-cli-plugin-api-mesh + +Adobe I/O CLI plugin (`@adobe/aio-cli-plugin-api-mesh`) for creating and managing API Mesh configurations. + +--- + +## Repository Overview + +This project is an [oclif](https://oclif.io/) plugin that extends the Adobe I/O CLI (`aio`). It lets developers create, update, and locally run API Meshes — GraphQL gateways that federate multiple upstream APIs into a single endpoint, powered by Cloudflare Workers. + +**How it works end-to-end:** + +1. The user writes a `mesh.json` file describing sources (REST, GraphQL, SOAP), transforms, hooks, and plugins. +2. The CLI validates and submits that config to the **Schema Management Service (SMS)** — the Adobe-hosted backend that builds and deploys the mesh to Cloudflare. +3. Once provisioned, the mesh is reachable at a Cloudflare-hosted GraphQL endpoint. +4. For local development, the CLI builds the mesh artifact with `@adobe-apimesh/mesh-builder` and runs it locally via Wrangler (Cloudflare Workers runtime). + +URL constants for stage and prod live in `src/constants.js`. Switch to stage with `aio config set cli.env stage`; each constant can also be overridden by a matching environment variable. + +--- + +## Domain Concepts + +| Term | Meaning | +|------|---------| +| **Mesh** | A GraphQL gateway config combining multiple upstream sources | +| **Mesh ID** | Unique identifier assigned by SMS on create, tied to org+project+workspace | +| **Mesh Config** | The `meshConfig` block in the user's JSON file — sources, transforms, plugins | +| **Mesh Artifact** | Compiled output of `mesh-builder` — stored in `mesh-artifact//` during `run` | +| **`.mesh/`** | Packaged artifact directory read by Wrangler during local dev | +| **Tenant Files** | JS hook/plugin files referenced in the mesh config — bundled into `tenantFiles/` | +| **Ray ID** | Per-request unique ID on the Cloudflare edge — used to look up individual logs | +| **Secrets** | YAML key-value pairs, encrypted client-side with the org's RSA public key before upload | +| **Log Forwarding** | Routing mesh request logs to an external destination (e.g. New Relic) | +| **SMS** | Schema Management Service — Adobe's backend for mesh CRUD and deployment | +| **Placeholder interpolation** | `${VAR}` syntax in `mesh.json` resolved against a `.env` file before submission | + +--- + +## Where Domain Logic Lives + +### API / Network layer +**`src/lib/smsClient.js`** — all HTTP calls to SMS and Adobe Developer Console go through here. If SMS behaviour changes, start here. + +### Auth and context resolution +**`src/helpers.js`** — `initSdk()` is the entry point for every command. Resolves the IMS token, looks up org/project/workspace via Dev Console, and caches the result. + +### Secrets handling +Two separate responsibilities — easy to confuse: +- **`src/secrets.js`** — runtime: parses the encrypted secrets JSON from the `SECRETS` env var inside the running worker. A Proxy (`getSecretsHandler`) throws on access to undefined keys and blocks mutation. +- **`src/utils.js`** — build-time: `encryptSecrets()` encrypts secrets with the org's RSA public key; `validateSecretsFile()` reads and validates the YAML secrets file before submission. + +### Plugin compatibility fix +**`src/fixPlugins.js`** — rewrites the compiled mesh `index.js` to redirect `@graphql-mesh/plugin-http-details-extensions` imports to a local fork. Run as a post-build step. Without this the worker fails at runtime with a missing module error. + +--- + +## Conventions + +### Command structure +- Every command calls `initSdk()` first to resolve org/project/workspace context. +- Destructive commands (`delete`, `update`, `cache:purge`, log-forwarding changes) always prompt for confirmation unless `--autoConfirmAction` (`-c`) is passed. +- Commands return a plain object from `run()` — oclif serialises it when `--json` is set. The return shape is consumed by downstream generators (`generator-app-api-mesh`), so don't rename return keys without checking what depends on them. +- Error messages always append `RequestId: ${global.requestId}` to aid support tracing. + +### Module format +Most files are CommonJS (`require`/`module.exports`). `src/worker.js` and `src/state.js` are ESM (`import`/`export`) because they run inside the Cloudflare Workers runtime, which requires ESM. Do not mix formats within a file. + +### Testing +Tests mock `src/helpers.js` and `src/lib/smsClient.js` entirely with `jest.mock()` — no real network calls. Fixtures live in `src/commands/__fixtures__/`. New shared flags go in `src/utils.js`, not inline in the command file. + +### Logging +The pino logger (`src/classes/logger.js`) is **off by default**. Enable with `ENABLE_LOGGER=true`; set level with `LOG_LEVEL` (default `info`). Use `logger.info/debug/error`, not `console.log`, except in `src/worker.js` and `src/fixPlugins.js` which run outside the logger context. + +### Secrets +Secrets files are YAML. Limits (max count and max size) are enforced in `src/constants.js`. Secrets are encrypted client-side before transmission — the plain-text YAML file must never be committed or logged. + +--- + +## Critical Gotchas + +### 1. Command `run()` return shape is consumed by external generators +Commands return a plain object from `run()`. `generator-app-api-mesh` and other downstream tools depend on specific key names in these return objects. Renaming a return key (e.g. `meshId` → `id`) is a silent breaking change for those consumers. Check for downstream usage before renaming. + +### 2. `aio api-mesh:run --select` skips the local build entirely +Without `--select`, `run` builds the mesh artifact locally from the provided `mesh.json`. With `--select`, it downloads the currently deployed remote artifact and runs that instead — no local build. The two modes behave very differently; passing `--select` with a local `mesh.json` file will silently ignore the file. + +### 3. `initSdk()` caches org/project/workspace selection to disk +`initSdk()` writes the resolved org/project/workspace to a local cache file. Subsequent command invocations reuse the cached values without prompting. If the context seems wrong (wrong org, wrong project), pass `--ignoreCache` (`-i`) to force re-selection. The cache persists across terminal sessions. + +### 4. Placeholder interpolation happens before submission — missing vars silently become empty strings +`interpolateMesh()` replaces `${VAR}` tokens in `mesh.json` using the `.env` file. If a variable is referenced in `mesh.json` but missing from `.env`, the token is replaced with an empty string without error. The mesh will be submitted with an empty value, which may cause a build failure in SMS with a misleading error. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eef4bd2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md \ No newline at end of file