# KSP API

Base URL: https://api.ronniri.com

KSP public catalogue and guest cart operations are live verified locally using the supplied Chrome-compatible HTTP transport. Search, full product data, regular/Eilat prices, stock, stores, worlds, brands, shops and promotions are exposed. Deployment-server access must be retested after redeploying. Account login, orders and checkout/payment remain unsupported.

## Authentication and credential choices

# RonNiri API: authentication and accounts

The **Copy to agent** button at `/docs` copies the complete Markdown reference: shared guidance and every service section. On `/docs/:service`, it copies that service's reference and its shared authentication guidance only. The full export is also available at `/docs/index.md`; individual exports use `/docs/:service.md`.

Base URL: `https://api.ronniri.com`. Local default: `http://localhost:3000`.

## Two ways to supply service credentials

Every service request needs gateway authorization: `X-API-Key: rni_...` or `Authorization: Bearer rni_...`. A browser signed in to the console can also call APIs with its HttpOnly session cookie and CSRF token for writes. Registration alone cannot access the service. Keys belong to approved users, including the admin. An optional deployment-wide `API_KEY` enables direct requests without creating a user account; treat it as a root secret. It cannot access any user's saved credentials or management endpoints.

1. **Inline credentials:** send provider credentials in a JSON POST body over HTTPS. They are used for the upstream operation and are not saved in MongoDB. Never put passwords or keys in query strings. No extra client-side encoding replaces HTTPS. Configure proxies and clients not to log request bodies or authorization headers.
2. **Saved credentials:** sign in, save credentials in the console, then send `useSavedCredentials: true` with a user API key. The server decrypts only the caller's service credentials. Passwords are never returned by the credential-list endpoint. Credentials are encrypted with AES-256-GCM, bound to the user and service. Back up the encryption key separately from the database.

### Inline PADI request

```http
POST /padi/certifications
X-API-Key: YOUR_GATEWAY_KEY
Content-Type: application/json

{"email":"you@example.com","password":"YOUR_PADI_PASSWORD","raw":false}
```

PADI uses `email` as the username field. Stateless calls clear their in-memory provider client after the response. To reuse login, POST the same email/password to `/padi/sessions`, then use its `sessionId` in `X-Session-Id` on GET `/padi/certifications`, GET `/padi/credentials`, GET `/padi/session`, POST `/padi/sessions/refresh`, and DELETE `/padi/sessions`.

### Saved PADI request

Save `{"email":"you@example.com","password":"..."}` with PUT `/auth/credentials/padi`, then:

```http
POST /padi/certifications
X-API-Key: YOUR_USER_KEY
Content-Type: application/json

{"useSavedCredentials":true,"raw":false}
```

The same flag works on POST `/padi/sessions`, POST `/padi/credentials`, POST `/lifesavingSociety/lookup` and POST `/piitel/login`. Do not mix saved and inline credential fields. Piitel login requires an existing Piitel `X-Session-Id`; create it with POST `/piitel/sessions` first.

## One-request wrapper

POST `/:service/request` accepts `method` (default GET), `path` (service-relative, no query string), `query` object, `body` object, and either `credentials` or `useSavedCredentials:true`. It dispatches only an existing service endpoint and enforces that endpoint's key permission. It cannot invoke account/admin routes or session/login routes. Query values follow the target endpoint's rules. Responses are wrapped in `{ "data": ... }`.

```json
{
  "method": "GET",
  "path": "/orders",
  "useSavedCredentials": true
}
```

Send this to `/piitel/request` with a key containing `piitel:account:login` and `piitel:orders:read`. For inline Piitel login replace the flag with `"credentials":{"username":"you@example.com","password":"..."}`. For PADI, POST `/padi/request` with `method:"POST"`, `path:"/certifications"`, credentials `{email,password}`, and optional `body:{raw:true}`. Lifesaving credentials are `{identifier}`; AliExpress buyer credentials are `{accessToken}`. KSP/Amazon account login is unsupported.

Provider sessions created by the wrapper are discarded after the operation by default. Set `keepSession:true` to return `sessionId` and `expiresAt` for follow-up cart/session operations; this additionally requires that service's `sessions:manage` permission. Set `sessionId` instead of credentials to reuse a session. Piitel supports `cartMode:"classic"` for bundles. Discarding a local session does not undo an upstream action: a Piitel account cart change persists at the store. Check state before retrying an uncertain mutation.

## Account lifecycle

On the first successful database initialization, the server creates admin username `admin`, generates a random password and prints it once to server logs. Sign in and change it before using APIs or creating keys. This is an ordinary user account with an additional admin role; its saved provider credentials are private to it. Startup does not reset an existing admin or reprint its password.

POST `/auth/register` with `{username,password}` creates a pending registration request, not an active user. Passwords must be 8–256 characters. Usernames are normalized to lowercase, 3–64 characters, using letters, digits, dots, dashes and underscores. The admin approves/rejects requests in the console. Pending and rejected applicants cannot sign in, save credentials, or create keys. There is no automatic approval. Admin can disable approved users; disabling invalidates access immediately. Re-enabling requires new login and keys.

POST `/auth/login` accepts `{username,password}` and sets an 8-hour HttpOnly, SameSite=Strict cookie (Secure in production). Response includes `user` and `csrfToken`. GET `/auth/me` returns the current user and CSRF token. All authenticated browser writes require `X-CSRF-Token`. Account management accepts browser sessions only; even a root API key cannot approve users or manage accounts.

PATCH `/auth/profile` accepts `currentPassword`, optional `username` and optional `password`. Changing profile requires the current password and invalidates existing browser sessions and API keys. Initial admin login requires a new password. POST `/auth/logout` invalidates the current browser session.

## API keys and permissions

POST `/auth/keys`: `{name,scopes,expiresAt?}` creates a key and returns the plaintext key exactly once. MongoDB stores only its SHA-256 hash and display prefix. GET `/auth/keys` lists metadata; DELETE `/auth/keys/:id` revokes one. Optional expiresAt is an ISO future timestamp. Maximum 50 unrevoked keys per user. Revocation, user disablement and password/profile changes are checked on every request.

GET `/auth/permissions` lists the current service permissions. Scope syntax is `service:resource:action`, for example `piitel:catalog:read`, `piitel:cart:write`, `padi:certifications:read`. A key may include several services and permissions. `root` grants all current and future service API permissions, **not** administrator account-management access. No scopes are granted by omission. A write permission does not imply read; select both if needed. Session creation/refresh/deletion requires `sessions:manage`. Checkout submission has its own `checkout:submit` permission. The wrapper checks its target and any login/session permission it needs. Each service documentation page lists the exact permission for every registered route.

GET `/capabilities` reports adapter support and verification state. Provider session IDs are random, scoped by service and owned by the authenticated user (or the legacy direct key). They cannot be reused across users. Users may use their own session across their own keys only if each key has the requested permission. Local provider sessions expire after 30 minutes idle and are lost on restart. Account data, keys and saved credentials persist in MongoDB.

## Saved credential management

GET `/auth/credentials` returns configured services and update timestamps, never plaintext secrets. PUT `/auth/credentials/:service` replaces credentials; DELETE removes them. Supported schemas:

| Service | JSON |
| --- | --- |
| padi | `{ "email": "...", "password": "..." }` |
| piitel | `{ "username": "...", "password": "..." }` |
| lifesavingSociety | `{ "identifier": "ABC123" }` |
| aliexpress | `{ "accessToken": "..." }` |

## Administrator endpoints

All require an admin browser session, completed initial password change, and CSRF on writes.

| Method | Endpoint | Effect |
| --- | --- | --- |
| GET | `/auth/admin/registrations` | Pending requests without password hashes |
| POST | `/auth/admin/registrations/:id/approve` | Explicitly approve; body `{}` |
| DELETE | `/auth/admin/registrations/:id` | Reject pending registration |
| GET | `/auth/admin/users` | Users without password hashes |
| PATCH | `/auth/admin/users/:id` | `{status:"disabled"}` or `{status:"approved"}`; cannot disable admin |

## Request security and errors

Production requires HTTPS, a configured PUBLIC_URL and the trusted Coolify reverse proxy. Do not expose the Node port directly to the internet. The app never logs request bodies, keys or saved provider passwords. The initial admin password is intentionally printed once as requested; rotate it immediately and restrict log access. Account passwords use salted scrypt (N=131072,r=8,p=1). Login/registration attempts are rate-limited, and concurrent password hashing is bounded.

Common errors: 400 INVALID_INPUT/JSON_REQUIRED, 401 UNAUTHORIZED/SESSION_EXPIRED, 403 FORBIDDEN/ADMIN_REQUIRED/CSRF_REQUIRED/SESSION_FORBIDDEN/PASSWORD_CHANGE_REQUIRED, 409 USERNAME_UNAVAILABLE/SESSION_BUSY, 429 RATE_LIMITED, 503 BUSY. Errors have `{code,error,details?}`. Provider errors preserve their existing codes. Keys and credentials belong in headers/JSON bodies, not URL query parameters. No live payment validation has been performed.


## Every registered endpoint

| Method | Path | Required key permission |
| --- | --- | --- |
| POST | `/ksp/sessions` | `ksp:sessions:manage` |
| DELETE | `/ksp/sessions` | `ksp:sessions:manage` |
| GET | `/ksp/cart` | `ksp:cart:read` |
| POST | `/ksp/cart/items` | `ksp:cart:write` |
| GET | `/ksp/autocomplete` | `ksp:catalog:read` |
| GET | `/ksp/search` | `ksp:catalog:read` |
| GET | `/ksp/products` | `ksp:catalog:read` |
| GET | `/ksp/products/:uin` | `ksp:catalog:read` |
| GET | `/ksp/products/:uin/price` | `ksp:catalog:read` |
| GET | `/ksp/products/:uin/stock` | `ksp:catalog:read` |
| GET | `/ksp/products/:uin/related` | `ksp:catalog:read` |
| GET | `/ksp/branches` | `ksp:catalog:read` |
| GET | `/ksp/branches/:id` | `ksp:catalog:read` |
| GET | `/ksp/stores` | `ksp:catalog:read` |
| GET | `/ksp/stores/:id` | `ksp:catalog:read` |
| GET | `/ksp/worlds/:id` | `ksp:catalog:read` |
| GET | `/ksp/brands/:id` | `ksp:catalog:read` |
| GET | `/ksp/shops/:slug` | `ksp:catalog:read` |
| GET | `/ksp/promotions` | `ksp:catalog:read` |
| DELETE | `/ksp/cart/items` | `ksp:cart:write` |
| PATCH | `/ksp/cart/items/:uin` | `ksp:cart:write` |
| DELETE | `/ksp/cart/items/:uin` | `ksp:cart:write` |
| POST | `/ksp/cart/branch` | `ksp:cart:write` |
| POST | `/ksp/cart/coupons` | `ksp:cart:write` |
| GET | `/ksp/cart/shipping` | `ksp:cart:read` |
| POST | `/ksp/request` | `Target operation permission` |

## KSP

The shared adapter integrates the supplied v0.3 catalogue API and its Chrome-compatible `curl_cffi` transport. No browser process is installed or kept running. Existing KSP cart routes remain in the same service and use isolated first-party cookie jars.

All routes require a gateway API key. Catalogue routes require `ksp:catalog:read`; cart routes use `ksp:cart:read` or `ksp:cart:write`, and sessions use `ksp:sessions:manage`.

| Method | Path | Input / result |
| --- | --- | --- |
| GET | `/ksp/search`, `/ksp/products` | `search` or `q`, or `category`; full search listing and original SSE partials |
| GET | `/ksp/autocomplete` | `q`; product/category/brand suggestions |
| GET | `/ksp/products/:uin` | Full product fields: price objects, Eilat price, specification/description, images, variants and related components when supplied upstream |
| GET | `/ksp/products/:uin/price` | `price`, `eilatPrice`, `hidePrice`, currency ILS, product ID and catalogue SKU |
| GET | `/ksp/products/:uin/stock` | Optional `sku`; otherwise resolves the product's `uinsql`. All returned branches with normalized availability and original fields |
| GET | `/ksp/products/:uin/related` | Complementary products |
| GET | `/ksp/stores`, `/ksp/branches` | Branch directory |
| GET | `/ksp/stores/:id`, `/ksp/branches/:id` | Branch details |
| GET | `/ksp/worlds/:id` | Category/world metadata; optional `select` tag path such as `31635..271` |
| GET | `/ksp/brands/:id` | Brand metadata/listing; upstream can reject nonexistent IDs |
| GET | `/ksp/shops/:slug` | Shop metadata; use a real upstream shop slug, not an assumed brand name |
| GET | `/ksp/promotions` | Current promotions page data |
| POST | `/ksp/sessions` | `{}`; returns private local session ID and cart |
| GET | `/ksp/cart` | `X-Session-Id`; cart groups and totals |
| POST | `/ksp/cart/items` | `{ "uin": 410052, "quantity": 1 }` and session header |
| PATCH | `/ksp/cart/items/:uin` | `{ "quantity": 2 }`; optional `compUin`, `idnt`, `branchId` for composite lines |
| DELETE | `/ksp/cart/items/:uin` | Optional composite-line fields as query parameters |
| DELETE | `/ksp/cart/items` | Clear this guest cart |
| POST | `/ksp/cart/branch` | `{ "branchId": 1 }`; selects branch context |
| POST | `/ksp/cart/coupons` | `{ "coupon": "...", "phone": "05..." }` |
| GET | `/ksp/cart/shipping` | Shipping groups for the selected branch |
| DELETE | `/ksp/sessions` | Delete local session; does not undo upstream cart changes |

Catalogue routes accept `lang=he` (default), `en`, `ru`, `ar` except the older related-products route. Translation depends on KSP. Search accepts `page` (default 1), `number` (1–100, default 20), `sort`, `category`, `min`, `max`, `minKg`, `maxKg`, `intersect`, `tags_size` (0–100), `tags_search`, `tags_parent_id`, `tags_page`, `anyway`, `exclude_auto`, and `disable_auto`. Category paths use numeric tags separated by `..`. Sort codes are preserved; their meanings have not been verified. Source pagination metadata is retained; do not assume the requested page size equals the returned page size.

```sh
curl 'https://api.ronniri.com/ksp/search?search=airpods&number=12&page=1' -H 'Accept: application/json' -H 'X-API-Key: YOUR_KEY'
curl 'https://api.ronniri.com/ksp/products/410052/price' -H 'Accept: application/json' -H 'X-API-Key: YOUR_KEY'
curl 'https://api.ronniri.com/ksp/products/410052/stock' -H 'Accept: application/json' -H 'X-API-Key: YOUR_KEY'
```

Search returns normalized source listing in `data`, untouched component envelopes in `partials`, and `done:true`. Product details expose normalized identity, full `data`, and raw `components`/`partials`. Catalogue responses include `source`, `fetchedAt` and `cached`; stores/autocomplete/promotions use `{data,source,fetchedAt,cached}`. Stock includes `checkedAt` based on its actual fetch time. Prices retain the upstream number/object format and are in ILS, not Piitel-style integer minor-unit strings. The `uin` in a product URL and internal `uinsql`/SKU differ; stock uses the latter.

Availability is `true`, `false` or `null` (unknown). A missing/failed required component or an incomplete SSE stream returns an explicit error; it never becomes an empty catalogue or false out-of-stock result. All upstream fields are retained when present. Description HTML is untrusted and must be sanitized before rendering.

Public catalogue data is cached for 60 seconds, bounded to 128 entries and 16 MiB of response payload. Requests are serialized with a 500 ms pause between completed upstream requests. Session-bound requests and all mutations bypass the cache. No stale-on-error fallback or automatic retries are used. Each network request has a 25-second timeout and 10 MiB response limit; TLS verification remains enabled and redirects are never followed. The Python helper accepts only KSP HTTPS paths and receives cookies/body through stdin, not command-line arguments.

The Dockerfile installs Python, a private virtual environment and pinned `curl_cffi`. Default `KSP_TRANSPORT=chrome`; `KSP_TRANSPORT=node` is available for diagnostics. For local development run `python -m pip install -r src/services/ksp/requirements.txt`. Set `KSP_PYTHON` to the interpreter where that dependency is installed when needed. Docker sets `/opt/ksp-http/bin/python`; do not override it with a Windows path in Coolify.

Live local verification on 2026-10-20: product 410052 returned all four requested components; price was ₪859 regular / ₪728 Eilat; AirPods search returned 84 matches and 12 rows; stock returned 75 branches; autocomplete, stores, promotions and world 271 returned successful responses. Guest cart create/read/add/update/remove passed, with totals matching quantity 1 and 2; the test item was removed and the cart confirmed empty. A sample brand request returned its original upstream status envelope, and an assumed shop slug returned 404; those successful catalogue flows remain unverified. Branch selection, shipping groups, coupon use, account login, orders and checkout/payment were not live-tested in this run. The latest transport still needs verification from the deployment server after redeployment; earlier Node-only deployment requests were blocked.
