# AdminJump Site API — Public Implementation Contract v1.0

This document is a standalone implementation specification for developers and coding assistants. No authentication is required to read it. This describes the protocol implemented by AdminJump; it is not proof that a particular external CMS has implemented it.

## Supported adapters and transport

- Generic Laravel: select Laravel API in AdminJump; status MUST report cms="laravel".
- MegaShop: select MegaShop; status MUST report cms="megashop".
- Dedicated WordPress and FastShop adapters are not active. A Laravel-based FastShop can implement this generic protocol and connect as Laravel API. There is no ready-to-install WordPress plugin in this release.
- All endpoints MUST be under the exact HTTPS public origin path /api-jump. Do not accidentally expose /api/api-jump through Laravel's default prefix.
- Root domain only, port 443, public DNS addresses, valid certificate, no redirects. localhost/private IP targets are rejected. Test externally on authorized public staging, or use local connector/HTTP fakes.
- Every request, including status, is authenticated with `Authorization: Bearer <API_KEY>` and `Accept: application/json`. Writes have `Content-Type: application/json`.
- Never put credentials in URLs, JS, responses or logs. Do not disable TLS verification.

## Build the owner key management UI in the customer's CMS

Create a page named "اتصال به AdminJump" inside the existing authenticated admin panel. Only the actual site owner may create, manage or revoke an owner key. Protect the panel form with CSRF, server policies and current-password confirmation.

Generate at least 32 random bytes of secret entropy, display the encoded key only once, and store SHA-256(key), not plaintext. Keys need explicit scopes, expiry, revoke, owner/tenant foreign keys and audit without secret values. Bearer middleware resolves the key's tenant and enforces expiry/revocation and scopes on EVERY endpoint. An owner claim supplied in request input is never sufficient.

Machine API routes may be stateless and independent of browser CSRF, but never disable CSRF for the admin panel. Require HTTPS, preserve the Authorization header through reverse proxies, rate-limit keys, and exclude Authorization from access/error logs.

## Stable site identity and GET /status

site_id MUST be a permanent identifier stored in the customer's CMS. It must not change for a domain alias or domain rename. The status endpoint is not a public health check: authenticate the key and truthfully report capabilities and permission scopes.

```json
{
  "success": true,
  "data": {
    "site_id": "site_permanent_uuid",
    "cms": "laravel",
    "site_name": "نام سایت",
    "api_version": "1.0",
    "authenticated": true,
    "permissions": ["site.owner", "articles.read", "articles.create", "articles.update", "articles.publish", "products.read", "products.update"],
    "capabilities": {
      "article_fields": ["title", "slug", "content", "excerpt", "featured_image", "category", "status", "seo_title", "meta_description"],
      "product_fields": ["name", "description", "price", "stock", "status"],
      "optimistic_locking": true
    }
  }
}
```

For MegaShop use cms="megashop". site_id/site_name <=120 characters. authenticated MUST be boolean true; api_version exactly "1.0". article_fields and product_fields arrays MUST exist; product_fields may be empty. AdminJump requires a nonempty article_fields schema in v1, but disabled endpoints MUST NOT grant corresponding scopes. Schema names do not authorize writes. Scopes for actual content operations are separate from site.owner.

Advertise only supported fields. For read-only products set product_fields=[] and omit products.update. If no atomic version control exists, report optimistic_locking=false. No UI autosave is enabled.

## Endpoints and authorization

| Method | Path under /api-jump | Scope and semantics |
|---|---|---|
| GET | /status | Authenticated credential; site.owner required for AdminJump registration and use |
| GET | /articles | articles.read |
| GET | /articles/{id} | articles.read OR articles.update, tenant-scoped |
| POST | /articles | articles.create, plus articles.publish when published |
| PATCH | /articles/{id} | articles.update, plus articles.publish if existing or resulting state is published |
| GET | /products | products.read |
| GET | /products/{id} | products.read |
| PATCH | /products/{id} | products.update, supported fields only |

Each endpoint MUST resolve resources inside the key's tenant. ID alone is not authorization. IDs in JSON MUST be strings matching `[A-Za-z0-9_-]{1,120}`; cast numerical database IDs to strings. Return resource id, title/name and status for every item.

## Pagination and filters

Both list endpoints accept q (<=200 chars), status (draft/published/archived or empty), page (positive integer), per_page (1..100). AdminJump sends per_page=20. Compute filtered total and use stable order such as updated_at DESC, id DESC. Do not load the full dataset in memory. Return at most per_page items.

```json
{"success":true,"data":[{"id":"42","title":"عنوان مقاله","status":"draft","version":"7"}],"meta":{"total":1,"current_page":1,"per_page":20}}
```

## Minimal two-field draft integration

The current AdminJump UI uses only title and Markdown content. Its backend renders and sanitizes Markdown into HTML before sending to the CMS; no Markdown parsing is required in the CMS. For a minimal draft-only integration, advertise article_fields=["title","content"], permissions=["site.owner","articles.read","articles.create"], product_fields=[] and optimistic_locking=false. Accept POST title/content only, generate slug and authorship server-side, and always save a draft. Do not advertise article update, publish or product scopes before implementation. Existing broader protocol fields below are optional capabilities, not required UI features.

## Article schema, create and update

```json
{
  "title": "عنوان مقاله",
  "slug": "article-slug",
  "content": "<h2>تیتر</h2><p>متن امن</p>",
  "excerpt": "خلاصه",
  "featured_image": "https://example.com/images/article.jpg",
  "category": "راهنما",
  "status": "draft",
  "seo_title": "عنوان سئو",
  "meta_description": "توضیحات متا",
  "version": "7"
}
```

title required <=250; content required <=200000 and nonempty after sanitization. status required when advertised; enum draft/published/archived. Optional nullable fields: slug<=250 using Unicode letters/numbers/underscore/hyphen; excerpt<=1000; category<=120; seo_title<=250; meta_description<=500; featured_image HTTPS URL <=2000. Nullable optional fields must be clearable. Use existing CMS models, not a second content database.

category is a textual existing category in v1. If your model uses IDs, implement a real mapping or omit the field. featured_image is an existing URL, not a file upload. Upload endpoint is not available in this release.

POST returns HTTP 201 and the persisted resource. PATCH returns HTTP 200 and the persisted resource with new version. GET response includes content and all supported fields. published_at may be ISO8601. Single resource envelope: `{"success":true,"data":{...}}`.

HTML allowlist: p,br,strong,b,em,i,h2,h3,h4,ul,ol,li,blockquote,a[href|title],img[src|alt|width|height],table,thead,tbody,tr,th,td. URLs HTTPS only. Strip scripts, iframes, event handlers, arbitrary CSS and dangerous schemes with a maintained sanitizer; never rely on regex alone. CMS must sanitize at storage/render boundaries too.

## Product schema

```json
{"id":"24","name":"نام محصول","sku":"SKU-24","image":"https://example.com/p.jpg","description":"توضیحات متنی","price":185000,"currency":"تومان","stock":24,"status":"published","version":"3"}
```

name<=250, description plain text <=10000, price numeric 0..999999999999, stock integer 0..99999999, status enum draft/published/archived. Currency is the CMS's unit: do not silently convert rial/toman. PATCH changes only advertised fields and preserves other data. Variable products and bulk operations are excluded. Every successful response includes id/name/status, plus version when locking is supported.

## Versioning and conflict prevention

When optimistic_locking=true, GET returns a string version <=120 and PATCH requires it. Compare and update atomically using a conditional UPDATE or locked transaction. A mismatch returns HTTP 409 without changing any field. ALL other CMS mutation paths, including its own admin UI, must increment the same version.

AdminJump may fetch the existing article before PATCH to check publication permissions. CMS MUST also enforce those permissions atomically with the update to prevent races.

Legacy connectors do not retry writes automatically. The optional `article_sync` extension below now provides durable queued operations, fixed Idempotency-Key and safe retries; implement that entire extension before advertising the capability.

## Errors

401 invalid/expired/revoked key; 403 insufficient scope/owner; 404 tenant-scoped resource absent; 409 version conflict; 422 validation; 429 throttled; 503 unavailable. Always JSON, no SQL/trace/credentials.

```json
{"success":false,"error":{"code":"version_conflict","message":"Resource has changed","fields":{}}}
```

AdminJump maps error statuses to safe Persian messages and does not expose the raw remote error body. Generic remote 422 currently receives a generic message; keep validation rules aligned with this contract.

## Staged implementation and acceptance

1. Existing project audit; owner panel key issuance/revoke; Bearer middleware; stable tenant identity.
2. Authenticated status and truthful capabilities. Do not grant unimplemented content scopes.
3. Existing article model adapter, all four endpoints, pagination, sanitization, atomic versioning and tests.
4. Products read first, then proven field updates and versioning.
5. Test revoked/expired/nonowner keys, limited scopes, alias identity, tenant isolation, CRUD, 409 without mutation, HTML attack payloads, pagination, TLS and redirects on authorized staging.
6. In AdminJump: account → Add site → correct adapter → root HTTPS domain → owner key → verify and connect. Test only authorized sample writes; never claim live success from fakes.

No server deployment is implied by these instructions. A coding assistant must inspect the existing project, implement and run tests, and distinguish local/Fake tests from external verification.

## WordPress and FastShop status

FastShop based on Laravel can implement this contract as cms=laravel and use Laravel API; a dedicated FastShop adapter is planned. A WordPress plugin would need WordPress capability checks, REST permission_callback, post/WooCommerce field mapping, secure owner key management and versioning, plus an active AdminJump adapter that handles /wp-json namespacing. No ready plugin or active dedicated adapter is shipped. An Application Password alone does not implement this contract.

Official WordPress references:
- https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/
- https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/

## Article synchronization extension — implemented in local MegaShop

This extension is optional for other adapters. Advertise `capabilities.article_sync=true` only after ALL endpoints and durability/locking guarantees below are implemented. The current MegaShop advertises `article_fields=["title","content","status","featured_image"]`, `product_fields=[]`, `optimistic_locking=true`. Older keys remain read/create only; new owner keys grant articles.update/publish/delete as well. No production deployment or ready WordPress plugin is implied.

### Durable writes and images

POST /articles, PATCH /articles/{id} and DELETE /articles/{id} use a UUID `Idempotency-Key` header. Persist method/path/body hash and the committed result in the same database transaction as the mutation, scoped to a stable owner identity across key rotation. Same key and same request returns the original result, even after lost HTTP response; same key with different input is 409. Existing result has no expiry in local MegaShop. Do not remove deduplication results while clients might retry retained operations.

POST/PATCH accept only title (<=250), content (sanitized HTML <=100000), status draft/published, featured_image (CMS-owned media URL or null); PATCH also requires numeric version. DELETE body is {"version":"7"}; success is {"success":true,"data":{"id":"42","deleted":true,"version":"8"}}. Use atomic version comparison and locking shared with panel writes; mismatch 409 does not mutate data. Published article edits/unpublishing require articles.publish too. Delete requires articles.delete. Image/status fields are not silently enabled for legacy clients.

Every article response includes string id/title/content/status/version, boolean editable, nullable featured_image/published_at and optional read-only cms_metadata. MegaShop complex Page Builder articles are editable=false; do not flatten their sections. Snapshot original section data in read-only metadata without exposing secrets. A supported restore is a new write of the four managed fields; arbitrary CMS metadata and complex layouts are not restored by AdminJump.

POST /media accepts multipart `image`, JPG/PNG/WebP <=5MB, and UUID Idempotency-Key. Validate file signature and pixel count, decode/reencode JPEG, use content-hash storage, enforce create or update permission. Return {"success":true,"data":{"url":"https://cms.example/img/blog/adminjump/media/HASH.jpg"}}. Repeated media operations return the same URL; different bytes with the same key conflict. CMS owns media lifetime after publishing; do not delete a live image when AdminJump prunes history.

### Initial import and cursor feed

GET /articles?after_id=0&per_page=20 uses ascending numeric ID/keyset ordering, full article resources and meta.next_after (null when complete), plus the normal list metadata. A job imports one page at a time. Live changes while importing are replayed from cursor 0 afterward.

GET /changes?cursor=0&per_page=20 requires articles.read:

```json
{"success":true,"data":[{"cursor":"10","event_id":"a-uuid","article_id":"42","revision":8,"type":"updated","article":{"id":"42","title":"Title","content":"<p>Text</p>","status":"draft","version":"8","editable":true}}],"meta":{"cursor":"10","has_more":false}}
```

Cursor is a durable increasing journal ID, not a timestamp. Event type is created/updated/deleted. Tombstones must survive deletion. Event article is an immutable snapshot if available; it can be null for deleted or compacted events, in which case AdminJump reads the current resource or processes the tombstone. MegaShop retains five immutable payload files per article and keeps older journal metadata. Intermediate revisions older than that retention cannot be guaranteed after a long outage. Each article revision increases for every actual change including panel section edits and deletion. Content fingerprints exclude counters, timestamps and transient database section IDs to avoid fake history entries.

### Subscriptions and signed notifications

POST /subscriptions with authenticated owner/read key:

```json
{"consumer_id":"UUID_FROM_ADMINJUMP","callback_url":"https://adminjump.example/api/webhooks/cms/UUID_FROM_ADMINJUMP","secret":"64_lowercase_hex_characters"}
```

The CMS must pin callback origin to its configured AdminJump host, validate exact path/UUID, HTTPS/public DNS/IPs, disable proxy and redirects, pin resolved IP for TLS requests. Local opt-in permits exactly localhost:8000 in local/testing only. Secret is encrypted at rest, excluded from logs/public responses and distinct from the bearer API key. Refresh subscription binds the new key for the same owner; revoked or expired subscriptions cannot send.

In the same committed mutation boundary, record an outbox event and durable delivery records; dispatch notification AFTER commit. Retrying delivery must not block the panel. CMS observers plus reconciliation handle mass/query-builder changes and final section state.

Send POST callback with these headers:

```text
Content-Type: application/json
X-AdminJump-Timestamp: UNIX_SECONDS
X-AdminJump-Signature: lowercase_hex_HMAC_SHA256(secret, timestamp + "." + RAW_BODY)
```

```json
{"event_id":"EVENT_UUID","site_id":"FIXED_CMS_SITE_ID","article_id":"42","revision":8,"type":"updated"}
```

AdminJump accepts at most 8KB, checks the five-minute timestamp window, constant-time signature and fixed site identity, deduplicates event_id, commits a lightweight inbox then returns 202 and queues feed consumption. The webhook does not carry content or cause another write to the CMS.

### Local storage and operations

AdminJump lists cached metadata with no CMS call and no body-file read. Current content, queued Markdown/payload and five changed history snapshots are private files; only paths, status, title, revision, actor and timestamps are database metadata. Queue workers reauthorize membership and current CMS scopes before writing. History stores source (AdminJump user or CMS panel), archives supported images, and prunes exclusive images together with obsolete JSON while preserving shared images. Failed/conflicting drafts are retained.

The import button explicitly restarts a full background import; otherwise webhooks and five-minute cursor reconciliation keep data current. Cached scopes expire after ten minutes if background verification is unavailable. Read permission is never inferred from a browser-supplied role/site ID.

Run AdminJump workers for article-writes and article-sync,article-media plus scheduler; run MegaShop's dedicated adminjump_database worker for adminjump-webhooks and scheduler. See docs/ARTICLE_SYNC_OPERATIONS.md for exact commands, backup DB/files/APP_KEY together, cleanup and five-version limits. Configure APP_URL to the callback host. Local read/write transport exceptions are narrowly scoped to localhost:8008 and localhost:8000 and always disabled in production.
