From 431b707f273fb1dc7dfd4685bbf0063c6544c32a Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Mon, 10 Aug 2026 14:30:19 +0300 Subject: [PATCH 01/11] =?UTF-8?q?feat(mcp):=20foundation=20=E2=80=94=20con?= =?UTF-8?q?fig=20schema,=20core=20registry,=20ping,=20stdio=20+=20HTTP=20t?= =?UTF-8?q?ransports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zod-validated AppConfig + loader, ToolDef/ToolRegistry core, ping tool, server factory, stdio and streamable-HTTP entrypoints (loopback bind, session store, DNS-rebinding protection), request-context threading. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/.gitignore | 3 + mcp/README.md | 54 + mcp/package-lock.json | 3672 ++++++++++++++++++++++++ mcp/package.json | 31 + mcp/src/bin/http.ts | 10 + mcp/src/bin/stdio.test.ts | 23 + mcp/src/bin/stdio.ts | 19 + mcp/src/config/load.test.ts | 34 + mcp/src/config/load.ts | 7 + mcp/src/config/schema.ts | 32 + mcp/src/core/types.ts | 25 + mcp/src/registry/tool-registry.test.ts | 30 + mcp/src/registry/tool-registry.ts | 20 + mcp/src/sanity.test.ts | 7 + mcp/src/server/build-server.test.ts | 48 + mcp/src/server/build-server.ts | 33 + mcp/src/server/http-app.test.ts | 103 + mcp/src/server/http-app.ts | 112 + mcp/src/server/request-ctx.test.ts | 28 + mcp/src/server/request-ctx.ts | 18 + mcp/src/tools/index.ts | 9 + mcp/src/tools/ping.ts | 8 + mcp/tsconfig.json | 16 + mcp/vitest.config.ts | 10 + 24 files changed, 4352 insertions(+) create mode 100644 mcp/.gitignore create mode 100644 mcp/README.md create mode 100644 mcp/package-lock.json create mode 100644 mcp/package.json create mode 100644 mcp/src/bin/http.ts create mode 100644 mcp/src/bin/stdio.test.ts create mode 100644 mcp/src/bin/stdio.ts create mode 100644 mcp/src/config/load.test.ts create mode 100644 mcp/src/config/load.ts create mode 100644 mcp/src/config/schema.ts create mode 100644 mcp/src/core/types.ts create mode 100644 mcp/src/registry/tool-registry.test.ts create mode 100644 mcp/src/registry/tool-registry.ts create mode 100644 mcp/src/sanity.test.ts create mode 100644 mcp/src/server/build-server.test.ts create mode 100644 mcp/src/server/build-server.ts create mode 100644 mcp/src/server/http-app.test.ts create mode 100644 mcp/src/server/http-app.ts create mode 100644 mcp/src/server/request-ctx.test.ts create mode 100644 mcp/src/server/request-ctx.ts create mode 100644 mcp/src/tools/index.ts create mode 100644 mcp/src/tools/ping.ts create mode 100644 mcp/tsconfig.json create mode 100644 mcp/vitest.config.ts diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 0000000000..3c25e1e49c --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000000..bf896064dd --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,54 @@ +# ditto-mcp-server + +Extensible MCP server for Ditto knowledge and tools. P1 = foundation +(config, plugin registry, server factory, stdio + streamable-HTTP transports, +`ping` tool). See the design spec and plans under `docs/superpowers/`. + +## Requirements +- Node >= 22 + +## Develop + npm install + npm test # vitest + npm run typecheck # tsc --noEmit + npm run dev:stdio # run stdio transport + npm run dev:http # run streamable-HTTP transport on :3000/mcp + +## Configuration + +Optional JSON config via `DITTO_MCP_CONFIG=/path/to/config.json`. All fields have defaults; see `src/config/schema.ts`. + +### HTTP Server Options (`server.http`) + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `host` | `string` | `"127.0.0.1"` | Bind address (loopback by default) | +| `port` | `number` | `3000` | Port to listen on | +| `enableDnsRebindingProtection` | `boolean` | `true` | Enable DNS rebinding protection (rejects requests with invalid Host/Origin headers) | +| `allowedHosts` | `string[]?` | `undefined` | Allowed Host header values (e.g., `["mcp.example.com:3000"]`). When undefined and protection is enabled, a loopback allowlist is derived: `["127.0.0.1:port", "localhost:port", "[::1]:port", "host:port"]` | +| `allowedOrigins` | `string[]?` | `undefined` | Allowed Origin header values (optional) | + +### Remote Deployments + +When binding a **non-loopback** host (e.g., `0.0.0.0` or a public IP), you **MUST** set `allowedHosts` explicitly. The SDK matches the full `Host` header (e.g., `mcp.example.com:3000`), so include the exact `host:port` values your clients will send. Without explicit `allowedHosts`, DNS-rebinding protection will reject all remote requests with HTTP 403. + +Example config for remote deployment: +```json +{ + "server": { + "http": { + "host": "0.0.0.0", + "port": 3000, + "allowedHosts": ["mcp.example.com:3000"] + } + } +} +``` + +## Layout +- `src/core/` — shared types (`ToolDef`, `RequestCtx`) +- `src/registry/` — `ToolRegistry` +- `src/config/` — zod schema + loader +- `src/tools/` — tool implementations (`ping`) + wiring +- `src/server/` — `buildServer`, `createHttpApp` +- `src/bin/` — `stdio` and `http` entrypoints diff --git a/mcp/package-lock.json b/mcp/package-lock.json new file mode 100644 index 0000000000..8502411c69 --- /dev/null +++ b/mcp/package-lock.json @@ -0,0 +1,3672 @@ +{ + "name": "ditto-mcp-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ditto-mcp-server", + "version": "0.1.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1", + "express": "^4.21.2", + "zod": "^3.23.8" + }, + "bin": { + "ditto-mcp-http": "dist/bin/http.js", + "ditto-mcp-stdio": "dist/bin/stdio.js" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", + "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express-rate-limit/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.31", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", + "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 0000000000..c4565a0df5 --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,31 @@ +{ + "name": "ditto-mcp-server", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { "node": ">=22" }, + "bin": { + "ditto-mcp-stdio": "dist/bin/stdio.js", + "ditto-mcp-http": "dist/bin/http.js" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "dev:stdio": "tsx src/bin/stdio.ts", + "dev:http": "tsx src/bin/http.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1", + "express": "^4.21.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} diff --git a/mcp/src/bin/http.ts b/mcp/src/bin/http.ts new file mode 100644 index 0000000000..385bf394f7 --- /dev/null +++ b/mcp/src/bin/http.ts @@ -0,0 +1,10 @@ +import { loadConfig } from "../config/load.js"; +import { createHttpApp } from "../server/http-app.js"; + +const config = loadConfig(process.env.DITTO_MCP_CONFIG); +const app = createHttpApp(config); +app.listen(config.server.http.port, config.server.http.host, () => { + process.stderr.write( + `[ditto-mcp] http server listening on ${config.server.http.host}:${config.server.http.port}/mcp\n`, + ); +}); diff --git a/mcp/src/bin/stdio.test.ts b/mcp/src/bin/stdio.test.ts new file mode 100644 index 0000000000..fb4d9b4bfb --- /dev/null +++ b/mcp/src/bin/stdio.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const entry = resolve(here, "stdio.ts"); + +describe("stdio entrypoint (spawn e2e)", () => { + it("serves ping over stdio", async () => { + const transport = new StdioClientTransport({ + command: process.execPath, + args: ["--import", "tsx", entry], + }); + const client = new Client({ name: "stdio-test", version: "0.0.0" }); + await client.connect(transport); + const res = await client.callTool({ name: "ping", arguments: {} }); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0].text).toBe("pong"); + await client.close(); + }); +}); diff --git a/mcp/src/bin/stdio.ts b/mcp/src/bin/stdio.ts new file mode 100644 index 0000000000..c6b6538634 --- /dev/null +++ b/mcp/src/bin/stdio.ts @@ -0,0 +1,19 @@ +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { loadConfig } from "../config/load.js"; +import { registerTools } from "../tools/index.js"; +import { buildServer } from "../server/build-server.js"; + +async function main(): Promise { + const config = loadConfig(process.env.DITTO_MCP_CONFIG); + const registry = registerTools(config); + const server = buildServer(registry, config); + const transport = new StdioServerTransport(); + await server.connect(transport); + // Never write to stdout except MCP protocol frames; logs go to stderr. + process.stderr.write(`[ditto-mcp] stdio server ready: ${config.server.name}\n`); +} + +main().catch((err) => { + process.stderr.write(`[ditto-mcp] fatal: ${String(err)}\n`); + process.exit(1); +}); diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts new file mode 100644 index 0000000000..eb816cf58f --- /dev/null +++ b/mcp/src/config/load.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { writeFileSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { loadConfig } from "./load.js"; + +describe("loadConfig", () => { + it("returns defaults when no path is given", () => { + const cfg = loadConfig(); + expect(cfg.server.name).toBe("ditto-mcp"); + expect(cfg.server.http.port).toBe(3000); + expect(cfg.server.http.host).toBe("127.0.0.1"); + expect(cfg.server.http.enableDnsRebindingProtection).toBe(true); + expect(cfg.tools.ping).toBe(true); + }); + + it("merges values from a JSON file over defaults", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "config.json"); + writeFileSync(file, JSON.stringify({ server: { http: { port: 8080 } } })); + const cfg = loadConfig(file); + expect(cfg.server.http.port).toBe(8080); + expect(cfg.server.http.host).toBe("127.0.0.1"); + expect(cfg.server.http.enableDnsRebindingProtection).toBe(true); + expect(cfg.tools.ping).toBe(true); + }); + + it("throws on an invalid value", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "config.json"); + writeFileSync(file, JSON.stringify({ server: { http: { port: "nope" } } })); + expect(() => loadConfig(file)).toThrow(); + }); +}); diff --git a/mcp/src/config/load.ts b/mcp/src/config/load.ts new file mode 100644 index 0000000000..c375e0a1d1 --- /dev/null +++ b/mcp/src/config/load.ts @@ -0,0 +1,7 @@ +import { readFileSync } from "node:fs"; +import { AppConfigSchema, type AppConfig } from "./schema.js"; + +export function loadConfig(path?: string): AppConfig { + const raw: unknown = path ? JSON.parse(readFileSync(path, "utf8")) : {}; + return AppConfigSchema.parse(raw); +} diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts new file mode 100644 index 0000000000..44b9d01e63 --- /dev/null +++ b/mcp/src/config/schema.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +export const AppConfigSchema = z + .object({ + server: z + .object({ + name: z.string().default("ditto-mcp"), + http: z + .object({ + port: z.number().int().positive().default(3000), + host: z.string().default("127.0.0.1"), + enableDnsRebindingProtection: z.boolean().default(true), + allowedHosts: z.array(z.string()).optional(), + allowedOrigins: z.array(z.string()).optional(), + }) + .default({ + port: 3000, + host: "127.0.0.1", + enableDnsRebindingProtection: true, + }), + }) + .default({ + name: "ditto-mcp", + http: { port: 3000, host: "127.0.0.1", enableDnsRebindingProtection: true }, + }), + tools: z + .object({ ping: z.boolean().default(true) }) + .default({ ping: true }), + }) + .default({}); + +export type AppConfig = z.infer; diff --git a/mcp/src/core/types.ts b/mcp/src/core/types.ts new file mode 100644 index 0000000000..ac6c78c6e5 --- /dev/null +++ b/mcp/src/core/types.ts @@ -0,0 +1,25 @@ +import type { ZodRawShape } from "zod"; +import type { AppConfig } from "../config/schema.js"; + +export interface ToolResult { + content: Array<{ type: "text"; text: string }>; + [key: string]: unknown; +} + +export interface RequestCtx { + config: AppConfig; + /** Present for HTTP sessions; absent over stdio. */ + sessionId?: string; + /** HTTP request headers when available (used by later credential passthrough). */ + headers?: Record; + /** Abort signal for the in-flight request; cancels downstream work. */ + signal?: AbortSignal; +} + +export interface ToolDef { + name: string; + description: string; + /** A zod raw shape (object of zod validators); `{}` for no inputs. */ + inputSchema: ZodRawShape; + handler(args: unknown, ctx: RequestCtx): Promise; +} diff --git a/mcp/src/registry/tool-registry.test.ts b/mcp/src/registry/tool-registry.test.ts new file mode 100644 index 0000000000..ddae13e23d --- /dev/null +++ b/mcp/src/registry/tool-registry.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from "vitest"; +import { ToolRegistry } from "./tool-registry.js"; +import type { ToolDef } from "../core/types.js"; + +const makeTool = (name: string): ToolDef => ({ + name, + description: `tool ${name}`, + inputSchema: {}, + handler: async () => ({ content: [{ type: "text", text: name }] }), +}); + +describe("ToolRegistry", () => { + it("registers and retrieves a tool", () => { + const r = new ToolRegistry(); + r.register(makeTool("a")); + expect(r.get("a")?.name).toBe("a"); + expect(r.list().map((t) => t.name)).toEqual(["a"]); + }); + + it("throws on duplicate tool names", () => { + const r = new ToolRegistry(); + r.register(makeTool("a")); + expect(() => r.register(makeTool("a"))).toThrow(/duplicate/i); + }); + + it("returns undefined for unknown tools", () => { + const r = new ToolRegistry(); + expect(r.get("missing")).toBeUndefined(); + }); +}); diff --git a/mcp/src/registry/tool-registry.ts b/mcp/src/registry/tool-registry.ts new file mode 100644 index 0000000000..368615ca8e --- /dev/null +++ b/mcp/src/registry/tool-registry.ts @@ -0,0 +1,20 @@ +import type { ToolDef } from "../core/types.js"; + +export class ToolRegistry { + private readonly tools = new Map(); + + register(def: ToolDef): void { + if (this.tools.has(def.name)) { + throw new Error(`duplicate tool: ${def.name}`); + } + this.tools.set(def.name, def); + } + + list(): ToolDef[] { + return [...this.tools.values()]; + } + + get(name: string): ToolDef | undefined { + return this.tools.get(name); + } +} diff --git a/mcp/src/sanity.test.ts b/mcp/src/sanity.test.ts new file mode 100644 index 0000000000..ea99891018 --- /dev/null +++ b/mcp/src/sanity.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from "vitest"; + +describe("toolchain sanity", () => { + it("runs vitest with ESM", () => { + expect(1 + 1).toBe(2); + }); +}); diff --git a/mcp/src/server/build-server.test.ts b/mcp/src/server/build-server.test.ts new file mode 100644 index 0000000000..fad7cd6d41 --- /dev/null +++ b/mcp/src/server/build-server.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { registerTools } from "../tools/index.js"; +import { buildServer } from "./build-server.js"; + +async function connectedClient() { + const config = AppConfigSchema.parse({}); + const registry = registerTools(config); + const server = buildServer(registry, config); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + const client = new Client({ name: "test-client", version: "0.0.0" }); + await client.connect(clientTransport); + return client; +} + +describe("buildServer + ping (in-memory e2e)", () => { + it("lists the ping tool", async () => { + const client = await connectedClient(); + const { tools } = await client.listTools(); + expect(tools.map((t) => t.name)).toContain("ping"); + await client.close(); + }); + + it("calls ping and gets pong", async () => { + const client = await connectedClient(); + const res = await client.callTool({ name: "ping", arguments: {} }); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0]).toEqual({ type: "text", text: "pong" }); + await client.close(); + }); + + it("omits ping when disabled in config", async () => { + const config = AppConfigSchema.parse({ tools: { ping: false } }); + const registry = registerTools(config); + const server = buildServer(registry, config); + const [ct, st] = InMemoryTransport.createLinkedPair(); + await server.connect(st); + const client = new Client({ name: "t", version: "0" }); + await client.connect(ct); + const { tools } = await client.listTools(); + expect(tools.map((t) => t.name)).not.toContain("ping"); + await client.close(); + }); +}); diff --git a/mcp/src/server/build-server.ts b/mcp/src/server/build-server.ts new file mode 100644 index 0000000000..2037f236d1 --- /dev/null +++ b/mcp/src/server/build-server.ts @@ -0,0 +1,33 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolRegistry } from "../registry/tool-registry.js"; +import type { AppConfig } from "../config/schema.js"; +import { buildCtx } from "./request-ctx.js"; + +export function buildServer( + registry: ToolRegistry, + config: AppConfig, +): McpServer { + const server = new McpServer({ name: config.server.name, version: "0.1.0" }); + + // Initialize tool handlers even when registry is empty + // by registering and immediately disabling a placeholder. + // This ensures tools/list is always available (SDK 1.29.0 lazy-initializes handlers). + if (registry.list().length === 0) { + const placeholder = server.registerTool( + "_init", + { description: "Placeholder to initialize tool handlers" }, + async () => ({ content: [] }), + ); + placeholder.disable(); + } + + for (const def of registry.list()) { + server.registerTool( + def.name, + { description: def.description, inputSchema: def.inputSchema }, + async (args: unknown, extra: unknown) => + def.handler(args, buildCtx(config, extra as Parameters[1])), + ); + } + return server; +} diff --git a/mcp/src/server/http-app.test.ts b/mcp/src/server/http-app.test.ts new file mode 100644 index 0000000000..839e66fbfc --- /dev/null +++ b/mcp/src/server/http-app.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect } from "vitest"; +import { request } from "node:http"; +import type { Server } from "node:http"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { createHttpApp } from "./http-app.js"; + +async function listen(): Promise<{ server: Server; url: string }> { + // protocol e2e; DNS-rebinding protection exercised separately below. + const config = AppConfigSchema.parse({ + server: { http: { enableDnsRebindingProtection: false } }, + }); + const app = createHttpApp(config); + return await new Promise((res) => { + const server = app.listen(0, () => { + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + res({ server, url: `http://127.0.0.1:${port}/mcp` }); + }); + }); +} + +describe("streamable HTTP app (e2e)", () => { + it("serves ping over streamable HTTP with a session", async () => { + const { server, url } = await listen(); + const transport = new StreamableHTTPClientTransport(new URL(url)); + const client = new Client({ name: "http-test", version: "0.0.0" }); + await client.connect(transport); + const res = await client.callTool({ name: "ping", arguments: {} }); + const content = res.content as Array<{ type: string; text: string }>; + expect(content[0].text).toBe("pong"); + await client.close(); + await new Promise((r) => server.close(() => r())); + }); + + it("rejects prototype pollution attempts without crashing", async () => { + const { server, url } = await listen(); + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "mcp-session-id": "__proto__", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "ping", arguments: {} }, + }), + }); + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error.message).toBe("Bad Request: no valid session"); + await new Promise((r) => server.close(() => r())); + }); + + it("rejects DNS rebinding attacks by default", async () => { + // Default config has DNS-rebinding protection enabled + const config = AppConfigSchema.parse({}); + const app = createHttpApp(config); + const server = await new Promise((res) => { + const srv = app.listen(0, () => res(srv)); + }); + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + + const body = JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "attacker", version: "1.0" }, + }, + }); + + const statusCode = await new Promise((resolve) => { + const req = request( + { + hostname: "127.0.0.1", + port, + path: "/mcp", + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(body), + host: "attacker.example", + }, + }, + (res) => { + resolve(res.statusCode ?? 0); + }, + ); + req.write(body); + req.end(); + }); + + expect(statusCode).toBe(403); + await new Promise((r) => server.close(() => r())); + }); +}); diff --git a/mcp/src/server/http-app.ts b/mcp/src/server/http-app.ts new file mode 100644 index 0000000000..8f0f690773 --- /dev/null +++ b/mcp/src/server/http-app.ts @@ -0,0 +1,112 @@ +import express, { type Express, type Request, type Response } from "express"; +import { randomUUID } from "node:crypto"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; +import type { AppConfig } from "../config/schema.js"; +import { registerTools } from "../tools/index.js"; +import { buildServer } from "./build-server.js"; + +export function createHttpApp(config: AppConfig): Express { + const app = express(); + app.use(express.json()); + + const transports = new Map(); + + app.post("/mcp", async (req: Request, res: Response) => { + try { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + let transport: StreamableHTTPServerTransport | undefined = + sessionId ? transports.get(sessionId) : undefined; + + if (!transport) { + if (sessionId || !isInitializeRequest(req.body)) { + res.status(400).json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Bad Request: no valid session" }, + id: null, + }); + return; + } + const http = config.server.http; + const allowedHosts = http.allowedHosts ?? [ + `${http.host}:${http.port}`, + `127.0.0.1:${http.port}`, + `localhost:${http.port}`, + `[::1]:${http.port}`, + ]; + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sid) => { + transports.set(sid, transport as StreamableHTTPServerTransport); + }, + enableDnsRebindingProtection: http.enableDnsRebindingProtection, + allowedHosts: http.enableDnsRebindingProtection + ? allowedHosts + : undefined, + allowedOrigins: http.allowedOrigins, + }); + transport.onclose = () => { + if (transport?.sessionId) transports.delete(transport.sessionId); + }; + const registry = registerTools(config); + const server = buildServer(registry, config); + await server.connect(transport); + } + + await transport.handleRequest(req, res, req.body); + } catch (error) { + process.stderr.write( + `[ditto-mcp] POST /mcp error: ${error instanceof Error ? error.message : String(error)}\n`, + ); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal error" }, + id: null, + }); + } + } + }); + + const handleSession = async (req: Request, res: Response) => { + try { + const sessionId = req.headers["mcp-session-id"] as string | undefined; + const transport = sessionId ? transports.get(sessionId) : undefined; + if (!transport) { + res.status(400).send("Invalid or missing session ID"); + return; + } + await transport.handleRequest(req, res); + } catch (error) { + process.stderr.write( + `[ditto-mcp] ${req.method} /mcp error: ${error instanceof Error ? error.message : String(error)}\n`, + ); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal error" }, + id: null, + }); + } + } + }; + + app.get("/mcp", handleSession); + app.delete("/mcp", handleSession); + + const http = config.server.http; + const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]); + if ( + http.enableDnsRebindingProtection && + !http.allowedHosts && + !LOOPBACK.has(http.host) + ) { + process.stderr.write( + `[ditto-mcp] WARNING: host '${http.host}' is non-loopback but server.http.allowedHosts is not set; ` + + `DNS-rebinding protection will 403 remote requests whose Host header is not in the derived loopback allowlist. ` + + `Set server.http.allowedHosts explicitly for remote deployments.\n`, + ); + } + + return app; +} diff --git a/mcp/src/server/request-ctx.test.ts b/mcp/src/server/request-ctx.test.ts new file mode 100644 index 0000000000..b1160d3de6 --- /dev/null +++ b/mcp/src/server/request-ctx.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { AppConfigSchema } from "../config/schema.js"; +import { buildCtx } from "./request-ctx.js"; + +describe("buildCtx", () => { + const config = AppConfigSchema.parse({}); + + it("maps sessionId, headers, and signal from extra", () => { + const controller = new AbortController(); + const ctx = buildCtx(config, { + sessionId: "sess-1", + requestInfo: { headers: { "x-test": "yes" } }, + signal: controller.signal, + }); + expect(ctx.config).toBe(config); + expect(ctx.sessionId).toBe("sess-1"); + expect(ctx.headers).toEqual({ "x-test": "yes" }); + expect(ctx.signal).toBe(controller.signal); + }); + + it("tolerates a minimal extra (stdio has no headers/session)", () => { + const ctx = buildCtx(config, {}); + expect(ctx.config).toBe(config); + expect(ctx.sessionId).toBeUndefined(); + expect(ctx.headers).toBeUndefined(); + expect(ctx.signal).toBeUndefined(); + }); +}); diff --git a/mcp/src/server/request-ctx.ts b/mcp/src/server/request-ctx.ts new file mode 100644 index 0000000000..89b8453349 --- /dev/null +++ b/mcp/src/server/request-ctx.ts @@ -0,0 +1,18 @@ +import type { AppConfig } from "../config/schema.js"; +import type { RequestCtx } from "../core/types.js"; + +/** Minimal structural view of the SDK's tool-handler `extra` argument. */ +export interface McpExtra { + sessionId?: string; + requestInfo?: { headers?: Record }; + signal?: AbortSignal; +} + +export function buildCtx(config: AppConfig, extra: McpExtra): RequestCtx { + return { + config, + sessionId: extra.sessionId, + headers: extra.requestInfo?.headers, + signal: extra.signal, + }; +} diff --git a/mcp/src/tools/index.ts b/mcp/src/tools/index.ts new file mode 100644 index 0000000000..0273ef584f --- /dev/null +++ b/mcp/src/tools/index.ts @@ -0,0 +1,9 @@ +import { ToolRegistry } from "../registry/tool-registry.js"; +import type { AppConfig } from "../config/schema.js"; +import { pingTool } from "./ping.js"; + +export function registerTools(config: AppConfig): ToolRegistry { + const registry = new ToolRegistry(); + if (config.tools.ping) registry.register(pingTool); + return registry; +} diff --git a/mcp/src/tools/ping.ts b/mcp/src/tools/ping.ts new file mode 100644 index 0000000000..47d0fabedf --- /dev/null +++ b/mcp/src/tools/ping.ts @@ -0,0 +1,8 @@ +import type { ToolDef } from "../core/types.js"; + +export const pingTool: ToolDef = { + name: "ping", + description: "Health check; returns pong", + inputSchema: {}, + handler: async () => ({ content: [{ type: "text", text: "pong" }] }), +}; diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json new file mode 100644 index 0000000000..622598a3eb --- /dev/null +++ b/mcp/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src"], + "exclude": ["dist", "node_modules", "**/*.test.ts"] +} diff --git a/mcp/vitest.config.ts b/mcp/vitest.config.ts new file mode 100644 index 0000000000..02817dfc34 --- /dev/null +++ b/mcp/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: false, + environment: "node", + include: ["src/**/*.test.ts"], + testTimeout: 20000, + }, +}); From e2ebb3941b99722e4bfd6d753c739868acecc4a1 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Mon, 10 Aug 2026 14:30:19 +0300 Subject: [PATCH 02/11] =?UTF-8?q?feat(mcp):=20knowledge=20core=20=E2=80=94?= =?UTF-8?q?=20FTS5=20keyword=20retrieval=20over=20Ditto=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chunk/KnowledgeSource/Retriever types, markdown chunker, SQLite FTS5 retriever, PublicSource llms.txt loader, KnowledgeService + search/get_chunk tools wired via config, shared async service across sessions. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/README.md | 36 +- mcp/package-lock.json | 421 ++++++++++++++++++++ mcp/package.json | 6 +- mcp/src/bin/http.ts | 4 +- mcp/src/bin/stdio.test.ts | 12 + mcp/src/bin/stdio.ts | 4 +- mcp/src/config/load.test.ts | 4 + mcp/src/config/schema.ts | 17 + mcp/src/knowledge/build.test.ts | 83 ++++ mcp/src/knowledge/build.ts | 39 ++ mcp/src/knowledge/chunker.test.ts | 39 ++ mcp/src/knowledge/chunker.ts | 61 +++ mcp/src/knowledge/fts-retriever.test.ts | 53 +++ mcp/src/knowledge/fts-retriever.ts | 62 +++ mcp/src/knowledge/knowledge-service.test.ts | 39 ++ mcp/src/knowledge/knowledge-service.ts | 26 ++ mcp/src/knowledge/public-source.test.ts | 59 +++ mcp/src/knowledge/public-source.ts | 86 ++++ mcp/src/knowledge/types.ts | 20 + mcp/src/server/build-server.test.ts | 9 +- mcp/src/server/http-app.test.ts | 5 +- mcp/src/server/http-app.ts | 8 +- mcp/src/tools/index.test.ts | 63 +++ mcp/src/tools/index.ts | 11 +- mcp/src/tools/knowledge.test.ts | 48 +++ mcp/src/tools/knowledge.ts | 43 ++ 26 files changed, 1247 insertions(+), 11 deletions(-) create mode 100644 mcp/src/knowledge/build.test.ts create mode 100644 mcp/src/knowledge/build.ts create mode 100644 mcp/src/knowledge/chunker.test.ts create mode 100644 mcp/src/knowledge/chunker.ts create mode 100644 mcp/src/knowledge/fts-retriever.test.ts create mode 100644 mcp/src/knowledge/fts-retriever.ts create mode 100644 mcp/src/knowledge/knowledge-service.test.ts create mode 100644 mcp/src/knowledge/knowledge-service.ts create mode 100644 mcp/src/knowledge/public-source.test.ts create mode 100644 mcp/src/knowledge/public-source.ts create mode 100644 mcp/src/knowledge/types.ts create mode 100644 mcp/src/tools/index.test.ts create mode 100644 mcp/src/tools/knowledge.test.ts create mode 100644 mcp/src/tools/knowledge.ts diff --git a/mcp/README.md b/mcp/README.md index bf896064dd..d153687026 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -2,7 +2,9 @@ Extensible MCP server for Ditto knowledge and tools. P1 = foundation (config, plugin registry, server factory, stdio + streamable-HTTP transports, -`ping` tool). See the design spec and plans under `docs/superpowers/`. +`ping` tool). P2a = knowledge: `search` and `get_chunk` tools backed by a +pluggable `KnowledgeSource` → `Retriever` core. See the design spec and plans +under `docs/superpowers/`. ## Requirements - Node >= 22 @@ -18,6 +20,33 @@ Extensible MCP server for Ditto knowledge and tools. P1 = foundation Optional JSON config via `DITTO_MCP_CONFIG=/path/to/config.json`. All fields have defaults; see `src/config/schema.ts`. +### Knowledge (P2a) + +The server exposes `search` and `get_chunk` tools backed by a pluggable +`KnowledgeSource` → `Retriever` core. P2a ships with `PublicSource` (Ditto +`llms.txt`) and a SQLite FTS5 keyword retriever. + +**On startup**, the server fetches the public docs (configurable with +`knowledge.publicSource.url` and `knowledge.publicSource.maxDocs`). To disable +this network fetch, set `knowledge.enabled=false` or +`knowledge.publicSource.enabled=false`. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.enabled` | `boolean` | `true` | Enable knowledge tools (`search`, `get_chunk`) | +| `knowledge.publicSource.enabled` | `boolean` | `true` | Enable PublicSource (Ditto `llms.txt`) | +| `knowledge.publicSource.url` | `string` | `"https://eclipse.dev/ditto/llms.txt"` | URL to the `llms.txt` index | +| `knowledge.publicSource.maxDocs` | `number?` | `undefined` | Optional limit on the number of docs to fetch | + +Example config to disable knowledge: +```json +{ + "knowledge": { + "enabled": false + } +} +``` + ### HTTP Server Options (`server.http`) | Field | Type | Default | Description | @@ -49,6 +78,9 @@ Example config for remote deployment: - `src/core/` — shared types (`ToolDef`, `RequestCtx`) - `src/registry/` — `ToolRegistry` - `src/config/` — zod schema + loader -- `src/tools/` — tool implementations (`ping`) + wiring +- `src/tools/` — tool implementations (`ping`, knowledge tools) + wiring +- `src/knowledge/` — corpus/retrieval core (`KnowledgeSource`, `Retriever`, `PublicSource`, `FtsRetriever`) - `src/server/` — `buildServer`, `createHttpApp` - `src/bin/` — `stdio` and `http` entrypoints + +Dependencies: `@modelcontextprotocol/sdk`, `express`, `zod`, `better-sqlite3` diff --git a/mcp/package-lock.json b/mcp/package-lock.json index 8502411c69..7889ac7ec1 100644 --- a/mcp/package-lock.json +++ b/mcp/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@modelcontextprotocol/sdk": "^1", + "better-sqlite3": "^11.10.0", "express": "^4.21.2", "zod": "^3.23.8" }, @@ -17,6 +18,7 @@ "ditto-mcp-stdio": "dist/bin/stdio.js" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/express": "^4.17.21", "@types/node": "^22.10.0", "tsx": "^4.19.2", @@ -1193,6 +1195,16 @@ "win32" ] }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1493,6 +1505,57 @@ "node": ">=12" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", @@ -1532,6 +1595,30 @@ "node": ">= 0.8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1607,6 +1694,12 @@ "node": ">= 16" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1683,6 +1776,21 @@ "ms": "2.0.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -1693,6 +1801,15 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1712,6 +1829,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1741,6 +1867,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1866,6 +2001,15 @@ "node": ">=18.0.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1987,6 +2131,12 @@ ], "license": "BSD-3-Clause" }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -2023,6 +2173,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2084,6 +2240,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2162,12 +2324,38 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -2305,6 +2493,33 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2330,6 +2545,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -2339,6 +2560,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2467,6 +2700,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2480,6 +2740,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -2536,6 +2806,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2665,6 +2964,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -2816,6 +3127,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2849,6 +3205,52 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -2921,6 +3323,18 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -2964,6 +3378,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2988,6 +3408,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/mcp/package.json b/mcp/package.json index c4565a0df5..8101ad2e7a 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "private": true, "type": "module", - "engines": { "node": ">=22" }, + "engines": { + "node": ">=22" + }, "bin": { "ditto-mcp-stdio": "dist/bin/stdio.js", "ditto-mcp-http": "dist/bin/http.js" @@ -18,10 +20,12 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1", + "better-sqlite3": "^11.10.0", "express": "^4.21.2", "zod": "^3.23.8" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/express": "^4.17.21", "@types/node": "^22.10.0", "tsx": "^4.19.2", diff --git a/mcp/src/bin/http.ts b/mcp/src/bin/http.ts index 385bf394f7..5e6c535703 100644 --- a/mcp/src/bin/http.ts +++ b/mcp/src/bin/http.ts @@ -1,8 +1,10 @@ import { loadConfig } from "../config/load.js"; import { createHttpApp } from "../server/http-app.js"; +import { buildKnowledgeService } from "../knowledge/build.js"; const config = loadConfig(process.env.DITTO_MCP_CONFIG); -const app = createHttpApp(config); +const knowledge = await buildKnowledgeService(config); +const app = createHttpApp(config, knowledge); app.listen(config.server.http.port, config.server.http.host, () => { process.stderr.write( `[ditto-mcp] http server listening on ${config.server.http.host}:${config.server.http.port}/mcp\n`, diff --git a/mcp/src/bin/stdio.test.ts b/mcp/src/bin/stdio.test.ts index fb4d9b4bfb..4b68873bbd 100644 --- a/mcp/src/bin/stdio.test.ts +++ b/mcp/src/bin/stdio.test.ts @@ -3,15 +3,27 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; +import { writeFileSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; const here = dirname(fileURLToPath(import.meta.url)); const entry = resolve(here, "stdio.ts"); describe("stdio entrypoint (spawn e2e)", () => { it("serves ping over stdio", async () => { + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const cfgPath = join(dir, "config.json"); + writeFileSync(cfgPath, JSON.stringify({ knowledge: { enabled: false } })); + const transport = new StdioClientTransport({ command: process.execPath, args: ["--import", "tsx", entry], + env: { + ...process.env, + DITTO_MCP_CONFIG: cfgPath, + }, }); const client = new Client({ name: "stdio-test", version: "0.0.0" }); await client.connect(transport); diff --git a/mcp/src/bin/stdio.ts b/mcp/src/bin/stdio.ts index c6b6538634..505fe18d97 100644 --- a/mcp/src/bin/stdio.ts +++ b/mcp/src/bin/stdio.ts @@ -2,10 +2,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { loadConfig } from "../config/load.js"; import { registerTools } from "../tools/index.js"; import { buildServer } from "../server/build-server.js"; +import { buildKnowledgeService } from "../knowledge/build.js"; async function main(): Promise { const config = loadConfig(process.env.DITTO_MCP_CONFIG); - const registry = registerTools(config); + const knowledge = await buildKnowledgeService(config); + const registry = registerTools(config, knowledge); const server = buildServer(registry, config); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts index eb816cf58f..7cd48f635d 100644 --- a/mcp/src/config/load.test.ts +++ b/mcp/src/config/load.test.ts @@ -12,6 +12,10 @@ describe("loadConfig", () => { expect(cfg.server.http.host).toBe("127.0.0.1"); expect(cfg.server.http.enableDnsRebindingProtection).toBe(true); expect(cfg.tools.ping).toBe(true); + expect(cfg.knowledge.enabled).toBe(true); + expect(cfg.knowledge.publicSource.url).toBe( + "https://eclipse.dev/ditto/llms.txt", + ); }); it("merges values from a JSON file over defaults", () => { diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index 44b9d01e63..80a332ea1f 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -26,6 +26,23 @@ export const AppConfigSchema = z tools: z .object({ ping: z.boolean().default(true) }) .default({ ping: true }), + knowledge: z + .object({ + enabled: z.boolean().default(true), + publicSource: z + .object({ + enabled: z.boolean().default(true), + url: z + .string() + .default("https://eclipse.dev/ditto/llms.txt"), + maxDocs: z.number().int().positive().optional(), + }) + .default({ enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }), + }) + .default({ + enabled: true, + publicSource: { enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }, + }), }) .default({}); diff --git a/mcp/src/knowledge/build.test.ts b/mcp/src/knowledge/build.test.ts new file mode 100644 index 0000000000..88cf1e6399 --- /dev/null +++ b/mcp/src/knowledge/build.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { buildKnowledgeService } from "./build.js"; +import { AppConfigSchema } from "../config/schema.js"; +import type { FetchFn } from "./public-source.js"; +import type { KnowledgeService } from "./knowledge-service.js"; + +let service: KnowledgeService | undefined; +afterEach(() => { + service = undefined; +}); + +const fakeIndex = ` +# Ditto Docs +- [Things](thing.md) +`; + +const fakeDoc = ` +# Things +A thing is a digital twin of a physical device. +`; + +const fakeFetch: FetchFn = async (url) => { + if (url.endsWith("index.md")) return fakeIndex; + if (url.endsWith("thing.md")) return fakeDoc; + throw new Error(`unexpected fetch: ${url}`); +}; + +const failFetch: FetchFn = async () => { + throw new Error("fetch failed"); +}; + +describe("buildKnowledgeService", () => { + it("returns undefined when knowledge disabled", async () => { + const config = AppConfigSchema.parse({ knowledge: { enabled: false } }); + const result = await buildKnowledgeService(config, { + fetchFn: fakeFetch, + }); + expect(result).toBeUndefined(); + }); + + it("returns undefined when no sources enabled", async () => { + const config = AppConfigSchema.parse({ + knowledge: { enabled: true, publicSource: { enabled: false } }, + }); + const result = await buildKnowledgeService(config, { + fetchFn: fakeFetch, + }); + expect(result).toBeUndefined(); + }); + + it("gracefully degrades on init failure (returns undefined, logs)", async () => { + const config = AppConfigSchema.parse({ + knowledge: { + enabled: true, + publicSource: { + enabled: true, + url: "http://example.com/index.md", + }, + }, + }); + const result = await buildKnowledgeService(config, { + fetchFn: failFetch, + }); + expect(result).toBeUndefined(); + }); + + it("builds and inits a working service with fake fetch", async () => { + const config = AppConfigSchema.parse({ + knowledge: { + enabled: true, + publicSource: { + enabled: true, + url: "http://example.com/index.md", + }, + }, + }); + service = await buildKnowledgeService(config, { fetchFn: fakeFetch }); + expect(service).toBeDefined(); + const hits = await service!.search("digital twin", 5); + expect(hits.length).toBeGreaterThan(0); + expect(hits[0].text).toContain("digital twin"); + }); +}); diff --git a/mcp/src/knowledge/build.ts b/mcp/src/knowledge/build.ts new file mode 100644 index 0000000000..197c335400 --- /dev/null +++ b/mcp/src/knowledge/build.ts @@ -0,0 +1,39 @@ +import type { AppConfig } from "../config/schema.js"; +import type { KnowledgeSource } from "./types.js"; +import { KnowledgeService } from "./knowledge-service.js"; +import { FtsRetriever } from "./fts-retriever.js"; +import { PublicSource, type FetchFn } from "./public-source.js"; + +export interface KnowledgeDeps { + fetchFn?: FetchFn; +} + +/** Build + init the knowledge service once, or return undefined if disabled/unavailable. + * Never throws: on init failure it logs a warning and returns undefined (graceful degradation). */ +export async function buildKnowledgeService( + config: AppConfig, + deps: KnowledgeDeps = {}, +): Promise { + if (!config.knowledge.enabled) return undefined; + const sources: KnowledgeSource[] = []; + if (config.knowledge.publicSource.enabled) { + sources.push( + new PublicSource({ + url: config.knowledge.publicSource.url, + maxDocs: config.knowledge.publicSource.maxDocs, + fetchFn: deps.fetchFn, + }), + ); + } + if (sources.length === 0) return undefined; + const service = new KnowledgeService(sources, new FtsRetriever()); + try { + await service.init(); + } catch (err) { + process.stderr.write( + `[ditto-mcp] knowledge init failed, disabling knowledge tools: ${String(err)}\n`, + ); + return undefined; + } + return service; +} diff --git a/mcp/src/knowledge/chunker.test.ts b/mcp/src/knowledge/chunker.test.ts new file mode 100644 index 0000000000..5eacf8e6ea --- /dev/null +++ b/mcp/src/knowledge/chunker.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { chunkMarkdown } from "./chunker.js"; + +const meta = { source: "doc", title: "Doc", cite: "https://x/doc" }; + +describe("chunkMarkdown", () => { + it("returns one chunk for short input, with stable id and metadata", () => { + const chunks = chunkMarkdown("# Title\n\nShort body.", meta); + expect(chunks).toHaveLength(1); + expect(chunks[0].id).toBe("doc#0"); + expect(chunks[0].source).toBe("doc"); + expect(chunks[0].title).toBe("Doc"); + expect(chunks[0].cite).toBe("https://x/doc"); + expect(chunks[0].text).toContain("Short body."); + }); + + it("splits long input into multiple size-bounded chunks with sequential ids", () => { + const long = "para. ".repeat(600); // ~3600 chars + const chunks = chunkMarkdown(long, { ...meta, maxChars: 1000, overlap: 100 }); + expect(chunks.length).toBeGreaterThan(1); + chunks.forEach((c, i) => expect(c.id).toBe(`doc#${i}`)); + chunks.forEach((c) => expect(c.text.length).toBeLessThanOrEqual(1000)); + }); + + it("is deterministic (same input -> identical chunks)", () => { + const a = chunkMarkdown("# H\n\n" + "word ".repeat(500), meta); + const b = chunkMarkdown("# H\n\n" + "word ".repeat(500), meta); + expect(a.length).toBeGreaterThan(1); + expect(a).toEqual(b); + }); + + it("returns no chunks for empty/whitespace input", () => { + expect(chunkMarkdown(" \n\n ", meta)).toEqual([]); + }); + + it("throws when overlap >= maxChars", () => { + expect(() => chunkMarkdown("x".repeat(50), { ...meta, maxChars: 100, overlap: 100 })).toThrow(/overlap/); + }); +}); diff --git a/mcp/src/knowledge/chunker.ts b/mcp/src/knowledge/chunker.ts new file mode 100644 index 0000000000..3ba065934f --- /dev/null +++ b/mcp/src/knowledge/chunker.ts @@ -0,0 +1,61 @@ +import type { Chunk } from "./types.js"; + +export interface ChunkOptions { + source: string; + title: string; + cite: string; + maxChars?: number; + overlap?: number; +} + +/** + * Split markdown into size-bounded chunks. Paragraphs (blank-line separated) + * are packed into windows of at most `maxChars`; a paragraph longer than + * `maxChars` is hard-split with `overlap` characters carried between pieces. + * Deterministic: same input yields identical chunks with ids `${source}#${n}`. + */ +export function chunkMarkdown(md: string, opts: ChunkOptions): Chunk[] { + const maxChars = opts.maxChars ?? 1000; + const overlap = opts.overlap ?? 150; + if (overlap < 0) throw new Error(`overlap must be >= 0 (got ${overlap})`); + if (overlap >= maxChars) { + throw new Error(`overlap (${overlap}) must be less than maxChars (${maxChars})`); + } + const paragraphs = md + .split(/\n\s*\n/) + .map((p) => p.trim()) + .filter((p) => p.length > 0); + + const pieces: string[] = []; + let buf = ""; + const flush = () => { + if (buf.trim().length > 0) pieces.push(buf.trim()); + buf = ""; + }; + + for (const para of paragraphs) { + if (para.length > maxChars) { + flush(); + let start = 0; + while (start < para.length) { + const end = Math.min(start + maxChars, para.length); + pieces.push(para.slice(start, end).trim()); + if (end >= para.length) break; + start = end - overlap; + if (start < 0) start = 0; + } + continue; + } + if (buf.length + para.length + 2 > maxChars) flush(); + buf = buf.length === 0 ? para : `${buf}\n\n${para}`; + } + flush(); + + return pieces.map((text, n) => ({ + id: `${opts.source}#${n}`, + source: opts.source, + title: opts.title, + text, + cite: opts.cite, + })); +} diff --git a/mcp/src/knowledge/fts-retriever.test.ts b/mcp/src/knowledge/fts-retriever.test.ts new file mode 100644 index 0000000000..4d2123ea6e --- /dev/null +++ b/mcp/src/knowledge/fts-retriever.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { FtsRetriever } from "./fts-retriever.js"; +import type { Chunk } from "./types.js"; + +const chunk = (id: string, text: string): Chunk => ({ + id, + source: "s", + title: "T", + text, + cite: `https://x/${id}`, +}); + +let r: FtsRetriever; +afterEach(() => r?.close()); + +describe("FtsRetriever", () => { + it("finds chunks by keyword, best match first", async () => { + r = new FtsRetriever(); + await r.add([ + chunk("a", "The Netty leak causes an out of memory crash on reconnect"), + chunk("b", "Policies define access control for things"), + chunk("c", "Connectivity manages MQTT and Kafka connections"), + ]); + const hits = await r.search("memory crash", 5); + expect(hits.length).toBeGreaterThanOrEqual(1); + expect(hits[0].id).toBe("a"); + }); + + it("respects the k limit", async () => { + r = new FtsRetriever(); + await r.add([chunk("a", "alpha token"), chunk("b", "alpha token"), chunk("c", "alpha token")]); + expect(await r.search("alpha", 2)).toHaveLength(2); + }); + + it("returns [] for a query with no matches", async () => { + r = new FtsRetriever(); + await r.add([chunk("a", "hello world")]); + expect(await r.search("nonexistentterm", 5)).toEqual([]); + }); + + it("does not throw on FTS-special characters in the query", async () => { + r = new FtsRetriever(); + await r.add([chunk("a", "quotes and parens matter")]); + await expect(r.search('"(quotes) AND *', 5)).resolves.not.toThrow(); + }); + + it("getChunk returns the stored chunk or undefined", async () => { + r = new FtsRetriever(); + await r.add([chunk("a", "hello")]); + expect((await r.getChunk("a"))?.text).toBe("hello"); + expect(await r.getChunk("missing")).toBeUndefined(); + }); +}); diff --git a/mcp/src/knowledge/fts-retriever.ts b/mcp/src/knowledge/fts-retriever.ts new file mode 100644 index 0000000000..1e7c44c146 --- /dev/null +++ b/mcp/src/knowledge/fts-retriever.ts @@ -0,0 +1,62 @@ +import Database from "better-sqlite3"; +import type { Chunk, Retriever } from "./types.js"; + +export class FtsRetriever implements Retriever { + private readonly db: Database.Database; + private readonly chunks = new Map(); + + constructor() { + this.db = new Database(":memory:"); + this.db.exec( + "CREATE VIRTUAL TABLE chunks USING fts5(id UNINDEXED, title, text);", + ); + } + + async add(chunks: Chunk[]): Promise { + const insert = this.db.prepare( + "INSERT INTO chunks (id, title, text) VALUES (?, ?, ?)", + ); + const tx = this.db.transaction((rows: Chunk[]) => { + for (const c of rows) { + insert.run(c.id, c.title, c.text); + this.chunks.set(c.id, c); + } + }); + tx(chunks); + } + + async search(query: string, k: number): Promise { + const match = toMatchQuery(query); + if (match === "") return []; + const rows = this.db + .prepare( + "SELECT id FROM chunks WHERE chunks MATCH ? ORDER BY rank, id LIMIT ?", + ) + .all(match, k) as Array<{ id: string }>; + const out: Chunk[] = []; + for (const row of rows) { + const c = this.chunks.get(row.id); + if (c) out.push(c); + } + return out; + } + + async getChunk(id: string): Promise { + return this.chunks.get(id); + } + + close(): void { + this.db.close(); + } +} + +/** + * Turn arbitrary user text into a safe FTS5 MATCH expression: extract + * word tokens, quote each as a phrase, join with OR for recall. Returns + * "" when there are no usable tokens (caller returns no results). + */ +function toMatchQuery(query: string): string { + const tokens = query.match(/[\p{L}\p{N}]+/gu); + if (!tokens || tokens.length === 0) return ""; + return tokens.map((t) => `"${t}"`).join(" OR "); +} diff --git a/mcp/src/knowledge/knowledge-service.test.ts b/mcp/src/knowledge/knowledge-service.test.ts new file mode 100644 index 0000000000..e9ca86f850 --- /dev/null +++ b/mcp/src/knowledge/knowledge-service.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { KnowledgeService } from "./knowledge-service.js"; +import { FtsRetriever } from "./fts-retriever.js"; +import type { KnowledgeSource, Chunk } from "./types.js"; + +const source = (id: string, chunks: Chunk[]): KnowledgeSource => ({ + id, + loadChunks: async () => chunks, +}); + +const chunk = (id: string, text: string): Chunk => ({ + id, source: "s", title: "T", text, cite: `https://x/${id}`, +}); + +let r: FtsRetriever; +afterEach(() => r?.close()); + +describe("KnowledgeService", () => { + it("indexes all sources on init and searches across them", async () => { + r = new FtsRetriever(); + const svc = new KnowledgeService( + [source("s1", [chunk("a", "reconnect memory crash")]), + source("s2", [chunk("b", "policy access control")])], + r, + ); + await svc.init(); + expect((await svc.search("memory", 5)).map((c) => c.id)).toContain("a"); + expect((await svc.search("policy", 5)).map((c) => c.id)).toContain("b"); + expect((await svc.getChunk("a"))?.text).toBe("reconnect memory crash"); + }); + + it("init is idempotent (does not double-index)", async () => { + r = new FtsRetriever(); + const svc = new KnowledgeService([source("s1", [chunk("a", "alpha")])], r); + await svc.init(); + await svc.init(); + expect(await svc.search("alpha", 10)).toHaveLength(1); + }); +}); diff --git a/mcp/src/knowledge/knowledge-service.ts b/mcp/src/knowledge/knowledge-service.ts new file mode 100644 index 0000000000..bb0fd2c3d6 --- /dev/null +++ b/mcp/src/knowledge/knowledge-service.ts @@ -0,0 +1,26 @@ +import type { Chunk, KnowledgeSource, Retriever } from "./types.js"; + +export class KnowledgeService { + private initialized = false; + constructor( + private readonly sources: KnowledgeSource[], + private readonly retriever: Retriever, + ) {} + + async init(signal?: AbortSignal): Promise { + if (this.initialized) return; + for (const source of this.sources) { + const chunks = await source.loadChunks(signal); + await this.retriever.add(chunks); + } + this.initialized = true; + } + + async search(query: string, k: number): Promise { + return await this.retriever.search(query, k); + } + + async getChunk(id: string): Promise { + return await this.retriever.getChunk(id); + } +} diff --git a/mcp/src/knowledge/public-source.test.ts b/mcp/src/knowledge/public-source.test.ts new file mode 100644 index 0000000000..06eef26929 --- /dev/null +++ b/mcp/src/knowledge/public-source.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from "vitest"; +import { PublicSource } from "./public-source.js"; + +const INDEX = `# Ditto docs +## Core +- [Things](https://eclipse.dev/ditto/things.md): about things +- [Policies](https://eclipse.dev/ditto/policies.md): about policies +Some prose that is not a link. +`; + +const DOCS: Record = { + "https://eclipse.dev/ditto/llms.txt": INDEX, + "https://eclipse.dev/ditto/things.md": "# Things\n\nA thing is a digital twin.", + "https://eclipse.dev/ditto/policies.md": "# Policies\n\nPolicies control access.", +}; + +const fakeFetch = async (url: string): Promise => { + if (!(url in DOCS)) throw new Error(`404 ${url}`); + return DOCS[url]; +}; + +describe("PublicSource", () => { + it("parses the index and chunks each linked document", async () => { + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: fakeFetch, + }); + const chunks = await src.loadChunks(); + expect(src.id).toBe("public"); + const cites = new Set(chunks.map((c) => c.cite)); + expect(cites.has("https://eclipse.dev/ditto/things.md")).toBe(true); + expect(cites.has("https://eclipse.dev/ditto/policies.md")).toBe(true); + expect(chunks.some((c) => c.text.includes("digital twin"))).toBe(true); + expect(chunks.every((c) => c.source === "public")).toBe(true); + }); + + it("honors maxDocs", async () => { + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: fakeFetch, + maxDocs: 1, + }); + const chunks = await src.loadChunks(); + expect(new Set(chunks.map((c) => c.cite)).size).toBe(1); + }); + + it("skips documents that fail to fetch instead of throwing", async () => { + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: async (u) => { + if (u.endsWith("policies.md")) throw new Error("boom"); + return fakeFetch(u); + }, + }); + const chunks = await src.loadChunks(); + expect(chunks.some((c) => c.cite.endsWith("things.md"))).toBe(true); + expect(chunks.some((c) => c.cite.endsWith("policies.md"))).toBe(false); + }); +}); diff --git a/mcp/src/knowledge/public-source.ts b/mcp/src/knowledge/public-source.ts new file mode 100644 index 0000000000..4f7238a4f0 --- /dev/null +++ b/mcp/src/knowledge/public-source.ts @@ -0,0 +1,86 @@ +import type { Chunk, KnowledgeSource } from "./types.js"; +import { chunkMarkdown } from "./chunker.js"; + +export type FetchFn = (url: string, signal?: AbortSignal) => Promise; + +export interface PublicSourceOptions { + url: string; + fetchFn?: FetchFn; + maxDocs?: number; + chunkOptions?: { maxChars?: number; overlap?: number }; +} + +interface Entry { + title: string; + url: string; +} + +const defaultFetch: FetchFn = async (url, signal) => { + const timeout = AbortSignal.timeout(15000); + const sig = signal ? AbortSignal.any([signal, timeout]) : timeout; + const res = await fetch(url, { signal: sig }); + if (!res.ok) throw new Error(`fetch ${url} -> ${res.status}`); + const text = await res.text(); + if (text.length > 5_000_000) { + throw new Error(`fetch ${url} -> response too large (${text.length} bytes)`); + } + return text; +}; + +export class PublicSource implements KnowledgeSource { + readonly id = "public"; + private readonly opts: PublicSourceOptions; + private readonly fetchFn: FetchFn; + + constructor(opts: PublicSourceOptions) { + this.opts = opts; + this.fetchFn = opts.fetchFn ?? defaultFetch; + } + + async loadChunks(signal?: AbortSignal): Promise { + const index = await this.fetchFn(this.opts.url, signal); + let entries = parseEntries(index, this.opts.url); + if (this.opts.maxDocs !== undefined) { + entries = entries.slice(0, this.opts.maxDocs); + } + const chunks: Chunk[] = []; + for (const entry of entries) { + let md: string; + try { + md = await this.fetchFn(entry.url, signal); + } catch (err) { + process.stderr.write( + `[ditto-mcp] public-source: skipping ${entry.url}: ${String(err)}\n`, + ); + continue; + } + chunks.push( + ...chunkMarkdown(md, { + source: this.id, + title: entry.title, + cite: entry.url, + maxChars: this.opts.chunkOptions?.maxChars, + overlap: this.opts.chunkOptions?.overlap, + }), + ); + } + // Re-key ids to be unique across documents (chunker numbers per-doc). + return chunks.map((c, i) => ({ ...c, id: `${this.id}#${i}` })); + } +} + +function parseEntries(index: string, baseUrl: string): Entry[] { + const re = /- \[([^\]]+)\]\(([^)]+)\)/g; + const entries: Entry[] = []; + let m: RegExpExecArray | null; + while ((m = re.exec(index)) !== null) { + const title = m[1].trim(); + const resolved = new URL(m[2].trim(), baseUrl); + // Guard against SSRF: only allow http/https schemes + if (resolved.protocol !== "http:" && resolved.protocol !== "https:") { + continue; + } + entries.push({ title, url: resolved.toString() }); + } + return entries; +} diff --git a/mcp/src/knowledge/types.ts b/mcp/src/knowledge/types.ts new file mode 100644 index 0000000000..d8d7057f31 --- /dev/null +++ b/mcp/src/knowledge/types.ts @@ -0,0 +1,20 @@ +export interface Chunk { + id: string; + source: string; + title: string; + text: string; + cite: string; +} + +/** A corpus provider: yields chunks. Does not search. */ +export interface KnowledgeSource { + id: string; + loadChunks(signal?: AbortSignal): Promise; +} + +/** Indexes chunks and searches them. */ +export interface Retriever { + add(chunks: Chunk[]): Promise; + search(query: string, k: number): Promise; + getChunk(id: string): Promise; +} diff --git a/mcp/src/server/build-server.test.ts b/mcp/src/server/build-server.test.ts index fad7cd6d41..b77842dad0 100644 --- a/mcp/src/server/build-server.test.ts +++ b/mcp/src/server/build-server.test.ts @@ -6,7 +6,8 @@ import { registerTools } from "../tools/index.js"; import { buildServer } from "./build-server.js"; async function connectedClient() { - const config = AppConfigSchema.parse({}); + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const config = AppConfigSchema.parse({ knowledge: { enabled: false } }); const registry = registerTools(config); const server = buildServer(registry, config); const [clientTransport, serverTransport] = @@ -34,7 +35,11 @@ describe("buildServer + ping (in-memory e2e)", () => { }); it("omits ping when disabled in config", async () => { - const config = AppConfigSchema.parse({ tools: { ping: false } }); + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const config = AppConfigSchema.parse({ + tools: { ping: false }, + knowledge: { enabled: false }, + }); const registry = registerTools(config); const server = buildServer(registry, config); const [ct, st] = InMemoryTransport.createLinkedPair(); diff --git a/mcp/src/server/http-app.test.ts b/mcp/src/server/http-app.test.ts index 839e66fbfc..79031b9833 100644 --- a/mcp/src/server/http-app.test.ts +++ b/mcp/src/server/http-app.test.ts @@ -8,8 +8,10 @@ import { createHttpApp } from "./http-app.js"; async function listen(): Promise<{ server: Server; url: string }> { // protocol e2e; DNS-rebinding protection exercised separately below. + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. const config = AppConfigSchema.parse({ server: { http: { enableDnsRebindingProtection: false } }, + knowledge: { enabled: false }, }); const app = createHttpApp(config); return await new Promise((res) => { @@ -57,7 +59,8 @@ describe("streamable HTTP app (e2e)", () => { it("rejects DNS rebinding attacks by default", async () => { // Default config has DNS-rebinding protection enabled - const config = AppConfigSchema.parse({}); + // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. + const config = AppConfigSchema.parse({ knowledge: { enabled: false } }); const app = createHttpApp(config); const server = await new Promise((res) => { const srv = app.listen(0, () => res(srv)); diff --git a/mcp/src/server/http-app.ts b/mcp/src/server/http-app.ts index 8f0f690773..fcb04f133b 100644 --- a/mcp/src/server/http-app.ts +++ b/mcp/src/server/http-app.ts @@ -5,8 +5,12 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import type { AppConfig } from "../config/schema.js"; import { registerTools } from "../tools/index.js"; import { buildServer } from "./build-server.js"; +import type { KnowledgeService } from "../knowledge/knowledge-service.js"; -export function createHttpApp(config: AppConfig): Express { +export function createHttpApp( + config: AppConfig, + knowledgeService?: KnowledgeService, +): Express { const app = express(); app.use(express.json()); @@ -48,7 +52,7 @@ export function createHttpApp(config: AppConfig): Express { transport.onclose = () => { if (transport?.sessionId) transports.delete(transport.sessionId); }; - const registry = registerTools(config); + const registry = registerTools(config, knowledgeService); const server = buildServer(registry, config); await server.connect(transport); } diff --git a/mcp/src/tools/index.test.ts b/mcp/src/tools/index.test.ts new file mode 100644 index 0000000000..15d994d7e1 --- /dev/null +++ b/mcp/src/tools/index.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { registerTools } from "./index.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { KnowledgeService } from "../knowledge/knowledge-service.js"; +import { FtsRetriever } from "../knowledge/fts-retriever.js"; +import type { KnowledgeSource, Chunk } from "../knowledge/types.js"; + +const chunk = (id: string, text: string): Chunk => ({ + id, + source: "test", + title: "T", + text, + cite: `https://x/${id}`, +}); + +const fakeSource = (chunks: Chunk[]): KnowledgeSource => ({ + id: "fake", + loadChunks: async () => chunks, +}); + +let retriever: FtsRetriever | undefined; +afterEach(() => retriever?.close()); + +describe("registerTools wiring", () => { + it("registers ping by default", () => { + const reg = registerTools( + AppConfigSchema.parse({ knowledge: { enabled: false } }), + ); + expect(reg.get("ping")).toBeDefined(); + }); + + it("omits knowledge tools when knowledge disabled", () => { + const reg = registerTools( + AppConfigSchema.parse({ knowledge: { enabled: false } }), + ); + expect(reg.get("search")).toBeUndefined(); + expect(reg.get("get_chunk")).toBeUndefined(); + }); + + it("omits knowledge tools when service not provided", () => { + const reg = registerTools( + AppConfigSchema.parse({ knowledge: { enabled: true } }), + ); + expect(reg.get("search")).toBeUndefined(); + expect(reg.get("get_chunk")).toBeUndefined(); + }); + + it("registers knowledge tools when enabled and service provided", async () => { + retriever = new FtsRetriever(); + const service = new KnowledgeService( + [fakeSource([chunk("a", "test content")])], + retriever, + ); + await service.init(); + const reg = registerTools( + AppConfigSchema.parse({ knowledge: { enabled: true } }), + service, + ); + expect(reg.get("ping")).toBeDefined(); + expect(reg.get("search")).toBeDefined(); + expect(reg.get("get_chunk")).toBeDefined(); + }); +}); diff --git a/mcp/src/tools/index.ts b/mcp/src/tools/index.ts index 0273ef584f..9668977fc4 100644 --- a/mcp/src/tools/index.ts +++ b/mcp/src/tools/index.ts @@ -1,9 +1,18 @@ import { ToolRegistry } from "../registry/tool-registry.js"; import type { AppConfig } from "../config/schema.js"; import { pingTool } from "./ping.js"; +import { makeKnowledgeTools } from "./knowledge.js"; +import type { KnowledgeService } from "../knowledge/knowledge-service.js"; -export function registerTools(config: AppConfig): ToolRegistry { +export function registerTools( + config: AppConfig, + knowledgeService?: KnowledgeService, +): ToolRegistry { const registry = new ToolRegistry(); if (config.tools.ping) registry.register(pingTool); + if (config.knowledge.enabled && knowledgeService) { + for (const tool of makeKnowledgeTools(knowledgeService)) + registry.register(tool); + } return registry; } diff --git a/mcp/src/tools/knowledge.test.ts b/mcp/src/tools/knowledge.test.ts new file mode 100644 index 0000000000..6eafa484ea --- /dev/null +++ b/mcp/src/tools/knowledge.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { KnowledgeService } from "../knowledge/knowledge-service.js"; +import { FtsRetriever } from "../knowledge/fts-retriever.js"; +import { makeKnowledgeTools } from "./knowledge.js"; +import { AppConfigSchema } from "../config/schema.js"; +import type { KnowledgeSource } from "../knowledge/types.js"; + +const src: KnowledgeSource = { + id: "s", + loadChunks: async () => [ + { id: "a", source: "s", title: "Things", text: "a thing is a digital twin", cite: "https://x/a" }, + ], +}; + +const ctx = { config: AppConfigSchema.parse({}) }; +let r: FtsRetriever; +afterEach(() => r?.close()); + +async function tools() { + r = new FtsRetriever(); + const svc = new KnowledgeService([src], r); + await svc.init(); + return Object.fromEntries(makeKnowledgeTools(svc).map((t) => [t.name, t])); +} + +describe("knowledge tools", () => { + it("search returns matching chunk text with citation", async () => { + const t = await tools(); + const res = await t.search.handler({ query: "digital twin" }, ctx); + const text = res.content.map((p) => p.text).join("\n"); + expect(text).toContain("digital twin"); + expect(text).toContain("https://x/a"); + }); + + it("search reports no results cleanly", async () => { + const t = await tools(); + const res = await t.search.handler({ query: "zzzznotfound" }, ctx); + expect(res.content[0].text.toLowerCase()).toContain("no results"); + }); + + it("get_chunk returns the chunk by id, or a not-found message", async () => { + const t = await tools(); + const ok = await t.get_chunk.handler({ id: "a" }, ctx); + expect(ok.content[0].text).toContain("digital twin"); + const miss = await t.get_chunk.handler({ id: "nope" }, ctx); + expect(miss.content[0].text.toLowerCase()).toContain("not found"); + }); +}); diff --git a/mcp/src/tools/knowledge.ts b/mcp/src/tools/knowledge.ts new file mode 100644 index 0000000000..da4c736d35 --- /dev/null +++ b/mcp/src/tools/knowledge.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; +import type { ToolDef, ToolResult } from "../core/types.js"; +import type { Chunk } from "../knowledge/types.js"; +import type { KnowledgeService } from "../knowledge/knowledge-service.js"; + +function formatChunk(c: Chunk): string { + return `## ${c.title}\n${c.text}\n\n[source: ${c.cite} · id: ${c.id}]`; +} + +function textResult(text: string): ToolResult { + return { content: [{ type: "text", text }] }; +} + +export function makeKnowledgeTools(service: KnowledgeService): ToolDef[] { + const search: ToolDef = { + name: "search", + description: + "Search the Ditto knowledge base and return the most relevant documentation excerpts.", + inputSchema: { + query: z.string(), + k: z.number().int().positive().max(20).optional(), + }, + handler: async (args: unknown): Promise => { + const { query, k } = args as { query: string; k?: number }; + const hits = await service.search(query, k ?? 5); + if (hits.length === 0) return textResult(`No results for "${query}".`); + return textResult(hits.map(formatChunk).join("\n\n---\n\n")); + }, + }; + + const getChunk: ToolDef = { + name: "get_chunk", + description: "Fetch a single knowledge chunk by its id.", + inputSchema: { id: z.string() }, + handler: async (args: unknown): Promise => { + const { id } = args as { id: string }; + const c = await service.getChunk(id); + return textResult(c ? formatChunk(c) : `Chunk "${id}" not found.`); + }, + }; + + return [search, getChunk]; +} From 9dab958e283efafb491ab2bca4b8ec3ae3b49f32 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Mon, 10 Aug 2026 14:30:19 +0300 Subject: [PATCH 03/11] =?UTF-8?q?feat(mcp):=20semantic=20+=20hybrid=20retr?= =?UTF-8?q?ieval=20=E2=80=94=20bge=20embeddings,=20sqlite-vec,=20RRF=20fus?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local ONNX bge-small embeddings, sqlite-vec store + VectorRetriever, HybridRetriever (reciprocal rank fusion), LocalDirSource corpus, config-selectable fts/vector/hybrid, lazy ONNX load, markdown-only ingest, bounded-concurrency fetch, batched embeddings (OOM fix), limit param + provenance. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/README.md | 76 +- mcp/package-lock.json | 1036 ++++++++++++++++++- mcp/package.json | 2 + mcp/src/config/load.test.ts | 5 + mcp/src/config/schema.ts | 21 + mcp/src/knowledge/build.test.ts | 57 +- mcp/src/knowledge/build.ts | 49 +- mcp/src/knowledge/embedding.itest.ts | 19 + mcp/src/knowledge/embedding.test.ts | 20 + mcp/src/knowledge/embedding.ts | 72 ++ mcp/src/knowledge/fts-retriever.test.ts | 8 +- mcp/src/knowledge/fts-retriever.ts | 9 +- mcp/src/knowledge/hybrid-retriever.test.ts | 62 ++ mcp/src/knowledge/hybrid-retriever.ts | 49 + mcp/src/knowledge/knowledge-service.test.ts | 4 +- mcp/src/knowledge/knowledge-service.ts | 4 +- mcp/src/knowledge/local-dir-source.test.ts | 33 + mcp/src/knowledge/local-dir-source.ts | 58 ++ mcp/src/knowledge/public-source.test.ts | 19 + mcp/src/knowledge/public-source.ts | 52 +- mcp/src/knowledge/sqlite-vec-store.test.ts | 46 + mcp/src/knowledge/sqlite-vec-store.ts | 50 + mcp/src/knowledge/types.ts | 8 +- mcp/src/knowledge/vector-retriever.test.ts | 52 + mcp/src/knowledge/vector-retriever.ts | 37 + mcp/src/knowledge/vector-store.ts | 15 + mcp/src/tools/knowledge.test.ts | 3 +- mcp/src/tools/knowledge.ts | 50 +- mcp/vitest.config.ts | 2 +- 29 files changed, 1860 insertions(+), 58 deletions(-) create mode 100644 mcp/src/knowledge/embedding.itest.ts create mode 100644 mcp/src/knowledge/embedding.test.ts create mode 100644 mcp/src/knowledge/embedding.ts create mode 100644 mcp/src/knowledge/hybrid-retriever.test.ts create mode 100644 mcp/src/knowledge/hybrid-retriever.ts create mode 100644 mcp/src/knowledge/local-dir-source.test.ts create mode 100644 mcp/src/knowledge/local-dir-source.ts create mode 100644 mcp/src/knowledge/sqlite-vec-store.test.ts create mode 100644 mcp/src/knowledge/sqlite-vec-store.ts create mode 100644 mcp/src/knowledge/vector-retriever.test.ts create mode 100644 mcp/src/knowledge/vector-retriever.ts create mode 100644 mcp/src/knowledge/vector-store.ts diff --git a/mcp/README.md b/mcp/README.md index d153687026..610a32b660 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -20,16 +20,32 @@ under `docs/superpowers/`. Optional JSON config via `DITTO_MCP_CONFIG=/path/to/config.json`. All fields have defaults; see `src/config/schema.ts`. -### Knowledge (P2a) +### Knowledge The server exposes `search` and `get_chunk` tools backed by a pluggable -`KnowledgeSource` → `Retriever` core. P2a ships with `PublicSource` (Ditto -`llms.txt`) and a SQLite FTS5 keyword retriever. +`KnowledgeSource` → `Retriever` core. You can index the public Ditto docs +(`llms.txt`), a local markdown directory, or both, and choose from three +retriever modes: keyword FTS (default), semantic vector search, or hybrid (RRF +fusion of both). -**On startup**, the server fetches the public docs (configurable with -`knowledge.publicSource.url` and `knowledge.publicSource.maxDocs`). To disable -this network fetch, set `knowledge.enabled=false` or -`knowledge.publicSource.enabled=false`. +**On startup**, the server fetches the public docs and/or indexes the local +directory (if enabled). To disable this, set `knowledge.enabled=false` or +disable individual sources. + +#### Retriever Modes + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.retriever` | `"fts" \| "vector" \| "hybrid"` | `"fts"` | Retriever mode: `fts` = SQLite FTS5 keyword search; `vector` = semantic vector search; `hybrid` = RRF fusion of FTS + vector | + +**Default (`fts`)**: fast keyword search, no model download, works offline. + +**Vector / Hybrid**: embed the entire corpus in memory at server startup and +download the BGE embedding model (~80MB, ONNX) on first run unless +`allowRemoteModels: false` + `modelPath` are set. The default `fts` mode loads +no embedding stack. See `embedding` config below. + +#### Sources | Field | Type | Default | Description | |-------|------|---------|-------------| @@ -37,8 +53,50 @@ this network fetch, set `knowledge.enabled=false` or | `knowledge.publicSource.enabled` | `boolean` | `true` | Enable PublicSource (Ditto `llms.txt`) | | `knowledge.publicSource.url` | `string` | `"https://eclipse.dev/ditto/llms.txt"` | URL to the `llms.txt` index | | `knowledge.publicSource.maxDocs` | `number?` | `undefined` | Optional limit on the number of docs to fetch | +| `knowledge.localDir.enabled` | `boolean` | `false` | Enable LocalDirSource (index a local markdown directory) | +| `knowledge.localDir.path` | `string?` | `undefined` | Path to a local directory containing `.md` files | +| `knowledge.localDir.id` | `string` | `"local"` | Source ID for local chunks | + +#### Embedding Config (for `vector` / `hybrid`) + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.embedding.model` | `string` | `"Xenova/bge-small-en-v1.5"` | Hugging Face model ID | +| `knowledge.embedding.dim` | `number` | `384` | Embedding dimension (must match the model) | +| `knowledge.embedding.modelPath` | `string?` | `undefined` | Local path to the ONNX model (offline mode) | +| `knowledge.embedding.allowRemoteModels` | `boolean` | `true` | Allow model download from Hugging Face | +| `knowledge.embedding.cacheDir` | `string?` | `undefined` | Custom cache directory for downloaded models | + +**Offline vector search**: set `allowRemoteModels: false` and provide +`modelPath` pointing to a pre-downloaded ONNX model directory. + +#### Examples + +**Hybrid retriever + local dir:** +```json +{ + "knowledge": { + "retriever": "hybrid", + "publicSource": { "enabled": true }, + "localDir": { "enabled": true, "path": "/home/user/my-docs" } + } +} +``` + +**Offline vector search:** +```json +{ + "knowledge": { + "retriever": "vector", + "embedding": { + "allowRemoteModels": false, + "modelPath": "/opt/models/bge-small-en-v1.5" + } + } +} +``` -Example config to disable knowledge: +**Disable knowledge:** ```json { "knowledge": { @@ -83,4 +141,4 @@ Example config for remote deployment: - `src/server/` — `buildServer`, `createHttpApp` - `src/bin/` — `stdio` and `http` entrypoints -Dependencies: `@modelcontextprotocol/sdk`, `express`, `zod`, `better-sqlite3` +Dependencies: `@modelcontextprotocol/sdk`, `express`, `zod`, `better-sqlite3`, `@huggingface/transformers`, `sqlite-vec` diff --git a/mcp/package-lock.json b/mcp/package-lock.json index 7889ac7ec1..80a3938d48 100644 --- a/mcp/package-lock.json +++ b/mcp/package-lock.json @@ -8,9 +8,11 @@ "name": "ditto-mcp-server", "version": "0.1.0", "dependencies": { + "@huggingface/transformers": "^3.8.1", "@modelcontextprotocol/sdk": "^1", "better-sqlite3": "^11.10.0", "express": "^4.21.2", + "sqlite-vec": "^0.1.9", "zod": "^3.23.8" }, "bin": { @@ -29,6 +31,16 @@ "node": ">=22" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -483,6 +495,504 @@ "hono": "^4" } }, + "node_modules/@huggingface/jinja": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.9.tgz", + "integrity": "sha512-uWTG+l3VJRsl7EXxYizuL3P+cCPoc3cRqbWWRcQN0FhejRfbdq0RNhCmbY/YDtnTcz9icdLYuLDjsnz4d8JMuw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/transformers": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.8.1.tgz", + "integrity": "sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.5.3", + "onnxruntime-node": "1.21.0", + "onnxruntime-web": "1.22.0-dev.20250409-89f8206ba4", + "sharp": "^0.34.1" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -845,6 +1355,63 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", @@ -1277,7 +1844,6 @@ "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1595,6 +2161,13 @@ "node": ">= 0.8" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -1810,6 +2383,40 @@ "node": ">=4.0.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1838,6 +2445,12 @@ "node": ">=8" } }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1913,6 +2526,12 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -1961,6 +2580,18 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2155,6 +2786,12 @@ "node": ">= 0.8" } }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2246,6 +2883,39 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2258,6 +2928,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2407,6 +3095,18 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2424,6 +3124,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2514,6 +3226,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -2593,6 +3326,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2614,6 +3356,49 @@ "wrappy": "1" } }, + "node_modules/onnxruntime-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0.tgz", + "integrity": "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.21.0.tgz", + "integrity": "sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "global-agent": "^3.0.0", + "onnxruntime-common": "1.21.0", + "tar": "^7.0.1" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.22.0-dev.20250409-89f8206ba4", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.22.0-dev.20250409-89f8206ba4", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.22.0-dev.20250409-89f8206ba4.tgz", + "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2671,6 +3456,12 @@ "node": ">=16.20.0" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.22", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", @@ -2727,6 +3518,29 @@ "node": ">=10" } }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2844,6 +3658,23 @@ "node": ">=0.10.0" } }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rollup": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", @@ -2976,6 +3807,12 @@ "node": ">=10" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -3006,6 +3843,21 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", @@ -3027,6 +3879,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3182,6 +4078,90 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/sqlite-vec": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", + "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", + "license": "MIT OR Apache", + "optionalDependencies": { + "sqlite-vec-darwin-arm64": "0.1.9", + "sqlite-vec-darwin-x64": "0.1.9", + "sqlite-vec-linux-arm64": "0.1.9", + "sqlite-vec-linux-x64": "0.1.9", + "sqlite-vec-windows-x64": "0.1.9" + } + }, + "node_modules/sqlite-vec-darwin-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", + "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-darwin-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", + "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/sqlite-vec-linux-arm64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", + "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", + "cpu": [ + "arm64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-linux-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", + "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", + "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", + "cpu": [ + "x64" + ], + "license": "MIT OR Apache", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3223,6 +4203,22 @@ "node": ">=0.10.0" } }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", @@ -3251,6 +4247,15 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3304,6 +4309,13 @@ "node": ">=0.6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.23.1", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", @@ -3335,6 +4347,18 @@ "node": "*" } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3366,7 +4390,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -4070,6 +5093,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/mcp/package.json b/mcp/package.json index 8101ad2e7a..6e52d47855 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -19,9 +19,11 @@ "dev:http": "tsx src/bin/http.ts" }, "dependencies": { + "@huggingface/transformers": "^3.8.1", "@modelcontextprotocol/sdk": "^1", "better-sqlite3": "^11.10.0", "express": "^4.21.2", + "sqlite-vec": "^0.1.9", "zod": "^3.23.8" }, "devDependencies": { diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts index 7cd48f635d..67a7db91d6 100644 --- a/mcp/src/config/load.test.ts +++ b/mcp/src/config/load.test.ts @@ -16,6 +16,11 @@ describe("loadConfig", () => { expect(cfg.knowledge.publicSource.url).toBe( "https://eclipse.dev/ditto/llms.txt", ); + expect(cfg.knowledge.retriever).toBe("fts"); + expect(cfg.knowledge.embedding.model).toBe("Xenova/bge-small-en-v1.5"); + expect(cfg.knowledge.embedding.dim).toBe(384); + expect(cfg.knowledge.embedding.batchSize).toBe(32); + expect(cfg.knowledge.localDir.enabled).toBe(false); }); it("merges values from a JSON file over defaults", () => { diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index 80a332ea1f..6043fcf6b6 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -29,6 +29,24 @@ export const AppConfigSchema = z knowledge: z .object({ enabled: z.boolean().default(true), + retriever: z.enum(["fts", "vector", "hybrid"]).default("fts"), + embedding: z + .object({ + model: z.string().default("Xenova/bge-small-en-v1.5"), + dim: z.number().int().positive().default(384), + modelPath: z.string().optional(), + allowRemoteModels: z.boolean().default(true), + cacheDir: z.string().optional(), + batchSize: z.number().int().positive().default(32), + }) + .default({ model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }), + localDir: z + .object({ + enabled: z.boolean().default(false), + path: z.string().optional(), + id: z.string().default("local"), + }) + .default({ enabled: false, id: "local" }), publicSource: z .object({ enabled: z.boolean().default(true), @@ -41,6 +59,9 @@ export const AppConfigSchema = z }) .default({ enabled: true, + retriever: "fts", + embedding: { model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }, + localDir: { enabled: false, id: "local" }, publicSource: { enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }, }), }) diff --git a/mcp/src/knowledge/build.test.ts b/mcp/src/knowledge/build.test.ts index 88cf1e6399..d12964112e 100644 --- a/mcp/src/knowledge/build.test.ts +++ b/mcp/src/knowledge/build.test.ts @@ -1,8 +1,12 @@ import { describe, it, expect, afterEach } from "vitest"; +import { mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { buildKnowledgeService } from "./build.js"; import { AppConfigSchema } from "../config/schema.js"; import type { FetchFn } from "./public-source.js"; import type { KnowledgeService } from "./knowledge-service.js"; +import type { EmbeddingProvider } from "./embedding.js"; let service: KnowledgeService | undefined; afterEach(() => { @@ -78,6 +82,57 @@ describe("buildKnowledgeService", () => { expect(service).toBeDefined(); const hits = await service!.search("digital twin", 5); expect(hits.length).toBeGreaterThan(0); - expect(hits[0].text).toContain("digital twin"); + expect(hits[0].chunk.text).toContain("digital twin"); + }); +}); + +const fakeEmbedder: EmbeddingProvider = { + dim: 3, + embed: async (texts) => + texts.map((t) => { + const s = t.toLowerCase(); + if (s.includes("reconnect") || s.includes("oom") || s.includes("memory")) return [1, 0, 0]; + if (s.includes("policy") || s.includes("access")) return [0, 1, 0]; + return [0, 0, 1]; + }), +}; + +describe("buildKnowledgeService — vector retriever over a local dir", () => { + it("returns semantic hits using an injected embedder (no network/model)", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(dir, "oom.md"), "# OOM\n\nNetty leak out of memory crash."); + writeFileSync(join(dir, "pol.md"), "# Policy\n\npolicy access control."); + + const config = AppConfigSchema.parse({ + knowledge: { + retriever: "vector", + embedding: { dim: 3 }, + publicSource: { enabled: false }, + localDir: { enabled: true, path: dir }, + }, + }); + const svc = await buildKnowledgeService(config, { embeddingProvider: fakeEmbedder }); + expect(svc).toBeDefined(); + const hits = await svc!.search("why does it die on reconnect", 1); + expect(hits[0].chunk.text.toLowerCase()).toContain("out of memory"); + }); + + it("hybrid retriever wiring end-to-end (injected embedder, no network/model)", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(dir, "oom.md"), "# OOM\n\nNetty leak out of memory crash."); + writeFileSync(join(dir, "keyword.md"), "# Keyword\n\nsomething with unique-keyword-token."); + + const config = AppConfigSchema.parse({ + knowledge: { + retriever: "hybrid", + embedding: { dim: 3 }, + publicSource: { enabled: false }, + localDir: { enabled: true, path: dir }, + }, + }); + const svc = await buildKnowledgeService(config, { embeddingProvider: fakeEmbedder }); + expect(svc).toBeDefined(); + const hits = await svc!.search("reconnect memory issues", 2); + expect(hits.some((rc) => rc.chunk.text.toLowerCase().includes("out of memory"))).toBe(true); }); }); diff --git a/mcp/src/knowledge/build.ts b/mcp/src/knowledge/build.ts index 197c335400..ce975feda3 100644 --- a/mcp/src/knowledge/build.ts +++ b/mcp/src/knowledge/build.ts @@ -1,20 +1,24 @@ import type { AppConfig } from "../config/schema.js"; -import type { KnowledgeSource } from "./types.js"; +import type { KnowledgeSource, Retriever } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; import { KnowledgeService } from "./knowledge-service.js"; import { FtsRetriever } from "./fts-retriever.js"; +import { VectorRetriever } from "./vector-retriever.js"; +import { HybridRetriever } from "./hybrid-retriever.js"; import { PublicSource, type FetchFn } from "./public-source.js"; +import { LocalDirSource } from "./local-dir-source.js"; export interface KnowledgeDeps { fetchFn?: FetchFn; + embeddingProvider?: EmbeddingProvider; } -/** Build + init the knowledge service once, or return undefined if disabled/unavailable. - * Never throws: on init failure it logs a warning and returns undefined (graceful degradation). */ export async function buildKnowledgeService( config: AppConfig, deps: KnowledgeDeps = {}, ): Promise { if (!config.knowledge.enabled) return undefined; + const sources: KnowledgeSource[] = []; if (config.knowledge.publicSource.enabled) { sources.push( @@ -25,8 +29,19 @@ export async function buildKnowledgeService( }), ); } + if (config.knowledge.localDir.enabled && config.knowledge.localDir.path) { + sources.push( + new LocalDirSource({ + dir: config.knowledge.localDir.path, + id: config.knowledge.localDir.id, + }), + ); + } if (sources.length === 0) return undefined; - const service = new KnowledgeService(sources, new FtsRetriever()); + + const retriever = await buildRetriever(config, deps); + + const service = new KnowledgeService(sources, retriever); try { await service.init(); } catch (err) { @@ -37,3 +52,29 @@ export async function buildKnowledgeService( } return service; } + +async function buildRetriever(config: AppConfig, deps: KnowledgeDeps): Promise { + const kind = config.knowledge.retriever; + if (kind === "fts") return new FtsRetriever(); + + const [{ LocalEmbeddings }, { SqliteVecStore }] = await Promise.all([ + import("./embedding.js"), + import("./sqlite-vec-store.js"), + ]); + const emb = config.knowledge.embedding; + const embedder: EmbeddingProvider = + deps.embeddingProvider ?? + new LocalEmbeddings({ + model: emb.model, + dim: emb.dim, + modelPath: emb.modelPath, + allowRemoteModels: emb.allowRemoteModels, + cacheDir: emb.cacheDir, + batchSize: emb.batchSize, + }); + const store = new SqliteVecStore(embedder.dim); + const vector = new VectorRetriever(embedder, store); + + if (kind === "vector") return vector; + return new HybridRetriever([new FtsRetriever(), vector]); +} diff --git a/mcp/src/knowledge/embedding.itest.ts b/mcp/src/knowledge/embedding.itest.ts new file mode 100644 index 0000000000..3abeb364c6 --- /dev/null +++ b/mcp/src/knowledge/embedding.itest.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { LocalEmbeddings } from "./embedding.js"; + +// Loads the real bge model (network on first run). Skipped unless RUN_EMBED_ITEST is set. +const run = process.env.RUN_EMBED_ITEST ? describe : describe.skip; + +run("LocalEmbeddings (real model)", () => { + it("produces 384-dim vectors and ranks paraphrase above unrelated", async () => { + const emb = new LocalEmbeddings(); + const [a, b, c] = await emb.embed([ + "why does it die on reconnect", + "the Netty leak causes an out of memory crash on reconnect", + "policies define access control for things", + ]); + expect(a).toHaveLength(384); + const cos = (x: number[], y: number[]) => x.reduce((s, xi, i) => s + xi * y[i], 0); + expect(cos(a, b)).toBeGreaterThan(cos(a, c)); + }, 120000); +}); diff --git a/mcp/src/knowledge/embedding.test.ts b/mcp/src/knowledge/embedding.test.ts new file mode 100644 index 0000000000..0b686bfcac --- /dev/null +++ b/mcp/src/knowledge/embedding.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from "vitest"; +import { toBatches } from "./embedding.js"; + +describe("toBatches", () => { + it("splits into fixed-size batches with a remainder", () => { + expect(toBatches([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]); + }); + + it("returns a single batch when size >= length", () => { + expect(toBatches([1, 2], 5)).toEqual([[1, 2]]); + }); + + it("returns [] for empty input", () => { + expect(toBatches([], 3)).toEqual([]); + }); + + it("throws on a non-positive batch size", () => { + expect(() => toBatches([1], 0)).toThrow(/batch size/i); + }); +}); diff --git a/mcp/src/knowledge/embedding.ts b/mcp/src/knowledge/embedding.ts new file mode 100644 index 0000000000..2d39447aed --- /dev/null +++ b/mcp/src/knowledge/embedding.ts @@ -0,0 +1,72 @@ +import { pipeline, env, type FeatureExtractionPipeline } from "@huggingface/transformers"; + +export interface EmbeddingProvider { + readonly dim: number; + embed(texts: string[], signal?: AbortSignal): Promise; +} + +export interface LocalEmbeddingsOptions { + model?: string; + dim?: number; + modelPath?: string; + allowRemoteModels?: boolean; + cacheDir?: string; + /** Texts embedded per forward pass. Bounds peak memory; default 32. */ + batchSize?: number; +} + +const DEFAULT_MODEL = "Xenova/bge-small-en-v1.5"; +const DEFAULT_DIM = 384; +const DEFAULT_BATCH_SIZE = 32; + +/** Split `items` into consecutive batches of at most `size`. */ +export function toBatches(items: T[], size: number): T[][] { + if (size <= 0) throw new Error(`batch size must be positive (got ${size})`); + const out: T[][] = []; + for (let i = 0; i < items.length; i += size) { + out.push(items.slice(i, i + size)); + } + return out; +} + +/** + * Local ONNX embeddings via Transformers.js. The model weights are downloaded + * from the HuggingFace hub on first use and cached to disk; set + * `allowRemoteModels: false` + `modelPath` for offline/gated deployments. + */ +export class LocalEmbeddings implements EmbeddingProvider { + readonly dim: number; + private readonly model: string; + private readonly batchSize: number; + private extractorPromise?: Promise; + + constructor(opts: LocalEmbeddingsOptions = {}) { + this.model = opts.model ?? DEFAULT_MODEL; + this.dim = opts.dim ?? DEFAULT_DIM; + this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE; + if (opts.allowRemoteModels !== undefined) env.allowRemoteModels = opts.allowRemoteModels; + if (opts.modelPath !== undefined) env.localModelPath = opts.modelPath; + if (opts.cacheDir !== undefined) env.cacheDir = opts.cacheDir; + } + + private extractor(): Promise { + if (!this.extractorPromise) { + // Type assertion needed due to complex union type from pipeline generic + this.extractorPromise = pipeline("feature-extraction", this.model) as unknown as Promise; + } + return this.extractorPromise; + } + + async embed(texts: string[]): Promise { + if (texts.length === 0) return []; + const extractor = await this.extractor(); + // Embed in bounded batches: a single forward pass over the whole corpus + // allocates a tensor for every input at once and can exhaust memory. + const out: number[][] = []; + for (const batch of toBatches(texts, this.batchSize)) { + const output = await extractor(batch, { pooling: "mean", normalize: true }); + out.push(...(output.tolist() as number[][])); + } + return out; + } +} diff --git a/mcp/src/knowledge/fts-retriever.test.ts b/mcp/src/knowledge/fts-retriever.test.ts index 4d2123ea6e..a3bc2352b1 100644 --- a/mcp/src/knowledge/fts-retriever.test.ts +++ b/mcp/src/knowledge/fts-retriever.test.ts @@ -14,6 +14,11 @@ let r: FtsRetriever; afterEach(() => r?.close()); describe("FtsRetriever", () => { + it("has kind 'fts'", () => { + r = new FtsRetriever(); + expect(r.kind).toBe("fts"); + }); + it("finds chunks by keyword, best match first", async () => { r = new FtsRetriever(); await r.add([ @@ -23,7 +28,8 @@ describe("FtsRetriever", () => { ]); const hits = await r.search("memory crash", 5); expect(hits.length).toBeGreaterThanOrEqual(1); - expect(hits[0].id).toBe("a"); + expect(hits[0].chunk.id).toBe("a"); + expect(hits[0].matchedBy).toEqual(["fts"]); }); it("respects the k limit", async () => { diff --git a/mcp/src/knowledge/fts-retriever.ts b/mcp/src/knowledge/fts-retriever.ts index 1e7c44c146..0d5720c6d4 100644 --- a/mcp/src/knowledge/fts-retriever.ts +++ b/mcp/src/knowledge/fts-retriever.ts @@ -1,7 +1,8 @@ import Database from "better-sqlite3"; -import type { Chunk, Retriever } from "./types.js"; +import type { Chunk, Retriever, RetrievedChunk } from "./types.js"; export class FtsRetriever implements Retriever { + readonly kind = "fts"; private readonly db: Database.Database; private readonly chunks = new Map(); @@ -25,7 +26,7 @@ export class FtsRetriever implements Retriever { tx(chunks); } - async search(query: string, k: number): Promise { + async search(query: string, k: number): Promise { const match = toMatchQuery(query); if (match === "") return []; const rows = this.db @@ -33,10 +34,10 @@ export class FtsRetriever implements Retriever { "SELECT id FROM chunks WHERE chunks MATCH ? ORDER BY rank, id LIMIT ?", ) .all(match, k) as Array<{ id: string }>; - const out: Chunk[] = []; + const out: RetrievedChunk[] = []; for (const row of rows) { const c = this.chunks.get(row.id); - if (c) out.push(c); + if (c) out.push({ chunk: c, matchedBy: ["fts"] }); } return out; } diff --git a/mcp/src/knowledge/hybrid-retriever.test.ts b/mcp/src/knowledge/hybrid-retriever.test.ts new file mode 100644 index 0000000000..3cb6ddc9ba --- /dev/null +++ b/mcp/src/knowledge/hybrid-retriever.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; +import { HybridRetriever } from "./hybrid-retriever.js"; +import type { Retriever, Chunk, RetrievedChunk } from "./types.js"; + +const chunk = (id: string): Chunk => ({ + id, source: "s", title: id, text: id, cite: `https://x/${id}`, +}); + +// Fake retrievers returning fixed ranked lists. +function fixed(kind: string, ids: string[]): Retriever { + const map = new Map(ids.map((id) => [id, chunk(id)])); + return { + kind, + add: async () => {}, + search: async (_q, k): Promise => ids.slice(0, k).map((id) => ({ + chunk: map.get(id)!, + matchedBy: [kind], + })), + getChunk: async (id) => map.get(id), + }; +} + +describe("HybridRetriever", () => { + it("has kind 'hybrid'", () => { + const h = new HybridRetriever([fixed("fts", ["a"]), fixed("vector", ["b"])]); + expect(h.kind).toBe("hybrid"); + }); + + it("fuses two ranked lists via RRF and dedupes", async () => { + // 'b' appears in both lists -> should rank at/near the top after fusion. + const a = fixed("fts", ["a", "b", "c"]); + const d = fixed("vector", ["b", "d", "e"]); + const h = new HybridRetriever([a, d]); + const hits = await h.search("q", 3); + const ids = hits.map((rc) => rc.chunk.id); + expect(ids[0]).toBe("b"); + expect(new Set(ids).size).toBe(ids.length); // no duplicates + expect(hits).toHaveLength(3); + }); + + it("merges matchedBy from both retrievers for duplicates", async () => { + // 'b' appears in both lists -> matchedBy should be ["fts","vector"] + const a = fixed("fts", ["a", "b", "c"]); + const d = fixed("vector", ["b", "d", "e"]); + const h = new HybridRetriever([a, d]); + const hits = await h.search("q", 5); + const b = hits.find((rc) => rc.chunk.id === "b"); + expect(b).toBeDefined(); + expect(b!.matchedBy).toEqual(["fts", "vector"]); + // Single-source chunks should have single matchedBy + const a_hit = hits.find((rc) => rc.chunk.id === "a"); + expect(a_hit!.matchedBy).toEqual(["fts"]); + const d_hit = hits.find((rc) => rc.chunk.id === "d"); + expect(d_hit!.matchedBy).toEqual(["vector"]); + }); + + it("getChunk finds a chunk from any retriever", async () => { + const h = new HybridRetriever([fixed("fts", ["a"]), fixed("vector", ["z"])]); + expect((await h.getChunk("z"))?.id).toBe("z"); + expect(await h.getChunk("missing")).toBeUndefined(); + }); +}); diff --git a/mcp/src/knowledge/hybrid-retriever.ts b/mcp/src/knowledge/hybrid-retriever.ts new file mode 100644 index 0000000000..214c6fdb43 --- /dev/null +++ b/mcp/src/knowledge/hybrid-retriever.ts @@ -0,0 +1,49 @@ +import type { Chunk, Retriever, RetrievedChunk } from "./types.js"; + +const RRF_K = 60; + +export class HybridRetriever implements Retriever { + readonly kind = "hybrid"; + + constructor(private readonly retrievers: Retriever[]) {} + + async add(chunks: Chunk[]): Promise { + await Promise.all(this.retrievers.map((r) => r.add(chunks))); + } + + async search(query: string, k: number): Promise { + const lists = await Promise.all( + this.retrievers.map((r) => r.search(query, k)), + ); + const scores = new Map(); + const byId = new Map(); + const matchedBy = new Map>(); + for (const list of lists) { + list.forEach((rc, rank) => { + byId.set(rc.chunk.id, rc.chunk); + scores.set(rc.chunk.id, (scores.get(rc.chunk.id) ?? 0) + 1 / (RRF_K + rank + 1)); + if (!matchedBy.has(rc.chunk.id)) { + matchedBy.set(rc.chunk.id, new Set()); + } + for (const kind of rc.matchedBy) { + matchedBy.get(rc.chunk.id)!.add(kind); + } + }); + } + return [...scores.entries()] + .sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)) + .slice(0, k) + .map(([id]) => ({ + chunk: byId.get(id)!, + matchedBy: Array.from(matchedBy.get(id)!).sort(), + })); + } + + async getChunk(id: string): Promise { + for (const r of this.retrievers) { + const c = await r.getChunk(id); + if (c) return c; + } + return undefined; + } +} diff --git a/mcp/src/knowledge/knowledge-service.test.ts b/mcp/src/knowledge/knowledge-service.test.ts index e9ca86f850..1d700eb271 100644 --- a/mcp/src/knowledge/knowledge-service.test.ts +++ b/mcp/src/knowledge/knowledge-service.test.ts @@ -24,8 +24,8 @@ describe("KnowledgeService", () => { r, ); await svc.init(); - expect((await svc.search("memory", 5)).map((c) => c.id)).toContain("a"); - expect((await svc.search("policy", 5)).map((c) => c.id)).toContain("b"); + expect((await svc.search("memory", 5)).map((rc) => rc.chunk.id)).toContain("a"); + expect((await svc.search("policy", 5)).map((rc) => rc.chunk.id)).toContain("b"); expect((await svc.getChunk("a"))?.text).toBe("reconnect memory crash"); }); diff --git a/mcp/src/knowledge/knowledge-service.ts b/mcp/src/knowledge/knowledge-service.ts index bb0fd2c3d6..9d8905be20 100644 --- a/mcp/src/knowledge/knowledge-service.ts +++ b/mcp/src/knowledge/knowledge-service.ts @@ -1,4 +1,4 @@ -import type { Chunk, KnowledgeSource, Retriever } from "./types.js"; +import type { Chunk, KnowledgeSource, Retriever, RetrievedChunk } from "./types.js"; export class KnowledgeService { private initialized = false; @@ -16,7 +16,7 @@ export class KnowledgeService { this.initialized = true; } - async search(query: string, k: number): Promise { + async search(query: string, k: number): Promise { return await this.retriever.search(query, k); } diff --git a/mcp/src/knowledge/local-dir-source.test.ts b/mcp/src/knowledge/local-dir-source.test.ts new file mode 100644 index 0000000000..05dc7a2d44 --- /dev/null +++ b/mcp/src/knowledge/local-dir-source.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { LocalDirSource } from "./local-dir-source.js"; + +let dir: string; +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(dir, "runbook.md"), "# Runbook\n\nRestart connectivity to clear the reconnect storm."); + mkdirSync(join(dir, "sub")); + writeFileSync(join(dir, "sub", "tuning.markdown"), "# Tuning\n\nDisable Netty leak detection."); + writeFileSync(join(dir, "ignore.txt"), "not markdown"); +}); + +describe("LocalDirSource", () => { + it("loads and chunks markdown files recursively, ignoring non-markdown", async () => { + const src = new LocalDirSource({ dir }); + const chunks = await src.loadChunks(); + expect(src.id).toBe("local"); + const texts = chunks.map((c) => c.text).join("\n"); + expect(texts).toContain("reconnect storm"); + expect(texts).toContain("Netty leak detection"); + expect(texts).not.toContain("not markdown"); + expect(chunks.every((c) => c.source === "local")).toBe(true); + chunks.forEach((c, i) => expect(c.id).toBe(`local#${i}`)); + }); + + it("returns [] for a missing directory (non-fatal)", async () => { + const src = new LocalDirSource({ dir: join(dir, "does-not-exist") }); + expect(await src.loadChunks()).toEqual([]); + }); +}); diff --git a/mcp/src/knowledge/local-dir-source.ts b/mcp/src/knowledge/local-dir-source.ts new file mode 100644 index 0000000000..856d4e8d93 --- /dev/null +++ b/mcp/src/knowledge/local-dir-source.ts @@ -0,0 +1,58 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join, basename, extname } from "node:path"; +import type { Chunk, KnowledgeSource } from "./types.js"; +import { chunkMarkdown } from "./chunker.js"; + +export interface LocalDirSourceOptions { + dir: string; + id?: string; + chunkOptions?: { maxChars?: number; overlap?: number }; +} + +const MD_EXT = new Set([".md", ".markdown"]); + +export class LocalDirSource implements KnowledgeSource { + readonly id: string; + private readonly opts: LocalDirSourceOptions; + + constructor(opts: LocalDirSourceOptions) { + this.opts = opts; + this.id = opts.id ?? "local"; + } + + async loadChunks(): Promise { + let files: string[]; + try { + files = walk(this.opts.dir); + } catch (err) { + process.stderr.write( + `[ditto-mcp] local-dir-source: cannot read ${this.opts.dir}: ${String(err)}\n`, + ); + return []; + } + const chunks: Chunk[] = []; + for (const file of files) { + const md = readFileSync(file, "utf8"); + chunks.push( + ...chunkMarkdown(md, { + source: this.id, + title: basename(file), + cite: file, + maxChars: this.opts.chunkOptions?.maxChars, + overlap: this.opts.chunkOptions?.overlap, + }), + ); + } + return chunks.map((c, i) => ({ ...c, id: `${this.id}#${i}` })); + } +} + +function walk(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) out.push(...walk(full)); + else if (MD_EXT.has(extname(entry.name).toLowerCase())) out.push(full); + } + return out.sort(); // deterministic order +} diff --git a/mcp/src/knowledge/public-source.test.ts b/mcp/src/knowledge/public-source.test.ts index 06eef26929..fe4c1327e0 100644 --- a/mcp/src/knowledge/public-source.test.ts +++ b/mcp/src/knowledge/public-source.test.ts @@ -44,6 +44,25 @@ describe("PublicSource", () => { expect(new Set(chunks.map((c) => c.cite)).size).toBe(1); }); + it("ingests only markdown links, skipping HTML/non-.md entries", async () => { + const index = `# Ditto docs +- [Repo](https://github.com/eclipse-ditto/ditto): source +- [OpenAPI](https://eclipse.dev/ditto/openapi/): api ui +- [Things](https://eclipse.dev/ditto/things.md): about things`; + const src = new PublicSource({ + url: "https://eclipse.dev/ditto/llms.txt", + fetchFn: async (u) => { + if (u === "https://eclipse.dev/ditto/llms.txt") return index; + if (u === "https://eclipse.dev/ditto/things.md") + return "# Things\n\nA thing is a digital twin."; + throw new Error(`should not fetch non-markdown url: ${u}`); + }, + }); + const chunks = await src.loadChunks(); + const cites = new Set(chunks.map((c) => c.cite)); + expect(cites).toEqual(new Set(["https://eclipse.dev/ditto/things.md"])); + }); + it("skips documents that fail to fetch instead of throwing", async () => { const src = new PublicSource({ url: "https://eclipse.dev/ditto/llms.txt", diff --git a/mcp/src/knowledge/public-source.ts b/mcp/src/knowledge/public-source.ts index 4f7238a4f0..38f6c250b1 100644 --- a/mcp/src/knowledge/public-source.ts +++ b/mcp/src/knowledge/public-source.ts @@ -43,29 +43,37 @@ export class PublicSource implements KnowledgeSource { if (this.opts.maxDocs !== undefined) { entries = entries.slice(0, this.opts.maxDocs); } - const chunks: Chunk[] = []; - for (const entry of entries) { - let md: string; - try { - md = await this.fetchFn(entry.url, signal); - } catch (err) { - process.stderr.write( - `[ditto-mcp] public-source: skipping ${entry.url}: ${String(err)}\n`, - ); - continue; - } - chunks.push( - ...chunkMarkdown(md, { - source: this.id, - title: entry.title, - cite: entry.url, - maxChars: this.opts.chunkOptions?.maxChars, - overlap: this.opts.chunkOptions?.overlap, + // Fetch documents with bounded concurrency (network-bound; sequential + // fetches make server startup block for a long time on large corpora). + // Order is preserved so chunk ids are deterministic across runs. + const CONCURRENCY = 8; + const perEntry: Chunk[][] = new Array(entries.length); + for (let i = 0; i < entries.length; i += CONCURRENCY) { + const batch = entries.slice(i, i + CONCURRENCY); + const results = await Promise.all( + batch.map(async (entry) => { + let md: string; + try { + md = await this.fetchFn(entry.url, signal); + } catch (err) { + process.stderr.write( + `[ditto-mcp] public-source: skipping ${entry.url}: ${String(err)}\n`, + ); + return []; + } + return chunkMarkdown(md, { + source: this.id, + title: entry.title, + cite: entry.url, + maxChars: this.opts.chunkOptions?.maxChars, + overlap: this.opts.chunkOptions?.overlap, + }); }), ); + results.forEach((r, j) => (perEntry[i + j] = r)); } // Re-key ids to be unique across documents (chunker numbers per-doc). - return chunks.map((c, i) => ({ ...c, id: `${this.id}#${i}` })); + return perEntry.flat().map((c, i) => ({ ...c, id: `${this.id}#${i}` })); } } @@ -80,6 +88,12 @@ function parseEntries(index: string, baseUrl: string): Entry[] { if (resolved.protocol !== "http:" && resolved.protocol !== "https:") { continue; } + // Only ingest markdown docs; llms.txt indexes also link to HTML pages + // (repo, openapi/jsonschema UIs) that would pollute the corpus with markup. + const path = resolved.pathname.toLowerCase(); + if (!path.endsWith(".md") && !path.endsWith(".markdown")) { + continue; + } entries.push({ title, url: resolved.toString() }); } return entries; diff --git a/mcp/src/knowledge/sqlite-vec-store.test.ts b/mcp/src/knowledge/sqlite-vec-store.test.ts new file mode 100644 index 0000000000..6b3a003936 --- /dev/null +++ b/mcp/src/knowledge/sqlite-vec-store.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { SqliteVecStore } from "./sqlite-vec-store.js"; + +let store: SqliteVecStore; +afterEach(() => store?.close()); + +describe("SqliteVecStore", () => { + it("returns nearest neighbors ordered by distance", async () => { + store = new SqliteVecStore(3); + await store.upsert([ + { id: "x", vector: [1, 0, 0] }, + { id: "y", vector: [0, 1, 0] }, + { id: "z", vector: [0.9, 0.1, 0] }, + ]); + const hits = await store.searchByVector([1, 0, 0], 2); + expect(hits.map((h) => h.id)).toEqual(["x", "z"]); + expect(hits[0].distance).toBeLessThanOrEqual(hits[1].distance); + }); + + it("respects k", async () => { + store = new SqliteVecStore(2); + await store.upsert([ + { id: "a", vector: [1, 0] }, + { id: "b", vector: [0, 1] }, + { id: "c", vector: [1, 1] }, + ]); + expect(await store.searchByVector([1, 0], 1)).toHaveLength(1); + }); + + it("upsert replaces an existing id", async () => { + store = new SqliteVecStore(2); + await store.upsert([{ id: "a", vector: [1, 0] }]); + await store.upsert([{ id: "a", vector: [0, 1] }]); + const hits = await store.searchByVector([0, 1], 5); + expect(hits.filter((h) => h.id === "a")).toHaveLength(1); + }); + + it("rejects a non-positive dim", () => { + expect(() => new SqliteVecStore(0)).toThrow(/dim/); + }); + + it("rejects a vector whose length != dim", async () => { + store = new SqliteVecStore(3); + await expect(store.upsert([{ id: "a", vector: [1, 0] }])).rejects.toThrow(/length/); + }); +}); diff --git a/mcp/src/knowledge/sqlite-vec-store.ts b/mcp/src/knowledge/sqlite-vec-store.ts new file mode 100644 index 0000000000..838d880462 --- /dev/null +++ b/mcp/src/knowledge/sqlite-vec-store.ts @@ -0,0 +1,50 @@ +import Database from "better-sqlite3"; +import * as sqliteVec from "sqlite-vec"; +import type { VectorRecord, VectorHit, VectorStore } from "./vector-store.js"; + +export class SqliteVecStore implements VectorStore { + private readonly db: Database.Database; + private readonly dim: number; + + constructor(dim: number) { + if (!Number.isInteger(dim) || dim <= 0) { + throw new Error(`SqliteVecStore: dim must be a positive integer (got ${dim})`); + } + this.dim = dim; + this.db = new Database(":memory:"); + sqliteVec.load(this.db); + this.db.exec( + `CREATE VIRTUAL TABLE vec_items USING vec0(id TEXT PRIMARY KEY, embedding float[${dim}]);`, + ); + } + + async upsert(records: VectorRecord[]): Promise { + const del = this.db.prepare("DELETE FROM vec_items WHERE id = ?"); + const ins = this.db.prepare( + "INSERT INTO vec_items (id, embedding) VALUES (?, ?)", + ); + const tx = this.db.transaction((rows: VectorRecord[]) => { + for (const r of rows) { + if (r.vector.length !== this.dim) { + throw new Error(`SqliteVecStore: vector length ${r.vector.length} != dim ${this.dim}`); + } + del.run(r.id); + ins.run(r.id, JSON.stringify(r.vector)); + } + }); + tx(records); + } + + async searchByVector(vector: number[], k: number): Promise { + const rows = this.db + .prepare( + "SELECT id, distance FROM vec_items WHERE embedding MATCH ? AND k = ? ORDER BY distance", + ) + .all(JSON.stringify(vector), k) as Array<{ id: string; distance: number }>; + return rows.map((r) => ({ id: r.id, distance: r.distance })); + } + + close(): void { + this.db.close(); + } +} diff --git a/mcp/src/knowledge/types.ts b/mcp/src/knowledge/types.ts index d8d7057f31..6f0d7e45d9 100644 --- a/mcp/src/knowledge/types.ts +++ b/mcp/src/knowledge/types.ts @@ -6,6 +6,11 @@ export interface Chunk { cite: string; } +export interface RetrievedChunk { + chunk: Chunk; + matchedBy: string[]; // leaf retriever kinds, e.g. ["fts"], ["vector"], ["fts","vector"] +} + /** A corpus provider: yields chunks. Does not search. */ export interface KnowledgeSource { id: string; @@ -14,7 +19,8 @@ export interface KnowledgeSource { /** Indexes chunks and searches them. */ export interface Retriever { + readonly kind: string; // "fts" | "vector" | "hybrid" add(chunks: Chunk[]): Promise; - search(query: string, k: number): Promise; + search(query: string, k: number): Promise; getChunk(id: string): Promise; } diff --git a/mcp/src/knowledge/vector-retriever.test.ts b/mcp/src/knowledge/vector-retriever.test.ts new file mode 100644 index 0000000000..9cf3e1b561 --- /dev/null +++ b/mcp/src/knowledge/vector-retriever.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { VectorRetriever } from "./vector-retriever.js"; +import { SqliteVecStore } from "./sqlite-vec-store.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import type { Chunk } from "./types.js"; + +// Deterministic fake: map each text to a fixed 3-d vector by keyword. +const fake: EmbeddingProvider = { + dim: 3, + embed: async (texts) => + texts.map((t) => { + const s = t.toLowerCase(); + if (s.includes("reconnect") || s.includes("oom") || s.includes("memory")) return [1, 0, 0]; + if (s.includes("policy") || s.includes("access")) return [0, 1, 0]; + return [0, 0, 1]; + }), +}; + +const chunk = (id: string, text: string): Chunk => ({ + id, source: "s", title: "T", text, cite: `https://x/${id}`, +}); + +let store: SqliteVecStore; +afterEach(() => store?.close()); + +describe("VectorRetriever", () => { + it("has kind 'vector'", () => { + store = new SqliteVecStore(3); + const r = new VectorRetriever(fake, store); + expect(r.kind).toBe("vector"); + }); + + it("matches semantically (query near the OOM chunk, not the policy chunk)", async () => { + store = new SqliteVecStore(3); + const r = new VectorRetriever(fake, store); + await r.add([ + chunk("oom", "Netty leak out of memory crash"), + chunk("pol", "policy access control"), + ]); + const hits = await r.search("why does it die on reconnect", 1); + expect(hits[0].chunk.id).toBe("oom"); + expect(hits[0].matchedBy).toEqual(["vector"]); + }); + + it("getChunk returns the stored chunk", async () => { + store = new SqliteVecStore(3); + const r = new VectorRetriever(fake, store); + await r.add([chunk("a", "reconnect memory")]); + expect((await r.getChunk("a"))?.text).toBe("reconnect memory"); + expect(await r.getChunk("missing")).toBeUndefined(); + }); +}); diff --git a/mcp/src/knowledge/vector-retriever.ts b/mcp/src/knowledge/vector-retriever.ts new file mode 100644 index 0000000000..89d3cd1fd6 --- /dev/null +++ b/mcp/src/knowledge/vector-retriever.ts @@ -0,0 +1,37 @@ +import type { Chunk, Retriever, RetrievedChunk } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import type { VectorStore } from "./vector-store.js"; + +export class VectorRetriever implements Retriever { + readonly kind = "vector"; + private readonly chunks = new Map(); + + constructor( + private readonly embedder: EmbeddingProvider, + private readonly store: VectorStore, + ) {} + + async add(chunks: Chunk[]): Promise { + if (chunks.length === 0) return; + const vectors = await this.embedder.embed(chunks.map((c) => c.text)); + await this.store.upsert( + chunks.map((c, i) => ({ id: c.id, vector: vectors[i] })), + ); + for (const c of chunks) this.chunks.set(c.id, c); + } + + async search(query: string, k: number): Promise { + const [vector] = await this.embedder.embed([query]); + const hits = await this.store.searchByVector(vector, k); + const out: RetrievedChunk[] = []; + for (const hit of hits) { + const c = this.chunks.get(hit.id); + if (c) out.push({ chunk: c, matchedBy: ["vector"] }); + } + return out; + } + + async getChunk(id: string): Promise { + return this.chunks.get(id); + } +} diff --git a/mcp/src/knowledge/vector-store.ts b/mcp/src/knowledge/vector-store.ts new file mode 100644 index 0000000000..8f082ed9c0 --- /dev/null +++ b/mcp/src/knowledge/vector-store.ts @@ -0,0 +1,15 @@ +export interface VectorRecord { + id: string; + vector: number[]; +} + +export interface VectorHit { + id: string; + distance: number; +} + +export interface VectorStore { + upsert(records: VectorRecord[]): Promise; + searchByVector(vector: number[], k: number): Promise; + close(): void; +} diff --git a/mcp/src/tools/knowledge.test.ts b/mcp/src/tools/knowledge.test.ts index 6eafa484ea..2a591ef3ec 100644 --- a/mcp/src/tools/knowledge.test.ts +++ b/mcp/src/tools/knowledge.test.ts @@ -24,12 +24,13 @@ async function tools() { } describe("knowledge tools", () => { - it("search returns matching chunk text with citation", async () => { + it("search returns matching chunk text with citation and matched provenance", async () => { const t = await tools(); const res = await t.search.handler({ query: "digital twin" }, ctx); const text = res.content.map((p) => p.text).join("\n"); expect(text).toContain("digital twin"); expect(text).toContain("https://x/a"); + expect(text).toContain("matched: fts"); }); it("search reports no results cleanly", async () => { diff --git a/mcp/src/tools/knowledge.ts b/mcp/src/tools/knowledge.ts index da4c736d35..3e3cd7a4b4 100644 --- a/mcp/src/tools/knowledge.ts +++ b/mcp/src/tools/knowledge.ts @@ -1,10 +1,15 @@ import { z } from "zod"; import type { ToolDef, ToolResult } from "../core/types.js"; -import type { Chunk } from "../knowledge/types.js"; +import type { Chunk, RetrievedChunk } from "../knowledge/types.js"; import type { KnowledgeService } from "../knowledge/knowledge-service.js"; -function formatChunk(c: Chunk): string { - return `## ${c.title}\n${c.text}\n\n[source: ${c.cite} · id: ${c.id}]`; +function formatChunk(c: Chunk, extraFooter = ""): string { + const footer = `source: ${c.cite} · id: ${c.id}${extraFooter}`; + return `## ${c.title}\n${c.text}\n\n[${footer}]`; +} + +function formatRetrievedChunk(rc: RetrievedChunk): string { + return formatChunk(rc.chunk, ` · matched: ${rc.matchedBy.join("+")}`); } function textResult(text: string): ToolResult { @@ -15,23 +20,46 @@ export function makeKnowledgeTools(service: KnowledgeService): ToolDef[] { const search: ToolDef = { name: "search", description: - "Search the Ditto knowledge base and return the most relevant documentation excerpts.", + "Search the Ditto knowledge base (official docs plus any configured corpora) and " + + "return the most relevant documentation excerpts, each with a source URL and a chunk id. " + + "Use natural-language questions about Ditto concepts, configuration, HTTP/Ditto protocol, " + + "connectivity, policies, or operations.", inputSchema: { - query: z.string(), - k: z.number().int().positive().max(20).optional(), + query: z + .string() + .describe( + "Natural-language search query about Ditto (e.g. 'how do policies grant access' " + + "or 'why does connectivity crash on reconnect').", + ), + limit: z + .number() + .int() + .positive() + .max(20) + .optional() + .describe( + "Maximum number of documentation excerpts to return. Default 5, maximum 20. " + + "Increase for broader context, decrease for only the top matches.", + ), }, handler: async (args: unknown): Promise => { - const { query, k } = args as { query: string; k?: number }; - const hits = await service.search(query, k ?? 5); + const { query, limit } = args as { query: string; limit?: number }; + const hits = await service.search(query, limit ?? 5); if (hits.length === 0) return textResult(`No results for "${query}".`); - return textResult(hits.map(formatChunk).join("\n\n---\n\n")); + return textResult(hits.map(formatRetrievedChunk).join("\n\n---\n\n")); }, }; const getChunk: ToolDef = { name: "get_chunk", - description: "Fetch a single knowledge chunk by its id.", - inputSchema: { id: z.string() }, + description: + "Fetch the full text of a single knowledge chunk by its id. Use an id returned by " + + "the `search` tool (shown as 'id: ', e.g. 'public#12').", + inputSchema: { + id: z + .string() + .describe("A chunk id returned by the `search` tool, e.g. 'public#12'."), + }, handler: async (args: unknown): Promise => { const { id } = args as { id: string }; const c = await service.getChunk(id); diff --git a/mcp/vitest.config.ts b/mcp/vitest.config.ts index 02817dfc34..bdaa94bf5c 100644 --- a/mcp/vitest.config.ts +++ b/mcp/vitest.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ test: { globals: false, environment: "node", - include: ["src/**/*.test.ts"], + include: ["src/**/*.test.ts", "src/**/*.itest.ts"], testTimeout: 20000, }, }); From 469d2e3bc118a2a2b85b20f6923d9be520216952 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Mon, 10 Aug 2026 14:30:19 +0300 Subject: [PATCH 04/11] =?UTF-8?q?feat(mcp):=20persistent=20knowledge=20ind?= =?UTF-8?q?ex=20=E2=80=94=20file-backed=20store,=20ingest=20CLI,=20pgvecto?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File-backed KnowledgeStore (chunks+FTS5+vectors), read-only retrievers over the store, ingest CLI, atomic ingest (temp+rename), index metadata validation, async store lifecycle + openStore factory, PgKnowledgeStore (pgvector + tsvector) with testcontainers pg tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/README.md | 104 + mcp/package-lock.json | 2253 ++++++++++++++++- mcp/package.json | 10 +- mcp/src/bin/ingest.test.ts | 67 + mcp/src/bin/ingest.ts | 29 + mcp/src/config/load.test.ts | 12 + mcp/src/config/schema.ts | 15 + mcp/src/knowledge/build-index.test.ts | 28 + mcp/src/knowledge/build-index.ts | 41 + mcp/src/knowledge/build.test.ts | 93 + mcp/src/knowledge/build.ts | 136 +- mcp/src/knowledge/factories.ts | 39 + mcp/src/knowledge/fts-retriever.test.ts | 60 +- mcp/src/knowledge/fts-retriever.ts | 60 +- mcp/src/knowledge/hybrid-retriever.test.ts | 10 +- mcp/src/knowledge/hybrid-retriever.ts | 12 - mcp/src/knowledge/ingest-store.test.ts | 37 + mcp/src/knowledge/ingest-store.ts | 39 + mcp/src/knowledge/knowledge-service.test.ts | 40 +- mcp/src/knowledge/knowledge-service.ts | 23 +- mcp/src/knowledge/knowledge-store.ts | 28 + mcp/src/knowledge/pg-e2e.pgtest.ts | 57 + .../knowledge/pg-knowledge-store.pgtest.ts | 84 + mcp/src/knowledge/pg-knowledge-store.ts | 174 ++ mcp/src/knowledge/pg-smoke.pgtest.ts | 18 + mcp/src/knowledge/pg-testcontainer.ts | 9 + .../knowledge/sqlite-knowledge-store.test.ts | 121 + mcp/src/knowledge/sqlite-knowledge-store.ts | 139 + mcp/src/knowledge/sqlite-vec-store.test.ts | 46 - mcp/src/knowledge/sqlite-vec-store.ts | 50 - mcp/src/knowledge/store-factory.test.ts | 14 + mcp/src/knowledge/store-factory.ts | 23 + mcp/src/knowledge/types.ts | 4 +- mcp/src/knowledge/vector-retriever.test.ts | 35 +- mcp/src/knowledge/vector-retriever.ts | 27 +- mcp/src/knowledge/vector-store.ts | 15 - mcp/src/tools/index.test.ts | 22 +- mcp/src/tools/knowledge.test.ts | 22 +- mcp/vitest.config.ts | 1 + mcp/vitest.pg.config.ts | 4 + 40 files changed, 3492 insertions(+), 509 deletions(-) create mode 100644 mcp/src/bin/ingest.test.ts create mode 100644 mcp/src/bin/ingest.ts create mode 100644 mcp/src/knowledge/build-index.test.ts create mode 100644 mcp/src/knowledge/build-index.ts create mode 100644 mcp/src/knowledge/factories.ts create mode 100644 mcp/src/knowledge/ingest-store.test.ts create mode 100644 mcp/src/knowledge/ingest-store.ts create mode 100644 mcp/src/knowledge/knowledge-store.ts create mode 100644 mcp/src/knowledge/pg-e2e.pgtest.ts create mode 100644 mcp/src/knowledge/pg-knowledge-store.pgtest.ts create mode 100644 mcp/src/knowledge/pg-knowledge-store.ts create mode 100644 mcp/src/knowledge/pg-smoke.pgtest.ts create mode 100644 mcp/src/knowledge/pg-testcontainer.ts create mode 100644 mcp/src/knowledge/sqlite-knowledge-store.test.ts create mode 100644 mcp/src/knowledge/sqlite-knowledge-store.ts delete mode 100644 mcp/src/knowledge/sqlite-vec-store.test.ts delete mode 100644 mcp/src/knowledge/sqlite-vec-store.ts create mode 100644 mcp/src/knowledge/store-factory.test.ts create mode 100644 mcp/src/knowledge/store-factory.ts delete mode 100644 mcp/src/knowledge/vector-store.ts create mode 100644 mcp/vitest.pg.config.ts diff --git a/mcp/README.md b/mcp/README.md index 610a32b660..ec2a33d604 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -105,6 +105,110 @@ no embedding stack. See `embedding` config below. } ``` +### Persistence & Ingestion (P2b-2) + +The knowledge index (chunks, FTS, and vectors) lives in a pluggable +`KnowledgeStore` backend. Currently, `SqliteKnowledgeStore` persists everything +to a single `.db` file; `PgKnowledgeStore` (pgvector + Postgres FTS) is next +(P2c). + +#### Store Configuration + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.store.kind` | `"sqlite" \| "pgvector"` | `"sqlite"` | Store backend: `sqlite` (file-based, default) or `pgvector` (Postgres + pgvector + FTS) | +| `knowledge.store.sqlite.path` | `string?` | `undefined` | Path to the SQLite file. If unset or missing, the server builds an in-memory index at startup. | +| `knowledge.store.pgvector.connectionString` | `string?` | `undefined` | Postgres connection string (e.g., `postgresql://user:pass@host:5432/ditto`). Required when `kind: "pgvector"`. | +| `knowledge.store.pgvector.table` | `string` | `"ditto_kn"` | Table name prefix for Postgres tables (chunks, FTS, vectors). | + +**Example (SQLite persistent store):** +```json +{ + "knowledge": { + "retriever": "fts", + "store": { "kind": "sqlite", "sqlite": { "path": "/var/lib/ditto-mcp/index.db" } } + } +} +``` + +**Example (Postgres / pgvector store — AWS RDS):** +```json +{ + "knowledge": { + "retriever": "hybrid", + "store": { + "kind": "pgvector", + "pgvector": { + "connectionString": "postgresql://user:password@my-rds.c9akciq32.us-east-1.rds.amazonaws.com:5432/ditto", + "table": "ditto_kn" + } + } + } +} +``` + +#### Postgres / pgvector (P2c-2) + +When `knowledge.store.kind: "pgvector"`, the server uses Postgres for persistence: +- **Vectors**: stored in a `pgvector` column (requires the `vector` extension) +- **Keyword index**: built using Postgres `tsvector` FTS +- **Chunks**: stored in a text table + +**Setup:** +1. Create a Postgres database (e.g., `ditto`). +2. Ensure the `vector` extension is available. For **AWS RDS**: + - Create a custom parameter group with `rds.extensions = 'vector'` + - Apply it to your Postgres instance + - Connect and run `CREATE EXTENSION IF NOT EXISTS vector;` +3. Configure the connection string in your config JSON. + +**Ingest:** The `ingest` command fetches the corpus, embeds vectors, and writes all chunks/FTS/vectors to Postgres: +```bash +DITTO_MCP_CONFIG=/path/to/config.json npm run dev:ingest +``` + +On the first run, the server validates index metadata (retriever, embedding model/dim, schema version). On re-ingest, the server calls `reset()` (drops the vec table and clears chunks/meta), rebuilds the index, and sets the completion flag — this is an **offline/maintenance operation** (not zero-downtime for a live instance). Re-ingest always replaces the full index from scratch, and can survive embedding-dim changes (reset() drops and recreates the vec table). + +**Server Load-or-Build Behavior:** +- **Index exists & metadata matches**: server connects and serves immediately (no rebuild). +- **Index missing or metadata mismatch**: server logs a warning and disables knowledge tools (pgvector backend does NOT fall back to in-memory build — you MUST run `ingest` to populate Postgres before the server can serve knowledge). +- **Connection failure**: server exits with an error (Postgres backend requires a live database). + +**Integration Tests:** +Postgres integration tests run via `npm run test:pg` (requires Docker and testcontainers): +```bash +npm run test:pg +``` +These tests are **excluded** from the default `npm test` suite to keep the default test run hermetic (no Docker, no network, no external dependencies). + +#### Ingest Command + +To pre-build the index and persist it to a file (SQLite) or database (Postgres), use the `ingest` command: + +```bash +# Development +DITTO_MCP_CONFIG=/path/to/config.json npm run dev:ingest + +# Built +DITTO_MCP_CONFIG=/path/to/config.json ditto-mcp-ingest +``` + +The `ingest` command reads the config, fetches/indexes the corpus, and writes the store (file path for SQLite, or Postgres for pgvector). The config must specify a valid store location, or `ingest` will error. + +#### Server Load-or-Build Behavior + +When the server starts: +- **Populated store exists & metadata matches**: opens the prebuilt store and validates index metadata (retriever mode, embedding model/dim, schema version, and completion flag). If metadata matches the config, the store is served instantly (no fetch/embed). +- **SQLite — missing/empty/mismatched store**: builds the index in memory (fallback mode). The server never writes the file automatically — use `ingest` to persist. A corrupt or mismatched file at `knowledge.store.sqlite.path` never crashes the server or disables knowledge — it triggers the same in-memory fallback as a missing file. +- **Postgres — missing/empty/mismatched store**: server logs a warning and disables knowledge tools (pgvector backend does NOT fall back to in-memory build — you MUST run `ingest` to populate Postgres before the server can serve knowledge). +- **Postgres connection failure**: exits with an error. Postgres backend requires a live database. + +The `ingest` command uses backend-specific atomic writes: +- **SQLite**: temp file + rename on success (zero-downtime, crash-safe). +- **Postgres**: `reset()` → rebuild → set completion flag (offline operation; re-ingest requires downtime, but can survive embedding-dim changes since reset() drops and recreates the vec table). + +The async `KnowledgeStore` lifecycle (`isPopulated()`, `getMeta()`, `setMeta()`, `reset()`, `close()`) enables both `SqliteKnowledgeStore` and `PgKnowledgeStore` to plug in behind the same interface with no churn to `build.ts` or `build-index.ts`. Both backends validate metadata and support offline re-ingest. + ### HTTP Server Options (`server.http`) | Field | Type | Default | Description | diff --git a/mcp/package-lock.json b/mcp/package-lock.json index 80a3938d48..72161e55b2 100644 --- a/mcp/package-lock.json +++ b/mcp/package-lock.json @@ -12,17 +12,21 @@ "@modelcontextprotocol/sdk": "^1", "better-sqlite3": "^11.10.0", "express": "^4.21.2", + "pg": "^8.22.0", "sqlite-vec": "^0.1.9", "zod": "^3.23.8" }, "bin": { "ditto-mcp-http": "dist/bin/http.js", + "ditto-mcp-ingest": "dist/bin/ingest.js", "ditto-mcp-stdio": "dist/bin/stdio.js" }, "devDependencies": { + "@testcontainers/postgresql": "^12.1.0", "@types/better-sqlite3": "^7.6.13", "@types/express": "^4.17.21", "@types/node": "^22.10.0", + "@types/pg": "^8.20.4", "tsx": "^4.19.2", "typescript": "^5.7.0", "vitest": "^2.1.0" @@ -31,6 +35,13 @@ "node": ">=22" } }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/runtime": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", @@ -483,6 +494,58 @@ "node": ">=18" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -981,6 +1044,24 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/fs-minipass": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", @@ -1000,6 +1081,52 @@ "dev": true, "license": "MIT" }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/file-exists/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@kwsites/file-exists/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", @@ -1355,6 +1482,17 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1762,6 +1900,16 @@ "win32" ] }, + "node_modules/@testcontainers/postgresql": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-12.1.0.tgz", + "integrity": "sha512-Pjf2VSVNirEPfz36nidyrVAnZvc2YhajOznY4VgyEsvfTd5qiMNOuPq96drREvxAUtXl5SFLX7vXj7sSq4aTcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "testcontainers": "^12.1.0" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -1793,6 +1941,29 @@ "@types/node": "*" } }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz", + "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1849,6 +2020,18 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.20.4", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.4.tgz", + "integrity": "sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.15.1", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", @@ -1896,6 +2079,43 @@ "@types/node": "*" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2-streams": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz", + "integrity": "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/expect": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", @@ -2009,6 +2229,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2055,26 +2288,75 @@ } } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -2089,89 +2371,34 @@ "url": "https://feross.org/support" } ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/body-parser/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 0.8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, "funding": [ { "type": "github", @@ -2189,13 +2416,362 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/archiver/node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.0.tgz", + "integrity": "sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.7.tgz", + "integrity": "sha512-o8CRCiJtib+ycO3mE4A5UChtGX4dDP2XxsWVu9P+Zc3H8tcmKwNVEDoDTXmwN+uuMhfKeT7/i7Y26xS8W7ohoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -2264,15 +2840,188 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 16" + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2309,6 +3058,13 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -2326,6 +3082,90 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2451,6 +3291,78 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT" }, + "node_modules/docker-compose": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.4.2.tgz", + "integrity": "sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/docker-modem/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/docker-modem/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dockerode": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-5.0.1.tgz", + "integrity": "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4" + }, + "engines": { + "node": ">= 14.17" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2465,12 +3377,26 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2574,6 +3500,16 @@ "@esbuild/win32-x64": "0.28.1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2611,6 +3547,36 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -2746,6 +3712,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", @@ -2792,6 +3765,23 @@ "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", "license": "Apache-2.0" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2840,6 +3830,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2864,6 +3864,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -2883,6 +3896,28 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", @@ -2928,6 +3963,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/guid-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", @@ -3062,18 +4104,64 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jose": { "version": "6.2.4", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", @@ -3101,6 +4189,66 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "license": "ISC" }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -3114,6 +4262,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3211,10 +4366,26 @@ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -3247,6 +4418,22 @@ "node": ">= 18" } }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -3259,6 +4446,14 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/nan": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", + "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.16", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", @@ -3305,6 +4500,16 @@ "node": ">=10" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3399,6 +4604,13 @@ "integrity": "sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==", "license": "MIT" }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3417,6 +4629,23 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -3440,6 +4669,96 @@ "node": ">= 14.16" } }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3491,6 +4810,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -3518,6 +4876,60 @@ "node": ">=10" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/properties-reader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-3.0.1.tgz", + "integrity": "sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "mkdirp": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/properties?sponsor=1" + } + }, "node_modules/protobufjs": { "version": "7.6.5", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", @@ -3649,6 +5061,39 @@ "node": ">= 6" } }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3658,6 +5103,16 @@ "node": ">=0.10.0" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -4023,6 +5478,19 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -4078,6 +5546,22 @@ "node": ">=0.10.0" } }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -4162,6 +5646,46 @@ "win32" ] }, + "node_modules/ssh-remote-port-forward": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", + "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ssh2": "^0.5.48", + "ssh2": "^1.4.0" + } + }, + "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { + "version": "0.5.52", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", + "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2-streams": "*" + } + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4175,23 +5699,139 @@ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/strip-json-comments": { @@ -4256,6 +5896,106 @@ "node": ">=18" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/testcontainers": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-12.1.0.tgz", + "integrity": "sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^4.0.1", + "archiver": "^7.0.1", + "async-lock": "^1.4.1", + "byline": "^5.0.0", + "debug": "^4.4.3", + "docker-compose": "^1.4.2", + "dockerode": "^5.0.1", + "get-port": "^5.1.1", + "proper-lockfile": "^4.1.2", + "properties-reader": "^3.0.1", + "ssh-remote-port-forward": "^1.0.4", + "tar-fs": "^3.1.3", + "tmp": "^0.2.7", + "undici": "^8.9.0" + }, + "engines": { + "node": ">= 22.22" + } + }, + "node_modules/testcontainers/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/testcontainers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/testcontainers/node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/testcontainers/node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4300,6 +6040,16 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4347,6 +6097,13 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -4386,6 +6143,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -5087,12 +6854,129 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -5102,6 +6986,153 @@ "node": ">=18" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/mcp/package.json b/mcp/package.json index 6e52d47855..bbd35b0559 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -8,28 +8,34 @@ }, "bin": { "ditto-mcp-stdio": "dist/bin/stdio.js", - "ditto-mcp-http": "dist/bin/http.js" + "ditto-mcp-http": "dist/bin/http.js", + "ditto-mcp-ingest": "dist/bin/ingest.js" }, "scripts": { "build": "tsc -p tsconfig.json", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest run", "test:watch": "vitest", + "test:pg": "vitest run --config vitest.pg.config.ts", "dev:stdio": "tsx src/bin/stdio.ts", - "dev:http": "tsx src/bin/http.ts" + "dev:http": "tsx src/bin/http.ts", + "dev:ingest": "tsx src/bin/ingest.ts" }, "dependencies": { "@huggingface/transformers": "^3.8.1", "@modelcontextprotocol/sdk": "^1", "better-sqlite3": "^11.10.0", "express": "^4.21.2", + "pg": "^8.22.0", "sqlite-vec": "^0.1.9", "zod": "^3.23.8" }, "devDependencies": { + "@testcontainers/postgresql": "^12.1.0", "@types/better-sqlite3": "^7.6.13", "@types/express": "^4.17.21", "@types/node": "^22.10.0", + "@types/pg": "^8.20.4", "tsx": "^4.19.2", "typescript": "^5.7.0", "vitest": "^2.1.0" diff --git a/mcp/src/bin/ingest.test.ts b/mcp/src/bin/ingest.test.ts new file mode 100644 index 0000000000..ef719a6b4f --- /dev/null +++ b/mcp/src/bin/ingest.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from "vitest"; +import { mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { execFileSync } from "node:child_process"; +import { SqliteKnowledgeStore } from "../knowledge/sqlite-knowledge-store.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const entry = resolve(here, "ingest.ts"); + +describe("ingest CLI (spawn e2e)", () => { + it("builds a persisted fts index from a local dir corpus", async () => { + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(corpus, "a.md"), "# Reconnect\n\nNetty leak out of memory crash."); + const idxDir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(idxDir, "index.db"); + const cfgPath = join(idxDir, "config.json"); + writeFileSync(cfgPath, JSON.stringify({ + knowledge: { + retriever: "fts", + publicSource: { enabled: false }, + localDir: { enabled: true, path: corpus }, + store: { kind: "sqlite", sqlite: { path } }, + }, + })); + + execFileSync(process.execPath, ["--import", "tsx", entry], { + env: { ...process.env, DITTO_MCP_CONFIG: cfgPath }, + stdio: "pipe", + }); + + expect(existsSync(path)).toBe(true); + const store = new SqliteKnowledgeStore(path); + expect(await store.isPopulated()).toBe(true); + expect((await store.ftsSearch("memory", 5)).length).toBeGreaterThan(0); + await store.close(); + }, 30000); + + it("re-ingest is a clean rebuild (no orphan chunks from a larger prior run)", async () => { + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + const idxDir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(idxDir, "index.db"); + const cfgPath = join(idxDir, "config.json"); + const cfg = (files: number) => { + // (re)write corpus with `files` docs + for (let i = 0; i < files; i++) writeFileSync(join(corpus, `d${i}.md`), `# D${i}\n\ndoc ${i} netty`); + writeFileSync(cfgPath, JSON.stringify({ + knowledge: { retriever: "fts", publicSource: { enabled: false }, + localDir: { enabled: true, path: corpus }, store: { kind: "sqlite", sqlite: { path } } }, + })); + }; + const run = () => execFileSync(process.execPath, ["--import", "tsx", entry], + { env: { ...process.env, DITTO_MCP_CONFIG: cfgPath }, stdio: "pipe" }); + + cfg(5); run(); + // shrink corpus: remove all, write 1 doc + for (let i = 0; i < 5; i++) rmSync(join(corpus, `d${i}.md`), { force: true }); + cfg(1); run(); + + const store = new SqliteKnowledgeStore(path); + // Only the single remaining doc's chunk id ("local#0") should exist. + expect(await store.getChunk("local#0")).toBeDefined(); + expect(await store.getChunk("local#4")).toBeUndefined(); // orphan from the 5-doc run is gone + await store.close(); + }, 30000); +}); diff --git a/mcp/src/bin/ingest.ts b/mcp/src/bin/ingest.ts new file mode 100644 index 0000000000..9157d4df4d --- /dev/null +++ b/mcp/src/bin/ingest.ts @@ -0,0 +1,29 @@ +import { loadConfig } from "../config/load.js"; +import { buildIndex, metaFor } from "../knowledge/build-index.js"; +import { makeSources, makeEmbedder } from "../knowledge/factories.js"; +import { withIngestStore } from "../knowledge/ingest-store.js"; +import type { EmbeddingProvider } from "../knowledge/embedding.js"; + +async function main(): Promise { + const config = loadConfig(process.env.DITTO_MCP_CONFIG); + if (!config.knowledge.enabled) throw new Error("knowledge is disabled in config"); + + const sources = makeSources(config); + if (sources.length === 0) throw new Error("no knowledge sources enabled"); + + let embedder: EmbeddingProvider | undefined; + if (config.knowledge.retriever !== "fts") { + embedder = await makeEmbedder(config); + } + + await withIngestStore(config, (store) => + buildIndex(sources, store, embedder, metaFor(config, embedder)), + ); + const dest = config.knowledge.store.kind === "sqlite" ? config.knowledge.store.sqlite.path : config.knowledge.store.kind; + process.stderr.write(`[ditto-mcp ingest] done: ${dest}\n`); +} + +main().catch((err) => { + process.stderr.write(`[ditto-mcp ingest] fatal: ${err instanceof Error ? err.stack : String(err)}\n`); + process.exit(1); +}); diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts index 67a7db91d6..b58aae15a2 100644 --- a/mcp/src/config/load.test.ts +++ b/mcp/src/config/load.test.ts @@ -21,6 +21,18 @@ describe("loadConfig", () => { expect(cfg.knowledge.embedding.dim).toBe(384); expect(cfg.knowledge.embedding.batchSize).toBe(32); expect(cfg.knowledge.localDir.enabled).toBe(false); + expect(cfg.knowledge.store.kind).toBe("sqlite"); + }); + + it("accepts a pgvector store config", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ + knowledge: { store: { kind: "pgvector", pgvector: { connectionString: "postgres://x" } } }, + })); + const cfg = loadConfig(file); + expect(cfg.knowledge.store.kind).toBe("pgvector"); + expect(cfg.knowledge.store.pgvector.connectionString).toBe("postgres://x"); }); it("merges values from a JSON file over defaults", () => { diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index 6043fcf6b6..7d201afba3 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -56,6 +56,20 @@ export const AppConfigSchema = z maxDocs: z.number().int().positive().optional(), }) .default({ enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }), + store: z + .object({ + kind: z.enum(["sqlite", "pgvector"]).default("sqlite"), + sqlite: z + .object({ path: z.string().optional() }) + .default({}), + pgvector: z + .object({ + connectionString: z.string().optional(), + table: z.string().default("ditto_kn"), + }) + .default({ table: "ditto_kn" }), + }) + .default({ kind: "sqlite", sqlite: {}, pgvector: { table: "ditto_kn" } }), }) .default({ enabled: true, @@ -63,6 +77,7 @@ export const AppConfigSchema = z embedding: { model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }, localDir: { enabled: false, id: "local" }, publicSource: { enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }, + store: { kind: "sqlite", sqlite: {}, pgvector: { table: "ditto_kn" } }, }), }) .default({}); diff --git a/mcp/src/knowledge/build-index.test.ts b/mcp/src/knowledge/build-index.test.ts new file mode 100644 index 0000000000..d5d9d4e1bb --- /dev/null +++ b/mcp/src/knowledge/build-index.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { buildIndex } from "./build-index.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; +import type { KnowledgeSource, Chunk } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; + +const src = (id: string, chunks: Chunk[]): KnowledgeSource => ({ id, loadChunks: async () => chunks }); +const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}` }); +const fake: EmbeddingProvider = { dim: 3, embed: async (t) => t.map(() => [1, 0, 0]) }; + +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); + +describe("buildIndex", () => { + it("adds chunks (fts) with no embedder", async () => { + store = new SqliteKnowledgeStore(); + await buildIndex([src("s1", [chunk("a", "reconnect memory")])], store); + expect(await store.isPopulated()).toBe(true); + expect((await store.ftsSearch("memory", 5))[0]).toBe("a"); + expect(await store.vectorSearch([1, 0, 0], 5)).toEqual([]); // no vectors + }); + + it("adds chunks + vectors with an embedder", async () => { + store = new SqliteKnowledgeStore(); + await buildIndex([src("s1", [chunk("a", "x")])], store, fake); + expect((await store.vectorSearch([1, 0, 0], 1))[0].id).toBe("a"); + }); +}); diff --git a/mcp/src/knowledge/build-index.ts b/mcp/src/knowledge/build-index.ts new file mode 100644 index 0000000000..d43ff7cc15 --- /dev/null +++ b/mcp/src/knowledge/build-index.ts @@ -0,0 +1,41 @@ +import type { KnowledgeSource } from "./types.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import { SCHEMA_VERSION } from "./knowledge-store.js"; +import type { AppConfig } from "../config/schema.js"; + +export function metaFor( + config: AppConfig, + embedder?: EmbeddingProvider, +): { retriever: string; embeddingModel?: string; embeddingDim?: number } { + return { + retriever: config.knowledge.retriever, + embeddingModel: embedder ? config.knowledge.embedding.model : undefined, + embeddingDim: embedder ? config.knowledge.embedding.dim : undefined, + }; +} + +export async function buildIndex( + sources: KnowledgeSource[], + store: KnowledgeStore, + embedder?: EmbeddingProvider, + meta?: { retriever: string; embeddingModel?: string; embeddingDim?: number }, + signal?: AbortSignal, +): Promise { + if (embedder) await store.ensureVectorTable(embedder.dim); + for (const source of sources) { + const chunks = await source.loadChunks(signal); + await store.addChunks(chunks); + if (embedder && chunks.length > 0) { + const vectors = await embedder.embed(chunks.map((c) => c.text), signal); + await store.upsertVectors(chunks.map((c, i) => ({ id: c.id, vector: vectors[i] }))); + } + } + await store.setMeta({ + schemaVersion: SCHEMA_VERSION, + retriever: meta?.retriever ?? "fts", + embeddingModel: meta?.embeddingModel, + embeddingDim: meta?.embeddingDim, + complete: true, + }); +} diff --git a/mcp/src/knowledge/build.test.ts b/mcp/src/knowledge/build.test.ts index d12964112e..f0cd92e979 100644 --- a/mcp/src/knowledge/build.test.ts +++ b/mcp/src/knowledge/build.test.ts @@ -136,3 +136,96 @@ describe("buildKnowledgeService — vector retriever over a local dir", () => { expect(hits.some((rc) => rc.chunk.text.toLowerCase().includes("out of memory"))).toBe(true); }); }); + +describe("buildKnowledgeService — prebuilt sqlite file", () => { + it("opens a populated index file without rebuilding (fts)", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(corpus, "a.md"), "# Reconnect\n\nNetty leak out of memory crash."); + const path = join(dir, "index.db"); + + // Pre-populate the file via the same buildIndex the CLI uses. + const { SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js"); + const { buildIndex } = await import("./build-index.js"); + const { LocalDirSource } = await import("./local-dir-source.js"); + const w = new SqliteKnowledgeStore(path); + await buildIndex([new LocalDirSource({ dir: corpus })], w); + await w.close(); + + const config = AppConfigSchema.parse({ + knowledge: { + retriever: "fts", + publicSource: { enabled: false }, + localDir: { enabled: true, path: "/nonexistent-should-not-be-read" }, + store: { kind: "sqlite", sqlite: { path } }, + }, + }); + // localDir points at a missing dir on purpose: if the server rebuilt, it would + // find nothing; since it must LOAD the prebuilt file, search still works. + const svc = await buildKnowledgeService(config); + expect(svc).toBeDefined(); + const hits = await svc!.search("out of memory", 5); + expect(hits[0].chunk.text.toLowerCase()).toContain("out of memory"); + }); + + it("rebuilds (fallback) when a prebuilt file's retriever metadata mismatches config", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const { SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js"); + const { buildIndex } = await import("./build-index.js"); + const w = new SqliteKnowledgeStore(path); + await buildIndex([{ id: "s", loadChunks: async () => [ + { id: "s#0", source: "s", title: "T", text: "netty oom", cite: "x" }] }], w, undefined, + { retriever: "fts" }); + await w.close(); + // Server configured for "vector" but the file was built for "fts" -> must not serve it. + const config = AppConfigSchema.parse({ + knowledge: { retriever: "vector", embedding: { dim: 3 }, publicSource: { enabled: false }, + localDir: { enabled: false }, store: { kind: "sqlite", sqlite: { path } } }, + }); + const fake = { dim: 3, embed: async (t: string[]) => t.map(() => [1, 0, 0]) }; + const svc = await buildKnowledgeService(config, { embeddingProvider: fake }); + // localDir disabled + publicSource disabled -> fallback build has no sources -> undefined. + // The point: it did NOT serve the mismatched fts file as a vector index. + expect(svc).toBeUndefined(); + }); + + it("falls back to in-memory build when the store path is a corrupt file", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + writeFileSync(path, "this is not a sqlite database"); + const corpus = mkdtempSync(join(tmpdir(), "ditto-corpus-")); + writeFileSync(join(corpus, "a.md"), "# A\n\nnetty out of memory"); + const config = AppConfigSchema.parse({ + knowledge: { retriever: "fts", publicSource: { enabled: false }, + localDir: { enabled: true, path: corpus }, store: { kind: "sqlite", sqlite: { path } } }, + }); + const svc = await buildKnowledgeService(config); + expect(svc).toBeDefined(); + expect((await svc!.search("memory", 5))[0].chunk.text.toLowerCase()).toContain("out of memory"); + }); + + it("refuses to serve a prebuilt vector index with mismatched embedding dim", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + // Build a vector index with dim=3 + const { SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js"); + const { buildIndex } = await import("./build-index.js"); + const fake3 = { dim: 3, embed: async (t: string[]) => t.map(() => [1, 0, 0]) }; + const w = new SqliteKnowledgeStore(path); + await buildIndex([{ id: "s", loadChunks: async () => [ + { id: "s#0", source: "s", title: "T", text: "netty oom", cite: "x" }] }], w, fake3, + { retriever: "vector", embeddingModel: "fake/model", embeddingDim: 3 }); + await w.close(); + // Now try to serve it with config expecting dim=4 + const config = AppConfigSchema.parse({ + knowledge: { retriever: "vector", embedding: { model: "fake/model", dim: 4 }, + publicSource: { enabled: false }, localDir: { enabled: false }, + store: { kind: "sqlite", sqlite: { path } } }, + }); + const fake4 = { dim: 4, embed: async (t: string[]) => t.map(() => [1, 0, 0, 0]) }; + const svc = await buildKnowledgeService(config, { embeddingProvider: fake4 }); + // The mismatched file must NOT be served; fallback has no sources -> undefined. + expect(svc).toBeUndefined(); + }); +}); diff --git a/mcp/src/knowledge/build.ts b/mcp/src/knowledge/build.ts index ce975feda3..e814aad25e 100644 --- a/mcp/src/knowledge/build.ts +++ b/mcp/src/knowledge/build.ts @@ -1,16 +1,25 @@ +import { existsSync } from "node:fs"; import type { AppConfig } from "../config/schema.js"; -import type { KnowledgeSource, Retriever } from "./types.js"; +import type { Retriever } from "./types.js"; import type { EmbeddingProvider } from "./embedding.js"; +import type { KnowledgeStore, IndexMeta } from "./knowledge-store.js"; +import { SCHEMA_VERSION } from "./knowledge-store.js"; +import { openStore } from "./store-factory.js"; import { KnowledgeService } from "./knowledge-service.js"; import { FtsRetriever } from "./fts-retriever.js"; import { VectorRetriever } from "./vector-retriever.js"; import { HybridRetriever } from "./hybrid-retriever.js"; -import { PublicSource, type FetchFn } from "./public-source.js"; -import { LocalDirSource } from "./local-dir-source.js"; +import { buildIndex, metaFor } from "./build-index.js"; +import { makeSources, makeEmbedder, type FactoryDeps } from "./factories.js"; -export interface KnowledgeDeps { - fetchFn?: FetchFn; - embeddingProvider?: EmbeddingProvider; +export type KnowledgeDeps = FactoryDeps; + +function metaMatches(meta: IndexMeta | null | undefined, config: AppConfig): boolean { + if (!meta || meta.complete !== true || meta.schemaVersion !== SCHEMA_VERSION) return false; + if (meta.retriever !== config.knowledge.retriever) return false; + if (config.knowledge.retriever === "fts") return true; + const e = config.knowledge.embedding; + return meta.embeddingModel === e.model && meta.embeddingDim === e.dim; } export async function buildKnowledgeService( @@ -19,62 +28,85 @@ export async function buildKnowledgeService( ): Promise { if (!config.knowledge.enabled) return undefined; - const sources: KnowledgeSource[] = []; - if (config.knowledge.publicSource.enabled) { - sources.push( - new PublicSource({ - url: config.knowledge.publicSource.url, - maxDocs: config.knowledge.publicSource.maxDocs, - fetchFn: deps.fetchFn, - }), - ); - } - if (config.knowledge.localDir.enabled && config.knowledge.localDir.path) { - sources.push( - new LocalDirSource({ - dir: config.knowledge.localDir.path, - id: config.knowledge.localDir.id, - }), - ); - } - if (sources.length === 0) return undefined; - - const retriever = await buildRetriever(config, deps); + const needsVectors = config.knowledge.retriever !== "fts"; + const embedder: EmbeddingProvider | undefined = needsVectors + ? await makeEmbedder(config, deps) + : undefined; - const service = new KnowledgeService(sources, retriever); try { - await service.init(); + // Non-sqlite stores (e.g. pgvector): open, check if populated + meta matches, serve; ELSE warn (no auto-build). + if (config.knowledge.store.kind !== "sqlite") { + const store = await openStore(config); + try { + if (await store.isPopulated()) { + const meta = await store.getMeta(); + if (metaMatches(meta, config)) return new KnowledgeService(store, makeRetriever(config, store, embedder)); + } + // No matching prebuilt index → warn and disable knowledge (do NOT auto-build for pgvector). + process.stderr.write( + `[ditto-mcp] no matching prebuilt index in the configured Postgres store; ` + + `run \`ingest\` to build it — the server does not build pgvector indexes\n`, + ); + await store.close(); + return undefined; + } catch (err) { + await store.close(); + process.stderr.write(`[ditto-mcp] knowledge init failed: ${String(err)}\n`); + return undefined; + } + } + + // Sqlite: prebuilt file path. + const path = config.knowledge.store.sqlite.path; + // Prebuilt file: open and serve without rebuilding. + if (path && existsSync(path)) { + try { + const store = await openStore(config, { path }); + if (await store.isPopulated()) { + const meta = await store.getMeta(); + if (metaMatches(meta, config)) return new KnowledgeService(store, makeRetriever(config, store, embedder)); + process.stderr.write( + `[ditto-mcp] prebuilt index at ${path} is incomplete or does not match config ` + + `(retriever/model/dim/version); rebuilding in memory\n`, + ); + await store.close(); + } else { + await store.close(); + } + } catch (err) { + process.stderr.write(`[ditto-mcp] cannot open prebuilt index at ${path}: ${String(err)}; rebuilding in memory\n`); + } + } + + // Fallback: build in-memory (never writes the configured file). + if (path) { + process.stderr.write( + `[ditto-mcp] no prebuilt index at ${path}; building in memory (run \`ingest\` to persist)\n`, + ); + } + const sources = makeSources(config, deps); + if (sources.length === 0) return undefined; + const store: KnowledgeStore = await openStore(config, { path: ":memory:" }); + try { + await buildIndex(sources, store, embedder, metaFor(config, embedder)); + } catch (err) { + await store.close(); + throw err; + } + return new KnowledgeService(store, makeRetriever(config, store, embedder)); } catch (err) { process.stderr.write( `[ditto-mcp] knowledge init failed, disabling knowledge tools: ${String(err)}\n`, ); return undefined; } - return service; } -async function buildRetriever(config: AppConfig, deps: KnowledgeDeps): Promise { +function makeRetriever(config: AppConfig, store: KnowledgeStore, embedder?: EmbeddingProvider): Retriever { const kind = config.knowledge.retriever; - if (kind === "fts") return new FtsRetriever(); - - const [{ LocalEmbeddings }, { SqliteVecStore }] = await Promise.all([ - import("./embedding.js"), - import("./sqlite-vec-store.js"), - ]); - const emb = config.knowledge.embedding; - const embedder: EmbeddingProvider = - deps.embeddingProvider ?? - new LocalEmbeddings({ - model: emb.model, - dim: emb.dim, - modelPath: emb.modelPath, - allowRemoteModels: emb.allowRemoteModels, - cacheDir: emb.cacheDir, - batchSize: emb.batchSize, - }); - const store = new SqliteVecStore(embedder.dim); - const vector = new VectorRetriever(embedder, store); - + if (kind === "fts") return new FtsRetriever(store); + if (!embedder) throw new Error("vector/hybrid retriever requires an embedder"); + const vector = new VectorRetriever(store, embedder); if (kind === "vector") return vector; - return new HybridRetriever([new FtsRetriever(), vector]); + return new HybridRetriever([new FtsRetriever(store), vector]); } diff --git a/mcp/src/knowledge/factories.ts b/mcp/src/knowledge/factories.ts new file mode 100644 index 0000000000..3bbef8b0a7 --- /dev/null +++ b/mcp/src/knowledge/factories.ts @@ -0,0 +1,39 @@ +import type { AppConfig } from "../config/schema.js"; +import type { KnowledgeSource } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; +import { PublicSource, type FetchFn } from "./public-source.js"; +import { LocalDirSource } from "./local-dir-source.js"; + +export interface FactoryDeps { + fetchFn?: FetchFn; + embeddingProvider?: EmbeddingProvider; +} + +export function makeSources(config: AppConfig, deps?: FactoryDeps): KnowledgeSource[] { + const sources: KnowledgeSource[] = []; + if (config.knowledge.publicSource.enabled) { + sources.push( + new PublicSource({ + url: config.knowledge.publicSource.url, + maxDocs: config.knowledge.publicSource.maxDocs, + fetchFn: deps?.fetchFn, + }), + ); + } + if (config.knowledge.localDir.enabled && config.knowledge.localDir.path) { + sources.push( + new LocalDirSource({ dir: config.knowledge.localDir.path, id: config.knowledge.localDir.id }), + ); + } + return sources; +} + +export async function makeEmbedder(config: AppConfig, deps?: FactoryDeps): Promise { + if (deps?.embeddingProvider) return deps.embeddingProvider; + const { LocalEmbeddings } = await import("./embedding.js"); + const e = config.knowledge.embedding; + return new LocalEmbeddings({ + model: e.model, dim: e.dim, modelPath: e.modelPath, + allowRemoteModels: e.allowRemoteModels, cacheDir: e.cacheDir, batchSize: e.batchSize, + }); +} diff --git a/mcp/src/knowledge/fts-retriever.test.ts b/mcp/src/knowledge/fts-retriever.test.ts index a3bc2352b1..a138e85902 100644 --- a/mcp/src/knowledge/fts-retriever.test.ts +++ b/mcp/src/knowledge/fts-retriever.test.ts @@ -1,59 +1,35 @@ import { describe, it, expect, afterEach } from "vitest"; import { FtsRetriever } from "./fts-retriever.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; import type { Chunk } from "./types.js"; const chunk = (id: string, text: string): Chunk => ({ - id, - source: "s", - title: "T", - text, - cite: `https://x/${id}`, + id, source: "s", title: "T", text, cite: `https://x/${id}`, }); -let r: FtsRetriever; -afterEach(() => r?.close()); +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); describe("FtsRetriever", () => { - it("has kind 'fts'", () => { - r = new FtsRetriever(); - expect(r.kind).toBe("fts"); - }); - - it("finds chunks by keyword, best match first", async () => { - r = new FtsRetriever(); - await r.add([ - chunk("a", "The Netty leak causes an out of memory crash on reconnect"), - chunk("b", "Policies define access control for things"), - chunk("c", "Connectivity manages MQTT and Kafka connections"), + async function retriever(chunks: Chunk[]) { + store = new SqliteKnowledgeStore(); + await store.addChunks(chunks); + return new FtsRetriever(store); + } + + it("returns keyword hits tagged matchedBy=['fts'], best first", async () => { + const r = await retriever([ + chunk("a", "Netty leak out of memory crash"), + chunk("b", "policies access control"), ]); const hits = await r.search("memory crash", 5); - expect(hits.length).toBeGreaterThanOrEqual(1); expect(hits[0].chunk.id).toBe("a"); expect(hits[0].matchedBy).toEqual(["fts"]); }); - it("respects the k limit", async () => { - r = new FtsRetriever(); - await r.add([chunk("a", "alpha token"), chunk("b", "alpha token"), chunk("c", "alpha token")]); - expect(await r.search("alpha", 2)).toHaveLength(2); - }); - - it("returns [] for a query with no matches", async () => { - r = new FtsRetriever(); - await r.add([chunk("a", "hello world")]); - expect(await r.search("nonexistentterm", 5)).toEqual([]); - }); - - it("does not throw on FTS-special characters in the query", async () => { - r = new FtsRetriever(); - await r.add([chunk("a", "quotes and parens matter")]); - await expect(r.search('"(quotes) AND *', 5)).resolves.not.toThrow(); - }); - - it("getChunk returns the stored chunk or undefined", async () => { - r = new FtsRetriever(); - await r.add([chunk("a", "hello")]); - expect((await r.getChunk("a"))?.text).toBe("hello"); - expect(await r.getChunk("missing")).toBeUndefined(); + it("honors k and returns [] on no match", async () => { + const r = await retriever([chunk("a", "alpha"), chunk("b", "alpha")]); + expect(await r.search("alpha", 1)).toHaveLength(1); + expect(await r.search("zzznope", 5)).toEqual([]); }); }); diff --git a/mcp/src/knowledge/fts-retriever.ts b/mcp/src/knowledge/fts-retriever.ts index 0d5720c6d4..25b2769a48 100644 --- a/mcp/src/knowledge/fts-retriever.ts +++ b/mcp/src/knowledge/fts-retriever.ts @@ -1,63 +1,17 @@ -import Database from "better-sqlite3"; -import type { Chunk, Retriever, RetrievedChunk } from "./types.js"; +import type { RetrievedChunk, Retriever } from "./types.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; export class FtsRetriever implements Retriever { readonly kind = "fts"; - private readonly db: Database.Database; - private readonly chunks = new Map(); - - constructor() { - this.db = new Database(":memory:"); - this.db.exec( - "CREATE VIRTUAL TABLE chunks USING fts5(id UNINDEXED, title, text);", - ); - } - - async add(chunks: Chunk[]): Promise { - const insert = this.db.prepare( - "INSERT INTO chunks (id, title, text) VALUES (?, ?, ?)", - ); - const tx = this.db.transaction((rows: Chunk[]) => { - for (const c of rows) { - insert.run(c.id, c.title, c.text); - this.chunks.set(c.id, c); - } - }); - tx(chunks); - } + constructor(private readonly store: KnowledgeStore) {} async search(query: string, k: number): Promise { - const match = toMatchQuery(query); - if (match === "") return []; - const rows = this.db - .prepare( - "SELECT id FROM chunks WHERE chunks MATCH ? ORDER BY rank, id LIMIT ?", - ) - .all(match, k) as Array<{ id: string }>; + const ids = await this.store.ftsSearch(query, k); const out: RetrievedChunk[] = []; - for (const row of rows) { - const c = this.chunks.get(row.id); - if (c) out.push({ chunk: c, matchedBy: ["fts"] }); + for (const id of ids) { + const chunk = await this.store.getChunk(id); + if (chunk) out.push({ chunk, matchedBy: ["fts"] }); } return out; } - - async getChunk(id: string): Promise { - return this.chunks.get(id); - } - - close(): void { - this.db.close(); - } -} - -/** - * Turn arbitrary user text into a safe FTS5 MATCH expression: extract - * word tokens, quote each as a phrase, join with OR for recall. Returns - * "" when there are no usable tokens (caller returns no results). - */ -function toMatchQuery(query: string): string { - const tokens = query.match(/[\p{L}\p{N}]+/gu); - if (!tokens || tokens.length === 0) return ""; - return tokens.map((t) => `"${t}"`).join(" OR "); } diff --git a/mcp/src/knowledge/hybrid-retriever.test.ts b/mcp/src/knowledge/hybrid-retriever.test.ts index 3cb6ddc9ba..c645e970b4 100644 --- a/mcp/src/knowledge/hybrid-retriever.test.ts +++ b/mcp/src/knowledge/hybrid-retriever.test.ts @@ -8,15 +8,12 @@ const chunk = (id: string): Chunk => ({ // Fake retrievers returning fixed ranked lists. function fixed(kind: string, ids: string[]): Retriever { - const map = new Map(ids.map((id) => [id, chunk(id)])); return { kind, - add: async () => {}, search: async (_q, k): Promise => ids.slice(0, k).map((id) => ({ - chunk: map.get(id)!, + chunk: chunk(id), matchedBy: [kind], })), - getChunk: async (id) => map.get(id), }; } @@ -54,9 +51,4 @@ describe("HybridRetriever", () => { expect(d_hit!.matchedBy).toEqual(["vector"]); }); - it("getChunk finds a chunk from any retriever", async () => { - const h = new HybridRetriever([fixed("fts", ["a"]), fixed("vector", ["z"])]); - expect((await h.getChunk("z"))?.id).toBe("z"); - expect(await h.getChunk("missing")).toBeUndefined(); - }); }); diff --git a/mcp/src/knowledge/hybrid-retriever.ts b/mcp/src/knowledge/hybrid-retriever.ts index 214c6fdb43..3e9a2d706a 100644 --- a/mcp/src/knowledge/hybrid-retriever.ts +++ b/mcp/src/knowledge/hybrid-retriever.ts @@ -7,10 +7,6 @@ export class HybridRetriever implements Retriever { constructor(private readonly retrievers: Retriever[]) {} - async add(chunks: Chunk[]): Promise { - await Promise.all(this.retrievers.map((r) => r.add(chunks))); - } - async search(query: string, k: number): Promise { const lists = await Promise.all( this.retrievers.map((r) => r.search(query, k)), @@ -38,12 +34,4 @@ export class HybridRetriever implements Retriever { matchedBy: Array.from(matchedBy.get(id)!).sort(), })); } - - async getChunk(id: string): Promise { - for (const r of this.retrievers) { - const c = await r.getChunk(id); - if (c) return c; - } - return undefined; - } } diff --git a/mcp/src/knowledge/ingest-store.test.ts b/mcp/src/knowledge/ingest-store.test.ts new file mode 100644 index 0000000000..313add52e6 --- /dev/null +++ b/mcp/src/knowledge/ingest-store.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from "vitest"; +import { mkdtempSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { AppConfigSchema } from "../config/schema.js"; +import { withIngestStore } from "./ingest-store.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; + +describe("withIngestStore (sqlite)", () => { + it("builds into a temp file then renames atomically", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const config = AppConfigSchema.parse({ knowledge: { store: { kind: "sqlite", sqlite: { path } } } }); + await withIngestStore(config, async (store) => { + await store.addChunks([{ id: "a", source: "s", title: "T", text: "netty", cite: "x" }]); + await store.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + }); + expect(existsSync(path)).toBe(true); + expect(existsSync(`${path}.tmp`)).toBe(false); + const s = new SqliteKnowledgeStore(path); + expect(await s.isPopulated()).toBe(true); + await s.close(); + }); + + it("removes tmp file on error, does not rename, and propagates error", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const config = AppConfigSchema.parse({ knowledge: { store: { kind: "sqlite", sqlite: { path } } } }); + await expect( + withIngestStore(config, async () => { + throw new Error("boom"); + }), + ).rejects.toThrow("boom"); + expect(existsSync(path)).toBe(false); + expect(existsSync(`${path}.tmp`)).toBe(false); + }); +}); diff --git a/mcp/src/knowledge/ingest-store.ts b/mcp/src/knowledge/ingest-store.ts new file mode 100644 index 0000000000..ae4b7fe673 --- /dev/null +++ b/mcp/src/knowledge/ingest-store.ts @@ -0,0 +1,39 @@ +import { existsSync, renameSync, rmSync } from "node:fs"; +import type { AppConfig } from "../config/schema.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; +import { openStore } from "./store-factory.js"; + +export async function withIngestStore( + config: AppConfig, + fn: (store: KnowledgeStore) => Promise, +): Promise { + const kind = config.knowledge.store.kind; + if (kind === "sqlite") { + const path = config.knowledge.store.sqlite.path; + if (!path) throw new Error("knowledge.store.sqlite.path is required for ingest"); + const tmp = `${path}.tmp`; + if (existsSync(tmp)) rmSync(tmp, { force: true }); + const store = await openStore(config, { path: tmp }); + try { + await fn(store); + await store.close(); + } catch (err) { + await store.close(); + rmSync(tmp, { force: true }); + throw err; + } + renameSync(tmp, path); + return; + } + if (kind === "pgvector") { + const store = await openStore(config); + try { + await store.reset(); + await fn(store); + } finally { + await store.close(); + } + return; + } + throw new Error(`unsupported store kind for ingest: ${kind}`); +} diff --git a/mcp/src/knowledge/knowledge-service.test.ts b/mcp/src/knowledge/knowledge-service.test.ts index 1d700eb271..d1e2462dc8 100644 --- a/mcp/src/knowledge/knowledge-service.test.ts +++ b/mcp/src/knowledge/knowledge-service.test.ts @@ -1,39 +1,23 @@ import { describe, it, expect, afterEach } from "vitest"; import { KnowledgeService } from "./knowledge-service.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; import { FtsRetriever } from "./fts-retriever.js"; +import { buildIndex } from "./build-index.js"; import type { KnowledgeSource, Chunk } from "./types.js"; -const source = (id: string, chunks: Chunk[]): KnowledgeSource => ({ - id, - loadChunks: async () => chunks, -}); - -const chunk = (id: string, text: string): Chunk => ({ - id, source: "s", title: "T", text, cite: `https://x/${id}`, -}); +const src = (id: string, chunks: Chunk[]): KnowledgeSource => ({ id, loadChunks: async () => chunks }); +const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}` }); -let r: FtsRetriever; -afterEach(() => r?.close()); +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); describe("KnowledgeService", () => { - it("indexes all sources on init and searches across them", async () => { - r = new FtsRetriever(); - const svc = new KnowledgeService( - [source("s1", [chunk("a", "reconnect memory crash")]), - source("s2", [chunk("b", "policy access control")])], - r, - ); - await svc.init(); - expect((await svc.search("memory", 5)).map((rc) => rc.chunk.id)).toContain("a"); - expect((await svc.search("policy", 5)).map((rc) => rc.chunk.id)).toContain("b"); + it("searches via the retriever and gets chunks via the store", async () => { + store = new SqliteKnowledgeStore(); + await buildIndex([src("s1", [chunk("a", "reconnect memory crash")])], store); + const svc = new KnowledgeService(store, new FtsRetriever(store)); + const hits = await svc.search("memory", 5); + expect(hits[0].chunk.id).toBe("a"); expect((await svc.getChunk("a"))?.text).toBe("reconnect memory crash"); }); - - it("init is idempotent (does not double-index)", async () => { - r = new FtsRetriever(); - const svc = new KnowledgeService([source("s1", [chunk("a", "alpha")])], r); - await svc.init(); - await svc.init(); - expect(await svc.search("alpha", 10)).toHaveLength(1); - }); }); diff --git a/mcp/src/knowledge/knowledge-service.ts b/mcp/src/knowledge/knowledge-service.ts index 9d8905be20..780247cfb1 100644 --- a/mcp/src/knowledge/knowledge-service.ts +++ b/mcp/src/knowledge/knowledge-service.ts @@ -1,26 +1,17 @@ -import type { Chunk, KnowledgeSource, Retriever, RetrievedChunk } from "./types.js"; +import type { Chunk, RetrievedChunk, Retriever } from "./types.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; export class KnowledgeService { - private initialized = false; constructor( - private readonly sources: KnowledgeSource[], + private readonly store: KnowledgeStore, private readonly retriever: Retriever, ) {} - async init(signal?: AbortSignal): Promise { - if (this.initialized) return; - for (const source of this.sources) { - const chunks = await source.loadChunks(signal); - await this.retriever.add(chunks); - } - this.initialized = true; + search(query: string, k: number): Promise { + return this.retriever.search(query, k); } - async search(query: string, k: number): Promise { - return await this.retriever.search(query, k); - } - - async getChunk(id: string): Promise { - return await this.retriever.getChunk(id); + getChunk(id: string): Promise { + return this.store.getChunk(id); } } diff --git a/mcp/src/knowledge/knowledge-store.ts b/mcp/src/knowledge/knowledge-store.ts new file mode 100644 index 0000000000..b7fcd70729 --- /dev/null +++ b/mcp/src/knowledge/knowledge-store.ts @@ -0,0 +1,28 @@ +import type { Chunk } from "./types.js"; + +export const SCHEMA_VERSION = 1; + +export interface IndexMeta { + schemaVersion: number; + retriever: string; + embeddingModel?: string; + embeddingDim?: number; + complete: boolean; +} + +/** Holds chunks, a keyword (FTS) index, and vectors. Backend-agnostic + * (SqliteKnowledgeStore now; PgKnowledgeStore in P2c). */ +export interface KnowledgeStore { + addChunks(chunks: Chunk[]): Promise; + getChunk(id: string): Promise; + ftsSearch(query: string, k: number): Promise; + ensureVectorTable(dim: number): Promise; + upsertVectors(items: { id: string; vector: number[] }[]): Promise; + vectorSearch(vector: number[], k: number): Promise<{ id: string; distance: number }[]>; + isPopulated(): Promise; + hasVectors(): Promise; + setMeta(meta: IndexMeta): Promise; + getMeta(): Promise; + reset(): Promise; + close(): Promise; +} diff --git a/mcp/src/knowledge/pg-e2e.pgtest.ts b/mcp/src/knowledge/pg-e2e.pgtest.ts new file mode 100644 index 0000000000..b37539c32a --- /dev/null +++ b/mcp/src/knowledge/pg-e2e.pgtest.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startPgVector } from "./pg-testcontainer.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { withIngestStore } from "./ingest-store.js"; +import { buildIndex, metaFor } from "./build-index.js"; +import { buildKnowledgeService } from "./build.js"; +import type { KnowledgeSource, Chunk } from "./types.js"; +import type { EmbeddingProvider } from "./embedding.js"; + +const fake: EmbeddingProvider = { + dim: 3, + embed: async (texts) => texts.map((t) => + /reconnect|oom|memory/i.test(t) ? [1, 0, 0] : /policy|access/i.test(t) ? [0, 1, 0] : [0, 0, 1]), +}; +const chunk = (id: string, text: string): Chunk => ({ id, source: "local", title: "T", text, cite: id }); +const src: KnowledgeSource = { id: "local", loadChunks: async () => [ + chunk("local#0", "Netty leak out of memory crash"), chunk("local#1", "policy access control")] }; + +let pg: Awaited>; +beforeAll(async () => { pg = await startPgVector(); }, 120000); +afterAll(async () => { await pg?.stop(); }); + +function cfg() { + return AppConfigSchema.parse({ + knowledge: { + retriever: "hybrid", embedding: { dim: 3 }, + publicSource: { enabled: false }, localDir: { enabled: false }, + store: { kind: "pgvector", pgvector: { connectionString: pg.connectionString, table: "e2e" } }, + }, + }); +} + +describe("pgvector end-to-end", () => { + it("ingest populates pg; server serves hybrid search from it", async () => { + await withIngestStore(cfg(), (store) => buildIndex([src], store, fake, metaFor(cfg(), fake))); + const svc = await buildKnowledgeService(cfg(), { embeddingProvider: fake }); + expect(svc).toBeDefined(); + const hits = await svc!.search("why does it die on reconnect", 2); + expect(hits[0].chunk.text.toLowerCase()).toContain("out of memory"); + expect(hits[0].matchedBy.length).toBeGreaterThan(0); + }); + + it("empty pg store → buildKnowledgeService returns undefined (no auto-write)", async () => { + const c = cfg(); + c.knowledge.store.pgvector!.table = `empty_${Math.floor(performance.now())}`; + const { openStore } = await import("./store-factory.js"); + const store = await openStore(c); + expect(await store.isPopulated()).toBe(false); // Confirm empty before building service. + await store.close(); + const svc = await buildKnowledgeService(c, { embeddingProvider: fake }); + expect(svc).toBeUndefined(); // Server does NOT auto-build for pgvector. + // Confirm the store is STILL empty (proves no auto-write). + const check = await openStore(c); + expect(await check.isPopulated()).toBe(false); + await check.close(); + }); +}); diff --git a/mcp/src/knowledge/pg-knowledge-store.pgtest.ts b/mcp/src/knowledge/pg-knowledge-store.pgtest.ts new file mode 100644 index 0000000000..f4f2ded84c --- /dev/null +++ b/mcp/src/knowledge/pg-knowledge-store.pgtest.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { startPgVector } from "./pg-testcontainer.js"; +import { PgKnowledgeStore } from "./pg-knowledge-store.js"; +import type { Chunk } from "./types.js"; + +const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}` }); + +let pg: Awaited>; +beforeAll(async () => { pg = await startPgVector(); }, 120000); +afterAll(async () => { await pg?.stop(); }); + +async function fresh() { + const store = await PgKnowledgeStore.connect(pg.connectionString, `t_${Math.floor(performance.now())}`); + await store.reset(); + return store; +} + +describe("PgKnowledgeStore", () => { + it("addChunks + getChunk + isPopulated", async () => { + const s = await fresh(); + expect(await s.isPopulated()).toBe(false); + await s.addChunks([chunk("a", "netty out of memory")]); + expect(await s.isPopulated()).toBe(true); + expect((await s.getChunk("a"))?.text).toBe("netty out of memory"); + await s.close(); + }); + + it("ftsSearch ranks keyword matches", async () => { + const s = await fresh(); + await s.addChunks([chunk("a", "netty leak out of memory crash"), chunk("b", "policy access control")]); + expect((await s.ftsSearch("memory crash", 5))[0]).toBe("a"); + expect(await s.ftsSearch("zzznope", 5)).toEqual([]); + await s.close(); + }); + + it("vectorSearch returns nearest; hasVectors reflects state", async () => { + const s = await fresh(); + await s.ensureVectorTable(3); + expect(await s.hasVectors()).toBe(false); + await s.addChunks([chunk("x", "a"), chunk("y", "b")]); + await s.upsertVectors([{ id: "x", vector: [1, 0, 0] }, { id: "y", vector: [0, 1, 0] }]); + expect(await s.hasVectors()).toBe(true); + expect((await s.vectorSearch([1, 0, 0], 1))[0].id).toBe("x"); + await s.close(); + }); + + it("meta round-trip + reset clears everything", async () => { + const s = await fresh(); + await s.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + expect((await s.getMeta())?.complete).toBe(true); + await s.addChunks([chunk("a", "x")]); + await s.reset(); + expect(await s.isPopulated()).toBe(false); + expect(await s.getMeta()).toBeUndefined(); + await s.close(); + }); + + it("reset() drops vec table → re-ingest can change dim", async () => { + const s = await fresh(); + await s.ensureVectorTable(3); + await s.addChunks([chunk("x", "a")]); + await s.upsertVectors([{ id: "x", vector: [1, 0, 0] }]); + expect(await s.hasVectors()).toBe(true); + await s.reset(); + expect(await s.isPopulated()).toBe(false); + // Re-ingest at a different dim succeeds (proves vec table was dropped + recreated). + await s.ensureVectorTable(4); + await s.addChunks([chunk("y", "b")]); + await s.upsertVectors([{ id: "y", vector: [0, 1, 0, 1] }]); + expect(await s.hasVectors()).toBe(true); + expect((await s.vectorSearch([0, 1, 0, 1], 1))[0].id).toBe("y"); + await s.close(); + }); + + it("ftsSearch uses OR semantics (chunk matches ANY token)", async () => { + const s = await fresh(); + await s.addChunks([chunk("a", "netty out of memory crash"), chunk("b", "policy access control")]); + // 2-term query where chunk b matches only "policy" (not "memory") → still returned (OR recall). + const hits = await s.ftsSearch("memory policy", 5); + expect(hits).toContain("a"); // matches "memory" + expect(hits).toContain("b"); // matches "policy" + await s.close(); + }); +}); diff --git a/mcp/src/knowledge/pg-knowledge-store.ts b/mcp/src/knowledge/pg-knowledge-store.ts new file mode 100644 index 0000000000..41eb277cdd --- /dev/null +++ b/mcp/src/knowledge/pg-knowledge-store.ts @@ -0,0 +1,174 @@ +import pg from "pg"; +import type { Chunk } from "./types.js"; +import type { IndexMeta, KnowledgeStore } from "./knowledge-store.js"; + +export class PgKnowledgeStore implements KnowledgeStore { + private constructor( + private readonly pool: pg.Pool, + private readonly t: string, // sanitized table prefix + private vecReady: boolean, + ) {} + + static async connect(connectionString: string, table: string): Promise { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) { + throw new Error(`invalid pgvector table prefix: ${table}`); + } + if (table.length > 50) { + throw new Error(`table prefix too long (max 50 chars): ${table}`); + } + const pool = new pg.Pool({ connectionString }); + try { + await pool.query("CREATE EXTENSION IF NOT EXISTS vector"); + await pool.query( + `CREATE TABLE IF NOT EXISTS ${table}_chunks ( + id TEXT PRIMARY KEY, source TEXT, title TEXT, text TEXT, cite TEXT, + tsv tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(text,''))) STORED + )`, + ); + await pool.query(`CREATE INDEX IF NOT EXISTS ${table}_chunks_tsv ON ${table}_chunks USING GIN (tsv)`); + await pool.query(`CREATE TABLE IF NOT EXISTS ${table}_meta (id INT PRIMARY KEY CHECK (id = 1), json JSONB NOT NULL)`); + const vec = await pool.query( + "SELECT to_regclass($1) AS reg", + [`${table}_vec`], + ); + return new PgKnowledgeStore(pool, table, vec.rows[0].reg !== null); + } catch (e) { + await pool.end(); + throw e; + } + } + + async addChunks(chunks: Chunk[]): Promise { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + for (const c of chunks) { + await client.query( + `INSERT INTO ${this.t}_chunks (id, source, title, text, cite) VALUES ($1,$2,$3,$4,$5) + ON CONFLICT (id) DO UPDATE SET source=$2, title=$3, text=$4, cite=$5`, + [c.id, c.source, c.title, c.text, c.cite], + ); + } + await client.query("COMMIT"); + } catch (e) { + await client.query("ROLLBACK").catch(() => {}); + throw e; + } finally { + client.release(); + } + } + + async getChunk(id: string): Promise { + const r = await this.pool.query( + `SELECT id, source, title, text, cite FROM ${this.t}_chunks WHERE id = $1`, + [id], + ); + return r.rows[0] as Chunk | undefined; + } + + async ftsSearch(query: string, k: number): Promise { + const tokens = [...query.matchAll(/[\p{L}\p{N}]+/gu)].map((m) => m[0]); + if (tokens.length === 0) return []; + const tsquery = tokens.join(" | "); + const r = await this.pool.query( + `SELECT id FROM ${this.t}_chunks + WHERE tsv @@ to_tsquery('english', $1) + ORDER BY ts_rank(tsv, to_tsquery('english', $1)) DESC, id + LIMIT $2`, + [tsquery, k], + ); + return r.rows.map((row) => row.id as string); + } + + async ensureVectorTable(dim: number): Promise { + if (this.vecReady) return; + if (!Number.isInteger(dim) || dim <= 0) throw new Error(`vector dim must be positive (got ${dim})`); + await this.pool.query( + `CREATE TABLE IF NOT EXISTS ${this.t}_vec (id TEXT PRIMARY KEY, embedding vector(${dim}))`, + ); + await this.pool.query( + `CREATE INDEX IF NOT EXISTS ${this.t}_vec_idx ON ${this.t}_vec USING hnsw (embedding vector_cosine_ops)`, + ); + this.vecReady = true; + } + + async upsertVectors(items: { id: string; vector: number[] }[]): Promise { + if (!this.vecReady) throw new Error("call ensureVectorTable(dim) before upsertVectors"); + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + for (const it of items) { + await client.query( + `INSERT INTO ${this.t}_vec (id, embedding) VALUES ($1, $2) + ON CONFLICT (id) DO UPDATE SET embedding = $2`, + [it.id, JSON.stringify(it.vector)], + ); + } + await client.query("COMMIT"); + } catch (e) { + await client.query("ROLLBACK").catch(() => {}); + throw e; + } finally { + client.release(); + } + } + + async vectorSearch(vector: number[], k: number): Promise<{ id: string; distance: number }[]> { + if (!this.vecReady) return []; + const r = await this.pool.query( + `SELECT id, embedding <=> $1 AS distance FROM ${this.t}_vec ORDER BY embedding <=> $1 LIMIT $2`, + [JSON.stringify(vector), k], + ); + return r.rows.map((row) => ({ id: row.id as string, distance: Number(row.distance) })); + } + + async isPopulated(): Promise { + const r = await this.pool.query(`SELECT EXISTS (SELECT 1 FROM ${this.t}_chunks) AS e`); + return r.rows[0].e === true; + } + + async hasVectors(): Promise { + if (!this.vecReady) return false; + const r = await this.pool.query(`SELECT EXISTS (SELECT 1 FROM ${this.t}_vec) AS e`); + return r.rows[0].e === true; + } + + async setMeta(meta: IndexMeta): Promise { + await this.pool.query( + `INSERT INTO ${this.t}_meta (id, json) VALUES (1, $1) ON CONFLICT (id) DO UPDATE SET json = $1`, + [JSON.stringify(meta)], + ); + } + + async getMeta(): Promise { + const r = await this.pool.query(`SELECT json FROM ${this.t}_meta WHERE id = 1`); + if (r.rows.length === 0) return undefined; + try { + const v = r.rows[0].json; + return (typeof v === "string" ? JSON.parse(v) : v) as IndexMeta; + } catch { + return undefined; + } + } + + async reset(): Promise { + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + await client.query(`TRUNCATE ${this.t}_chunks`); + await client.query(`DELETE FROM ${this.t}_meta`); + await client.query(`DROP TABLE IF EXISTS ${this.t}_vec`); + this.vecReady = false; + await client.query("COMMIT"); + } catch (e) { + await client.query("ROLLBACK").catch(() => {}); + throw e; + } finally { + client.release(); + } + } + + async close(): Promise { + await this.pool.end(); + } +} diff --git a/mcp/src/knowledge/pg-smoke.pgtest.ts b/mcp/src/knowledge/pg-smoke.pgtest.ts new file mode 100644 index 0000000000..403cf62c1c --- /dev/null +++ b/mcp/src/knowledge/pg-smoke.pgtest.ts @@ -0,0 +1,18 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { Client } from "pg"; +import { startPgVector } from "./pg-testcontainer.js"; + +let pg: Awaited>; +beforeAll(async () => { pg = await startPgVector(); }, 120000); +afterAll(async () => { await pg?.stop(); }); + +describe("pgvector container", () => { + it("has the vector extension available", async () => { + const client = new Client({ connectionString: pg.connectionString }); + await client.connect(); + await client.query("CREATE EXTENSION IF NOT EXISTS vector"); + const r = await client.query("SELECT '[1,2,3]'::vector AS v"); + expect(r.rows[0].v).toBeDefined(); + await client.end(); + }); +}); diff --git a/mcp/src/knowledge/pg-testcontainer.ts b/mcp/src/knowledge/pg-testcontainer.ts new file mode 100644 index 0000000000..983af0b93c --- /dev/null +++ b/mcp/src/knowledge/pg-testcontainer.ts @@ -0,0 +1,9 @@ +import { PostgreSqlContainer } from "@testcontainers/postgresql"; + +export async function startPgVector(): Promise<{ connectionString: string; stop: () => Promise }> { + const container = await new PostgreSqlContainer("pgvector/pgvector:pg16").start(); + return { + connectionString: container.getConnectionUri(), + stop: () => container.stop().then(() => undefined), + }; +} diff --git a/mcp/src/knowledge/sqlite-knowledge-store.test.ts b/mcp/src/knowledge/sqlite-knowledge-store.test.ts new file mode 100644 index 0000000000..741195673d --- /dev/null +++ b/mcp/src/knowledge/sqlite-knowledge-store.test.ts @@ -0,0 +1,121 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; +import type { Chunk } from "./types.js"; + +const chunk = (id: string, text: string): Chunk => ({ + id, source: "s", title: "T", text, cite: `https://x/${id}`, +}); + +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); + +describe("SqliteKnowledgeStore", () => { + it("stores chunks and returns them by id", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([chunk("a", "hello world")]); + expect((await store.getChunk("a"))?.text).toBe("hello world"); + expect(await store.getChunk("missing")).toBeUndefined(); + }); + + it("keyword-searches via FTS, best match first, honoring k, injection-safe", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([ + chunk("a", "Netty leak out of memory crash on reconnect"), + chunk("b", "policies define access control"), + ]); + expect((await store.ftsSearch("memory crash", 5))[0]).toBe("a"); + expect(await store.ftsSearch("alpha", 1)).toEqual([]); + expect(await store.ftsSearch('"(bad) AND *', 5)).toEqual([]); // no throw + }); + + it("vector-searches nearest first after ensureVectorTable", async () => { + store = new SqliteKnowledgeStore(); + await store.ensureVectorTable(3); + await store.upsertVectors([ + { id: "x", vector: [1, 0, 0] }, + { id: "y", vector: [0, 1, 0] }, + ]); + const hits = await store.vectorSearch([1, 0, 0], 1); + expect(hits[0].id).toBe("x"); + }); + + it("vectorSearch returns [] when no vector table exists", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.vectorSearch([1, 0, 0], 5)).toEqual([]); + }); + + it("isPopulated reflects whether chunks exist", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.isPopulated()).toBe(false); + await store.addChunks([chunk("a", "x")]); + expect(await store.isPopulated()).toBe(true); + }); + + it("hasVectors is false until ensureVectorTable and upsertVectors; true after", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.hasVectors()).toBe(false); + await store.ensureVectorTable(3); + expect(await store.hasVectors()).toBe(false); + await store.upsertVectors([{ id: "a", vector: [1, 0, 0] }]); + expect(await store.hasVectors()).toBe(true); + }); + + it("addChunks is idempotent (re-adding an id does not duplicate FTS results)", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([chunk("a", "reconnect memory crash")]); + await store.addChunks([chunk("a", "reconnect memory crash")]); + expect(await store.ftsSearch("memory", 10)).toEqual(["a"]); + }); + + it("persists to a file: data survives close + reopen", async () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "index.db"); + const w = new SqliteKnowledgeStore(path); + await w.ensureVectorTable(3); + await w.addChunks([chunk("a", "reconnect memory")]); + await w.upsertVectors([{ id: "a", vector: [1, 0, 0] }]); + await w.close(); + + store = new SqliteKnowledgeStore(path); + expect(await store.isPopulated()).toBe(true); + expect((await store.getChunk("a"))?.text).toBe("reconnect memory"); + expect((await store.ftsSearch("memory", 5))[0]).toBe("a"); + expect((await store.vectorSearch([1, 0, 0], 1))[0].id).toBe("a"); + }); + + it("isPopulated/hasVectors are async and correct", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.isPopulated()).toBe(false); + expect(await store.hasVectors()).toBe(false); + }); + + it("stores and returns index metadata", async () => { + store = new SqliteKnowledgeStore(); + expect(await store.getMeta()).toBeUndefined(); + await store.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + expect(await store.getMeta()).toEqual({ schemaVersion: 1, retriever: "fts", complete: true }); + }); + + it("closes the DB handle on failed init (corrupt/non-sqlite file)", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-idx-")); + const path = join(dir, "corrupt.db"); + const { writeFileSync } = require("node:fs"); + writeFileSync(path, "not a db"); + expect(() => new SqliteKnowledgeStore(path)).toThrow(); + }); + + it("reset() clears chunks, fts, vectors, and meta", async () => { + store = new SqliteKnowledgeStore(); + store.ensureVectorTable ? await store.ensureVectorTable(3) : null; + await store.addChunks([{ id: "a", source: "s", title: "T", text: "netty", cite: "x" }]); + await store.upsertVectors([{ id: "a", vector: [1, 0, 0] }]); + await store.setMeta({ schemaVersion: 1, retriever: "fts", complete: true }); + await store.reset(); + expect(await store.isPopulated()).toBe(false); + expect(await store.getChunk("a")).toBeUndefined(); + expect(await store.getMeta()).toBeUndefined(); + }); +}); diff --git a/mcp/src/knowledge/sqlite-knowledge-store.ts b/mcp/src/knowledge/sqlite-knowledge-store.ts new file mode 100644 index 0000000000..2bd859dedb --- /dev/null +++ b/mcp/src/knowledge/sqlite-knowledge-store.ts @@ -0,0 +1,139 @@ +import Database from "better-sqlite3"; +import * as sqliteVec from "sqlite-vec"; +import type { Chunk } from "./types.js"; +import type { KnowledgeStore, IndexMeta } from "./knowledge-store.js"; + +export class SqliteKnowledgeStore implements KnowledgeStore { + private readonly db: Database.Database; + private vecReady = false; + + constructor(path = ":memory:") { + this.db = new Database(path); + try { + sqliteVec.load(this.db); + this.db.exec(` + CREATE TABLE IF NOT EXISTS chunks ( + id TEXT PRIMARY KEY, source TEXT, title TEXT, text TEXT, cite TEXT + ); + CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(id UNINDEXED, title, text); + CREATE TABLE IF NOT EXISTS index_meta (id INTEGER PRIMARY KEY CHECK (id = 1), json TEXT NOT NULL); + `); + // Detect a pre-existing vec table (reopened prebuilt file). + const row = this.db + .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='vec_items'") + .get(); + this.vecReady = row !== undefined; + } catch (e) { + this.db.close(); + throw e; + } + } + + async addChunks(chunks: Chunk[]): Promise { + const insC = this.db.prepare( + "INSERT OR REPLACE INTO chunks (id, source, title, text, cite) VALUES (?, ?, ?, ?, ?)", + ); + const delF = this.db.prepare("DELETE FROM chunks_fts WHERE id = ?"); + const insF = this.db.prepare( + "INSERT INTO chunks_fts (id, title, text) VALUES (?, ?, ?)", + ); + const tx = this.db.transaction((rows: Chunk[]) => { + for (const c of rows) { + insC.run(c.id, c.source, c.title, c.text, c.cite); + delF.run(c.id); + insF.run(c.id, c.title, c.text); + } + }); + tx(chunks); + } + + async getChunk(id: string): Promise { + const r = this.db + .prepare("SELECT id, source, title, text, cite FROM chunks WHERE id = ?") + .get(id) as Chunk | undefined; + return r; + } + + async ftsSearch(query: string, k: number): Promise { + const match = toMatchQuery(query); + if (match === "") return []; + const rows = this.db + .prepare("SELECT id FROM chunks_fts WHERE chunks_fts MATCH ? ORDER BY rank, id LIMIT ?") + .all(match, k) as Array<{ id: string }>; + return rows.map((r) => r.id); + } + + async ensureVectorTable(dim: number): Promise { + if (this.vecReady) return; + if (!Number.isInteger(dim) || dim <= 0) { + throw new Error(`vector dim must be a positive integer (got ${dim})`); + } + this.db.exec( + `CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0(id TEXT PRIMARY KEY, embedding float[${dim}]);`, + ); + this.vecReady = true; + } + + async upsertVectors(items: { id: string; vector: number[] }[]): Promise { + if (!this.vecReady) throw new Error("call ensureVectorTable(dim) before upsertVectors"); + const del = this.db.prepare("DELETE FROM vec_items WHERE id = ?"); + const ins = this.db.prepare("INSERT INTO vec_items (id, embedding) VALUES (?, ?)"); + const tx = this.db.transaction((rows: { id: string; vector: number[] }[]) => { + for (const r of rows) { + del.run(r.id); + ins.run(r.id, JSON.stringify(r.vector)); + } + }); + tx(items); + } + + async vectorSearch(vector: number[], k: number): Promise<{ id: string; distance: number }[]> { + if (!this.vecReady) return []; + const rows = this.db + .prepare("SELECT id, distance FROM vec_items WHERE embedding MATCH ? AND k = ? ORDER BY distance") + .all(JSON.stringify(vector), k) as Array<{ id: string; distance: number }>; + return rows.map((r) => ({ id: r.id, distance: r.distance })); + } + + async isPopulated(): Promise { + const row = this.db.prepare("SELECT COUNT(*) AS n FROM chunks").get() as { n: number }; + return row.n > 0; + } + + async hasVectors(): Promise { + if (!this.vecReady) return false; + const row = this.db.prepare("SELECT COUNT(*) AS n FROM vec_items").get() as { n: number }; + return row.n > 0; + } + + async setMeta(meta: IndexMeta): Promise { + this.db.prepare("INSERT OR REPLACE INTO index_meta (id, json) VALUES (1, ?)") + .run(JSON.stringify(meta)); + } + + async getMeta(): Promise { + const row = this.db.prepare("SELECT json FROM index_meta WHERE id = 1").get() as { json: string } | undefined; + if (!row) return undefined; + try { + return JSON.parse(row.json) as IndexMeta; + } catch { + return undefined; + } + } + + async reset(): Promise { + this.db.exec("DELETE FROM chunks; DELETE FROM chunks_fts; DELETE FROM index_meta;"); + if (this.vecReady) this.db.exec("DELETE FROM vec_items;"); + } + + async close(): Promise { + this.db.close(); + } +} + +/** Arbitrary user text -> safe FTS5 MATCH (word tokens, quoted, OR-joined). */ +function toMatchQuery(query: string): string { + const tokens = query.match(/[\p{L}\p{N}]+/gu); + if (!tokens || tokens.length === 0) return ""; + return tokens.map((t) => `"${t}"`).join(" OR "); +} diff --git a/mcp/src/knowledge/sqlite-vec-store.test.ts b/mcp/src/knowledge/sqlite-vec-store.test.ts deleted file mode 100644 index 6b3a003936..0000000000 --- a/mcp/src/knowledge/sqlite-vec-store.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, it, expect, afterEach } from "vitest"; -import { SqliteVecStore } from "./sqlite-vec-store.js"; - -let store: SqliteVecStore; -afterEach(() => store?.close()); - -describe("SqliteVecStore", () => { - it("returns nearest neighbors ordered by distance", async () => { - store = new SqliteVecStore(3); - await store.upsert([ - { id: "x", vector: [1, 0, 0] }, - { id: "y", vector: [0, 1, 0] }, - { id: "z", vector: [0.9, 0.1, 0] }, - ]); - const hits = await store.searchByVector([1, 0, 0], 2); - expect(hits.map((h) => h.id)).toEqual(["x", "z"]); - expect(hits[0].distance).toBeLessThanOrEqual(hits[1].distance); - }); - - it("respects k", async () => { - store = new SqliteVecStore(2); - await store.upsert([ - { id: "a", vector: [1, 0] }, - { id: "b", vector: [0, 1] }, - { id: "c", vector: [1, 1] }, - ]); - expect(await store.searchByVector([1, 0], 1)).toHaveLength(1); - }); - - it("upsert replaces an existing id", async () => { - store = new SqliteVecStore(2); - await store.upsert([{ id: "a", vector: [1, 0] }]); - await store.upsert([{ id: "a", vector: [0, 1] }]); - const hits = await store.searchByVector([0, 1], 5); - expect(hits.filter((h) => h.id === "a")).toHaveLength(1); - }); - - it("rejects a non-positive dim", () => { - expect(() => new SqliteVecStore(0)).toThrow(/dim/); - }); - - it("rejects a vector whose length != dim", async () => { - store = new SqliteVecStore(3); - await expect(store.upsert([{ id: "a", vector: [1, 0] }])).rejects.toThrow(/length/); - }); -}); diff --git a/mcp/src/knowledge/sqlite-vec-store.ts b/mcp/src/knowledge/sqlite-vec-store.ts deleted file mode 100644 index 838d880462..0000000000 --- a/mcp/src/knowledge/sqlite-vec-store.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Database from "better-sqlite3"; -import * as sqliteVec from "sqlite-vec"; -import type { VectorRecord, VectorHit, VectorStore } from "./vector-store.js"; - -export class SqliteVecStore implements VectorStore { - private readonly db: Database.Database; - private readonly dim: number; - - constructor(dim: number) { - if (!Number.isInteger(dim) || dim <= 0) { - throw new Error(`SqliteVecStore: dim must be a positive integer (got ${dim})`); - } - this.dim = dim; - this.db = new Database(":memory:"); - sqliteVec.load(this.db); - this.db.exec( - `CREATE VIRTUAL TABLE vec_items USING vec0(id TEXT PRIMARY KEY, embedding float[${dim}]);`, - ); - } - - async upsert(records: VectorRecord[]): Promise { - const del = this.db.prepare("DELETE FROM vec_items WHERE id = ?"); - const ins = this.db.prepare( - "INSERT INTO vec_items (id, embedding) VALUES (?, ?)", - ); - const tx = this.db.transaction((rows: VectorRecord[]) => { - for (const r of rows) { - if (r.vector.length !== this.dim) { - throw new Error(`SqliteVecStore: vector length ${r.vector.length} != dim ${this.dim}`); - } - del.run(r.id); - ins.run(r.id, JSON.stringify(r.vector)); - } - }); - tx(records); - } - - async searchByVector(vector: number[], k: number): Promise { - const rows = this.db - .prepare( - "SELECT id, distance FROM vec_items WHERE embedding MATCH ? AND k = ? ORDER BY distance", - ) - .all(JSON.stringify(vector), k) as Array<{ id: string; distance: number }>; - return rows.map((r) => ({ id: r.id, distance: r.distance })); - } - - close(): void { - this.db.close(); - } -} diff --git a/mcp/src/knowledge/store-factory.test.ts b/mcp/src/knowledge/store-factory.test.ts new file mode 100644 index 0000000000..74570c88a8 --- /dev/null +++ b/mcp/src/knowledge/store-factory.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { AppConfigSchema } from "../config/schema.js"; +import { openStore } from "./store-factory.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; + +let store: KnowledgeStore; +afterEach(async () => { await store?.close(); }); + +describe("openStore", () => { + it("opens an in-memory sqlite store by default", async () => { + store = await openStore(AppConfigSchema.parse({}), { path: ":memory:" }); + expect(await store.isPopulated()).toBe(false); + }); +}); diff --git a/mcp/src/knowledge/store-factory.ts b/mcp/src/knowledge/store-factory.ts new file mode 100644 index 0000000000..e3936d5bc0 --- /dev/null +++ b/mcp/src/knowledge/store-factory.ts @@ -0,0 +1,23 @@ +import type { AppConfig } from "../config/schema.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; + +/** Open a KnowledgeStore for the configured backend. `opts.path` overrides the + * sqlite path (e.g. ":memory:" for the in-memory fallback). */ +export async function openStore( + config: AppConfig, + opts: { path?: string } = {}, +): Promise { + const kind = config.knowledge.store.kind; + if (kind === "sqlite") { + const path = opts.path ?? config.knowledge.store.sqlite.path ?? ":memory:"; + return new SqliteKnowledgeStore(path); + } + if (kind === "pgvector") { + const { connectionString, table } = config.knowledge.store.pgvector; + if (!connectionString) throw new Error("knowledge.store.pgvector.connectionString is required"); + const { PgKnowledgeStore } = await import("./pg-knowledge-store.js"); + return PgKnowledgeStore.connect(connectionString, table); + } + throw new Error(`unsupported knowledge.store.kind: ${kind}`); +} diff --git a/mcp/src/knowledge/types.ts b/mcp/src/knowledge/types.ts index 6f0d7e45d9..f8aa8d3201 100644 --- a/mcp/src/knowledge/types.ts +++ b/mcp/src/knowledge/types.ts @@ -19,8 +19,6 @@ export interface KnowledgeSource { /** Indexes chunks and searches them. */ export interface Retriever { - readonly kind: string; // "fts" | "vector" | "hybrid" - add(chunks: Chunk[]): Promise; + readonly kind: string; search(query: string, k: number): Promise; - getChunk(id: string): Promise; } diff --git a/mcp/src/knowledge/vector-retriever.test.ts b/mcp/src/knowledge/vector-retriever.test.ts index 9cf3e1b561..c82aa98354 100644 --- a/mcp/src/knowledge/vector-retriever.test.ts +++ b/mcp/src/knowledge/vector-retriever.test.ts @@ -1,10 +1,9 @@ import { describe, it, expect, afterEach } from "vitest"; import { VectorRetriever } from "./vector-retriever.js"; -import { SqliteVecStore } from "./sqlite-vec-store.js"; +import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; import type { EmbeddingProvider } from "./embedding.js"; import type { Chunk } from "./types.js"; -// Deterministic fake: map each text to a fixed 3-d vector by keyword. const fake: EmbeddingProvider = { dim: 3, embed: async (texts) => @@ -15,25 +14,25 @@ const fake: EmbeddingProvider = { return [0, 0, 1]; }), }; - const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}`, }); -let store: SqliteVecStore; -afterEach(() => store?.close()); +let store: SqliteKnowledgeStore; +afterEach(async () => await store?.close()); describe("VectorRetriever", () => { - it("has kind 'vector'", () => { - store = new SqliteVecStore(3); - const r = new VectorRetriever(fake, store); - expect(r.kind).toBe("vector"); - }); + async function retriever(chunks: Chunk[]) { + store = new SqliteKnowledgeStore(); + await store.ensureVectorTable(fake.dim); + await store.addChunks(chunks); + const vectors = await fake.embed(chunks.map((c) => c.text)); + await store.upsertVectors(chunks.map((c, i) => ({ id: c.id, vector: vectors[i] }))); + return new VectorRetriever(store, fake); + } - it("matches semantically (query near the OOM chunk, not the policy chunk)", async () => { - store = new SqliteVecStore(3); - const r = new VectorRetriever(fake, store); - await r.add([ + it("returns semantic hits tagged matchedBy=['vector']", async () => { + const r = await retriever([ chunk("oom", "Netty leak out of memory crash"), chunk("pol", "policy access control"), ]); @@ -41,12 +40,4 @@ describe("VectorRetriever", () => { expect(hits[0].chunk.id).toBe("oom"); expect(hits[0].matchedBy).toEqual(["vector"]); }); - - it("getChunk returns the stored chunk", async () => { - store = new SqliteVecStore(3); - const r = new VectorRetriever(fake, store); - await r.add([chunk("a", "reconnect memory")]); - expect((await r.getChunk("a"))?.text).toBe("reconnect memory"); - expect(await r.getChunk("missing")).toBeUndefined(); - }); }); diff --git a/mcp/src/knowledge/vector-retriever.ts b/mcp/src/knowledge/vector-retriever.ts index 89d3cd1fd6..a8a56e0c5c 100644 --- a/mcp/src/knowledge/vector-retriever.ts +++ b/mcp/src/knowledge/vector-retriever.ts @@ -1,37 +1,22 @@ -import type { Chunk, Retriever, RetrievedChunk } from "./types.js"; +import type { RetrievedChunk, Retriever } from "./types.js"; import type { EmbeddingProvider } from "./embedding.js"; -import type { VectorStore } from "./vector-store.js"; +import type { KnowledgeStore } from "./knowledge-store.js"; export class VectorRetriever implements Retriever { readonly kind = "vector"; - private readonly chunks = new Map(); - constructor( + private readonly store: KnowledgeStore, private readonly embedder: EmbeddingProvider, - private readonly store: VectorStore, ) {} - async add(chunks: Chunk[]): Promise { - if (chunks.length === 0) return; - const vectors = await this.embedder.embed(chunks.map((c) => c.text)); - await this.store.upsert( - chunks.map((c, i) => ({ id: c.id, vector: vectors[i] })), - ); - for (const c of chunks) this.chunks.set(c.id, c); - } - async search(query: string, k: number): Promise { const [vector] = await this.embedder.embed([query]); - const hits = await this.store.searchByVector(vector, k); + const hits = await this.store.vectorSearch(vector, k); const out: RetrievedChunk[] = []; for (const hit of hits) { - const c = this.chunks.get(hit.id); - if (c) out.push({ chunk: c, matchedBy: ["vector"] }); + const chunk = await this.store.getChunk(hit.id); + if (chunk) out.push({ chunk, matchedBy: ["vector"] }); } return out; } - - async getChunk(id: string): Promise { - return this.chunks.get(id); - } } diff --git a/mcp/src/knowledge/vector-store.ts b/mcp/src/knowledge/vector-store.ts deleted file mode 100644 index 8f082ed9c0..0000000000 --- a/mcp/src/knowledge/vector-store.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface VectorRecord { - id: string; - vector: number[]; -} - -export interface VectorHit { - id: string; - distance: number; -} - -export interface VectorStore { - upsert(records: VectorRecord[]): Promise; - searchByVector(vector: number[], k: number): Promise; - close(): void; -} diff --git a/mcp/src/tools/index.test.ts b/mcp/src/tools/index.test.ts index 15d994d7e1..d74900db44 100644 --- a/mcp/src/tools/index.test.ts +++ b/mcp/src/tools/index.test.ts @@ -2,8 +2,9 @@ import { describe, it, expect, afterEach } from "vitest"; import { registerTools } from "./index.js"; import { AppConfigSchema } from "../config/schema.js"; import { KnowledgeService } from "../knowledge/knowledge-service.js"; +import { SqliteKnowledgeStore } from "../knowledge/sqlite-knowledge-store.js"; import { FtsRetriever } from "../knowledge/fts-retriever.js"; -import type { KnowledgeSource, Chunk } from "../knowledge/types.js"; +import type { Chunk } from "../knowledge/types.js"; const chunk = (id: string, text: string): Chunk => ({ id, @@ -13,13 +14,8 @@ const chunk = (id: string, text: string): Chunk => ({ cite: `https://x/${id}`, }); -const fakeSource = (chunks: Chunk[]): KnowledgeSource => ({ - id: "fake", - loadChunks: async () => chunks, -}); - -let retriever: FtsRetriever | undefined; -afterEach(() => retriever?.close()); +let store: SqliteKnowledgeStore | undefined; +afterEach(() => store?.close()); describe("registerTools wiring", () => { it("registers ping by default", () => { @@ -46,12 +42,10 @@ describe("registerTools wiring", () => { }); it("registers knowledge tools when enabled and service provided", async () => { - retriever = new FtsRetriever(); - const service = new KnowledgeService( - [fakeSource([chunk("a", "test content")])], - retriever, - ); - await service.init(); + store = new SqliteKnowledgeStore(":memory:"); + await store.addChunks([chunk("a", "test content")]); + const retriever = new FtsRetriever(store); + const service = new KnowledgeService(store, retriever); const reg = registerTools( AppConfigSchema.parse({ knowledge: { enabled: true } }), service, diff --git a/mcp/src/tools/knowledge.test.ts b/mcp/src/tools/knowledge.test.ts index 2a591ef3ec..567f9032bf 100644 --- a/mcp/src/tools/knowledge.test.ts +++ b/mcp/src/tools/knowledge.test.ts @@ -1,25 +1,21 @@ import { describe, it, expect, afterEach } from "vitest"; import { KnowledgeService } from "../knowledge/knowledge-service.js"; +import { SqliteKnowledgeStore } from "../knowledge/sqlite-knowledge-store.js"; import { FtsRetriever } from "../knowledge/fts-retriever.js"; import { makeKnowledgeTools } from "./knowledge.js"; import { AppConfigSchema } from "../config/schema.js"; -import type { KnowledgeSource } from "../knowledge/types.js"; - -const src: KnowledgeSource = { - id: "s", - loadChunks: async () => [ - { id: "a", source: "s", title: "Things", text: "a thing is a digital twin", cite: "https://x/a" }, - ], -}; const ctx = { config: AppConfigSchema.parse({}) }; -let r: FtsRetriever; -afterEach(() => r?.close()); +let store: SqliteKnowledgeStore | undefined; +afterEach(() => store?.close()); async function tools() { - r = new FtsRetriever(); - const svc = new KnowledgeService([src], r); - await svc.init(); + store = new SqliteKnowledgeStore(":memory:"); + await store.addChunks([ + { id: "a", source: "s", title: "Things", text: "a thing is a digital twin", cite: "https://x/a" }, + ]); + const retriever = new FtsRetriever(store); + const svc = new KnowledgeService(store, retriever); return Object.fromEntries(makeKnowledgeTools(svc).map((t) => [t.name, t])); } diff --git a/mcp/vitest.config.ts b/mcp/vitest.config.ts index bdaa94bf5c..54bc60652a 100644 --- a/mcp/vitest.config.ts +++ b/mcp/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ globals: false, environment: "node", include: ["src/**/*.test.ts", "src/**/*.itest.ts"], + exclude: ["**/node_modules/**", "**/*.pgtest.ts"], testTimeout: 20000, }, }); diff --git a/mcp/vitest.pg.config.ts b/mcp/vitest.pg.config.ts new file mode 100644 index 0000000000..17a0216006 --- /dev/null +++ b/mcp/vitest.pg.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "vitest/config"; +export default defineConfig({ + test: { globals: false, environment: "node", include: ["src/**/*.pgtest.ts"], testTimeout: 120000, hookTimeout: 120000 }, +}); From f6653dff1cb3b18123d7d74a5f67ee1766318aa1 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Mon, 10 Aug 2026 14:30:19 +0300 Subject: [PATCH 05/11] =?UTF-8?q?feat(mcp):=20Ditto=20action=20tools=20?= =?UTF-8?q?=E2=80=94=20swagger-gen,=20credential=20passthrough,=20ToolPoli?= =?UTF-8?q?cy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenAPI operation parser + operation-to-tool generation with pinned bundled spec, HttpDittoClient, credential passthrough (basic/devops/OIDC client-credentials, config + per-session), ToolPolicy (read-only default, write allowlist, spec-security-aware sudo gating), typed request-body schemas. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/README.md | 165 + mcp/assets/ditto-openapi.yml | 12377 ++++++++++++++++++++++++++ mcp/package-lock.json | 3 +- mcp/package.json | 1 + mcp/src/bin/stdio.ts | 2 +- mcp/src/config/load.test.ts | 15 + mcp/src/config/schema.ts | 33 + mcp/src/ditto/action-tool.test.ts | 108 + mcp/src/ditto/action-tool.ts | 59 + mcp/src/ditto/action-tools.test.ts | 70 + mcp/src/ditto/action-tools.ts | 66 + mcp/src/ditto/bundled-spec.test.ts | 42 + mcp/src/ditto/client.test.ts | 53 + mcp/src/ditto/client.ts | 55 + mcp/src/ditto/credential.test.ts | 51 + mcp/src/ditto/credential.ts | 95 + mcp/src/ditto/fake-ditto.ts | 35 + mcp/src/ditto/fake-oidc.ts | 33 + mcp/src/ditto/openapi.test.ts | 136 + mcp/src/ditto/openapi.ts | 131 + mcp/src/ditto/tool-policy.test.ts | 48 + mcp/src/ditto/tool-policy.ts | 18 + mcp/src/server/build-server.test.ts | 4 +- mcp/src/server/http-app.ts | 2 +- mcp/src/tools/index.test.ts | 19 +- mcp/src/tools/index.ts | 8 +- 26 files changed, 13614 insertions(+), 15 deletions(-) create mode 100644 mcp/assets/ditto-openapi.yml create mode 100644 mcp/src/ditto/action-tool.test.ts create mode 100644 mcp/src/ditto/action-tool.ts create mode 100644 mcp/src/ditto/action-tools.test.ts create mode 100644 mcp/src/ditto/action-tools.ts create mode 100644 mcp/src/ditto/bundled-spec.test.ts create mode 100644 mcp/src/ditto/client.test.ts create mode 100644 mcp/src/ditto/client.ts create mode 100644 mcp/src/ditto/credential.test.ts create mode 100644 mcp/src/ditto/credential.ts create mode 100644 mcp/src/ditto/fake-ditto.ts create mode 100644 mcp/src/ditto/fake-oidc.ts create mode 100644 mcp/src/ditto/openapi.test.ts create mode 100644 mcp/src/ditto/openapi.ts create mode 100644 mcp/src/ditto/tool-policy.test.ts create mode 100644 mcp/src/ditto/tool-policy.ts diff --git a/mcp/README.md b/mcp/README.md index ec2a33d604..badc984455 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -209,6 +209,171 @@ The `ingest` command uses backend-specific atomic writes: The async `KnowledgeStore` lifecycle (`isPopulated()`, `getMeta()`, `setMeta()`, `reset()`, `close()`) enables both `SqliteKnowledgeStore` and `PgKnowledgeStore` to plug in behind the same interface with no churn to `build.ts` or `build-index.ts`. Both backends validate metadata and support offline re-ingest. +## Action tools (P3-1) + +The server exposes action tools dynamically generated from a Ditto OpenAPI specification. +Each action tool makes HTTP calls to a Ditto instance, enforcing a configurable **ToolPolicy** +(read-only by default) and **credential passthrough** (the MCP forwards credentials to Ditto, +which enforces authorization; the MCP never decides access beyond policy gating). + +### Enable Action Tools + +Set `ditto.enabled: true` and provide either a `ditto.baseUrl` or fetch the OpenAPI spec from a custom location: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `ditto.enabled` | `boolean` | `false` | Enable action tools | +| `ditto.baseUrl` | `string` | (required if enabled) | Base URL to the Ditto instance (e.g., `http://localhost:8080`) | +| `ditto.openApi.path` | `string?` | `undefined` | Path to the OpenAPI spec file (reads locally). If unset, falls back to bundled spec. | +| `ditto.openApi.url` | `string?` | `undefined` | URL to fetch the OpenAPI spec from (remote fetch). If unset, falls back to bundled spec. | + +If both `path` and `url` are unset, the server uses the bundled pinned Ditto OpenAPI spec +(`mcp/assets/ditto-openapi.yml`), which is a snapshot of a known Ditto release and works offline. + +### Credential Passthrough + +Action tools support two credential modes: + +1. **Session-level credentials** (per tool call): The caller can pass an `Authorization` header + with each tool invocation. The header is forwarded to the Ditto backend. + +2. **Config-level credentials** (`ditto.credential`): A static credential (basic auth or devops token) + configured at server startup, forwarded to every action-tool call unless overridden by a per-session + `Authorization` header. + +**Credential types:** +- `basic` — username + password (sent as `Authorization: Basic `) +- `devops` — a static token (sent as `Authorization: Bearer `) +- `oidc` — OAuth2 client-credentials flow (sends `Authorization: Bearer `) + +**OIDC client-credentials:** Set `ditto.credential.kind: "oidc"` to enable OAuth2 client-credentials. +The MCP exchanges `clientId` + `clientSecret` for an access token on the first action-tool call, caches it, +and auto-refreshes ~30 seconds before expiry. The access token is forwarded as `Authorization: Bearer `. + +OIDC credential fields: +- `tokenUrl` (required) — OAuth2 token endpoint (e.g., `https://auth.example.com/oauth/token`) +- `clientId` (required) — OAuth2 client identifier +- `clientSecret` (required) — OAuth2 client secret (never logged) +- `scope` (optional) — OAuth2 scopes (space-separated; e.g., `"scope1 scope2"`) + +Example OIDC config: +```json +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "kind": "oidc", + "tokenUrl": "https://auth.example.com/oauth/token", + "clientId": "my-client-id", + "clientSecret": "my-client-secret", + "scope": "ditto:read ditto:write" + } + } +} +``` + +**Authorization enforcement:** The MCP never decides authorization. It forwards the credential +(or `Authorization` header) and lets the Ditto backend enforce access control. Credentials are +never logged by the server. + +### ToolPolicy + +By default, action tools only expose read (`GET`) operations. Write and privileged operations +require explicit allowlisting: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `ditto.policy.allowMethods` | `string[]` | `["GET"]` | Wholesale HTTP method allowlist (applies to all non-sudo operations). Keep this `["GET"]` for read-only; expand for writes. | +| `ditto.policy.writeAllowlist` | `string[]` | `[]` | Per-operation granular allowlist for enabling specific write operations (operationIds). Use this to enable individual writes when `allowMethods` includes write verbs. | +| `ditto.policy.sudoAllowlist` | `string[]` | `[]` | Per-operation allowlist for sudo/devops-privileged operations (operationIds). Required for `/api/2/connections*`, `/devops/*`, and `sudo*` operations. | + +**Sudo operations & devops flag:** +Ditto secures `/api/2/connections*` (secret-bearing) with `DevOpsBasic`/`DevOpsBearer` security, +and `/devops/*` paths are devops-privileged. These operations are classified as "sudo" and: +- Must be explicitly listed in `sudoAllowlist` (by operationId). +- Require a devops-capable credential. If a devops credential is not present, the operation is refused and never sent to Ditto. +- Are NOT auto-allowed even if the method is `GET` and in `allowMethods`. + +Set `credential.devops: true` as an **operator assertion** that the credential is devops-capable. +This gates `sudo*`/`/devops`/`/connections` tools at the MCP layer — Ditto still enforces the real +authorization. Deriving `devops` from token introspection/claims is a future enhancement (currently, +only `basic` and `devops` kinds are implicitly devops-capable; `oidc` requires explicit `devops: true`). + +**Examples:** +- Read-only (default): `{ "allowMethods": ["GET"] }` — only non-sudo GET operations are allowed. +- Enable specific writes: `{ "allowMethods": ["GET", "POST", "PATCH"], "writeAllowlist": ["putThing", "modifyThing"] }` — enables specific write operations. +- Enable sudo: `{ "allowMethods": ["GET"], "sudoAllowlist": ["getConnections", "getLogging"] }` — enables specific devops-privileged operations (requires devops credential). + +### Typed Request Bodies + +Write tools (POST, PATCH, PUT) expose their top-level request body fields in the tool schema, +allowing clients to discover and validate the shape of the request. Nested objects are passed through +as freeform JSON (no further schema introspection). This surfaces the Ditto OpenAPI operation's +request body schema to the MCP tool layer without deeply traversing `allOf`, `oneOf`, or nested `$ref`s. + +For example, a `createThing` operation with a top-level `body.attributes` field will expose +`attributes` as a schema input field; callers can then pass nested objects like `{ "color": "blue" }` +within that field. + +### Action Tools Configuration Examples + +**Example 1: Read-only access (default policy):** +```json +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "type": "basic", + "username": "ditto", + "password": "ditto" + } + } +} +``` +This uses the bundled spec (offline) and forwards basic auth to Ditto. Only `GET` operations are available. + +**Example 2: Write + sudo operations with devops credential:** +```json +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "type": "devops", + "token": "my-devops-secret" + }, + "policy": { + "allowMethods": ["GET", "POST", "PATCH", "DELETE"], + "writeAllowlist": ["/api/2/things", "/api/2/things/{thingId}"], + "sudoAllowlist": ["/devops/piggyback/send"] + } + } +} +``` +This enables write operations on things and `/devops/piggyback/send` (sudo). The devops credential +is forwarded to Ditto for all requests. + +**Example 3: Custom OpenAPI spec from URL:** +```json +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "openApi": { + "url": "http://my-ditto:8080/openapi.json" + }, + "credential": { + "type": "basic", + "username": "user", + "password": "pass" + } + } +} +``` +This fetches the OpenAPI spec from a remote URL instead of using the bundled spec. + ### HTTP Server Options (`server.http`) | Field | Type | Default | Description | diff --git a/mcp/assets/ditto-openapi.yml b/mcp/assets/ditto-openapi.yml new file mode 100644 index 0000000000..00c078bc97 --- /dev/null +++ b/mcp/assets/ditto-openapi.yml @@ -0,0 +1,12377 @@ +openapi: 3.0.0 +info: + title: Eclipse Ditto™ HTTP API + version: '2' + description: |- + JSON-based, REST-like API for Eclipse Ditto + + The Eclipse Ditto HTTP API uses response status codes (see [RFC 7231](https://tools.ietf.org/html/rfc7231#section-6)) + to indicate whether a specific request has been successfully completed, or not. + + The information Ditto provides additionally to the status code (e.g. in API docs, or error codes like. "things:thing.tooLarge") might change without advance notice. + These are not be considered as official API, and must therefore not be applied in your applications or tests. +servers: + - url: 'https://ditto.eclipseprojects.io/' + description: online Ditto Sandbox + - url: / + description: local Ditto +tags: + - name: Things + description: Manage every thing + - name: Features + description: Structure the features of your things + - name: Policies + description: Control access to your things + - name: Things-Search + description: Find every thing + - name: Messages + description: Talk with your things + - name: CloudEvents + description: Process CloudEvents in Ditto + - name: Connections + description: Manage connections + - name: WoT + description: WoT (Web of Things) Discovery endpoints + - name: Devops + description: Devops APIs to manage log levels and configuration in runtime and send piggyback command +security: + - OpenIDConnect: [] + - NginxBasic: [] + - Bearer: [] +paths: + /api/2/things: + get: + summary: Retrieve visible things or things with specified IDs + description: |- + Returns all visible things or things passed in by the required parameter `ids`, which you (the authorized subject) are allowed to read. + + Optionally, if you want to retrieve only some of the thing's fields, you can use the specific field selectors (see parameter `fields`) . + + Tip: In order to formulate a `filter` which things to search for, take a look at the `/search` resource. + tags: + - Things + parameters: + - name: ids + in: query + description: Contains a comma-separated list of `thingId`s to retrieve in one single request. + required: false + schema: + type: string + - $ref: '#/components/parameters/ThingFieldsQueryParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: |- + The successfully completed request contains a list of the for the user available Things, or the Things asked for via the `ids` paramter. + The Things are sorted either by their ID, or in the same order as the Thing IDs were provided in the `ids` parameter. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Thing' + application/td+json: + schema: + type: array + items: + $ref: '#/components/schemas/WotThingDescription' + '400': + description: The request could not be completed. At least one of the defined query parameters was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '414': + description: The request could not be completed due to an URI length exceeding 8k characters. + post: + summary: Create a new thing + description: |- + Creates a thing with a default `thingId` and a default `policyId`. + + The thing will be empty, i.e. no features, definition, attributes etc. by default. + + The default `thingId` consists of your default namespace and a UUID. + + The default `policyId` is identical with the default `thingId`, and allows the currently authorized subject all permissions. + + In case you need to create a thing with a specific ID, use a *PUT* request instead, as any `thingId` specified in the request body will be ignored. + + The field `_created` is filled automatically with the timestamp of the creation. The field is read-only and can + be retrieved later by explicitly selecting it or used in search filters. + tags: + - Things + parameters: + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/AllowPolicyLockoutParam' + - $ref: '#/components/parameters/Namespace' + responses: + '201': + description: The thing was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created thing resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` must not be set in the request body + * the JSON body of the thing to be created is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. + Possible reasons: + * the caller would not have access to the thing after creating it with the given policy. + * the caller has insufficient permissions. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller had insufficient permissions to read the referenced thing. + * the policy that should be copied does not exist. + * the caller had insufficient permissions to read the policy that should be copied. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewThing' + example: + definition: 'com.acme:coffeebrewer:0.1.0' + attributes: + manufacturer: ACME demo corp. + location: 'Berlin, main floor' + serialno: '42' + model: Speaking coffee machine + features: + coffee-brewer: + definition: + - 'com.acme:coffeebrewer:0.1.0' + properties: + brewed-coffees: 0 + water-tank: + properties: + configuration: + smartMode: true + brewingTemp: 87 + tempToHold: 44 + timeoutSeconds: 6000 + status: + waterAmount: 731 + temperature: 44 + description: 'JSON representation of the thing to be created. Use ''{}'' to create an empty thing with a default policy.' + '/api/2/things/{thingId}': + get: + summary: Retrieve a specific thing + description: |- + Returns the thing identified by the `thingId` path parameter. The response includes details about the thing, + including the `policyId`, attributes, definition and features. + + Optionally, you can use the field selectors (see parameter `fields`) to only get specific fields, + which you are interested in. + + ### Example: + Use the field selector `_policy` to retrieve the content of the policy. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/ThingFieldsQueryParam' + - $ref: '#/components/parameters/IfMatchHeaderParam' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParamPutDescription' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The request successfully returned the specific thing. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + application/td+json: + schema: + $ref: '#/components/schemas/WotThingDescription' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * at least one of the defined query parameters is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update a thing with a specified ID + description: |- + Create or update the thing specified by the `thingId` path parameter and the optional JSON body. + + * If you set a new `thingId` in the path, a thing will be created. + * If you set an existing `thingId` in the path, the thing will be updated. + + + ### Create a new thing + At the initial creation of a thing, only a valid `thingId` is required. + However, you can create a full-fledged thing all at once. + + ### Example: + To create a coffee maker thing, set the `thingId` in the path, e.g. to "com.acme.coffeemaker:BE-42" + and the body part, like in the following snippet. + + ``` + { + "definition": "com.acme:coffeebrewer:0.1.0", + "attributes": { + "manufacturer": "ACME demo corp.", + "location": "Berlin, main floor", + "serialno": "42", + "model": "Speaking coffee machine" + }, + "features": { + "coffee-brewer": { + "definition": [ "com.acme:coffeebrewer:0.1.0" ], + "properties": { + "brewed-coffees": 0 + } + }, + "water-tank": { + "properties": { + "configuration": { + "smartMode": true, + "brewingTemp": 87, + "tempToHold": 44, + "timeoutSeconds": 6000 + }, + "status": { + "waterAmount": 731, + "temperature": 44 + } + } + } + } + } + ``` + As the example does not set a policy in the request body, but the thing concept requires one, + the service will create a default policy. The default policy, has the exactly same id + as the thing, and grants ALL permissions to the authorized subject. + + In case you need to associate the new thing to an already existing policy you can additionally + set a policy e.g. "policyId": "com.acme.coffeemaker:policy-1" as the first element in the body part. + Keep in mind, that you can also change the assignment to another policy anytime, + with a request on the sub-resource "PUT /things/{thingId}/policyId" + + The field `_created` is filled automatically with the timestamp of the creation. The field is read-only and can + be retrieved later by explicitly selecting it or used in search filters. + + ### Update an existing thing + + For updating an existing thing, the authorized subject needs **WRITE** permission on the thing's root resource. + + The ID of a thing cannot be changed after creation. Any `thingId` + specified in the request body is therefore ignored. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParam' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The thing was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created thing resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + '204': + description: The thing was successfully modified. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the thing to be created/modified is invalid + * the JSON body of the thing to be created/modified contains a `thingId` + which does not match the ID in the path + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller would not have access to the thing after creating it with the given policy + * the caller has insufficient permissions. + For modifying an existing thing, an unrestricted `WRITE` permission on the thing's root resource is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller has insufficient permissions to read the referenced thing. + * the policy that should be copied does not exist. + * the caller has insufficient permissions to read the policy that should be copied. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewThing' + example: + definition: 'com.acme:coffeebrewer:0.1.0' + attributes: + manufacturer: ACME demo corp. + location: 'Berlin, main floor' + serialno: '42' + model: Speaking coffee machine + features: + coffee-brewer: + definition: + - 'com.acme:coffeebrewer:0.1.0' + properties: + brewed-coffees: 0 + water-tank: + properties: + configuration: + smartMode: true + brewingTemp: 87 + tempToHold: 44 + timeoutSeconds: 6000 + status: + waterAmount: 731 + temperature: 44 + description: JSON representation of the thing to be modified. + patch: + summary: Create or patch a thing with a specified ID + description: |- + Create or patch an existing thing specified by the `thingId` path parameter. + + If the thing did not yet exist, it will be created. + For an existing thing, patching a thing will merge the provided request body with the existing thing values. + This makes it possible to change only some parts of a thing in single request without providing the full thing + structure in the request body. + + + ### Patch a thing + + With this resource it is possible to add, update or delete parts of an existing thing or to create the thing if it + does not yet exist. + The request body provided in *JSON merge patch* (RFC-7396) format will be merged with the existing thing. + Notice that the `null` value in the JSON body will delete the specified JSON key from the thing. + For further documentation of JSON merge patch see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + + ### Example + A Thing already exists with the following content: + + ``` + { + "definition": "com.acme:coffeebrewer:0.1.0", + "attributes": { + "manufacturer": "ACME demo corp.", + "location": "Berlin, main floor", + "serialno": "42", + "model": "Speaking coffee machine" + }, + "features": { + "coffee-brewer": { + "definition": ["com.acme:coffeebrewer:0.1.0"], + "properties": { + "brewed-coffees": 0 + } + }, + "water-tank": { + "properties": { + "configuration": { + "smartMode": true, + "brewingTemp": 87, + "tempToHold": 44, + "timeoutSeconds": 6000 + }, + "status": { + "waterAmount": 731, + "temperature": 44 + } + } + } + } + } + ``` + + To make changes that only affect parts of the existing thing, e.g. add some attribute and delete a + specific feature property, the content of the request body could look like this: + + ``` + { + "attributes": { + "manufacturingYear": "2020" + }, + "features": { + "water-tank": { + "properties": { + "configuration": { + "smartMode": null, + "tempToHold": 50, + } + } + } + } + } + ``` + + The request body will be merged with the existing thing and the result will be the following thing: + + ``` + { + "definition": "com.acme:coffeebrewer:0.1.0", + "attributes": { + "manufacturer": "ACME demo corp.", + "manufacturingYear": "2020", + "location": "Berlin, main floor", + "serialno": "42", + "model": "Speaking coffee machine" + }, + "features": { + "coffee-brewer": { + "definition": ["com.acme:coffeebrewer:0.1.0"], + "properties": { + "brewed-coffees": 0 + } + }, + "water-tank": { + "properties": { + "configuration": { + "brewingTemp": 87, + "tempToHold": 50, + "timeoutSeconds": 6000 + }, + "status": { + "waterAmount": 731, + "temperature": 44 + } + } + } + } + } + ``` + + ### Permissions for patching an existing Thing + + For updating an existing thing, the authorized subject needs **WRITE** permission on those parts of the thing + that are affected by the merge update. + + For example, to successfully execute the above example the authorized subject needs to have unrestricted + *WRITE* permissions on all affected paths of the JSON merge patch: `attributes/manufacturingYear`, + `features/water-tank/properties/configuration/smartMode`, + `features/water-tank/properties/configuration/tempToHold`. The *WRITE* permission must not be revoked on any + level further down the hierarchy. Consequently it is also sufficient for the authorized subject to have + unrestricted *WRITE* permission at root level or unrestricted *WRITE* permission at `/attributes` and + `/features` etc. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParam' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The thing was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created thing resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Thing' + '204': + description: The thing was successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the JSON body of the thing to be patched is invalid + * the JSON body of the thing to be patched contains a `thingId` which does not match the ID in the path + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller would not have access to the thing after creating it with the given policy + * the caller has insufficient permissions. + For modifying an existing thing, an unrestricted `WRITE` permission on the thing's root resource is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller has insufficient permissions to read the referenced thing. + * the policy that should be copied does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/PatchThing' + example: + attributes: + manufacturingYear: '2020' + features: + water-tank: + properties: + configuration: + smartMode: null + tempToHold: 50 + description: JSON representation of the thing to be patched. + delete: + summary: Delete a specific thing + description: |- + Deletes the thing identified by the `thingId` path parameter. + + This will not delete the policy, which is used for controlling access to this thing. + + You can delete the policy afterwards via DELETE `/policies/{policyId}` if you don't need it for other things. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParam' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The thing was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller had insufficient permissions. + For deleting an existing thing, an unrestricted `WRITE` permission on the thing's root resource is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/migrateDefinition': + post: + summary: Update the definition of an existing Thing + description: |- + Updates the definition of the specified thing by providing a new definition URL along with an optional migration payload. + + The request body allows specifying: + - A new Thing definition URL. + - A migration payload containing updates to attributes and features. + - Patch conditions to ensure consistent updates. + - Whether properties should be initialized if missing. + + **Placeholders in migration payload:** String values in `migrationPayload` may use the thing-json + placeholder. Both brace `{{ thing-json: }}` and legacy `${ thing-json: }` + are supported. + + If the `dry-run` query parameter or header is set to `true`, the request will return the calculated migration result without applying any changes. + + ### Example: + ```json + { + "thingDefinitionUrl": "https://example.com/new-thing-definition.json", + "migrationPayload": { + "attributes": { + "manufacturer": "New Corp" + }, + "features": { + "sensor": { + "properties": { + "status": { + "temperature": { + "value": 25.0 + } + } + } + } + } + }, + "patchConditions": { + "thing:/features/sensor": "not(exists(/features/sensor))" + }, + "initializeMissingPropertiesFromDefaults": true + } + ``` + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - name: dry-run + in: query + description: 'If set to `true`, performs a dry-run and returns the migration result without applying changes.' + required: false + schema: + type: boolean + default: false + requestBody: + $ref: '#/components/requestBodies/MigrateThingDefinitionRequest' + responses: + '200': + $ref: '#/components/responses/MigrateThingDefinitionResponse' + '202': + description: Dry-run successful. The migration result is returned without applying changes. + content: + application/json: + schema: + $ref: '#/components/schemas/MigrateThingDefinitionResponse' + '400': + description: The request could not be processed due to invalid input. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: Unauthorized request due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The specified thing could not be found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + description: The update conditions were not met. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '/api/2/things/{thingId}/definition': + get: + summary: Retrieve the definition of a specific thing + description: Returns the definition of the thing identified by the `thingId` path parameter. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The request successfully returned the definition of the specific thing. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Definition' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation + (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the definition of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update the definition of a specific thing + description: |- + * If the thing does not have a definition yet, this request will create it. + * If the thing already has a definition you can assign it to a new one by setting the new definition in the request body. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The definition was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created definition resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Definition' + '204': + description: The definition was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation + (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON was invalid + * the request body was not a JSON object. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying a definition of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Definition' + patch: + summary: Patch the definition of a specific thing + description: |- + * If the thing does not have a definition yet, this request will create it. + * If the thing already has a definition you can replace it by providing the new definition in the request body. + * If the request body is set to `null` then the defintion will be deleted. + + Notice that the `null` value in the JSON body has a special meaning and will delete the definition from the thing. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The definition was successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation + (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON was invalid + * the request body was not a JSON object. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying a definition of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/Definition' + example: '"example:test:definition"' + description: |- + JSON string representation of the definition to be patched. + + Consider that the value has to be a JSON string. + + Examples: + + * a string: `"value"` - Currently the definition should follow the pattern: [_a-zA-Z0-9\-]:[_a-zA-Z0-9\-]:[_a-zA-Z0-9\-] + * an empty string: `""` + * `null`: the definition will be deleted + delete: + summary: Delete the definition of a specific thing + description: Deletes the definition of the thing identified by the `thingId`. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The definition was successfully deleted. + '400': + description: |- + The request could not be completed. The `thingId` does not conform to the namespaced entity ID notation + (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying a definition of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID or its definition was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/policyId': + get: + summary: Retrieve the policy ID of a thing + description: Returns the policy ID of the thing identified by the `thingId` path parameter. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The request successfully returned the policy ID. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + type: string + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Update the policy ID of a thing + description: |- + Update the policy ID of the thing identified by the `thingId` path parameter. + + ### Update + If the thing already has a `policyId` you can assign it to an existing policy by setting the new `policyId` + in the request body. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The policy ID was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The thing with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + type: string + example: '"your.namespace:your-policy-name"' + description: |- + The policy is used for controlling access to this thing. It is managed by + resource `/policies/{policyId}`. + + The ID of a policy needs to conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + required: true + patch: + summary: Patch the policy ID of a thing + description: |- + Patch the policy ID of the thing identified by the `thingId` path parameter. + + The `policyId` of the thing will be updated. + Notice that for this resource it is not possible to remove the `policyId` from the thing by setting the + payload to `null`. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: 'The policy ID was successfully patched. Note: You will need to create the policy content separately.' + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the `policyId` can not be removed from a thing. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + type: string + example: '"your.namespace:your-policy-name"' + description: |- + The policy is used for controlling access to this thing. It is managed by resource `/policies/{policyId}`. + + The ID of a policy needs to conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + required: true + '/api/2/things/{thingId}/attributes': + get: + summary: List all attributes of a specific thing + description: Returns all attributes of the thing identified by the `thingId` path parameter. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/AttributesFieldsQueryParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The attributes of the specific thing were successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Attributes' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update all attributes of a specific thing at once + description: |- + Create or update the attributes of a thing identified by the `thingId` + path parameter. The attributes will be overwritten - all at once - with the + content (JSON) set in the request body. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The attributes were successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created attribute resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Attributes' + '204': + description: The attributes were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the attributes to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the attributes of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Attributes' + patch: + summary: Patch all attributes of a specific thing + description: |- + Patch the attributes of a thing identified by the `thingId` path parameter. + The existing attributes will be merged with the JSON content set in the request body. + + Notice that the `null` value has a special meaning and can be used to delete all or specific attributes from a thing. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + **Note**: In contrast to the "PUT things/{thingId}/attributes" request, + a partial update is supported here and request body is merged with the existing attributes. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The attributes were successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the attributes to be patched is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the attributes of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/Attributes' + example: + manufacturer: + name: ACME demo corp. + location: 'Berlin, main floor' + coffeemaker: + serialno: '42' + model: Speaking coffee machine + description: |- + JSON object of all attributes to be patched. Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). + + Examples: + * a simple object: `{ "key": "value"}` - We strongly recommend to use a restricted set of characters for the key (identifier), as the key might be needed for the (URL) path later.
Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + * a nested object as shown in the example value + * `null`: deletes all attributes + required: true + delete: + summary: Delete all attributes of a specific thing at once + description: Deletes all attributes of the thing identified by the `thingId` path parameter. + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The attributes were successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting all attributes of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID or its attributes were not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/attributes/{attributePath}': + get: + summary: Retrieve a specific attribute of a specific thing + description: |- + Returns a specific attribute of the thing identified by the `thingId` path parameter. + + The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). + + ### Example: + + In order to retrieve the `name` field of an `manufacturer` attribute, the full path would be + `/things/{thingId}/attributes/manufacturer/name` + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/AttributesPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The attribute was successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The thing with the given ID or + the attribute at the specified path was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update a specific attribute of a specific thing + description: |- + Create or update a specific attribute of the thing identified by the `thingId` path parameter. + + * If you specify a new attribute path, this will be created + * If you specify an existing attribute path, this will be updated + + The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). + + ### Example: + + In order to put the `name` field of an `manufacturer` attribute, the full path would be + `/things/{thingId}/attributes/manufacturer/name` + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/AttributesPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The attribute was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created attribute resource + schema: + type: string + '204': + description: The attribute was successfully modified. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying an attribute of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Value' + patch: + summary: Patch a specific attribute of a specific thing + description: |- + Patch a specific attribute of a thing identified by the `thingId` path parameter. + + * If you specify a new attribute path, this will be created + * If you specify an existing attribute path, this will be merged + * If you set the request body to `null` for an existing attribute path then the attribute will be deleted. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). + + ### Example: + + In order to patch the `name` field of an `manufacturer` attribute, the full path would be + `/things/{thingId}/attributes/manufacturer/name` + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/AttributesPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The attribute was successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying an attribute of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/PatchValue' + delete: + summary: Delete a specific attribute of a specific thing + description: |- + Deletes a specific attribute of the thing identified by the `thingId` path parameter. + + The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). + + ### Example: + In order to delete the `name` field of an `manufacturer` attribute, the full path would be + `/things/{thingId}/attributes/manufacturer/name` + tags: + - Things + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/AttributesPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The attribute was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting a single attribute of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID or the attribute at the specified path was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/features': + get: + summary: List all features of a specific thing + description: Returns all features of the thing identified by the `thingId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeaturesFieldsQueryParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: |- + The list of features of the specific thing were successfully + retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Features' + example: + featureId1: + definition: + - 'namespace:definition1:v1.0' + properties: + property1: value1 + featureId2: + definition: + - 'namespace:definition2:v1.0' + properties: + property2: value2 + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined query parameters is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The thing with the given ID was + not found or the features have not been defined. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify all features of a specific thing at once + description: |- + Create or modify all features of a thing identified by the `thingId` path parameter. + + ### Create all features at once + In case at the initial creation of your thing you have not specified any features, these can be created here. + + ### Update all features at once + To update all features at once prepare the JSON body accordingly. + + Note: In contrast to the "PUT thing" request, a partial update is not supported here, + but the content will be **overwritten**. + If you need to update single features or their paths, please use the sub-resources instead. + + ### Example: + + ``` + { + "coffee-brewer": { + "definition": ["com.acme:coffeebrewer:0.1.0"], + "properties": { + "brewed-coffees": 0 + } + }, + "water-tank": { + "properties": { + "configuration": { + "smartMode": true, + "brewingTemp": 87, + "tempToHold": 44, + "timeoutSeconds": 6000 + }, + "status": { + "waterAmount": 731, + "temperature": 44 + } + } + } + } + ``` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The features were successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created features resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Features' + example: {} + '204': + description: The features were successfully modified. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the feature to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying all features of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Features' + example: + coffee-brewer: + properties: + definition: + - 'com.acme:coffeebrewer:0.1.0' + brewed-coffees: 0 + water-tank: + properties: + configuration: + smartMode: true + brewingTemp: 87 + tempToHold: 44 + timeoutSeconds: 6000 + status: + waterAmount: 731 + temperature: 44 + description: |- + JSON object of all features to be modified at once. Consider that the value has to be a JSON object or null. + + Examples: + * an empty object: {} - would just delete all old features + * an empty feature: { "featureId": {} } - We strongly recommend to use a restricted set of characters + for the `featureId`, as it might be needed for the (URL) path later. + + Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9-]* + + * a nested object with multiple features as shown in the example value field + required: true + patch: + summary: Patch all features of a specific thing + description: |- + Patch all features of a thing identified by the `thingId` path parameter. + + The existing features will be merged with the JSON content set in the request body. + + Notice that the `null` value has a special meaning and can be used to delete specific features from the thing. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + **Note**: In contrast to the "PUT thing/{thingId}/features" request, a partial update is supported here + and request body is merged with the existing features. + + ### Example + + The following example will add/update the properties `brewed-coffees`, `tempToHold` and `failState`. + The configuration property `smartMode` will be deleted from the thing. + + + ``` + { + "coffee-brewer": { + "properties": { + "brewed-coffees": 10 + } + }, + "water-tank": { + "properties": { + "configuration": { + "smartMode": null, + "tempToHold": 50, + }, + "status": { + "failState": true + } + } + } + } + ``` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The features were successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the feature to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying all features of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/Features' + example: + coffee-brewer: + properties: + brewed-coffees: 10 + water-tank: + properties: + configuration: + smartMode: null + tempToHold: 50 + status: + failState: true + description: |- + JSON object of all features to be patched. Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). + + Examples: + * a nested object with multiple features as shown in the example value field + + * **Note**: To delete certain entries of a feature the `null` value can be used. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + required: true + delete: + summary: Delete all features of a specific thing + description: Deletes all features of the thing identified by the `thingId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The features were successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting all features of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The thing with the given ID was + not found or the features have not been defined. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/features/{featureId}': + get: + summary: Retrieve a specific feature of a specific thing + description: |- + Returns a specific feature identified by the `featureId` path parameter of the thing + identified by the `thingId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/FeatureFieldsQueryParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The feature was successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Feature' + application/td+json: + schema: + $ref: '#/components/schemas/WotThingDescription' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined query parameters is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The thing with the given ID or + the feature with the specified `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify a specific feature of a specific thing + description: |- + Create or modify a specific feature identified by the `featureId` path + parameter of the thing identified by the `thingId` path parameter. + + ### Create feature + If the feature ID is new, the feature and all content from the JSON body will be created + + ### Update feature + If the feature ID is used already in this thing, the feature will be overwrittern + with the content from the JSON body. + + ### Example: + Set the `featureId` to **coffee-brewer** and all properties in the body part. + + ``` + { + "definition": ["com.acme:coffeebrewer:0.1.0"], + "properties": { + "brewed-coffees": 42 + } + } + ``` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The feature was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created feature resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Feature' + '204': + description: The feature was successfully modified. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the feature to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying a single feature of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Feature' + example: + definition: + - 'com.acme:coffeemaker:0.1.0' + - 'com.acme:coffeemaker:1.1.0' + properties: + connected: true + brewed-coffees: 0 + description: |- + JSON representation of the feature to be created/modified. + Consider that the value has to be a JSON object or null. + + Examples: + * an empty object: {} - would just create the featureID but would delete all content of the feature + * a nested object with multiple model definitions and multiple properties as shown in the example value field + required: true + patch: + summary: Patch a specific feature of a specific thing + description: |- + Patch a specific feature identified by the `featureId` path parameter of a thing identified by the `thingId` path parameter. + + The existing feature will be merged with the JSON content set in the request body. + + Notice that the `null` value can be used to delete the whole feature or specific parts of it. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + **Note**: In contrast to the "PUT things/{thingId}/features/{featureId}" request, + a partial update is supported here and request body is merged with the existing feature. + + ### Example + + Set the `featureId` to **coffee-brewer** and all properties in the body part + to update the `brewed-coffees` property and delete the definition. + + ``` + { + "definition": null, + "properties": { + "brewed-coffees": 42 + } + } + ``` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The feature was successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the feature to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying a single feature of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/Feature' + example: + definition: null + properties: + connected: true + brewed-coffees: 0 + description: |- + JSON representation of the feature to be patched. Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). + + Examples: + * a nested object with multiple model definitions and multiple properties as shown in the example value field + * **Note**: To delete certain properties of a feature the `null` value can be used. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + required: true + delete: + summary: Delete a specific feature of a specific thing + description: |- + Deletes a specific feature identified by the `featureId` path parameter + of the thing identified by the `thingId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The feature was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting a single feature of an existing thing, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing with the given ID or the feature at the specified path was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/features/{featureId}/definition': + get: + summary: List the definition of a feature + description: Returns the complete definition field of the feature identified by the `thingId` and `featureId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The definition was successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureDefinition' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined query parameters is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified feature has no + definition or the thing with the specified `thingId` or the feature + with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update the definition of a feature + description: |- + Create or update the complete definition of a feature identified by the `thingId` and `featureId` path parameter. + + The definition field will be overwritten with the JSON array set in the request body + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The definition was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created definition resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureDefinition' + '204': + description: The definition was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the definition of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureDefinition' + example: + - 'com.acme:coffeebrewer:0.1.0' + - 'com.acme:coffeebrewer:1.0.0' + description: |- + JSON array of the complete definition to be updated. + + Consider that the value has to be a JSON array or `null`. + + The content of the JSON array are strings in the format `"::"` or a valid HTTP(s) URL, which is enforced. + required: true + patch: + summary: Patch the definition of a feature + description: |- + Patch the definition of a feature identified by the `thingId` and `featureId` path parameter. + + The existing definition field will be overwritten with the JSON array set in the request body. + + Notice that the `null` value can be used to delete the definition of a feature. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The definition was successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the definition of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/FeatureDefinition' + example: + - 'com.acme:coffeebrewer:0.1.0' + - 'com.acme:coffeebrewer:1.1.0' + description: |- + JSON array of the complete definition to be patched. Consider that the value has to be a JSON array. + + The content of the JSON array are strings in the format `"::"` or a valid HTTP(s) URL, which is enforced. + To delete the definition use `null` as content in the request body. + required: true + delete: + summary: Delete the definition of a feature + description: Deletes the complete definition of the feature identified by the `thingId` and `featureId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The definition was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting the definition of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified feature has no definition or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/features/{featureId}/properties': + get: + summary: List all properties of a feature + description: Returns all properties of the feature identified by the `thingId` and `featureId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertiesFieldsQueryParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The properties were successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureProperties' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined query parameters is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified feature has no properties or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update all properties of a feature at once + description: |- + Create or update the properties of a feature identified by the `thingId` and `featureId` path parameter. + + The properties will be overwritten with the JSON content from the request body. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The properties were successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureProperties' + '204': + description: The properties were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the feature properties to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureProperties' + example: + configuration: + smartMode: true + brewingTemp: 87 + tempToHold: 44 + timeoutSeconds: 6000 + status: + waterAmount: 731 + temperature: 44 + description: |- + JSON object of all properties to be updated at once. + + Consider that the value has to be a JSON object or `null`. We strongly recommend to use + a restricted set of characters for the key (identifier). + + Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + required: true + patch: + summary: Patch all properties of a feature + description: |- + Patch the properties of a feature identified by the `thingId` and `featureId` path parameter. + + The existing properties will be merged with the JSON content set in the request body. + + Notice that the `null` value can be used to delete specific feature properties. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + **Note**: In contrast to the "PUT things/{thingId}/features/{featureId}/properties" request, + a partial update is supported here and request body is merged with the existing properties. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The properties were successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the feature properties to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/FeatureProperties' + example: + configuration: + smartMode: null + brewingTemp: 87 + tempToHold: 44 + timeoutSeconds: 6000 + status: + waterAmount: 731 + temperature: 44 + description: |- + JSON object of all properties to be patched. + + Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). + We strongly recommend to use a restricted set of characters for the key (identifier). + + Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + required: true + delete: + summary: Delete all properties of a feature + description: Deletes all properties of the feature identified by the `thingId` and `featureId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The properties were successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting the properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified feature has no properties or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/features/{featureId}/properties/{propertyPath}': + get: + summary: Retrieve a specific property of a feature + description: |- + Returns the a specific property path of the feature identified by the `thingId` and `featureId` path parameter. + + The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901) + + ### Example + To retrieve the value of the `brewingTemp` in the `water-tank` of our coffeemaker example the full path is: + `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The property was successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified property or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update a specific property of a feature + description: |- + Create or update a specific property of a feature identified by the `thingId` and `featureId` path parameter. + + The property will be created if it doesn't exist or else updated. + + The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901), + + ### Example + To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: + `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The property was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '204': + description: The property was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For creating/updating a property of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Value' + patch: + summary: Patch a specific property of a feature + description: |- + Patch a specific property of a feature identified by the `thingId` and `featureId` path parameter. + + The existing property will be merged with the existing one of the thing. + + Notice that the `null` value can be used to delete the specified propertyPath. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). + + ### Example + To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: + + `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The property was successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For creating/updating a property of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/PatchValue' + delete: + summary: Delete a specific property of a feature + description: |- + Deletes a specific property of the feature identified by the `thingId` and `featureId` path parameter. + + The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901) + + ### Example + To delete the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: + `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The property was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting the properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified property or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/features/{featureId}/desiredProperties': + get: + summary: List all desired properties of a feature + description: Returns all desired properties of the feature identified by the `thingId` and `featureId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/DesiredPropertiesFieldsQueryParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The desired properties were successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureProperties' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined query parameters is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified feature has no desired properties or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update all desired properties of a feature at once + description: |- + Create or update the desired properties of a feature identified by the `thingId` and `featureId` path parameter. + + The desired properties will be overwritten with the JSON content from the request body. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The desired properties were successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureProperties' + '204': + description: The desired properties were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the desired feature roperties to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the desired properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/FeatureProperties' + example: + configuration: + smartMode: true + brewingTemp: 87 + tempToHold: 44 + timeoutSeconds: 6000 + status: + waterAmount: 731 + temperature: 44 + description: |- + JSON object of all desried properties to be updated at once. + + Consider that the value has to be a JSON object or `null`. We strongly recommend to use + a restricted set of characters for the key (identifier). + + Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + required: true + patch: + summary: Patch all desired properties of a feature + description: |- + Patch the desired properties of a feature identified by the `thingId` and `featureId` path parameter. + + The existing desired properties will be merged with the JSON content set in the request body. + + Notice that the `null` value can be used to delete the whole feature or specific parts of it. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + **Note**: In contrast to the "PUT things/{thingId}/features/{featureId}/desiredProperties" request, + a partial update is supported here and request body is merged with the existing desired properties. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The desired properties were successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body of the desired feature roperties to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For modifying the desired properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/FeatureProperties' + example: + configuration: + smartMode: null + brewingTemp: 87 + tempToHold: 44 + timeoutSeconds: 6000 + status: + waterAmount: 731 + temperature: 44 + description: |- + JSON object of all desried properties to be patched. + + Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). We strongly recommend to use + a restricted set of characters for the key (identifier). + + Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + required: true + delete: + summary: Delete all desired properties of a feature + description: Deletes all desired properties of the feature identified by the `thingId` and `featureId` path parameter. + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The desired properties were successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting the desired properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified feature has no desired properties or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/features/{featureId}/desiredProperties/{propertyPath}': + get: + summary: Retrieve a specific desired property of a feature + description: |- + Returns the a specific desired property path of the feature identified by the `thingId` and `featureId` path parameter. + + The desired property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901) + + ### Example + To retrieve the value of the `brewingTemp` in the `water-tank` of our coffeemaker example the full path is: + + `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/GetMetadataParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + - $ref: '#/components/parameters/LiveChannelConditionParam' + - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' + responses: + '200': + description: The desired property was successfully retrieved. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + live-channel-condition-matched: + description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. + schema: + type: boolean + channel: + description: The cannel which was used to retrieve the thing. + schema: + type: string + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified desired property or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update a specific desired property of a feature + description: |- + Create or update a specific desired property of a feature identified by the `thingId` and `featureId` path parameter. + + The desired property will be created if it doesn't exist or else updated. + + The desired property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901), + + ### Example + To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: + + `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '201': + description: The desired property was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '204': + description: The desired property was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For creating/updating a desired property of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Value' + patch: + summary: Patch a specific desired property of a feature + description: |- + Patch a specific desired property of a feature identified by the `thingId` and `featureId` path parameter. + + The exisiting desired property of a feature will be merged with the JSON content set in the request body. + + Notice that the `null` value can be used to delete the specified propertyPath. + For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + + The desired property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). + + ### Example + To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: + `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/PutMetadataParam' + - $ref: '#/components/parameters/DeleteMetadataParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The desired property was successfully patched. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * the JSON body is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For creating/updating a desired property of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The thing or the feature with the given ID was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/PatchValue' + delete: + summary: Delete a specific desired property of a feature + description: |- + Deletes a specific desired property of the feature identified by the `thingId` + and `featureId` path parameter. + + The desired property (JSON) can be referenced + hierarchically, by applying JSON Pointer notation (RFC-6901) + + ### Example + To delete the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: + + `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` + tags: + - Features + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/PropertyPathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/RequestedAcksParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/ConditionParam' + - $ref: '#/components/parameters/ChannelParam' + responses: + '204': + description: The desired property was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + For deleting the properties of an existing feature, `WRITE` permission is required. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The specified desired property or + the thing with the specified `thingId` or the feature with `featureId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '424': + $ref: '#/components/responses/DependencyFailed' + '/api/2/things/{thingId}/inbox/claim': + post: + summary: Initiates claiming a specific thing in order to gain access + description: |- + ### Why + A claiming process may enable an end-user to claim things and proof ownership thereof. + Such a process is initially triggered via a claim message. + This message can be sent to the things service with the HTTP API or the things-client. + + ### How + At this resource you can send a "claim" message to the thing identified + by the `thingId` path parameter in order to gain access to it. The "claim" message is forwarded + together with the request body and `Content-Type` header to client(s) + which registered for Claim messages of the specific thing. + + The decision whether to grant access (by setting permissions) is + completely up to the client(s) which handle the "claim" message. + + The HTTP request blocks until all acknowledgement requests are fulfilled. + By default, it blocks until a response to the issued "claim" message is + available or until the `timeout` is expired. If many clients respond to + the issued message, the first response will complete the HTTP request. + + Note that the client chooses which HTTP status code it wants to return. Ditto + will forward the status code to you. (Also note that '204 - No Content' status code + will never return a body, even if the client responded with a body). + + ### Who + No special permission is required to issue a claim message. + + ### Example + See [Claiming](https://www.eclipse.dev/ditto/protocol-specification-things-messages.html#claim-messages) concept in detail and example in GitHub. + However, in that scenario, the policy should grant you READ and WRITE permission on + the "message:/" resource in order to be able to send the message and read the response. + Further, the things-client which handles the "claim" message, needs permission to change the policy itself + (i.e. READ and WRITE permission on the "policy:/" resource). + tags: + - Messages + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/MessageClaimTimeoutParam' + - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' + responses: + '200': + description: |- + The Claim message was processed successfully and the response body + contains the custom response. The response body may contain + arbitrary data chosen by the recipient. The response code defaults + to `200` but may be chosen by the recipient too. + '204': + description: |- + The Claim message was processed successfully and no custom response + body was set. The response code defaults to `204` but may be chosen + by the recipient. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined path parameters is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '408': + description: The request could not be completed due to timeout. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '413': + $ref: '#/components/responses/MessageTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + '429': + description: |- + The user has sent too many requests in a given amount of time ("rate + limiting"). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + requestBody: + $ref: '#/components/requestBodies/Payload' + '/api/2/things/{thingId}/inbox/messages/{messageSubject}': + post: + summary: Send a message TO a specific thing + description: |- + ### Why + A message can be sent to a thing or one of its features in order to invoke an operation on the device. + + ### How + Send a message with a `messageSubject` **to** the thing + identified by the `thingId` path parameter. The request body contains + the message payload and the `Content-Type` header defines its type. + + The HTTP request blocks until all acknowledgement requests are fulfilled. + By default, it blocks until a response to the message is available + or until the `timeout` is expired. If many clients respond to + the issued message, the first response will complete the HTTP request. + + In order to handle the message in a fire and forget manner, add + a query-parameter `timeout=0` to the request. + + Note that the client chooses which HTTP status code it wants to return. Ditto + will forward the status code to you. (Also note that '204 - No Content' status code + will never return a body, even if the client responded with a body). + + ### Who + You will need `WRITE` permission on the root "message:/" resource, or at least + the resource `message:/inbox/messages/messageSubject`. The receiving device needs `READ` permission on the resource. + Such permission is managed within the policy which controls the access on the thing. + + ### Example + Given you have a "coffemaker" thing as shown in the examples for the `things` resources. + The `messageSubject` understood by such a device would be "makeCoffee". + + Further, as in our example the "brewed-coffees" counter would increase as a response, you would need `WRITE` + permission for the things resource, at least at the respective path + + `/things/{thingId}/features/coffee-brewer/properties/brewed-coffees` + tags: + - Messages + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/MessageSubjectPathParam' + - $ref: '#/components/parameters/MessageTimeoutParam' + - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' + - $ref: '#/components/parameters/ConditionParam' + responses: + '202': + description: The message was sent but not necessarily received by the thing (fire and forget). + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined path parameters is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the resource `message:/inbox/messages/{messageSubject}`. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller has insufficient permissions to interact with the messages of referenced thing. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '408': + description: The request could not be completed due to timeout. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/MessageTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Payload' + '/api/2/things/{thingId}/outbox/messages/{messageSubject}': + post: + summary: Send a message FROM a specific thing + description: |- + Send a message with the subject `messageSubject` **from** the thing + identified by the `thingId` path parameter. The request body contains + the message payload and the `Content-Type` header defines its type. + + The HTTP request blocks until all acknowledgement requests are fulfilled. + By default, it blocks until a response to the message is available + or until the `timeout` is expired. If many clients respond to + the issued message, the first response will complete the HTTP request. + + In order to handle the message in a fire and forget manner, add + a query-parameter `timeout=0` to the request. + + Note that the client chooses which HTTP status code it wants to return. Ditto + will forward the status code to you. (Also note that '204 - No Content' status code + will never return a body, even if the client responded with a body). + + ### Who + You will need `WRITE` permission on the root "message:/" resource, or at least + the resource `message:/outbox/messages/messageSubject`. + Such permission is managed within the policy which controls the access on the thing. + tags: + - Messages + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/MessageSubjectPathParam' + - $ref: '#/components/parameters/MessageTimeoutParam' + - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' + - $ref: '#/components/parameters/ConditionParam' + responses: + '202': + description: The message was sent (fire and forget). + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined path parameters is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the resource `message:/outbox/messages/{messageSubject}`. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller has insufficient permissions to interact with the messages of referenced thing. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '408': + description: The request could not be completed due to timeout. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/MessageTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Payload' + '/api/2/things/{thingId}/features/{featureId}/inbox/messages/{messageSubject}': + post: + summary: Send a message TO a specific feature of a specific thing + description: |- + Send a message with the subject `messageSubject` **to** the feature + specified by the `featureId` and `thingId` path parameter. The request + body contains the message payload and the `Content-Type` header defines + its type. + + The HTTP request blocks until all acknowledgement requests are fulfilled. + By default, it blocks until a response to the message is available + or until the `timeout` is expired. If many clients respond to + the issued message, the first response will complete the HTTP request. + + In order to handle the message in a fire and forget manner, add + a query-parameter `timeout=0` to the request. + + Note that the client chooses which HTTP status code it wants to return. Ditto + will forward the status code to you. (Also note that '204 - No Content' status code + will never return a body, even if the client responded with a body). + + ### Who + You will need `WRITE` permission on the root "message:/" resource, or at least + the resource `message:/features/featureId/inbox/messages/messageSubject`. The receiving device needs `READ` permission on the resource. + Such permission is managed within the policy which controls the access on the thing. + tags: + - Messages + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/MessageSubjectPathParam' + - $ref: '#/components/parameters/MessageTimeoutParam' + - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' + - $ref: '#/components/parameters/ConditionParam' + responses: + '202': + description: |- + The message was sent but not necessarily received by the feature + (fire and forget). + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined path parameters is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the resource `message:/features/{featureId}/inbox/messages/{messageSubject}`. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller has insufficient permissions to interact with the messages of referenced thing. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '408': + description: The request could not be completed due to timeout. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/MessageTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Payload' + '/api/2/things/{thingId}/features/{featureId}/outbox/messages/{messageSubject}': + post: + summary: Send a message FROM a specific feature of a specific thing + description: |- + Send a message with the subject `messageSubject` **from** the feature + specified by the `featureId` and `thingId` path parameter. The request + body contains the message payload and the `Content-Type` header defines + its type. + + The HTTP request blocks until all acknowledgement requests are fulfilled. + By default, it blocks until a response to the message is available + or until the `timeout` is expired. If many clients respond to + the issued message, the first response will complete the HTTP request. + + In order to handle the message in a fire and forget manner, add + a query-parameter `timeout=0` to the request. + + Note that the client chooses which HTTP status code it wants to return. Ditto + will forward the status code to you. (Also note that '204 - No Content' status code + will never return a body, even if the client responded with a body). + + ### Who + You will need `WRITE` permission on the root "message:/" resource, or at least + the resource `message:/features/featureId/outbox/messages/messageSubject`. + Such permission is managed within the policy which controls the access on the thing. + tags: + - Messages + parameters: + - $ref: '#/components/parameters/ThingIdPathParam' + - $ref: '#/components/parameters/FeatureIdPathPathParam' + - $ref: '#/components/parameters/MessageSubjectPathParam' + - $ref: '#/components/parameters/MessageTimeoutParam' + - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' + - $ref: '#/components/parameters/ConditionParam' + responses: + '202': + description: The message was sent (fire and forget). + '400': + description: |- + The request could not be completed. Possible reasons: + * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + * at least one of the defined path parameters is valid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the resource `message:/features/{featureId}/outbox/messages/{messageSubject}`. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller has insufficient permissions to interact with the messages of referenced thing. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '408': + description: The request could not be completed due to timeout. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/MessageTooLarge' + '424': + $ref: '#/components/responses/DependencyFailed' + requestBody: + $ref: '#/components/requestBodies/Payload' + '/api/2/policies/{policyId}': + get: + summary: Retrieve a specific policy + description: |- + Returns the complete policy identified by the `policyId` path parameter. The + response contains the policy as JSON object. + + Tip: If you don't know the policy ID of a thing, request it via GET `/things/{thingId}`. + + Optionally, you can use the field selectors (see parameter `fields`) to only get specific fields, + which you are interested in. + + ### Example: + Use the field selector `_revision` to retrieve the revision of the policy. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/PolicyFieldsQueryParam' + - $ref: '#/components/parameters/IfMatchHeaderParam' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: |- + The request successfully returned completed and returned is the + policy. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Policy' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or update a policy with a specified ID + description: |- + Create or update the policy specified by the policyId path parameter. + * If you set a new policyId in the path, a new policy will be created. + * If you set an existing policyId in the path, the policy will be updated. + + ### Create a new policy + At the initial creation of a policy, at least one valid entry is required. However, you can create a full-fledged policy all at once. + + By default the authorized subject needs WRITE permission on the root resource of the created policy. You can + however omit this check by setting the parameter `allow-policy-lockout` to `true`. + + Example: To create a policy for multiple coffee maker things, + which gives **yourself** all permissions on all resources, set the policyId in the path, + e.g. to "com.acme.coffeemaker:policy-01" and the body part, like in the following snippet. + + ``` + { + "entries": { + "DEFAULT": { + "subjects": { + "{{ request:subjectId }}": { + "type": "the creator" + } + }, + "resources": { + "policy:/": { + "grant": [ + "READ", + "WRITE" + ], + "revoke": [] + }, + "thing:/": { + "grant": [ + "READ", + "WRITE" + ], + "revoke": [] + }, + "message:/": { + "grant": [ + "READ", + "WRITE" + ], + "revoke": [] + } + } + } + }, + "imports": { + "com.acme:importedPolicy" : { + "entries": [ "IMPORTED" ] + } + } + } + ``` + + ### Update an existing policy + For updating an existing policy, the authorized subject needs WRITE permission on the policy's root resource. + + The ID of a policy cannot be changed after creation. Any `policyId` specified in the request body is therefore ignored. + + ### Partially update an existing policy + Partial updates are not supported. + + If you need to create or update a specific label, resource, or subject, please use the respective sub-resources. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParam' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + - $ref: '#/components/parameters/AllowPolicyLockoutParam' + responses: + '201': + description: The policy was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created policy resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/NewPolicy' + '204': + description: The policy was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the policy to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the root `policy:/` resource, + without any revoke in a deeper path of the policy resource. + (You can omit this check by setting the `allow-policy-lockout` parameter.) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or policy referenced in a policy import was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewPolicy' + example: + entries: + DEFAULT: + subjects: + '{{ request:subjectId }}': + type: the creator + resources: + 'policy:/': + grant: + - READ + - WRITE + revoke: [] + 'thing:/': + grant: + - READ + - WRITE + revoke: [] + 'message:/': + grant: + - READ + - WRITE + revoke: [] + description: |- + JSON representation of the policy. + Use the placeholder `{{ request:subjectId }}` in order to let the + backend insert the authenticated subjectId of the HTTP request. + required: true + delete: + summary: Delete a specific policy + description: |- + Deletes the policy identified by the `policyId` path parameter. Deleting + a policy does not implicitly delete other entities (e.g. things) which + use this policy. + + Note: Delete the respective things **before** deleting the + policy, otherwise nobody has permission to read, update, or delete the things. + If you accidentally run into such a scenario, re-create the policy via + PUT `/policies/{policyId}`. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParam' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The policy was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the root `policy:/` resource, + without any revoke in a deeper path of the policy resource.having any revoke. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or policy referenced in a policy import was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '/api/2/policies/{policyId}/actions/activateTokenIntegration': + post: + summary: Activate subjects for this policy derived from the token + description: |- + **This action only works when authenticated with a Json Web Token (JWT).** + + Based on the authenticated token (JWT), **for each policy entry** matching those conditions: + * the authenticated token is granted the `EXECUTE` permission to perform the `activateTokenIntegration` action + * one of the subject IDs is contained in the authenticated token + * at least one `READ` permission to a `thing:/` resource path is granted + + a new subject is **injected into the matched policy entry** calculated with information extracted from the + authenticated JWT. + + The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) + specifies how long the specific subject will have access to the resource secured by the policy. + The subject will be automatically deleted from the policy once this timestamp is reached. + To give the subject a chance to prolong the access he can configure a connection to get announcements. + Policy announcements are published to websockets and connections that have the relevant subject ID. + + The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). + If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until + the acknowledgement requests under labels are fulfilled. + If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" + announcement was acknowledged, the "whenDeleted" announcement will not be triggered. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + responses: + '204': + description: The request was successful. Subjects were injected into authorized policy entries. + '400': + description: The request could not be completed because the authentication was not performed with a JWT. + '403': + description: |- + The request could not be completed because the authenticated JWT did not have the `EXECUTE` permission on any + entries of the policy. + '404': + description: |- + The request could not be completed because no policy entry matched the following conditions: + * containing a a subject ID matching the JWT's authenticated subject + * containing a `READ` permission granted to a `thing:/` resource path + requestBody: + $ref: '#/components/requestBodies/ActivateTokenIntegration' + '/api/2/policies/{policyId}/actions/deactivateTokenIntegration': + post: + summary: Deactivate subjects for this policy derived from the token + description: |- + **This action only works when authenticated with a Json Web Token (JWT).** + + Based on the authenticated token (JWT), **for each policy entry** matching those conditions: + * the authenticated token is granted the `EXECUTE` permission to perform the `deactivateTokenIntegration` action + * one of the subject IDs is contained in the authenticated token + + the calculated subject with information extracted from the authenticated JWT is **removed + from the matched policy entry**. + + The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) + specifies how long the specific subject will have access to the resource secured by the policy. + The subject will be automatically deleted from the policy once this timestamp is reached. + To give the subject a chance to prolong the access he can configure a connection to get announcements. + Policy announcements are published to websockets and connections that have the relevant subject ID. + + The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). + If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until + the acknowledgement requests under labels are fulfilled. + If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" + announcement was acknowledged, the "whenDeleted" announcement will not be triggered. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + responses: + '204': + description: The request was successful. Subjects were removed from authorized policy entries. + '400': + description: The request could not be completed because the authentication was not performed with a JWT. + '403': + description: |- + The request could not be completed because the authenticated JWT did not have the `EXECUTE` permission on any + entries of the policy. + '404': + description: |- + The request could not be completed because no policy entry matched the following conditions: + * containing a a subject ID matching the JWT's authenticated subject + '/api/2/policies/{policyId}/entries': + get: + summary: Retrieve the entries of a specific policy + description: |- + Returns all policy entries of the policy identified by the `policyId` + path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: |- + The request successfully returned completed and returned are the + policy entries. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyEntries' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the entries of a specific policy + description: |- + Modify the policy entries of the policy identified by the `policyId` + path parameter. + + Note: Take care to not lock yourself out. Use the placeholder {{ request:subjectId }} + in order to let the backend insert the authenticated subjectId of the HTTP request. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The policy entries were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the policy to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyEntries' + example: + DEFAULT: + subjects: + '{{ request:subjectId }}': + type: the creator + resources: + 'policy:/': + grant: + - READ + - WRITE + revoke: [] + 'thing:/': + grant: + - READ + - WRITE + revoke: [] + 'message:/': + grant: + - READ + - WRITE + revoke: [] + description: |- + JSON representation of the policy entries. + Use the placeholder `{{ request:subjectId }}` in order to let the + backend insert the authenticated subjectId of the HTTP request. + required: true + '/api/2/policies/{policyId}/entries/{label}': + get: + summary: Retrieve the entries of a specific Label of a specific policy + description: |- + Returns all entries (subjects, resources, etc.) of the policy identified by the `policyId` path + parameter, and by the `label` path parameter. + Example label: DEFAULT. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: |- + The request successfully returned completed and returned is the + policy entry. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyEntry' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify the entries of a specific Label of a specific policy + description: |- + Create or modify the policy entry identified by the + `policyId` path parameter and with the label identified by the `label` + path parameter. + * If you specify a new label, the respective policy entry will be created + * If you specify an existing label, the respective policy entry will be updated + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '201': + description: The policy entry was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created policy entry + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyEntry' + '204': + description: The policy entry was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the policy entry to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyEntry' + example: + subjects: + '{{ request:subjectId }}': + type: the creator + resources: + 'policy:/': + grant: + - READ + - WRITE + revoke: [] + 'thing:/': + grant: + - READ + - WRITE + revoke: [] + 'message:/': + grant: + - READ + - WRITE + revoke: [] + description: |- + JSON representation of the policy entry. + Use the placeholder `{{ request:subjectId }}` in order to let the + backend insert the authenticated subjectId of the HTTP request. + ### Example + Given your policy "com.acme.coffeemaker:policy-01" only has the + DEFAULT entry, and you want to add a "Consumer" section which additionally allows USER-01 + (managed within a Nginx reverse proxy) to + *read* the thing and to trigger a "makeCoffee" operation (i.e. POST such a message - see + POST /things/{thingId}/inbox/messages/{messageSubject}). + Set the label value to **Consumer** and the following request body: + ``` + { + "subjects": { + "nginx:USER-01": { + "type": "pre authenticated user from nginx" + } + }, + "resources": { + "thing:/": { + "grant": [ + "READ" + ], + "revoke": [] + }, + "message:/": { + "grant": [ + "WRITE" + ], + "revoke": [] + } + } + } + ``` + required: true + delete: + summary: Delete the entries of a specific Label of a specific policy + description: |- + Deletes the entry of the policy identified by the `policyId` path + parameter and with the label identified by the `label` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The policy entry was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '/api/2/policies/{policyId}/entries/{label}/actions/activateTokenIntegration': + post: + summary: Activate a subject for this policy entry derived from the token + description: |- + **This action only works when authenticated with a Json Web Token (JWT).** + + Based on the authenticated token (JWT), **this policy entry** is checked to match those conditions: + * the authenticated token is granted the `EXECUTE` permission to perform the `activateTokenIntegration` action + * one of the subject IDs is contained in the authenticated token + * at least one `READ` permission to a `thing:/` resource path is granted + + When all conditions match, a new subject is **injected into this policy entry** calculated with information + extracted from the authenticated JWT. + + The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) + specifies how long the specific subject will have access to the resource secured by the policy. + The subject will be automatically deleted from the policy once this timestamp is reached. + To give the subject a chance to prolong the access he can configure a connection to get announcements. + Policy announcements are published to websockets and connections that have the relevant subject ID. + + The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). + If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until + the acknowledgement requests under labels are fulfilled. + If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" + announcement was acknowledged, the "whenDeleted" announcement will not be triggered. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + responses: + '204': + description: The request was successful. The subject was injected. + '400': + description: The request could not be completed because the authentication was not performed with a JWT. + '403': + description: |- + The request could not be completed because the authenticated JWT did not have the `EXECUTE` permission on this + policy entry. + '404': + description: |- + The request could not be completed because this policy entry did not match the following conditions: + * containing a a subject ID matching the JWT's authenticated subject + * containing a `READ` permission granted to a `thing:/` resource path + requestBody: + $ref: '#/components/requestBodies/ActivateTokenIntegration' + '/api/2/policies/{policyId}/entries/{label}/actions/deactivateTokenIntegration': + post: + summary: Deactivate a subject for this policy entry derived from the token + description: |- + **This action only works when authenticated with a Json Web Token (JWT).** + + Based on the authenticated token (JWT), **this policy entry** is checked to match those conditions: + * the authenticated token is granted the `EXECUTE` permission to perform the `deactivateTokenIntegration` action + * one of the subject IDs is contained in the authenticated token + + When all conditions match, the calculated subject with information extracted from the authenticated JWT is **removed + from this policy entry**. + + The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) + specifies how long the specific subject will have access to the resource secured by the policy. + The subject will be automatically deleted from the policy once this timestamp is reached. + To give the subject a chance to prolong the access he can configure a connection to get announcements. + Policy announcements are published to websockets and connections that have the relevant subject ID. + + The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). + If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until + the acknowledgement requests under labels are fulfilled. + If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" + announcement was acknowledged, the "whenDeleted" announcement will not be triggered. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + responses: + '204': + description: The request was successful. The subject was removed. + '400': + description: The request could not be completed because the authentication was not performed with a JWT. + '403': + description: The request could not be completed because the user did not have the `EXECUTE` permission on this policy entry. + '404': + description: |- + The request could not be completed because this policy entry did not match the following conditions: + * containing a a subject ID matching the JWT's authenticated subject + '/api/2/policies/{policyId}/entries/{label}/subjects': + get: + summary: Retrieve all Subjects for a specific Label of a specific policy + description: |- + Returns all subject entries of the policy identified by the + `policyId` path parameter, and by the `label` + path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The subjects are returned. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Subjects' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify all Subjects for a specific Label of a specific policy + description: |- + Create or modify at once ALL subjects of the policy entry identified + by the `policyId` path parameter, and by the `label` path parameter. + + ### Example - delete all subjects + To delete all subjects set an empty body { } + + ### Example - entities authenticated by nginx + To add a user authenticated via pre-authentication at nginx: + + ``` + { + "nginx:ID-user": { + "type": "pre authenticated user from nginx" + } + } + ``` + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The Subjects were successfully created or updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the policy subjects to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/subjects` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Subjects' + description: |- + JSON representation of the Subjects. + + + Use the placeholder `{{ request:subjectId }}` in order to let the + backend insert the authenticated subjectId of the HTTP request. + required: true + '/api/2/policies/{policyId}/entries/{label}/subjects/{subjectId}': + get: + summary: Retrieve one specific Subject for a specific Label of a specific policy + description: |- + Returns the subject with ID `subjectId` of the policy entry identified + by the `policyId` path parameter, and by the `label` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/SubjectIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: |- + The request successfully returned completed and returned is the + Subject. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/SubjectEntry' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID, + the policy entry or the Subject was not found in the context of the + authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify one specific Subject for a specific Label of a specific policy + description: |- + Create or modify the subject with ID `subjectId` of the policy identified + by the `policyId` path parameter, and by the `label` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/SubjectIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '201': + description: The Subject was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created Subject + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/SubjectEntry' + '204': + description: The Subject was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id))) + * the JSON body of the policy subject to be created/modified is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the root `policy:/entries/{label}/subjects/{subjectId}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SubjectEntry' + description: JSON representation of the Subject + required: true + delete: + summary: Delete one specific Subject for a specific Label of a specific policy + description: |- + Deletes the subject with ID `subjectId` from the policy identified + by the `policyId` path parameter and + by the `label` path parameter. + + Note: If the subject is used in other labels, it will not be deleted there, + i.e. it will not lose those permissions, but only the permissions defined in the + label specified at this path. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/SubjectIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The Subject was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the root `policy:/entries/{label}/subjects/{subjectId}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID, + the policy entry or the Subject was not found in the context of the + authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '/api/2/policies/{policyId}/entries/{label}/resources': + get: + summary: Retrieve all Resources for a specific Label of a specific policy + description: |- + Returns all resource entries of the policy identified by + the `policyId` path parameter, + and by the `label` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The resources are returned. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Resources' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify all Resources for a specific Label of a specific policy + description: |- + Create or modify all resources of the policy + identified by the `policyId` path parameter, + and by the `label` path parameter. + + ### Delete all resource entries + + Set the empty body part, if you need to delete all resource entries: { } + + ### Set max permissions on all ressources + ``` + { + "policy:/": { + "grant": [ + "READ", + "WRITE" + ], + "revoke": [] + }, + "thing:/": { + "grant": [ + "READ", + "WRITE" + ], + "revoke": [] + }, + "message:/": { + "grant": [ + "READ", + "WRITE" + ], + "revoke": [] + } + } + ``` + ### Allow to read all parts of a thing except the "confidential" feature + ``` + { + "thing:/": { + "grant": [ + "READ" + ], + "revoke": [] + }, + "things:/features/confidential": { + "grant": [], + "revoke": [ + "READ" + ] + } + } + ``` + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The Resources were successfully created or updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON is invalid, or no valid Resources JSON object. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the root `policy:/entries/{label}/resources` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Resources' + description: JSON representation of the Resources + required: true + '/api/2/policies/{policyId}/entries/{label}/resources/{resourcePath}': + get: + summary: Retrieve one specific Resource for a specific Label of a specific policy + description: |- + Returns the resource with path `resourcePath` of the policy identified + by the `policyId` path parameter, and + by the `label` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/ResourcePathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: |- + The request successfully returned completed and returned is the + Resource. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceEntry' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID, + the policy entry or the Resource was not found in the context of the + authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify one specific Resource for a specific Label of a specific policy + description: |- + Create or modify the Resource with path `resourcePath` of the policy + entry identified by the `label` path parameter belonging to the policy + identified by the `policyId` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/ResourcePathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '201': + description: The Resource was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created Resource + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceEntry' + '204': + description: The Resource was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON is invalid, or no valid Resource JSON object. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/resources/{resourcePath}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceEntry' + description: JSON representation of the Resource + required: true + delete: + summary: Delete one specific Resource for a specific Label of a specific policy + description: |- + Deletes the resource with path `resourcePath` from the policy + identified by the the `policyId` path parameter, and by the + `label` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/ResourcePathPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The Resource was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/resources/{resourcePath}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID, + the policy entry or the Resource was not found in the context of the + authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '/api/2/policies/{policyId}/entries/{label}/allowedAdditions': + get: + summary: Retrieve the allowed import additions for a specific policy entry + description: |- + Returns the allowed import additions of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + Allowed import additions control which types of additions (subjects, resources) are permitted + when this entry is referenced by other entries via `references`. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The allowed import additions are returned. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/AllowedAdditions' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the allowed import additions for a specific policy entry + description: |- + Modify the allowed import additions of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + Allowed import additions control which types of additions (subjects, resources) are permitted + when this entry is referenced by other entries via `references`. Setting an empty array + disables all additions for this entry. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The allowed import additions were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the allowed import additions is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/allowedAdditions` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AllowedAdditions' + example: + - subjects + - resources + description: JSON array of allowed import addition types. + required: true + '/api/2/policies/{policyId}/entries/{label}/namespaces': + get: + summary: Retrieve the namespace patterns for a specific policy entry + description: |- + Returns the namespace patterns of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + Namespace patterns restrict which thing namespaces this entry applies to. + An empty list (or absent field) means the entry applies to all namespaces. + * `com.acme` matches only that exact namespace + * `com.acme.*` matches namespaces below `com.acme`, but not `com.acme` itself + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The namespace patterns are returned. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyEntry/properties/namespaces' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the namespace patterns for a specific policy entry + description: |- + Modify the namespace patterns of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + Namespace patterns restrict which thing namespaces this entry applies to. + Setting an empty array makes the entry apply to all namespaces (backward compatible default). + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The namespace patterns were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the namespace patterns is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/namespaces` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyEntry/properties/namespaces' + example: + - com.acme + - com.acme.* + description: JSON array of namespace patterns. + required: true + '/api/2/policies/{policyId}/entries/{label}/importable': + get: + summary: Retrieve the importable type for a specific policy entry + description: |- + Returns the importable type of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + The importable type controls whether and how a policy entry can be imported by other policies: + * `implicit` (default): the entry is imported without being listed individually + * `explicit`: the entry is only imported if it is listed in the importing policy + * `never`: the entry is not imported, regardless of being listed + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The importable type is returned. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Importable' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the importable type for a specific policy entry + description: |- + Modify the importable type of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + The importable type controls whether and how a policy entry can be imported by other policies: + * `implicit` (default): the entry is imported without being listed individually + * `explicit`: the entry is only imported if it is listed in the importing policy + * `never`: the entry is not imported, regardless of being listed + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The importable type was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the importable type is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/importable` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Importable' + example: explicit + description: 'The importable type value. Must be one of: "implicit", "explicit", "never".' + required: true + '/api/2/policies/{policyId}/imports': + get: + summary: Retrieve the imports of a specific policy + description: |- + Returns all policy imports of the policy identified by the `policyId` + path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned completed and returned are the policy imports. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyImports' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '402': + description: The request could not be completed due to exceeded data volume or exceeded transaction count. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to a missing or invalid API Token. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the imports of a specific policy + description: Modify the policy imports of the policy identified by the `policyId` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The policy imports were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the policy imports to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '402': + description: The request could not be completed due to exceeded data volume or exceeded transaction count. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/imports` resource, + without any revoke in a deeper path of the policy resource. + * the caller has insufficient permissions. + You need `READ` permission on the policy entries of the imported policies. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy (or an imported policy) with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyImports' + example: + 'com.acme:imported1': + entries: + - IMPORTED_ENTRY + 'com.acme:imported2': {} + description: JSON representation of the policy imports. + required: true + delete: + summary: Delete all imports of a specific policy + description: |- + Removes all imports from the policy identified by the `policyId` path parameter. + If any entry references point to an import, the deletion is rejected. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The policy imports were successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/imports` resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was not found in the + context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '409': + description: |- + The request could not be completed. An entry reference still points to one of the imports. + Remove the entry references first before deleting imports. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '/api/2/policies/{policyId}/imports/{importedPolicyId}': + get: + summary: Retrieve a specific policy import. + description: |- + Returns the policy import of the policy identified by the `policyId` path + parameter and imported policy identified by the `importedPolicyId` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/ImportedPolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned completed and returned is the policy import. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyImport' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '402': + description: The request could not be completed due to exceeded data volume or exceeded transaction count. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to a missing or invalid API Token. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy import was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Create or modify a specific policy import of a policy. + description: |- + Create or modify the policy import of a specific policy identified by the `policyId` path parameter + and the imported policy identified by the `importedPolicyId` path parameter. + + * If you specify a new policy import, the respective policy import will be created + * If you specify an existing policy import, the respective policy import will be updated + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/ImportedPolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '201': + description: The policy import was successfully created. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created policy import + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyImport' + '204': + description: The policy import was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the policy import to be created/modified is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '402': + description: The request could not be completed due to exceeded data volume or exceeded transaction count. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/imports/{importedPolicyId}` resource, + without any revoke in a deeper path of the policy resource. + * the caller has insufficient permissions. + You need `READ` permission on the `policy:/entries/{label}` resource of the *imported* policy, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PolicyImport' + example: + entries: + - IMPORTED + description: JSON representation of the policy import. + required: true + delete: + summary: Delete a specific policy import. + description: |- + Deletes a specific policy import identified by the `policyId` path parameter + and the `importedPolicyId` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/ImportedPolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The policy import was successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '402': + description: The request could not be completed due to exceeded data volume or exceeded transaction count. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/imported/{importedPolicyId}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID was + not found in the context of the authenticated user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '/api/2/policies/{policyId}/imports/{importedPolicyId}/entries': + get: + summary: Retrieve the entries of a specific policy import + description: |- + Returns the entries (imported labels) of the policy import identified by the `policyId` path + parameter and the `importedPolicyId` path parameter. + + The entries define which policy entries from the imported policy should be imported, + identified by their labels. An empty array means all implicit entries are imported. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/ImportedPolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The entries are returned as a JSON array of label strings. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + type: array + items: + type: string + description: Label of a policy entry to import from the referenced policy. + example: + - default + - import + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy import was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the entries of a specific policy import + description: |- + Modify the entries (imported labels) of the policy import identified by the `policyId` path + parameter and the `importedPolicyId` path parameter. + + The entries define which policy entries from the imported policy should be imported, + identified by their labels. Provide an empty array to import all implicit entries. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/ImportedPolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The entries were successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the entries is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/imports/{importedPolicyId}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy import was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + type: array + items: + type: string + description: Label of a policy entry to import from the referenced policy. + example: + - default + - import + description: JSON array of policy entry labels to import. + required: true + '/api/2/policies/{policyId}/imports/{importedPolicyId}/transitiveImports': + get: + summary: Retrieve the transitive resolution policy IDs of a specific policy import + description: |- + Returns the "transitiveImports" array of the policy import identified by the `policyId` path + parameter and the `importedPolicyId` path parameter. + + The array lists policy IDs from the imported policy's own imports that should be resolved + transitively before extracting entries. This enables multi-level import chains. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/ImportedPolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The transitiveImports array is returned. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/TransitiveImports' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy import was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the transitive resolution policy IDs of a specific policy import + description: |- + Modify the "transitiveImports" array of the policy import identified by the `policyId` path + parameter and the `importedPolicyId` path parameter. + + The array lists policy IDs from the imported policy's own imports that should be resolved + transitively before extracting entries. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/ImportedPolicyIdPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The transitiveImports array was successfully updated. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body is not a valid JSON array of policy ID strings + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/imports/{importedPolicyId}` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy import was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TransitiveImports' + example: + - 'org.eclipse.ditto:policy-template' + description: JSON array of policy IDs to resolve transitively. + required: true + '/api/2/policies/{policyId}/entries/{label}/references': + get: + summary: Retrieve the references of a specific policy entry + description: |- + Returns the references of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + References define links to other policy entries, optionally from imported policies. + Each reference object contains a required `entry` field (the label of the referenced entry) + and an optional `import` field (the policy ID of the import to reference from). + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + responses: + '200': + description: The request successfully returned. The references are returned. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/References' + '304': + $ref: '#/components/responses/NotModified' + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + put: + summary: Modify the references of a specific policy entry + description: |- + Sets the references of the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + + References define links to other policy entries, optionally from imported policies. + Each reference object contains a required `entry` field (the label of the referenced entry) + and an optional `import` field (the policy ID of the import to reference from). + Setting an empty array removes all references from this entry. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/IfEqualHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '201': + description: The references were successfully created (the entry had no references before). + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + Location: + description: The location of the created references resource. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/References' + '204': + description: The references were successfully updated (the entry already had references). + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + * the JSON body of the references is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/references` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + '413': + $ref: '#/components/responses/EntityTooLarge' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/References' + example: + - import: 'acme:fleet-roles' + entry: driver + - entry: shared-subjects + description: |- + JSON array of reference objects. Each object must contain a required `entry` field + (the label of the referenced policy entry) and may contain an optional `import` field + (the policy ID of the import to reference from). + required: true + delete: + summary: Remove all references from a specific policy entry + description: |- + Removes all references from the policy entry identified by the + `policyId` path parameter and the `label` path parameter. + tags: + - Policies + parameters: + - $ref: '#/components/parameters/PolicyIdPathParam' + - $ref: '#/components/parameters/LabelPathParam' + - $ref: '#/components/parameters/IfMatchHeaderParamHash' + - $ref: '#/components/parameters/IfNoneMatchHeaderParam' + - $ref: '#/components/parameters/TimeoutParam' + - $ref: '#/components/parameters/ResponseRequiredParam' + responses: + '204': + description: The references were successfully deleted. + '400': + description: |- + The request could not be completed. Possible reasons: + + * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + + * the caller has insufficient permissions. + You need `WRITE` permission on the `policy:/entries/{label}/references` resource, + without any revoke in a deeper path of the policy resource. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. The policy with the given ID or + the policy entry was not found in the context of the authenticated + user. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '412': + $ref: '#/components/responses/PreconditionFailed' + /api/2/whoami: + get: + summary: Retrieve information about the current caller + description: 'Get information about the current caller, e.g. the auth subjects that are generated for the caller.' + tags: + - Policies + responses: + '200': + description: The request successfully returned information about the caller. + content: + application/json: + schema: + $ref: '#/components/schemas/WhoAmI' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + /api/2/checkPermissions: + post: + summary: Check permissions for specified entities + description: This endpoint allows you to verify permissions for various entities on specific resources. + tags: + - Policies + requestBody: + $ref: '#/components/requestBodies/PermissionCheckRequest' + responses: + '200': + $ref: '#/components/responses/PermissionCheckResponse' + '401': + description: Unauthorized request due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + /api/2/search/things: + get: + summary: Search for things + description: |- + This resource can be used to search for things. + + * The query parameter `filter` is not mandatory. If it is not set, the + result contains all things which the logged in user is allowed to read. + + * The search is case sensitive. In case you don't know how exactly the + spelling of value of the namespace, name, attribute, feature etc. is, use the *like* + notation instead of *eq* for filtering. + + * The resource supports sorting and paging. If paging is not explicitly + specified by means of the `size` option, a default count of `25` + documents is returned. + + * The internal search index is "eventually consistent". Consistency with the latest + thing updates should recover within milliseconds. + parameters: + - $ref: '#/components/parameters/SearchFilter' + - $ref: '#/components/parameters/NamespacesFilter' + - $ref: '#/components/parameters/ThingFieldsQueryParam' + - $ref: '#/components/parameters/TimeoutParam' + - name: option + in: query + description: |- + Possible values for the parameter: + + #### Sort operations + + * ```sort([+|-]{property})``` + * ```sort([+|-]{property},[+|-]{property},...)``` + + #### Paging operations + + * ```size({page-size})``` Maximum allowed page size is `200`. Default page size is `25`. + * ```cursor({cursor-id})``` Start the search from the cursor location. Specify the cursor ID without + quotation marks. Cursor IDs are given in search responses and mark the position after the last entry of + the previous search. The meaning of cursor IDs is unspecified and may change without notice. + + The paging option `limit({offset},{count})` is deprecated. + It may result in slow queries or timeouts and will be removed eventually. + + #### Examples: + + * ```sort(+thingId)``` + * ```sort(-attributes/manufacturer)``` + * ```sort(+thingId,-attributes/manufacturer)``` + * ```size(10)``` return 10 results + * ```cursor(LOREMIPSUM)``` return results after the position of the cursor `LOREMIPSUM`. + + #### Combine: + + If you need to specify multiple options, when using the swagger UI just write each option in a new line. + When using the plain REST API programmatically, + you will need to separate the options using a comma (,) character. + + ```size(200),cursor(LOREMIPSUM)``` + + The deprecated paging option `limit` may not be combined with the other paging options `size` and `cursor`. + required: false + schema: + type: string + tags: + - Things-Search + responses: + '200': + description: An array of the matching things. + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResultThings' + '400': + description: |- + The request could not be completed. A provided parameter is in a + wrong format. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to an invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '504': + description: The request ran out of time to execute on the the back-end. Optimize your query and try again. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + post: + summary: Search for things + description: |- + This resource can be used to search for things. + + * The parameter `filter` is not mandatory. If it is not set, the + result contains all things which the logged in user is allowed to read. + + * The search is case sensitive. In case you don't know how exactly the + spelling of value of the namespace, name, attribute, feature etc. is, use the *like* + notation instead of *eq* for filtering. + + * The resource supports sorting and paging. If paging is not explicitly + specified by means of the `size` option, a default count of `25` + documents is returned. + + * The internal search index is "eventually consistent". Consistency with the latest + thing updates should recover within milliseconds. + tags: + - Things-Search + responses: + '200': + description: An array of the matching things. + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResultThings' + '400': + description: |- + The request could not be completed. A provided parameter is in a + wrong format. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to an invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '504': + description: The request ran out of time to execute on the the back-end. Optimize your query and try again. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + filter: + $ref: '#/components/schemas/SearchFilterProperty' + namespaces: + $ref: '#/components/schemas/NamespaceProperty' + fields: + description: |- + Contains a comma-separated list of fields to be included in the returned + JSON. attributes can be selected in the same manner. + + #### Selectable fields + + * `thingId` + * `policyId` + * `definition` + * `attributes` + + Supports selecting arbitrary sub-fields by using a comma-separated list: + * several attribute paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + + For example: + * `?fields=attributes/model` would select only `model` attribute value (if present) + * `?fields=attributes/model,attributes/location` would select only `model` and + `location` attribute values (if present) + + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) + separated with `/`) to select + + * sub-selectors can be used to request only specific sub-fields by placing expressions + in parentheses `( )` after a selected subfield + + For example: + * `?fields=attributes(model,location)` would select only `model` + and `location` attribute values (if present) + * `?fields=attributes(coffeemaker/serialno)` would select the `serialno` value + inside the `coffeemaker` object + * `?fields=attributes/address/postal(city,street)` would select the `city` and + `street` values inside the `postal` object inside the `address` object + + * `features` + + Supports selecting arbitrary fields in features similar to `attributes` (see also features documentation for more details) + + * `_namespace` + + Specifically selects the namespace also contained in the `thingId` + + * `_revision` + + Specifically selects the revision of the thing. The revision is a counter, which is incremented on each modification of a thing. + + * `_created` + + Specifically selects the created timestamp of the thing in ISO-8601 UTC format. The timestamp is set on creation of a thing. + + * `_modified` + + Specifically selects the modified timestamp of the thing in ISO-8601 UTC format. The timestamp is set on each modification of a thing. + + * `_metadata` + + Specifically selects the Metadata of the thing. The content is a JSON object having the Thing's JSON structure with the difference that the JSON leaves of the Thing are JSON objects containing the metadata. + + * `_policy` + + Specifically selects the content of the policy associated to the thing. (By default, only the policyId is returned.) + + #### Examples + + * `?fields=thingId,attributes,features` + * `?fields=attributes(model,manufacturer),features` + type: string + option: + description: |- + Possible values for the parameter: + + #### Sort operations + + * ```sort([+|-]{property})``` + * ```sort([+|-]{property},[+|-]{property},...)``` + + #### Paging operations + + * ```size({page-size})``` Maximum allowed page size is `200`. Default page size is `25`. + * ```cursor({cursor-id})``` Start the search from the cursor location. Specify the cursor ID without + quotation marks. Cursor IDs are given in search responses and mark the position after the last entry of + the previous search. The meaning of cursor IDs is unspecified and may change without notice. + + The paging option `limit({offset},{count})` is deprecated. + It may result in slow queries or timeouts and will be removed eventually. + + #### Examples: + + * ```sort(+thingId)``` + * ```sort(-attributes/manufacturer)``` + * ```sort(+thingId,-attributes/manufacturer)``` + * ```size(10)``` return 10 results + * ```cursor(LOREMIPSUM)``` return results after the position of the cursor `LOREMIPSUM`. + + #### Combine: + + If you need to specify multiple options, when using the swagger UI just write each option in a new line. + When using the plain REST API programmatically, + you will need to separate the options using a comma (,) character. + + ```size(200),cursor(LOREMIPSUM)``` + + The deprecated paging option `limit` may not be combined with the other paging options `size` and `cursor`. + type: string + condition: + description: |- + Similar to the `filter`, a `condition` may be passed to ensure strong consistency when querying things. + + This `condition` has the same syntax and semantics than the `filter` - it is however applied on the matched things + selected by the `filter` - on their current state. + + So combining this together with `filter` can provide strong consistency when performing a search. + type: string + encoding: + filter: + style: form + explode: false + namespaces: + style: form + explode: false + fields: + style: form + explode: false + option: + style: form + explode: false + example: + filter: 'and(like(definition,"*test*"))' + namespaces: 'org.eclipse.ditto,foo.bar' + fields: 'attributes/model,attributes/location' + option: 'limit(0,5)' + /api/2/search/things/count: + get: + summary: Count things + description: |- + This resource can be used to count things. + + The query parameter `filter` is not mandatory. If it is not set there is + returned the total amount of things which the logged in user is allowed + to read. + + To search for nested properties, we use JSON Pointer notation + (RFC-6901). See the following example how to search for the sub property + `location` of the parent property `attributes` with a forward slash as + separator: + + ```eq(attributes/location,"kitchen")``` + parameters: + - $ref: '#/components/parameters/SearchFilter' + - $ref: '#/components/parameters/NamespacesFilter' + - $ref: '#/components/parameters/TimeoutParam' + tags: + - Things-Search + responses: + '200': + description: A number indicating the amount of matched things + content: + application/json: + schema: + type: integer + '400': + description: |- + The request could not be completed. A provided parameter is in a + wrong format. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to an invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '504': + description: The request ran out of time to execute on the the back-end. Optimize your query and try again. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + post: + summary: Count things + description: |- + This resource can be used to count things. + + The parameter `filter` is not mandatory. If it is not set there is + returned the total amount of things which the logged in user is allowed + to read. + + To search for nested properties, we use JSON Pointer notation + (RFC-6901). See the following example how to search for the sub property + `location` of the parent property `attributes` with a forward slash as + separator: + + ```eq(attributes/location,"kitchen")``` + tags: + - Things-Search + responses: + '200': + description: A number indicating the amount of matched things + content: + application/json: + schema: + type: integer + '400': + description: |- + The request could not be completed. A provided parameter is in a + wrong format. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to an invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '504': + description: The request ran out of time to execute on the the back-end. Optimize your query and try again. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + filter: + $ref: '#/components/schemas/SearchFilterProperty' + namespaces: + $ref: '#/components/schemas/NamespaceProperty' + encoding: + filter: + style: form + explode: false + namespaces: + style: form + explode: false + example: + filter: 'and(like(definition,"*test*"))' + namespaces: 'org.eclipse.ditto,foo.bar' + /api/2/cloudevents: + post: + summary: Processes a CloudEvent sent in Ditto Protocol + description: |- + Provides an endpoint accepting [CloudEvents via its HTTP protocol binding](https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md) + in [Ditto Protocol JSON](https://www.eclipse.dev/ditto/protocol-specification.html). + + The endpoint can also directly be configured as a [Knative eventing](https://knative.dev/docs/eventing/) destination. + + Find more documentation on that [here](https://www.eclipse.dev/ditto/httpapi-protocol-bindings-cloudevents.html). + tags: + - CloudEvents + parameters: + - in: header + name: ce-specversion + description: The CloudEvents "specversion". + schema: + type: string + example: '1.0' + required: true + - in: header + name: ce-type + description: The CloudEvents event "type". + schema: + type: string + example: com.example.someevent + required: true + - in: header + name: ce-source + description: The CloudEvents event "source". + schema: + type: string + example: /mycontext + required: true + - in: header + name: ce-id + description: The CloudEvents event "id". + schema: + type: string + example: 1234-1234-1234 + required: true + - in: header + name: ce-time + description: The CloudEvents event "time". + schema: + type: string + format: date-time + example: '2020-12-31T23:59:59Z' + required: true + - in: header + name: ce-dataschema + description: 'The CloudEvents event "dataschema". If provided, this must start with `ditto:`.' + schema: + type: string + required: false + responses: + '202': + description: 'The Ditto Protocol CloudEvent was successfully parsed, the authentication was valid and also reached the persistence.' + '400': + description: |- + The request could not be completed. Possible reasons: + * the CloudEvent could not be parsed as some mandatory CloudEvent headers were missing from the request + * the payload was missing from the CloudEvent + * the [Ditto Protocol JSON](https://www.eclipse.dev/ditto/protocol-specification.html) message could not be parsed or was missing a required field + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: |- + The request could not be completed. Possible reasons: + * the caller has insufficient permissions. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: |- + The request could not be completed. Possible reasons: + * the referenced thing does not exist. + * the caller has insufficient permissions to perform the contained Ditto Protocol command. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '408': + description: The request could not be completed due to timeout. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '415': + description: The `Content-Type` of the request was not supported. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + requestBody: + content: + application/vnd.eclipse.ditto+json: + schema: + type: object + properties: + topic: + type: string + description: |- + Contains information about the contents of the payload: + * the affected Thing (namespace and Thing ID) + * the type of operation (command/event, create/retrieve/modify/delete) + example: org.eclipse.ditto/thing-1/things/twin/commands/modify + headers: + type: object + description: Additional headers. + properties: + correlation-id: + type: string + description: |- + The correlation-id header is used for linking one message with another. + It typically links a reply message with its requesting message. + example: + correlation-id: 1234-4321-1234 + path: + type: string + description: References the part of a Thing which is affected by this message. + example: /features/location/properties/longitude + value: + oneOf: + - type: object + - type: string + - type: number + - type: array + - type: boolean + description: The `value` field contains the actual payload e.g. a sensor value. + required: + - topic + - path + example: + topic: org.eclipse.ditto/thing-1/things/twin/commands/modify + path: / + value: + attributes: + foo: 42 + description: |- + The [Ditto Protocol JSON](https://www.eclipse.dev/ditto/protocol-specification.html) payload defining which + command to process. + /api/2/connections: + get: + summary: Retrieve all connections + description: Returns all connections. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionFieldsQueryParam' + - name: ids-only + in: query + description: 'When set to true, the request will return the registered ids only and not the whole connections objects.' + required: false + schema: + type: boolean + responses: + '200': + description: The request successfully returned the connections. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Connection' + '400': + description: The request could not be completed. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to an invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. Connections not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + post: + summary: Create a new connection + description: |- + Creates the connection defined in the JSON body. + The ID of the connection will be **generated** by the backend. Any `ID` specified in the request body is therefore + prohibited. + Supported connection types are `amqp-091`, `amqp-10`, `mqtt`, `mqtt-5`, `kafka`, `hono` and `http-push`. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - name: dry-run + in: query + description: |- + When set to true, the request will not try to create the connection, but only try to connect it. + You can use this parameter to verify that the given connection is able to communicate with your external + system. + required: false + schema: + type: boolean + responses: + '200': + description: |- + Will be returned when a dry-run succeeded (see description of the dry-run query parameter for further + information). + '201': + description: The connection was successfully created. + headers: + Location: + description: The location of the created connection resource. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Connection' + '400': + description: |- + The request could not be completed. Possible reasons: + * an `ID` was set in the request body, but the ID will be generated by Ditto + * the JSON of the connection to be created is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. Connections not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewConnection' + example: + name: hono-example-connection-123 + connectionType: hono + connectionStatus: open + sources: + - addresses: + - telemetry + - event + - ... + authorizationContext: + - 'ditto:inbound-auth-subject' + - ... + consumerCount: 1 + enforcement: + input: '{{ header:device_id }}' + filters: + - '{{ thing:id }}' + payloadMapping: + - Ditto + - status + targets: + - address: command + topics: + - _/_/things/twin/events + authorizationContext: + - 'ditto:outbound-auth-subject' + - ... + headerMapping: {} + description: The example below shows a connection to Eclipse Hono. + required: true + '/api/2/connections/{connectionId}': + get: + summary: Retrieve a specific connection + description: Returns the connection identified by the `connectionId` path parameter. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionIdPathParam' + - $ref: '#/components/parameters/ConnectionFieldsQueryParam' + responses: + '200': + description: The request successfully returned the connection. + content: + application/json: + schema: + $ref: '#/components/schemas/Connection' + '400': + description: The request could not be completed. The `connectionId` must be an URI. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The connection with ID `connectionId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + put: + summary: Create or update a connection with a specified ID + description: Update the connection identified by the `connectionId` path parameter. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionIdPathParam' + responses: + '204': + description: The connection was successfully updated. + '400': + description: |- + The request could not be completed. Possible reasons: + * the `connectionId` must be an URI, + * the `ID` was wrongly set in the request body, + * the JSON of the connection to be created is invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The connection with ID `connectionId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/NewConnection' + example: + name: hono-example-connection-123 + connectionType: hono + connectionStatus: open + sources: + - addresses: + - telemetry + - event + - ... + authorizationContext: + - 'ditto:inbound-auth-subject' + - ... + consumerCount: 1 + enforcement: + input: '{{ header:device_id }}' + filters: + - '{{ thing:id }}' + payloadMapping: + - Ditto + - status + targets: + - address: command + topics: + - _/_/things/twin/events + authorizationContext: + - 'ditto:outbound-auth-subject' + - ... + headerMapping: {} + required: true + delete: + summary: Delete a specific connection + description: Delete the connection identified by the `connectionId` path parameter. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionIdPathParam' + responses: + '204': + description: The connection was successfully deleted. + '400': + description: The request could not be completed. The `connectionId` must be an URI. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to an invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The connection with ID `connectionId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '/api/2/connections/{connectionId}/command': + post: + summary: Send a command to a specific connection + description: |- + Sends the command specified in the body to the connection identified by the `connectionId` + path parameter. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionIdPathParam' + responses: + '200': + description: The command was sent to the connection successfully. + '400': + description: The request could not be completed. The `connectionId` must be an URI. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The connection with ID `connectionId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + requestBody: + content: + text/plain: + schema: + type: string + example: + description: |- + The command to send. Supported commands are + * `connectivity.commands:openConnection` + * `connectivity.commands:closeConnection` + * `connectivity.commands:resetConnectionMetrics` + * `connectivity.commands:enableConnectionLogs` + * `connectivity.commands:resetConnectionLogs` + required: true + '/api/2/connections/{connectionId}/status': + get: + summary: Retrieve status of a specific connection + description: Returns the status of the connection identified by the `connectionId` path parameter. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionIdPathParam' + responses: + '200': + description: The request successfully returned the connection status. + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionStatus' + '400': + description: The request could not be completed. The `connectionId` must be an URI. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The connection with ID `connectionId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '/api/2/connections/{connectionId}/metrics': + get: + summary: Retrieve metrics of a specific connection + description: Returns the metrics of the connection identified by the `connectionId` path parameter. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionIdPathParam' + responses: + '200': + description: The request successfully returned the connection metrics. + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionMetrics' + '400': + description: The request could not be completed. The `connectionId` must be an URI. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The connection with ID `connectionId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '/api/2/connections/{connectionId}/logs': + get: + summary: Retrieve logs of a specific connection + description: |- + Returns the logs of the connection identified by the `connectionId` path parameter. + **Before** log entries are generated and returned, logging needs be enabled with the `command` + `connectivity.commands:enableConnectionLogs`. When creating or opening an connection the logging is enabled per + default. This allows to log possible errors on connection establishing. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Connections + parameters: + - $ref: '#/components/parameters/ConnectionIdPathParam' + responses: + '200': + description: The request successfully returned the connection logs. + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionLogs' + '400': + description: The request could not be completed. The `connectionId` must be an URI. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '403': + description: The request could not be completed due to invalid authentication. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '404': + description: The request could not be completed. The connection with ID `connectionId` was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + /devops/logging: + get: + summary: Retrieve all currently configured log levels + description: Return configured log level for all ditto cluster pod + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/LoggingFieldsQueryParam' + responses: + '200': + description: Return The current value of logging level + content: + application/json: + schema: + $ref: '#/components/schemas/RetrieveLoggingConfig' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + put: + summary: Update log levels + description: Modify log level for eatch pods menaged + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + requestBody: + description: Fields to update level log for each pods + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoggingUpdateFields' + example: |- + { + "level": "info", + "logger": "org.apache.pekko.actor.CoordinatedShutdown" + } + responses: + '201': + $ref: '#/components/responses/SuccessUpdateLogLevel' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + '/devops/logging/{moduleName}': + get: + summary: Retrieve currently configured log levels for a specific module + description: Return the configured log + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/ModuleNamePathParam' + - $ref: '#/components/parameters/LoggingFieldsQueryParam' + responses: + '200': + description: Return The current value of logging level + content: + application/json: + schema: + $ref: '#/components/schemas/Module' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + put: + summary: Update log levels for a specific module + description: Return outcome modify log level for a specific module + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/ModuleNamePathParam' + requestBody: + description: Fields to update level log for module + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoggingUpdateFields' + example: |- + { + "level": "info", + "logger": "org.apache.pekko.actor.CoordinatedShutdown" + } + responses: + '201': + $ref: '#/components/responses/SuccessUpdateLogLevelSinglePod' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + /devops/config: + get: + summary: Retrieve the configuration at the specified path parameter + description: |- + It is recommended to not omit the query parameter path. + Otherwise, the full configurations of all services are aggregated in the response, which can become megabytes big. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/PathParam' + responses: + '200': + description: Return the configuration at the path + content: + application/json: + schema: + $ref: '#/components/schemas/RetrieveConfig' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + '/devops/config/{moduleName}/{podName}': + get: + summary: Retrieving the configuration of a specific service instance. + description: Return the configuration of a specific service instance. + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/ModuleNamePathParam' + - $ref: '#/components/parameters/NamePodParam' + - $ref: '#/components/parameters/PathParam' + responses: + '200': + description: Return The current value of specific service instance. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RetrieveConfigService' + example: |- + { + "gateway": { + "podName": { + "type": "common.responses:retrieveConfig", + "status": 200, + "config": { + "cluster": { + "cluster-status-roles-blocklist": [ + "cluster1", + "......" , + "clusterN" + ], + "number-of-shards": 20 + }, + "ddata": { + "vm arg1": "string", + ".............": "string", + "vm argn" : "string" + }, + "devops": { + "feature": { + "merge-things-enabled": true + }, + "namespace": { + "block-time": "string" + } + }, + "gateway": { + "authentication": { + "devops": { + "devops-authentication-method": "string", + "password": "string", + "secured": true, + "status-authentication-method": "string", + "statusPassword": "string" + }, + "http": { + "proxy": { + "enabled": false + } + }, + "oauth": { + "allowed-clock-skew": "string", + "openid-connect-issuers": { + "google": { + "issuer": "string" + } + }, + "protocol": "https", + "token-integration-subject": "string" + }, + "pre-authentication": { + "enabled": "true" + } + } + } + } + } + } + } + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + /devops/piggyback: + post: + summary: Send a piggyback command + description: Send a piggyback command to Pekko’s pub-sub-mediator + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/TimeoutParam' + requestBody: + description: Fields to send a command + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BasePiggybackCommandRequestSchema' + examples: + blockNamespace: + description: Block all messages to a namespace + value: |- + { + "targetActorSelection": "/system/distributedPubSubMediator", + "headers": { + "aggregate": false + }, + "piggybackCommand": { + "type": "namespaces.commands:blockNamespace", + "namespace": "namespaceToBlock" + } + } + shutdown: + description: Shutdown all actors in a namespace + value: |- + { + "targetActorSelection": "/system/distributedPubSubMediator", + "piggybackCommand": { + "type": "common.commands:shutdown", + "reason": { + "type": "purge-namespace", + "details": "namespaceToShutdown" + } + } + } + purgeNamespace: + description: Erase all data in a namespace from the persistence + value: |- + { + "targetActorSelection": "/system/distributedPubSubMediator", + "headers": { + "aggregate": true, + "is-group-topic": true + }, + "piggybackCommand": { + "type": "namespaces.commands:purgeNamespace", + "namespace": "namespaceToPurge" + } + } + unblockNamespace: + description: Unblock messages to a namespace + value: |- + { + "targetActorSelection": "/system/distributedPubSubMediator", + "headers": { + "aggregate": false + }, + "piggybackCommand": { + "type": "namespaces.commands:unblockNamespace", + "namespace": "namespaceToUnblock" + } + } + responses: + '200': + description: Response of command + content: + application/json: + schema: + $ref: '#/components/schemas/PiggybackManagingBackgroundCleanup' + examples: + blockNamespace: + value: |- + { + "type": "namespaces.responses:blockNamespace", + "status": 200, + "namespace": "namespaceToBlock", + "resourceType": "namespaces" + } + '400': + description: The request could not be completed. At least one of the defined query parameters was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + '/devops/piggyback/{serviceName}': + post: + summary: Send a piggyback command to a specific service + description: Send a piggyback command to a specific service + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/ServiceNameParam' + - $ref: '#/components/parameters/TimeoutParam' + requestBody: + description: Fields to send a command + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BasePiggybackCommandRequestSchema' + examples: + persistenceCleanup: + description: Query background cleanup coordinator state + value: |- + { + "targetActorSelection": "/user/Root/persistenceCleanup", + "headers": {}, + "piggybackCommand": { + "type": "status.commands:retrieveHealth" + } + } + responses: + '200': + description: Return The current value of logging level + content: + application/json: + schema: + $ref: '#/components/schemas/PiggybackManagingBackgroundCleanup' + example: |- + { + "things": { + "ditto-things-65f6dd5d7-htkwt": { + "type": "status.responses:retrieveHealth", + "status": 200, + "statusInfo": { + "status": "UP", + "details": [ + { + "INFO": { + "state": "IN_QUIET_PERIOD", + "pid": "" + } + } + ] + } + } + } + } + '400': + description: The request could not be completed. At least one of the defined query parameters was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + '/devops/piggyback/{serviceName}/{instanceIndex}': + post: + summary: Send a piggyback command to a specific instance of service + description: Send a piggyback command to a specific instance of service + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + tags: + - Devops + parameters: + - $ref: '#/components/parameters/ServiceNameParam' + - $ref: '#/components/parameters/InstanceIndex' + - $ref: '#/components/parameters/TimeoutParam' + requestBody: + description: Fields to send a command + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BasePiggybackCommandRequestSchema' + examples: + cleanup: + description: Cleanup events and snapshots of an entity + value: |- + { + "targetActorSelection": "/system/sharding/thing", + "headers": { + "aggregate": false + }, + "piggybackCommand": { + "type": "cleanup.sudo.commands:cleanupPersistence", + "entityId": "ditto:thing1" + } + } + responses: + '200': + description: response of command + content: + application/json: + example: |- + { + "type": "cleanup.sudo.responses:cleanupPersistence", + "status": 200, + "entityId": "thing:ditto:thing1" + } + '400': + description: The request could not be completed. At least one of the defined query parameters was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + '401': + description: The request could not be completed due to missing authentication. + content: + text/plain: + schema: + $ref: '#/components/schemas/TextUnauthorizeError' + /.well-known/wot: + get: + summary: Retrieve WoT Thing Directory + description: |- + Returns a WoT (Web of Things) Thing Description of the Ditto Thing Directory, + as specified by the [WoT Discovery](https://www.w3.org/TR/wot-discovery/) specification. + + By default, this endpoint is publicly accessible without authentication. This can be configured + via the `GATEWAY_WOT_DIRECTORY_AUTHENTICATION_REQUIRED` environment variable. + + Both `GET` and `HEAD` methods are supported per the WoT Discovery specification. + tags: + - WoT + responses: + '200': + description: The WoT Thing Directory description was successfully retrieved. + content: + application/td+json: + schema: + $ref: '#/components/schemas/WotThingDescription' + example: + '@context': + - 'https://www.w3.org/2022/wot/td/v1.1' + - 'https://www.w3.org/2022/wot/discovery' + '@type': ThingDirectory + id: 'urn:ditto:wot:thing-directory' + title: Thing Description Directory (TDD) of Eclipse Ditto + version: + model: 1.0.0 + instance: 1.0.0 + '401': + description: The request could not be completed due to missing authentication (when authentication is required). + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + head: + summary: Retrieve WoT Thing Directory headers + description: |- + Returns headers for the WoT Thing Directory endpoint without a response body. + Supports the same authentication and configuration as the GET method. + tags: + - WoT + responses: + '200': + description: The WoT Thing Directory headers were successfully retrieved. + '401': + description: The request could not be completed due to missing authentication (when authentication is required). + /devops/wot/config: + get: + summary: Get the WoT validation config + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + responses: + '200': + $ref: '#/components/responses/WotValidationConfigResponse' + '404': + description: Not found + put: + summary: Update the WoT validation config + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/WotValidationConfig' + responses: + '204': + description: Updated config + delete: + summary: Delete the WoT validation config + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + responses: + '204': + description: Deleted successfully + '404': + description: Not found + /devops/wot/config/merged: + get: + summary: Get the merged WoT validation config + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + responses: + '200': + $ref: '#/components/responses/WotValidationConfigResponse' + '404': + description: Not found + /devops/wot/config/dynamicConfigs: + get: + summary: List all dynamic WoT validation config sections + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + responses: + '200': + description: List of dynamic config sections + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/DynamicValidationConfig' + '/devops/wot/config/dynamicConfigs/{scopeId}': + parameters: + - name: scopeId + in: path + required: true + schema: + type: string + get: + summary: Get a dynamic WoT validation config section by scopeId + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + responses: + '200': + $ref: '#/components/responses/DynamicValidationConfigResponse' + '404': + description: Not found + put: + summary: Create or update a dynamic WoT validation config section + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + requestBody: + $ref: '#/components/requestBodies/DynamicValidationConfigRequest' + responses: + '204': + description: Updated dynamic config section + delete: + summary: Delete a dynamic WoT validation config section + tags: + - Devops + security: + - DevOpsBasic: [] + - DevOpsBearer: [] + responses: + '204': + description: Deleted successfully + '404': + description: Not found +components: + requestBodies: + Attributes: + content: + application/json: + schema: + $ref: '#/components/schemas/Attributes' + example: + manufacturer: + name: ACME demo corp. + location: 'Berlin, main floor' + coffeemaker: + serialno: '42' + model: Speaking coffee machine + description: |- + JSON object of all attributes to be modified at once. Consider that the + value has to be a JSON object or `null`. + + Examples: + * an empty object: `{}` - would just delete all old attributes + * a simple object: `{ "key": "value"}` - We strongly recommend to use a restricted set of characters for the key (identifier), as the key might be needed for the (URL) path later.
Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + * a nested object as shown in the example value + required: true + Definition: + content: + application/json: + schema: + $ref: '#/components/schemas/Definition' + example: '"example:test:definition"' + description: |- + JSON string of the definition to be modified. Consider that the + value has to be a JSON string or `null`, examples: + + * a string: `"value"` - Currently the definition should follow the pattern: [_a-zA-Z0-9\-]:[_a-zA-Z0-9\-]:[_a-zA-Z0-9\-] + * an empty string: `""` + Payload: + content: + application/json: + schema: + type: string + example: '' + application/octet-stream: + schema: + type: string + example: '' + text/plain: + schema: + type: string + example: '' + description: |- + Payload of the message with max size of 250 kB. It can be any HTTP + supported content, including binary content. + Value: + content: + application/json: + schema: + type: object + example: {} + description: |- + JSON representation of the value to be created/updated. This may be as + well `null` or an empty object. + + Consider that the value has to be a JSON value, examples: + + * for a number, the JSON value is the number: `42` + + * for a string, the JSON value must be quoted: `"aString"` + + * for a boolean, the JSON value is the boolean: `true` + + * for an object, the JSON value is the object: `{ "key": "value"}` -} We strongly recommend to use a restricted set of characters for the key (identifier). Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + + * for an list, the JSON value is the list: `[ 1,2,3 ]` + required: true + PatchValue: + content: + application/merge-patch+json: + schema: + type: object + example: {} + description: |- + JSON representation of the value to be patched. This may be as well an empty object. + + Consider that the value has to be a JSON value. + + Examples: + * for a number, the JSON value is the number: `42` + * for a string, the JSON value must be quoted: `"aString"` + * for a boolean, the JSON value is the boolean: `true` + * for an object, the JSON value is the object: `{ "key": "value"}` -} We strongly recommend to use a restricted set of characters for the key (identifier). Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* + * for an list, the JSON value is the list: `[ 1,2,3 ]` + * special value `null` will delete the referenced key. For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). + required: true + ActivateTokenIntegration: + content: + application/json: + schema: + properties: + announcement: + $ref: '#/components/schemas/SubjectAnnouncement' + example: + announcement: + beforeExpiry: 5m + whenDeleted: true + requestedAcks: + labels: + - 'my-connection-id:my-issued-acknowledgement' + timeout: 30s + randomizationInterval: 5m + description: Optional request payload for `activateTokenIntegration` policy action. + required: false + MigrateThingDefinitionRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/MigrateThingDefinitionRequest' + description: 'JSON payload containing the new definition URL, migration payload, patch conditions, and initialization flag.' + required: true + PermissionCheckRequest: + content: + application/json: + schema: + type: object + description: Request to check permissions for various entities and resources. + additionalProperties: + type: object + description: Details for a specific permission check request. + properties: + resource: + type: string + description: Resource path the permission check applies to. + entityId: + type: string + description: thingId of the entity performing the action. + hasPermissions: + type: array + items: + type: string + enum: + - READ + - WRITE + description: Required permissions on the resource. + description: 'JSON object containing permission check requests, keyed by an arbitrary identifier.' + required: true + DynamicValidationConfigRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/DynamicValidationConfig' + ConfigOverridesRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/ConfigOverrides' + responses: + EntityTooLarge: + description: The created or modified entity is larger than the accepted limit of 100 kB. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + MessageTooLarge: + description: The size of the sent message is larger than the accepted limit of 250 kB. + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + NotModified: + description: |- + The (sub-)resource has not been modified. This happens when you specified a If-None-Match header which + matches the current ETag of the (sub-)resource. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + PreconditionFailed: + description: |- + A precondition for reading or writing the (sub-)resource failed. This will happen for write requests, if you + specified an If-Match or If-None-Match header, which fails the precondition check against the current ETag of + the (sub-)resource. For read requests, this error may only happen for a failing If-Match header. In case of a + failing If-None-Match header for a read request, status 304 will be returned instead. + headers: + ETag: + description: |- + The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format + "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/AdvancedError' + DependencyFailed: + description: |- + One or more acknowledgement requests in the parameter `requested-acks` + were not fulfilled. + content: + application/json: + schema: + properties: + acknowledgementLabel1: + properties: + status: + type: integer + description: The HTTP status of the acknowledgement + payload: + oneOf: + - type: object + - type: string + - type: number + - type: array + - type: boolean + description: The payload of the acknowledgement + required: + - status + example: + status: 200 + payload: OK + example: + acknowledgementLabel1: + status: 200 + payload: OK + acknnowledgementLabelN: + status: 403 + payload: Forbidden + SuccessUpdateLogLevel: + description: Return The summary of the outcome of all modified pods + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ResultUpdateRequest' + SuccessUpdateLogLevelSinglePod: + description: Return The summary of the outcome of modified pod + content: + application/json: + schema: + $ref: '#/components/schemas/ModuleUpdatedLogLevel' + MigrateThingDefinitionResponse: + description: 'The thing definition was successfully updated, and the updated Thing is returned.' + content: + application/json: + schema: + $ref: '#/components/schemas/MigrateThingDefinitionResponse' + PermissionCheckResponse: + description: Response with permission check results for each entity. + content: + application/json: + schema: + type: object + description: Response with permission check results for each entity. + additionalProperties: + type: boolean + WotValidationConfigResponse: + description: The WoT validation configuration. + content: + application/json: + schema: + $ref: '#/components/schemas/WotValidationConfig' + DynamicValidationConfigResponse: + description: The dynamic WoT validation configuration. + content: + application/json: + schema: + $ref: '#/components/schemas/DynamicValidationConfig' + ConfigOverridesResponse: + description: The WoT validation configuration overrides. + content: + application/json: + schema: + $ref: '#/components/schemas/ConfigOverrides' + parameters: + AllowPolicyLockoutParam: + name: allow-policy-lockout + in: query + description: |- + Defines whether a subject is allowed to create a policy without having WRITE permission on the policy + resource of the created policy. + + The default (if ommited) is `false`. + required: false + schema: + type: boolean + AttributesPathPathParam: + name: attributePath + in: path + description: 'The path to the attribute, e.g. **manufacturer/name**' + required: true + schema: + type: string + AttributesFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields from the attributes to be + included in the returned JSON. + + #### Selectable fields + + Supports selecting arbitrary sub-fields as defined in the attributes by + using a comma-separated list: + * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + + For example: + * `?fields=model` would select only `model` attribute value (if present) + * `?fields=model,make` would select `model` and `make` attribute values (if present) + + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields + inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select + * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield + + For example: + * `?fields=location(longitude,latitude)` would select the `longitude` and `latitude` value inside the `location` attribute + + #### Examples + + * `?fields=model,make,location(longitude,latitude)` + + * `?fields=listOfAddresses/postal(city,street))` + required: false + schema: + type: string + ChannelParam: + name: channel + in: query + description: |- + Defines to which channel to route the command: `twin` (digital twin) or `live` (the device). + * If setting the channel parameter is omitted, the `twin` channel is set by default and the command is routed to the persisted representation of a thing in Eclipse Ditto. + * When using the `live` channel, the command/message is sent towards the device. + required: false + schema: + type: string + enum: + - twin + - live + ChannelParamPutDescription: + name: channel + in: query + description: |- + Defines to which channel to route the command: `twin` (digital twin) or `live` (the device). + * If setting the channel parameter is omitted, the `twin` channel is set by default and the command is routed to the persisted representation of a thing in Eclipse Ditto. + * When using the `live` channel, the command/message is sent towards the device. + + The option `live` is not available when a new thing should be created, only for updating an + existing thing. + required: false + schema: + type: string + enum: + - twin + - live + ConditionParam: + name: condition + in: query + description: |- + Defines that the request should only be processed if the given condition is met. The condition can be specified using RQL syntax. + #### Examples + E.g. if the temperature is not 23.9 update it to 23.9: + * ```PUT /api/2/things/{thingId}/features/temperature/properties/value?condition=ne(features/temperature/properties/value,23.9)``` + + `body: 23.9` + + Further example conditions: + * ```?condition=eq(features/temperature/properties/unit,"Celsius")``` + * ```?condition=ge(features/temperature/properties/lastModified,"2021-08-22T19:45:00Z")``` + * ```?condition=gt(_modified,"2021-08-05T12:17:00Z")``` + * ```?condition=exists(features/temperature/properties/value)``` + * ```?condition=empty(features/temperature/properties/value)``` + * ```?condition=and(gt(features/temperature/properties/value,18.5),lt(features/temperature/properties/value,25.2))``` + * ```?condition=or(gt(features/temperature/properties/value,18.5),not(exists(features/temperature/properties/value))``` + required: false + schema: + type: string + LiveChannelConditionParam: + name: live-channel-condition + in: query + description: |- + Defines that the request should fetch thing data via `live` channel if the given condition is met. The condition can be specified using RQL syntax. + #### Examples + + * ```?live-channel-condition=lt(_modified,"2021-12-24T12:23:42Z")``` + + * ```?live-channel-condition=ge(features/ConnectionStatus/properties/status/readyUntil,time:now)``` + required: false + schema: + type: string + LiveChannelTimeoutStrategyParam: + name: live-channel-timeout-strategy + in: query + description: Defines a strategy how to handle timeouts of a live response to a request sent via `channel=live` or with a matching live-channel-condition. + required: false + schema: + enum: + - fail + - use-twin + DesiredPropertiesFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields from the desiredProperties to be + included in the returned JSON. + + #### Selectable fields + + Supports selecting arbitrary sub-fields as defined in the desiredProperties by + using a comma-separated list: + * several desiredProperties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + + For example: + * `?fields=temperature` would select only `temperature` property value of desiredProperties (if present) + * `?fields=temperature,humidity` would select only `temperature` and `humidity` property values of desiredProperties (if present) + + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields + inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select + * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield + + For example: + * `?fields=location(longitude,latitude)` would select the `longitude` and `latitude` value inside the `location` property of desiredProperties + + #### Examples + + * `?fields=temperature,humidity,location(longitude,latitude)` + + * `?fields=configuration,status(powerConsumption/watts)` + required: false + schema: + type: string + FeatureFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields from the selected feature to be + included in the returned JSON. + + #### Selectable fields + + * `properties` + + Supports selecting arbitrary sub-fields by using a comma-separated list: + * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + + For example: + * `?fields=properties/color` would select only `color` property value (if present) + * `?fields=properties/color,properties/brightness` would select only `color` and `brightness` property values (if present) + + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select + * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield + + For example: + * `?fields=properties(color,brightness)` would select only `color` and `brightness` property values (if present) + * `?fields=properties(location/longitude)` would select the `longitude` value inside the `location` object + + #### Examples + + * `?fields=properties(color,brightness)` + required: false + schema: + type: string + FeatureIdPathPathParam: + name: featureId + in: path + description: The ID of the feature - has to conform to RFC-3986 (URI) + required: true + schema: + type: string + FeaturesFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields from one or more features to be + included in the returned JSON. + + #### Selectable fields + + * `{featureId}` The ID of the feature to select properties in + * `properties` + Supports selecting arbitrary sub-fields by using a comma-separated list: + * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + For example: + * `?fields={featureId}/properties/color` would select only `color` property value (if present) of the feature identified with `{featureId}` + * `?fields={featureId}/properties/color,properties/brightness` would select only `color` and `brightness` property values (if present) of the feature identified with `{featureId}` + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select + * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield + For example: + * `?fields={featureId}/properties(color,brightness)` would select only `color` and `brightness` property values (if present) of the feature identified with `{featureId}` + * `?fields={featureId}/properties(location/longitude)` would select the `longitude` value inside the `location` object of the feature identified with `{featureId}` + + + #### Examples + * `?fields=EnvironmentScanner/properties(temperature,humidity)` + * `?fields=EnvironmentScanner/properties(temperature,humidity),Vehicle/properties/configuration` + required: false + schema: + type: string + IfMatchHeaderParam: + name: If-Match + in: header + description: |- + The `If-Match` header, which has to conform to RFC-7232 (Conditional Requests). Common usages are: + * optimistic locking by specifying the `ETag` from a previous GET response, e.g. `If-Match: "rev:4711"` + * retrieving or modifying a resource only if it already exists, e.g. `If-Match: *` + required: false + schema: + type: string + IfMatchHeaderParamHash: + name: If-Match + in: header + description: |- + The `If-Match` header which has to conform to RFC-7232 (Conditional Requests). Common usages are: + * optimistic locking by specifying the `ETag` from a previous HTTP response, e.g. `If-Match: "hash:a75ece4e"` + * retrieving or modifying a resource only if it already exists, e.g. `If-Match: *` + required: false + schema: + type: string + IfNoneMatchHeaderParam: + name: If-None-Match + in: header + description: 'The `If-None-Match` header, which has to conform to RFC-7232 (Conditional Requests). A common usage scenario is to modify a resource only if it does not yet exist, thus to create it, by specifying `If-None-Match: *`.' + required: false + schema: + type: string + IfEqualHeaderParam: + name: if-equal + in: header + description: 'The `if-equal` header can take the values ''update'' (which is the default if omitted), ''skip'' or ''skip-minimizing-merge''. If ''update'' is defined, the entity will always be updated, even if it is equal before the update. If ''skip'' is defined, the entity not be updated if it is equal before the update. In this case a ''Precondition Failed'' 412 status is returned. If ''skip-minimizing-merge'' is defined, the entity will not be updated if it is equal before the update. In this case a ''Precondition Failed'' 412 status is returned. Additionally, merge/patch commands will be minimized to only the fields which actually changed, compared to the current state of the entity.' + required: false + schema: + type: string + enum: + - update + - skip + - skip-minimizing-merge + ImportedPolicyIdPathParam: + name: importedPolicyId + in: path + description: |- + The ID of the imported policy needs to follow the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + + The namespace needs to: + * conform to the reverse domain name notation + required: true + schema: + type: string + LabelPathParam: + name: label + in: path + description: The label of a policy entry + required: true + schema: + type: string + LiveMessageRequestedAcksParam: + name: requested-acks + in: query + description: |- + Contains the "requested acknowledgements" for this request as comma separated list. The HTTP call will + block until all requested acknowledgements were aggregated or will time out based on the specified `timeout` + parameter. + + The default (if omitted) requested acks is `requested-acks=live-response` which will block the + HTTP call until a subscriber of the live message sends a response. + required: false + schema: + type: string + MessageClaimTimeoutParam: + name: timeout + in: query + description: |- + Contains an optional timeout (in seconds) of how long to wait for the Claim response and therefore block the + HTTP request. Default value (if omitted): 60 seconds. Maximum value: 600 seconds. A value of 0 seconds applies + fire and forget semantics for the message. + required: false + schema: + type: integer + MessageSubjectPathParam: + name: messageSubject + in: path + description: The subject of the Message - has to conform to RFC-3986 (URI) + required: true + schema: + type: string + MessageTimeoutParam: + name: timeout + in: query + description: |- + Contains an optional timeout (in seconds) of how long to wait for the message response and therefore block the + HTTP request. Default value (if omitted): 10 seconds. Maximum value: 60 seconds. A value of 0 seconds applies + fire and forget semantics for the message. + required: false + schema: + type: integer + Namespace: + name: namespace + in: query + description: Defines a custom namespace for the thing while generating a new thing ID. + required: false + schema: + type: string + example: com.example.namespace + NamespacesFilter: + name: namespaces + in: query + description: |- + A comma-separated list of namespaces. This list is used to limit the query to things in the given namespaces + only. + + + #### Examples: + + * `?namespaces=com.example.namespace` + + * `?namespaces=com.example.namespace1,com.example.namespace2` + required: false + schema: + type: string + PolicyFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields to be included in the returned + JSON. + + #### Selectable fields + + * `policyId` + * `entries` + + Supports selecting arbitrary sub-fields by using a comma-separated list: + * several entry paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + + For example: + * `?fields=entries/ditto` would select only the `ditto` entry value(if present) + * `?fields=entries/ditto,entries/user` would select only `ditto` and + `user` entry values (if present) + + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) + separated with `/`) to select + + * sub-selectors can be used to request only specific sub-fields by placing expressions + in parentheses `( )` after a selected subfield + + For example: + * `?fields=entries(ditto,user)` would select only `ditto` + and `user` entry values (if present) + * `?fields=entries(ditto/subjects)` would select the `subjects` value + inside the `ditto` entry + * `?fields=entries/ditto/subjects(issuer:google,issuer:azure)` would select the `issuer:google` and + `issuer:azure` values inside the `subjects` object inside the `entries` object + + * `_namespace` + + Specifically selects the namespace also contained in the `policyId` + + * `_revision` + + Specifically selects the revision of the policy. The revision is a counter, which is incremented on each modification of a policy. + + * `_created` + + Specifically selects the created timestamp of the policy in ISO-8601 UTC format. The timestamp is set on creation of a policy. + + * `_modified` + + Specifically selects the modified timestamp of the policy in ISO-8601 UTC format. The timestamp is set on each modification of a policy. + + * `_metadata` + + Specifically selects the Metadata of the policy. The content is a JSON object having the policy's JSON structure with the difference that the JSON leaves of the policy are JSON objects containing the metadata. + + #### Examples + + * `?fields=policyId,entries,_revision` + * `?fields=entries(ditto,user),_namespace` + required: false + schema: + type: string + PolicyIdPathParam: + name: policyId + in: path + description: |- + The ID of the policy needs to follow the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). + + The namespace needs to: + * conform to the reverse domain name notation + required: true + schema: + type: string + PropertiesFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields from the properties to be + included in the returned JSON. + + #### Selectable fields + + Supports selecting arbitrary sub-fields as defined in the properties by + using a comma-separated list: + * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + + For example: + * `?fields=temperature` would select only `temperature` property value (if present) + * `?fields=temperature,humidity` would select only `temperature` and `humidity` property values (if present) + + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields + inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select + * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield + + For example: + * `?fields=location(longitude,latitude)` would select the `longitude` and `latitude` value inside the `location` property + + #### Examples + + * `?fields=temperature,humidity,location(longitude,latitude)` + + * `?fields=configuration,status(powerConsumption/watts)` + required: false + schema: + type: string + PropertyPathPathParam: + name: propertyPath + in: path + description: The path to the property + required: true + schema: + type: string + PutMetadataParam: + name: put-metadata + in: header + description: 'The `put-metadata` header, which sets Metadata information in the Thing.' + required: false + schema: + type: array + description: An array of objects containing metadata to apply. + items: + type: object + description: Object containing a `key` where to apply the metadata and a `value` with the metadata value to apply. + additionalProperties: + properties: + key: + type: string + description: The JsonPointer to set the metadata `value` to. May start with `*/` in order to apply the metadata to all affected JSON leaves. + value: + description: The arbitrary JSON value to set as metadata. + GetMetadataParam: + name: get-metadata + in: header + description: 'The `get-metadata` header, which retrieves Metadata of the Thing.' + required: false + schema: + type: string + description: A string of comma separated JsonPointers to retrieve from the Metadata of the Thing. + DeleteMetadataParam: + name: delete-metadata + in: header + description: 'The `delete-metadata` header, which deletes Metadata of the Thing.' + required: false + schema: + type: string + description: A string of comma separated JsonPointers to delete from the Metadata of the Thing. + RequestedAcksParam: + name: requested-acks + in: query + description: |- + Contains the "requested acknowledgements" for this modifying request as comma separated list. The HTTP call will + block until all requested acknowledgements were aggregated or will time out based on the specified `timeout` + parameter. + + The default (if omitted) requested acks is `requested-acks=twin-persisted` which will block the + HTTP call until the change was persited to the twin. + required: false + schema: + type: string + ResourcePathPathParam: + name: resourcePath + in: path + description: The path of an (Authorization) Resource + required: true + schema: + type: string + ResponseRequiredParam: + name: response-required + in: query + description: |- + Defines whether a response is required to the API call or not - if set to `false` the response will directly + sent back with a status code of `202` (Accepted). + + The default (if ommited) response is `true`. + required: false + schema: + type: boolean + SearchFilter: + name: filter + in: query + description: |- + + #### Filter predicates: + + * ```eq({property},{value})``` (i.e. equal to the given value) + + * ```ne({property},{value})``` (i.e. not equal to the given value) + + * ```gt({property},{value})``` (i.e. greater than the given value) + + * ```ge({property},{value})``` (i.e. equal to the given value or greater than it) + + * ```lt({property},{value})``` (i.e. lower than the given value or equal to it) + + * ```le({property},{value})``` (i.e. lower than the given value) + + * ```in({property},{value},{value},...)``` (i.e. contains at least one of the values listed) + + * ```like({property},{value})``` (i.e. contains values similar to the expressions listed) + + * ```ilike({property},{value})``` (i.e. contains values similar and case insensitive to the expressions listed) + + * ```exists({property})``` (i.e. all things in which the given path exists) + + * ```empty({property})``` (i.e. all things in which the given path is absent, null, an empty array, an empty object or an empty string) + + + Note: When using filter operations, only things with the specified properties are returned. + For example, the filter `ne(attributes/owner, "SID123")` will only return things that do have + the `owner` attribute. + + + #### Logical operations: + + + * ```and({query},{query},...)``` + + * ```or({query},{query},...)``` + + * ```not({query})``` + + + #### Examples: + + * ```eq(attributes/location,"kitchen")``` + + * ```ge(thingId,"myThing1")``` + + * ```gt(_created,"2020-08-05T12:17")``` + + * ```exists(features/featureId)``` + + * ```empty(attributes/tags)``` + + * ```and(eq(attributes/location,"kitchen"),eq(attributes/color,"red"))``` + + * ```or(eq(attributes/location,"kitchen"),eq(attributes/location,"living-room"))``` + + * ```like(attributes/key1,"known-chars-at-start*")``` + + * ```like(attributes/key1,"*known-chars-at-end")``` + + * ```like(attributes/key1,"*known-chars-in-between*")``` + + * ```like(attributes/key1,"just-som?-char?-unkn?wn")``` + + The `like` filters with the wildcard `*` at the beginning can slow down your search request. + required: false + schema: + type: string + SubjectIdPathParam: + name: subjectId + in: path + description: The ID of an (Authorization) Subject + required: true + schema: + type: string + ThingFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields to be included in the returned + JSON. attributes can be selected in the same manner. + + #### Selectable fields + + * `thingId` + * `policyId` + * `definition` + * `attributes` + + Supports selecting arbitrary sub-fields by using a comma-separated list: + * several attribute paths can be passed as a comma-separated list of JSON pointers (RFC-6901) + + For example: + * `?fields=attributes/model` would select only `model` attribute value (if present) + * `?fields=attributes/model,attributes/location` would select only `model` and + `location` attribute values (if present) + + Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: + * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) + separated with `/`) to select + + * sub-selectors can be used to request only specific sub-fields by placing expressions + in parentheses `( )` after a selected subfield + + For example: + * `?fields=attributes(model,location)` would select only `model` + and `location` attribute values (if present) + * `?fields=attributes(coffeemaker/serialno)` would select the `serialno` value + inside the `coffeemaker` object + * `?fields=attributes/address/postal(city,street)` would select the `city` and + `street` values inside the `postal` object inside the `address` object + + * `features` + + Supports selecting arbitrary fields in features similar to `attributes` (see also features documentation for more details) + + * `_namespace` + + Specifically selects the namespace also contained in the `thingId` + + * `_revision` + + Specifically selects the revision of the thing. The revision is a counter, which is incremented on each modification of a thing. + + * `_created` + + Specifically selects the created timestamp of the thing in ISO-8601 UTC format. The timestamp is set on creation of a thing. + + * `_modified` + + Specifically selects the modified timestamp of the thing in ISO-8601 UTC format. The timestamp is set on each modification of a thing. + + * `_metadata` + + Specifically selects the Metadata of the thing. The content is a JSON object having the Thing's JSON structure with the difference that the JSON leaves of the Thing are JSON objects containing the metadata. + + * `_policy` + + Specifically selects the content of the policy associated to the thing. (By default, only the policyId is returned.) + + #### Examples + + * `?fields=thingId,attributes,features` + * `?fields=attributes(model,manufacturer),features` + required: false + schema: + type: string + ThingIdPathParam: + name: thingId + in: path + description: 'The ID of a thing needs to follow the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)).' + required: true + schema: + type: string + TimeoutParam: + name: timeout + in: query + description: |- + Defines how long the backend should wait for completion of the request, e.g. applied when waiting for requested + acknowledgements via the `requested-acks` param. Can be specified without unit (then seconds are assumed) or + together with `s`, `ms` or `m` unit. Example: `42s`, `1m`. + + The default (if omitted) and maximum timeout is `60s`. A value of `0` applies fire and forget semantics for + the command resulting in setting `response-required=false`. + required: false + schema: + type: string + ConnectionIdPathParam: + name: connectionId + in: path + description: The ID of the connection + required: true + schema: + type: string + ConnectionFieldsQueryParam: + name: fields + in: query + description: |- + Contains a comma-separated list of fields to be included in the returned + JSON. + + #### Selectable fields + + * `id` + * `name` + * `_revision` + + Specifically selects the revision of the connection. The revision is a counter, which is incremented on each modification of a connection. + + * `_created` + + Specifically selects the created timestamp of the connection in ISO-8601 UTC format. The timestamp is set on creation of a connection. + + * `_modified` + + Specifically selects the modified timestamp of the connection in ISO-8601 UTC format. The timestamp is set on each modification of a connection. + + * `connectionType` + * `connectionStatus` + * `credentials` + * `uri` + * `sources` + * `targets` + * `sshTunnel` + * `clientCount` + * `failoverEnabled` + * `validateCertificates` + * `processorPoolSize` + * `specificConfig` + * `mappingDefinitions` + * `tags` + * `ca` + + #### Examples + + * `?fields=id,_revision,sources` + required: false + schema: + type: string + LoggingFieldsQueryParam: + name: includeDisabledLoggers + in: query + description: Include disabled loggers + required: false + schema: + type: boolean + ModuleNamePathParam: + name: moduleName + in: path + description: The name of module + required: true + schema: + type: string + example: gateway + PathParam: + name: path + in: query + description: 'The path points to information on service name, service instance index, JVM arguments and environment variables.' + schema: + type: string + example: ditto.info + required: false + NamePodParam: + name: podName + in: path + description: The name of pod + required: true + schema: + type: string + example: ditto-gateway-764fc5f474-qrm2r + ServiceNameParam: + name: serviceName + in: path + description: Specified service target for the command execution + required: true + schema: + type: string + enum: + - things + - policies + - connectivity + InstanceIndex: + name: instanceIndex + in: path + description: The index of the current instance + required: true + schema: + type: string + schemas: + Error: + properties: + status: + type: integer + description: The HTTP status of the error + message: + type: string + description: The message of the error - what went wrong + description: + type: string + description: A description how to fix the error or more details + href: + type: string + description: A link to further information about the error and how to fix it + required: + - status + - message + AdvancedError: + properties: + status: + type: integer + description: The HTTP status of the error + error: + type: string + description: The error code of the occurred exception + message: + type: string + description: The message of the error - what went wrong + description: + type: string + description: A description how to fix the error or more details + href: + type: string + description: A link to further information about the error and how to fix it + required: + - status + - error + - message + Attributes: + type: object + description: An arbitrary JSON object describing the attributes of a thing. + Definition: + type: string + description: 'A single fully qualified identifier of a definition in the form ''::'' or a valid HTTP(s) URL' + pattern: '([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+)' + FeatureDefinition: + type: array + description: The definitions of a feature. + items: + type: string + description: 'A single fully qualified identifier of a feature definition in the form ''::'' or a valid HTTP(s) URL' + pattern: '([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+)' + FeatureProperties: + type: object + description: An arbitrary JSON object describing the properties of a feature. + FeatureDesiredProperties: + type: object + description: An arbitrary JSON object describing the desired properties of a feature. + Feature: + type: object + properties: + definition: + $ref: '#/components/schemas/FeatureDefinition' + properties: + $ref: '#/components/schemas/FeatureProperties' + desiredProperties: + $ref: '#/components/schemas/FeatureDesiredProperties' + SearchResultThings: + properties: + items: + type: array + items: + $ref: '#/components/schemas/Thing' + cursor: + type: string + SearchResultThingsCount: + type: integer + NewThing: + type: object + properties: + _policy: + allOf: + - $ref: '#/components/schemas/Policy' + description: |- + The initial policy to create for this thing. This will create a separate policy entity managed by resource `/policies/{thingId}`. + + + Use the placeholder `{{ request:subjectId }}` in order to let the backend insert the authenticated subjectId of the HTTP request. + _copyPolicyFrom: + type: string + description: |- + This field may contain + * the policy ID of an existing policy. + + The policy is copied and used for this newly created thing. The + caller needs to have READ and WRITE* access to the policy. + * a placeholder reference to a thing in the format {{ ref:things/[thingId]/policyId }} where you need to + replace [thingId] with a valid thing ID. + + The newly created thing will then obtain a copy of the policy of + the referenced thing. The caller needs to have READ access to the thing and READ and WRITE* + access to the policy of the thing. + + + * The check for WRITE permission avoids locking yourself out of the newly created policy. You can + bypass this check by setting the header `allowPolicyLockout` to `true`. Be aware that the authorized + subject cannot modify the policy if you do not assign WRITE permission on the policy resource! + + If you want to specify a policy ID for the copied policy, use the policyId field. + + This field must not be used together with the field _policy. If you specify both _policy and _copyPolicyFrom + this will lead to an error response. + policyId: + type: string + description: |- + The policy ID used for controlling access to this thing. Managed by + resource `/policies/{policyId}`. + definition: + $ref: '#/components/schemas/Definition' + attributes: + $ref: '#/components/schemas/Attributes' + features: + $ref: '#/components/schemas/Features' + required: + - policyId + PatchThing: + type: object + properties: + thingId: + type: string + description: Unique identifier representing the thing + policyId: + type: string + description: 'The ID of the policy which controls the access to this thing. policies are managed by resource `/policies/{policyId}`' + definition: + $ref: '#/components/schemas/Definition' + attributes: + $ref: '#/components/schemas/Attributes' + features: + $ref: '#/components/schemas/Features' + required: + - thingId + - policyId + Thing: + type: object + properties: + thingId: + type: string + description: Unique identifier representing the thing + policyId: + type: string + description: 'The ID of the policy which controls the access to this thing. policies are managed by resource `/policies/{policyId}`' + definition: + $ref: '#/components/schemas/Definition' + attributes: + $ref: '#/components/schemas/Attributes' + features: + $ref: '#/components/schemas/Features' + _revision: + type: string + description: |- + _(read-only)_ The revision is a counter which is incremented on each modification of a Thing. This field + is not returned by default but must be selected explicitly. + _created: + type: string + description: |- + _(read-only)_ The created timestamp of the Thing in ISO-8601 UTC format. The timestamp is set on creation + of a Thing. This field is not returned by default but must be selected explicitly. + _modified: + type: string + description: |- + _(read-only)_ The modified timestamp of the Thing in ISO-8601 UTC format. The timestamp is set on each + modification of a Thing. This field is not returned by default but must be selected explicitly. + _metadata: + type: object + description: _(read-only)_ The Metadata of the Thing. This field is not returned by default but must be selected explicitly. + required: + - thingId + - policyId + MigrateThingDefinitionRequest: + type: object + description: JSON payload to migrate the definition of a Thing. + properties: + thingDefinitionUrl: + type: string + format: uri + description: The URL of the new Thing definition to be applied. + example: 'https://models.example.com/thing-definition-1.0.0.tm.jsonld' + migrationPayload: + type: object + description: | + Optional migration payload with updates to attributes and features. + String values may contain {{ thing-json: }} placeholders to reference + existing Thing data (e.g. attributes/location, features/sensor/properties/temp). + Resolved values keep their JSON type; missing paths cause the request to fail. + properties: + attributes: + type: object + additionalProperties: true + description: Attributes to be updated in the thing. + example: + manufacturer: New Corp + location: 'Berlin, main floor' + features: + type: object + additionalProperties: + type: object + properties: + properties: + type: object + additionalProperties: true + description: Features to be updated in the thing. + example: + thermostat: + properties: + status: + temperature: + value: 23.5 + unit: DEGREE_CELSIUS + patchConditions: + type: object + description: Optional conditions to apply the migration only if the existing thing matches the specified values. + additionalProperties: + type: string + example: + 'thing:/features/thermostat': not(exists(/features/thermostat)) + initializeMissingPropertiesFromDefaults: + type: boolean + description: Flag indicating whether missing properties should be initialized with default values. + example: true + default: false + required: + - thingDefinitionUrl + MigrateThingDefinitionResponse: + type: object + description: Response payload after applying or simulating a migration to a Thing. + properties: + thingId: + type: string + description: Unique identifier representing the migrated Thing. + patch: + type: object + description: The patch containing updates to the Thing. + properties: + definition: + $ref: '#/components/schemas/Definition' + attributes: + $ref: '#/components/schemas/Attributes' + features: + $ref: '#/components/schemas/Features' + mergeStatus: + type: string + description: | + Indicates the result of the migration process. + - `APPLIED`: The migration was successfully applied. + - `DRY_RUN`: The migration result was calculated but not applied. + enum: + - APPLIED + - DRY_RUN + example: APPLIED + required: + - thingId + - patch + - mergeStatus + NewPolicy: + type: object + description: Policy consisting of policy entries + properties: + entries: + $ref: '#/components/schemas/PolicyEntries' + imports: + $ref: '#/components/schemas/PolicyImports' + required: + - entries + Policy: + type: object + description: Policy consisting of policy entries + properties: + policyId: + type: string + description: Unique identifier representing the policy + entries: + $ref: '#/components/schemas/PolicyEntries' + imports: + $ref: '#/components/schemas/PolicyImports' + required: + - policyId + - entries + PolicyImports: + type: object + description: Policy imports containing one policy import for each key. The key is the policy ID of the referenced policy. + properties: + policyImport1: + $ref: '#/components/schemas/PolicyImport' + policyImportN: + $ref: '#/components/schemas/PolicyImport' + example: + 'com.acme:policyId1': + entries: + - label1 + - label2 + 'com.acme:policyId2': + entries: + - import + 'com.acme:policyId3': {} + PolicyImport: + type: object + description: Single policy import defining which policy entries of the referenced policy are imported. + properties: + entries: + type: array + default: [] + description: |- + The policy entries to import from the referenced policy identified by their labels. + In case the field is omitted or an empty array is provided, + all policy entries defined as implicit ("importable": "implicit") are imported. + items: + type: string + description: Label of a policy entry to import from the referenced policy. + transitiveImports: + $ref: '#/components/schemas/TransitiveImports' + example: + entries: + - default + - import + Importable: + type: string + description: |- + Controls the import behavior of this policy entry i.e. whether this policy entry is implicitly, + explicitly or never imported when referenced from another policy. + * `implicit` (default): the policy entry is imported without being listed in the importing policy individually + * `explicit`: the policy entry is only imported if it is listed in the importing policy + * `never`: the policy entry is not imported, regardless of being listed in the importing policy + If the field is not specified, default value is `implicit`. + enum: + - implicit + - explicit + - never + default: implicit + example: explicit + PolicyEntries: + type: object + description: Policy entries containing one policy entry for each arbitrary `label` key + properties: + label1: + $ref: '#/components/schemas/PolicyEntry' + labelN: + $ref: '#/components/schemas/PolicyEntry' + PolicyEntry: + type: object + description: |- + Single policy entry. Both `subjects` and `resources` are optional — they + default to empty when absent. An entry may define `references` to inherit + subjects, resources, and namespaces from other entries (local or imported). + properties: + subjects: + $ref: '#/components/schemas/Subjects' + resources: + $ref: '#/components/schemas/Resources' + namespaces: + type: array + description: |- + Restricts this policy entry to things whose namespace matches at least one pattern. + If omitted or empty, the entry applies to all namespaces. + * `com.acme` matches only that exact namespace + * `com.acme.*` matches namespaces below `com.acme`, but not `com.acme` itself + items: + type: string + example: + - com.acme + - com.acme.* + importable: + $ref: '#/components/schemas/Importable' + allowedAdditions: + $ref: '#/components/schemas/AllowedAdditions' + references: + $ref: '#/components/schemas/References' + Subjects: + type: object + description: A SubjectEntry defines who is addressed. + properties: + 'nginx:subjectId1': + $ref: '#/components/schemas/SubjectEntry' + 'nginx:subjectIdN': + $ref: '#/components/schemas/SubjectEntry' + SubjectEntry: + type: object + description: Single (Authorization) Subject entry holding its type. + required: + - type + properties: + type: + type: string + description: 'The type is offered only for documentation purposes. You are not restricted to any specific types, but we recommend to use it to specify the kind of the subject as shown in our examples.' + expiry: + type: string + description: The optional expiry timestamp (formatted in ISO-8601) indicates how long this subject should be considered active before it is automatically deleted from the Policy. + format: date-time + announcement: + $ref: '#/components/schemas/SubjectAnnouncement' + example: + type: 'This is some description for this subject, adjust as needed.' + expiry: '2020-12-07T11:36:40Z' + announcement: + beforeExpiry: 5m + whenDeleted: true + Resources: + type: object + description: |- + (Authorization) Resources containing one ResourceEntry for each + `type:path` key, `type` being one of the following `thing`, `policy`, `message`. + additionalProperties: + $ref: '#/components/schemas/ResourceEntry' + example: + 'thing:/': + grant: + - READ + - WRITE + revoke: null + 'thing:/attributes/some/path': + grant: null + revoke: + - READ + 'policy:/': + grant: + - READ + - WRITE + revoke: null + 'message:/': + grant: + - READ + - WRITE + revoke: null + ResourceEntry: + type: object + description: |- + Single (Authorization) Resource entry defining permissions per effect. + Allowed effects are `grant` and `revoke`. + properties: + grant: + type: array + items: + $ref: '#/components/schemas/Permission' + revoke: + type: array + items: + $ref: '#/components/schemas/Permission' + Permission: + type: string + description: A Permission allows a certain action on an entity + enum: + - READ + - WRITE + AllowedAdditions: + type: array + description: |- + Defines which types of additions are allowed when this entry is referenced by other entries + via `references`. + + Semantics: + * Field absent (omitted) — no restriction; the referencing entry's own subjects, resources, + and namespaces are merged in as usual. This is the upgrade-friendly default. + * Field present and empty (`[]`) — no additions allowed; only the referenced entry's content is + effective. + * Field present with values — only the listed kinds of additions survive on the referencing entry. + + This field is enforced as a runtime filter, not as a write-time policy contract: a referencing + entry that declares own subjects/resources/namespaces not permitted here can still be persisted, + but the disallowed own additions are silently stripped at enforcement time. The same filter + applies whether the reference is local (within the same policy) or an import reference. + * `subjects` — allows referencing entries to add additional subjects on top of this entry + * `resources` — allows referencing entries to add additional resources on top of this entry + * `namespaces` — allows referencing entries to add additional namespace patterns on top of this entry + items: + type: string + enum: + - subjects + - resources + - namespaces + example: + - subjects + TransitiveImports: + type: array + description: |- + List of policy IDs from the imported policy's own imports that should be resolved transitively + before extracting entries. This enables multi-level import chains where a template policy defines + resources and an intermediate policy defines entries with "references" that add local subjects. + + Each entry is the policy ID of a policy that the directly imported policy itself imports from. + Only the listed policy IDs are resolved — this is an explicit whitelist, not a recursive flag. + items: + type: string + description: Policy ID of a policy that the imported policy itself imports from. + example: + - 'org.eclipse.ditto:policy-template' + References: + type: array + description: |- + An optional list of references to other policy entries. Each reference points to an entry + either in the same policy (local reference) or in an imported policy (import reference). + When set, subjects, resources, and namespaces from the referenced entries are additively + merged into this entry. + + * Import reference: contains both `entry` (the label) and `import` (the policy ID of the imported policy) + * Local reference: contains only `entry` (the label of another entry in the same policy) + items: + type: object + description: A single reference to a policy entry. + properties: + entry: + type: string + description: The label of the referenced entry. + import: + type: string + description: |- + The ID of the imported policy this reference points to. + If absent, the reference points to a local entry within the same policy. + required: + - entry + example: + - entry: operator + import: 'energy-corp:power-plant-roles' + - entry: local-admin + SubjectAnnouncement: + type: object + description: Settings for announcements to be made about the subject. + properties: + beforeExpiry: + type: string + description: |- + The duration before expiry when an announcement should be made. + Must be a positive integer followed by one of `h` (hour), `m` (minute) or `s` (second). + whenDeleted: + type: boolean + description: Whether an announcement should be made when this subject is deleted. + requestedAcks: + type: object + description: Settings to enable at-least-once delivery for policy announcements. + properties: + labels: + type: array + description: Acknowledgement labels to request when an announcement is published. + items: + type: string + timeout: + type: string + description: How long to wait for requested announcements before retrying publication of an announcement. + example: + labels: + - 'my-connection-id:my-issued-acknowledgement' + timeout: 5s + randomizationInterval: + type: string + default: 5m + description: 'Interval in which the announcement can be sent earlier than the configured `beforeExpiry`. The actual point in time when the announcement will be sent is `beforeExpire` plus a randomly chosen time within the `randomizationInterval`. E.g assuming `beforeExpiry` is set to 5m and `randomizationInterval` is set to 1m, the announcements will be sent between 5 and 6 minutes before the subject expires. If omitted, the default value will be applied. If set to minimum, no randomization will be applied.' + example: + beforeExpiry: 5m + whenDeleted: true + randomizationInterval: 5m + Features: + type: object + description: |- + List of features where the key represents the `featureId` of each feature. + The `featureId` key must be unique in the list. + additionalProperties: + $ref: '#/components/schemas/Feature' + Connection: + allOf: + - type: object + properties: + id: + type: string + description: The generated unique identifier of the connection + - $ref: '#/components/schemas/NewConnection' + NewConnection: + type: object + required: + - connectionType + - connectionStatus + - uri + - sources + - targets + properties: + name: + type: string + description: The name of the connection + connectionType: + $ref: '#/components/schemas/ConnectionType' + connectionStatus: + $ref: '#/components/schemas/ConnectivityStatus' + uri: + type: string + description: The URI of the connection + sources: + $ref: '#/components/schemas/Sources' + targets: + $ref: '#/components/schemas/Targets' + specificConfig: + type: object + description: Configuration which is only applicable for a specific connection type + clientCount: + type: number + description: How many clients on different cluster nodes should establish the connection + failoverEnabled: + type: boolean + description: Whether or not failover is enabled for this connection + validateCertificates: + type: boolean + description: Whether or not to validate server certificates on connection establishment + mappingDefinitions: + $ref: '#/components/schemas/PayloadMappingDefinitions' + mappingContext: + $ref: '#/components/schemas/MappingContext' + sshTunnel: + type: object + description: The configuration of a local SSH port forwarding used to tunnel the connection to the actual endpoint. + required: + - enabled + - uri + - credentials + properties: + enabled: + type: boolean + description: Whether the tunnel is enabled + example: true + uri: + type: string + description: 'The URI of the SSH host in the format `ssh://[host]:[port]`.' + example: 'ssh://some.host:2222' + credentials: + type: object + description: The credentials used to authenticate at the SSH host. Password and public key authentication are supported. + required: + - type + - username + properties: + type: + type: string + description: The type of credentials used to authenticate. Either `password` or `public-key`. + enum: + - password + - public-key + example: password + username: + type: string + description: The username used for the authentication. + example: user42 + password: + type: string + description: The password used for authentication when credentials type `password` is used. + example: secret! + publicKey: + type: string + description: |- + Public key in PEM base64-encoded format using X.509 syntax. This field is required for credentials type + `public-key`. + example: | + -----BEGIN PUBLIC KEY----- + ... + -----END PUBLIC KEY----- + privateKey: + type: string + description: |- + Private key in PEM base64-encoded format using PKCS #8 syntax. This field is required for credentials type + `public-key`. + example: | + -----BEGIN PRIVATE KEY----- + ... + -----END PRIVATE KEY----- + validateHost: + type: boolean + description: Whether the SSH host is validated using the provided fingerprints. + example: true + knownHosts: + type: array + description: |- + A list of accepted public key fingerprints. One of these fingerprints must match the fingerprint + of the public key the SSH host provides. + example: + - 'MD5:e0:3a:34:1c:68:ed:c6:bc:7c:ca:a8:67:c7:45:2b:19' + items: + type: string + description: |- + The fingerprint is in the format which the command line tool `ssh-keygen` produces, + e.g. `MD5:e0:3a:34:1c:68:ed:c6:bc:7c:ca:a8:67:c7:45:2b:19`. The fingerprint is prefixed with the hash algorithm + used to calculate the fingerprint. Supported algorithms are `MD5`, `SHA1`, `SHA224`, `SHA256`, `SHA384` and `SHA512`. + tags: + type: array + items: + type: string + description: The tags of the connection + Sources: + type: array + title: The subscription sources of this connection + description: The subscription sources of this connection + uniqueItems: true + items: + $ref: '#/components/schemas/Source' + Source: + type: object + title: Source + description: A subscription source subscribed by this connection + properties: + addresses: + type: array + uniqueItems: true + title: Array of source addresses + description: | + The source addresses this connection consumes messages from. The "telemetry", "events", + "command_response" aliases should be used for connections of type "hono". + items: + type: string + title: Source address + description: A source address to consume messages from + consumerCount: + type: integer + title: Consumer count + description: The number of consumers that should be attached to each source address + default: 1 + qos: + type: integer + title: Quality of service level + description: Maximum Quality-of-Service level to request when subscribing for messages + authorizationContext: + type: array + title: The authorization context + description: The authorization context defines all authorization subjects associated for this source + uniqueItems: true + items: + type: string + title: Authorization Subject + description: |- + An authorization subject associated with this source. + You can either use a fixed subject or use a placeholder that resolves header values from incoming messages. + For example to use the `device_id` header in the subject, you can specify the placeholder + `{{ header:device_id }}` which is then replaced by Ditto when a message from this source is processed. + By using a placeholder you can access any header value: `{{ header: }}` + example: + - 'ditto:myAuthorizationSubject' + - 'device:{{ header:device_id }}' + enforcement: + type: object + title: Enforcement configuration + description: Defines an enforcement for this source to make sure that a device can only access its associated Thing. + required: + - input + - filters + properties: + input: + type: string + title: Input value of enforcement + description: |- + The input value of the enforcement that should identify the origin of the message (e.g. a + device id). You can use placeholders within this field depending on the connection type. E.g. for AMQP + 1.0 connections you can use `{{ header:[any-header-name] }}` to resolve the value from a message header. + example: '{{ header:device_id }}' + filters: + type: array + title: The enforcement filters + description: An array of filters. One of the defined filters must match the input value from the message otherwise the message is rejected. + uniqueItems: true + items: + type: string + title: Enforcement filter + description: |- + A filter that must match the input value for a message to be accepted. You can use the placeholders + `{{ thing:id }}`, `{{ thing:name }}` or `{{ thing:namespace }}` in a filter. + example: + - '{{ thing:id }}' + - '{{ thing:namespace }}/{{ thing:name }}' + acknowledgementRequests: + type: object + title: Acknowledgement requests configuration + description: Contains requests to acknowledgements which must be fulfilled before a message consumed from this source is technically settled/ACKed at the e.g. message broker. + additionalProperties: false + properties: + includes: + type: array + title: Included acknowledgement requests + description: Acknowledgement requests to be included for each message consumed by this source. + items: + title: String representation of a single acknowledgement request + type: string + filter: + type: string + title: Filter expression whether to include acknowledgements at all + description: 'Optional filter to be applied to the requested acknowledgements - takes an `fn:filter()` function expression' + example: + - 'fn:filter(header:qos,''ne'',0)' + required: + - includes + payloadMapping: + type: array + title: The payload mappings + description: A list of payload mappings that are applied to messages received via this source. If no payload mapping is specified the standard Ditto mapping is used as default. + items: + type: string + title: Payload Mapping + description: References a payload mapping definition by its ID (the key of the PayloadMappingDefinition) + example: + - Ditto + - status + headerMapping: + type: object + title: Header mapping configuration + description: Ditto protocol headers computed from external headers and certain properties of the Ditto protocol messages created by payload mapping. + replyTarget: + type: object + title: Reply target configuration + description: Configuration for sending responses of incoming commands. + additionalProperties: false + properties: + enabled: + type: boolean + title: Whether reply target is enabled + description: Whether reply target is enabled. + address: + type: string + title: Reply target address + description: |- + The target address where responses of incoming commands from the parent source are published to. + The following placeholders are allowed within the target address: + + * Thing ID: `{{ thing:id }}` + + * Thing Namespace: `{{ thing:namespace }}` + + * Thing Name: `{{ thing:name }}` (the part of the ID without the namespace) + + * Ditto protocol topic attribute: `{{ topic:[topic-placeholder-attr] }}` + + * Ditto protocol header value: `{{ header:[any-header-name] }}` + + If placeholder resolution fails for a response, then the response is dropped. + NOTE Use "command" alias for connections of type "hono". + example: + - '{{ header:device_id }}' + - '{{ source:address }}' + headerMapping: + type: object + title: Header mapping configuration + description: External headers computed from headers and other properties of Ditto protocol messages. + expectedResponseTypes: + type: array + title: Expected response types + description: Contains a list of response types that should be published to the reply target. + uniqueItems: true + items: + type: string + title: Response types + enum: + - response + - error + - nack + required: + - address + Targets: + type: array + title: The publish targets of this connection + description: The publish targets of this connection + uniqueItems: true + items: + $ref: '#/components/schemas/Target' + Target: + type: object + title: Target + description: A publish target served by this connection + properties: + address: + type: string + title: Target address + description: |- + The target address where events, commands and messages are published to. + The following placeholders are allowed within the target address: + + * Thing ID: `{{ thing:id }}` + + * Thing Namespace: `{{ thing:namespace }}` + + * Thing Name: `{{ thing:name }}` (the part of the ID without the namespace) + NOTE Use "command" alias for connections of type "hono". + topics: + type: array + title: Topics + description: The topics to which this target is registered for + uniqueItems: true + items: + type: string + enum: + - _/_/things/twin/events + - _/_/things/live/commands + - _/_/things/live/events + - _/_/things/live/messages + - _/_/policies/announcements + - _/_/connections/announcements + title: Subscribed topics + description: |- + Contains the type of messages that are delivered to this target. You can receive + + * Thing events: `_/_/things/twin/events` (notification about twin change) + + * Live events: `_/_/things/live/events` + + * Live commands: `_/_/things/live/commands` + + * Live messages: `_/_/things/live/messages` + + * Policy announcements: `_/_/policies/announcements` + + * Connection announcements: `_/_/connections/announcements` + qos: + type: integer + title: Quality of service level + description: Maximum Quality-of-Service level to request when subscribing for messages + authorizationContext: + type: array + title: The authorisation context + description: The authorization context defines all authorization subjects associated for this target + uniqueItems: true + items: + type: string + title: Authorization Subject + description: An authorization subject associated with this target + example: + - 'ditto:myAuthorizationSubject' + issuedAcknowledgementLabel: + type: string + title: Issued acknowledgement label for this target + description: The optional label of an acknowledgement which should automatically be issued by this target based on the technical settlement/ACK the connection channel provides. + payloadMapping: + type: array + title: The payload mappings + description: A list of payload mappings that are applied to messages sent via this target. If no payload mapping is specified the standard Ditto mapping is used as default. + items: + type: string + title: Payload Mapping + description: References a payload mapping definition by its ID (the key of the PayloadMappingDefinition) + example: + - javascript + headerMapping: + type: object + title: Header mapping configuration + description: External headers computed from headers and other properties of Ditto protocol messages. + ConnectionType: + type: string + description: The type of a connection + enum: + - amqp-091 + - amqp-10 + - http-push + - mqtt + - mqtt-5 + - 'kafka,' + - hono + ConnectivityStatus: + type: string + description: The status of a connection or resource + enum: + - open + - closed + - failed + - misconfigured + - unknown + PayloadMappingDefinitions: + type: object + additionalProperties: + $ref: '#/components/schemas/PayloadMappingDefinition' + description: |- + List of mapping definitions where the key represents the ID of each mapping that can be used in sources and + targets to reference a mapping. + PayloadMappingDefinition: + type: object + description: A mapping definition consisting of the used mappingEngine and the options required by this engine. + required: + - mappingEngine + - options + properties: + mappingEngine: + type: string + description: |- + The mapping engine used to process incoming and outgoing messages. Available mapping engines are + `JavaScript`, `Normalized`, `ConnectionStatus`, `RawMessage`, `Ditto`, `ImplicitThingCreation`, and `UpdateTwinWithLiveResponse`. + options: + type: object + description: |- + Configuration options specific to the used mapping engine: + + #### JavaScript + * `incomingScript` (`string`, required): The mapping script for incoming messages + * `outgoingScript` (`string`, required): The mapping script for outgoing messages + * `loadBytebufferJS` (`boolean`, optional): Whether or not ByteBufferJS library should be included + (default: `false`) + * `loadLongJS` (`boolean`, optional): Whether or not LongJS library should be included (default: `false`) + + #### Normalized + * `fields` (`string`, optional): Comma separated list of fields included in the normalized message + (default: all fields included) + + #### ConnectionStatus + * `thingId` (`string`, required): The ID of the thing + * `featureId` (`string`, optional): The ID of the modified feature (default: `ConnectionStatus`) + + #### RawMessage + * `outgoingContentType` (`string`, optional): The fallback content type for outgoing messages. + * `incomingMessageHeaders` (`object`, optional): The fallback headers for incoming messages + containing the necessary information to map them to message commands and responses. + The relevant header keys are: `content-type`, `ditto-message-subject`, `ditto-message-direction`, + `ditto-message-thing-id`, `ditto-message-feature-id` and `status`. The header values may contain + placeholder expressions. + + #### Ditto + * no options required + + #### ImplicitThingCreation + * `thing` (`object`, required): The template of the thing to be implicitly created + + #### UpdateTwinWithLiveResponse + * `dittoHeadersForMerge` (`object`, optional): The Ditto headers to use for constructing the "merge thing" + command for updating the twin, may for example add a condition to apply in order to update the twin + (default ditto headers: `response-required: false`, `if-match: "*"`). + incomingConditions: + type: object + description: |- + Optional conditions to be checked before applying the mapping engine to inbound messages. + Can use placeholders and functional expressions. + outgoingConditions: + type: object + description: |- + Optional conditions to be checked before applying the mapping engine to outbound messages. + Can use placeholders and functional expressions. + MappingContext: + type: object + deprecated: true + description: |- + MappingContext to apply in this connection containing JavaScript scripts mapping from external messages to + internal Ditto Protocol messages. Usage of MappingContext is deprecated, use PayloadMappingDefinitions instead. + required: + - incomingScript + - outgoingScript + - loadBytebufferJS + - loadLongJS + properties: + incomingScript: + type: string + description: The mapping script for incoming messages + outgoingScript: + type: string + description: The mapping script for outgoing messages + loadBytebufferJS: + type: boolean + description: Whether or not ByteBufferJS library should be included + loadLongJS: + type: boolean + description: Whether or not LongJS library should be included + ConnectionStatus: + type: object + description: Status of a connection and its resources + required: + - connectionId + - connectionStatus + - liveStatus + - connectedSince + properties: + connectionId: + type: string + description: The connection ID + connectionStatus: + allOf: + - $ref: '#/components/schemas/ConnectivityStatus' + description: The desired/target status of the connection + liveStatus: + allOf: + - $ref: '#/components/schemas/ConnectivityStatus' + description: The current/actual status of the connection + connectedSince: + type: string + description: The timestamp since when the connection is connected + example: '2019-01-21T08:57:24.710Z' + clientStatus: + type: array + items: + $ref: '#/components/schemas/ResourceStatus' + description: The client states of the of the connection + sourceStatus: + type: array + items: + $ref: '#/components/schemas/ResourceStatus' + description: The states of the sources the of the connection + targetStatus: + type: array + items: + $ref: '#/components/schemas/ResourceStatus' + description: The states of the targets the of the connection + sshTunnelStatus: + type: array + items: + $ref: '#/components/schemas/ResourceStatus' + description: The states of the ssh tunnel the of the connection + ResourceStatus: + type: object + description: The status of a single resource (e.g. a client or a source/target resource) + required: + - type + - client + - status + properties: + type: + type: string + description: The type of the resource + enum: + - client + - source + - target + client: + type: string + description: A client identifier where the resource is held (e.g. a cluster instance ID) + address: + type: string + description: The address information of the resource (optional) + status: + $ref: '#/components/schemas/ConnectivityStatus' + statusDetails: + type: string + description: Details to the status of the resource + inStateSince: + type: string + description: Date since when the resource is in the present state + ConnectionMetrics: + type: object + description: Metrics of a connection + required: + - connectionId + - containsFailures + - connectionMetrics + - sourceMetrics + - targetMetrics + properties: + connectionId: + type: string + description: The connection ID + containsFailures: + type: boolean + description: Whether the connection metrics contains any failures + example: false + connectionMetrics: + $ref: '#/components/schemas/OverallConnectionMetrics' + sourceMetrics: + $ref: '#/components/schemas/SourceMetrics' + targetMetrics: + $ref: '#/components/schemas/TargetMetrics' + OverallConnectionMetrics: + type: object + description: Overall metrics of the connection + required: + - inbound + - outbound + properties: + inbound: + $ref: '#/components/schemas/InboundMetrics' + outbound: + $ref: '#/components/schemas/OutboundMetrics' + SourceMetrics: + type: object + description: Source metrics of the connection + required: + - addressMetrics + properties: + addressMetrics: + type: object + additionalProperties: + $ref: '#/components/schemas/InboundMetrics' + description: Contains "inbound" from external sources consumed metric counts + TargetMetrics: + type: object + description: Target metrics of the connection + required: + - addressMetrics + properties: + addressMetrics: + type: object + additionalProperties: + $ref: '#/components/schemas/OutboundMetrics' + description: Contains "outbound" towards external targets messages metric counts + InboundMetrics: + type: object + description: Metrics of an inbound (e.g. a Source) resource + required: + - consumed + - mapped + - dropped + - enforced + properties: + consumed: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains from external sources consumed metric counts + mapped: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains mapped (payload mapping) messages metric counts + dropped: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains dropped (in the payload mapping) messages metric counts + enforced: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains enforced (e.g. source address enforcement) messages metric counts + OutboundMetrics: + type: object + description: Metrics of an outbound (e.g. a Target) resource + required: + - dispatched + - filtered + - mapped + - dropped + - published + properties: + dispatched: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains internally dispatched (e.g. a Ditto event) metric counts + filtered: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains the metric counts for messages which passed the filter (e.g. namespace or RQL filter for events) + mapped: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains mapped (payload mapping) messages metric counts + dropped: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains dropped (in the payload mapping) messages metric counts + published: + allOf: + - $ref: '#/components/schemas/TypedMetric' + description: Contains published messages metric counts meaning those messages were published to the external source + TypedMetric: + type: object + description: Metrics of a single metric `type` containing "success" and "failure" metrics + required: + - success + - failure + properties: + success: + allOf: + - $ref: '#/components/schemas/SingleMetric' + description: Contains the successfully processed message counts + failure: + allOf: + - $ref: '#/components/schemas/SingleMetric' + description: Contains the failed processed message counts + SingleMetric: + type: object + description: Contains a single metric consisting of several time intervals and counter values for those intervals including the last message date. + required: + - PT1M + - PT1H + - PT24H + - lastMessageAt + properties: + PT1M: + type: integer + description: The counter containing how many messages were processed in the last minute + example: 0 + PT1H: + type: integer + description: The counter containing how many messages were processed in the last hour + example: 42 + PT24H: + type: integer + description: The counter containing how many messages were processed in the last 24 hours / last day + example: 46346 + lastMessageAt: + type: string + description: The timestamp when the last message was processed + example: '2019-01-21T08:57:24.710Z' + ConnectionLogs: + type: object + description: Log entries of a connection. + required: + - connectionId + - connectionLogs + properties: + connectionId: + type: string + description: ID of the connection for which the log entries were logged. + example: 759304b8-8056-11e9-bc42-526af7764f64 + connectionLogs: + type: array + description: Log entries for the connection. + items: + $ref: '#/components/schemas/LogEntry' + enabledSince: + type: string + description: Since when logging is enabled. Might be missing / null if logging is not enabled. + example: '2019-01-21T08:57:24.710Z' + enabledUntil: + type: string + description: Until when logging is enabled. Might be missing / null if logging is not enabled. + example: '2019-01-22T08:57:24.710Z' + LogEntry: + type: object + description: Represents a log entry for a connection. + required: + - timestamp + - correlationId + - message + - category + - type + - level + properties: + timestamp: + type: string + description: Timestamp of the log entry. + example: '2019-01-21T08:57:24.710Z' + correlationId: + type: string + description: Correlation ID that is associated with the log entry. + example: 759304b8-8056-11e9-bc42-526af7764f64 + message: + type: string + description: The log message. + example: Successfully connected to ... at ... + category: + $ref: '#/components/schemas/LogCategory' + type: + $ref: '#/components/schemas/LogType' + level: + $ref: '#/components/schemas/LogLevel' + address: + type: string + description: Connection address on which the log occurred. + example: telemetry/address + thingId: + type: string + description: The thing for which the log entry was created. + example: 'org.ditto:theThing' + LogCategory: + type: string + description: A category to which the log entry can be referred to. + enum: + - source + - target + - response + - connection + LogType: + type: string + description: The type of a log entry describing during what kind of activity the entry was created. + enum: + - consumed + - dispatched + - filtered + - mapped + - dropped + - enforced + - published + - other + LogLevel: + type: string + description: Escalation level of a log entry. + enum: + - success + - failure + WhoAmI: + type: object + description: Contains information about the current user and the auth subjects available for the used authentication. + properties: + defaultSubject: + $ref: '#/components/schemas/WhoAmISubject' + subjects: + type: array + items: + $ref: '#/components/schemas/WhoAmISubject' + WhoAmISubject: + type: string + description: An auth subject that can be used to provide access for a caller (e.g. in subject entries of policies). + WotThingDescription: + type: object + description: A WoT Thing Description version 1.1 + properties: + '@context': + oneOf: + - type: array + items: + type: string + enum: + - 'https://www.w3.org/2019/wot/td/v1' + - 'http://www.w3.org/ns/td' + - 'https://www.w3.org/2022/wot/td/v1.1' + - type: string + enum: + - 'https://www.w3.org/2019/wot/td/v1' + - 'http://www.w3.org/ns/td' + - 'https://www.w3.org/2022/wot/td/v1.1' + example: + - 'https://www.w3.org/2022/wot/td/v1.1' + title: + type: string + example: My fancy Thing + titles: + type: object + additionalProperties: + type: string + description: + type: string + example: Does fancy stuff with IoT + descriptions: + type: object + additionalProperties: + type: string + '@type': + oneOf: + - type: string + - type: array + items: + type: string + example: Thing + id: + type: string + example: 'urn:org.eclipse.ditto:my-fancy-thing' + base: + type: string + format: iri-reference + example: 'https://ditto.eclipseprojects.io/api/2/org.eclipse.ditto:my-fancy-thing' + version: + type: object + properties: + model: + type: string + instance: + type: string + required: + - instance + example: + model: 1.0.0 + instance: 1.0.0 + links: + type: array + items: + type: object + properties: + href: + type: string + format: iri-reference + rel: + type: string + type: + type: string + anchor: + type: string + required: + - href + additionalProperties: true + security: + oneOf: + - type: string + - type: array + items: + type: string + example: basic_sc + securityDefinitions: + type: object + additionalProperties: + type: object + example: + basic_sc: + in: header + scheme: basic + support: + type: string + format: iri-reference + example: 'https://www.eclipse.dev/ditto/' + created: + type: string + format: date-time + modified: + type: string + format: date-time + forms: + type: array + items: + type: object + properties: + op: + type: string + href: + type: string + 'htv:methodName': + type: string + contentType: + type: string + additionalResponses: + type: array + items: + type: object + properties: + success: + type: boolean + schema: + type: string + properties: + type: object + additionalProperties: + type: object + actions: + type: object + additionalProperties: + type: object + events: + type: object + additionalProperties: + type: object + uriVariables: + type: object + additionalProperties: + type: object + schemaDefinitions: + type: object + additionalProperties: + type: object + profile: + oneOf: + - type: array + items: + type: string + format: iri-reference + - type: string + format: iri-reference + required: + - '@context' + - title + - security + - securityDefinitions + additionalProperties: true + TextUnauthorizeError: + type: string + example: The supplied authentication is invalid + RetrieveConfig: + type: object + properties: + gateway: + type: object + description: Module + properties: + pod: + type: object + description: Return the configuration at the path ditto.info + properties: + type: + type: string + description: 'devops.responses:ResultConfig' + status: + type: integer + description: The HTTP status + config: + type: object + description: name of service + properties: + env: + items: + type: string + properties: + PATH: + type: string + service: + items: + type: string + properties: + instance-index: + type: integer + service-name: + type: string + vm-args: + items: + type: string + RetrieveLoggingConfig: + properties: + gateway: + $ref: '#/components/schemas/Module' + Module: + type: object + description: Module + properties: + pod: + type: object + description: Details of logging configuration + properties: + type: + type: string + description: 'devops.responses:retrieveLoggerConfig' + status: + type: integer + description: The HTTP status + serviceName: + type: string + description: name of service + instance: + type: string + description: instance of module + loggerConfigs: + type: array + items: + type: object + properties: + level: + type: string + logger: + type: string + LoggingUpdateFields: + properties: + level: + type: string + logger: + type: string + description: class where apply logger level + UpdatedLogLevel: + type: object + description: Details of logging configuration + properties: + type: + type: string + description: 'devops.responses:changeLogLevel' + status: + type: integer + description: http code 200 for success operation + serviceName: + type: string + description: name of service that has been updated + instance: + type: string + description: identifier of pod instance + successfull: + type: boolean + description: outcome of the change + ModuleUpdatedLogLevel: + type: object + description: Module that has been updated + properties: + pod: + $ref: '#/components/schemas/UpdatedLogLevel' + ResultUpdateRequest: + type: object + properties: + gateway: + $ref: '#/components/schemas/ModuleUpdatedLogLevel' + things-search: + $ref: '#/components/schemas/ModuleUpdatedLogLevel' + policies: + $ref: '#/components/schemas/ModuleUpdatedLogLevel' + things: + $ref: '#/components/schemas/ModuleUpdatedLogLevel' + connectivity: + $ref: '#/components/schemas/ModuleUpdatedLogLevel' + ModuleConfigService: + type: object + description: Module + properties: + pod: + $ref: '#/components/schemas/ResultConfigService' + ResultConfigService: + type: object + description: Details of specific service instance. + properties: + type: + type: string + description: 'devops.responses:ResultConfigService' + status: + type: integer + description: The HTTP status + config: + type: object + description: name of service + properties: + cluster: + items: + type: string + properties: + number-of-shards: + type: integer + gateway: + items: + type: object + properties: + authentication: + type: object + properties: + devops: + type: object + properties: + password: + type: string + secured: + type: boolean + RetrieveConfigService: + type: object + properties: + gateway: + $ref: '#/components/schemas/ModuleConfigService' + BasePiggybackCommandRequestSchema: + properties: + targetActorSelection: + type: string + headers: + type: object + properties: + aggregate: + type: boolean + default: false + is-group-topic: + type: boolean + default: true + piggybackCommand: + type: object + properties: + type: + type: string + PiggybackManagingBackgroundCleanup: + properties: + targetActorSelection: + type: string + headers: + type: object + properties: + aggregate: + type: boolean + default: false + is-group-topic: + type: boolean + default: true + piggybackCommand: + type: object + properties: + type: + type: string + SearchFilterProperty: + description: |- + + #### Filter predicates: + + * ```eq({property},{value})``` (i.e. equal to the given value) + + * ```ne({property},{value})``` (i.e. not equal to the given value) + + * ```gt({property},{value})``` (i.e. greater than the given value) + + * ```ge({property},{value})``` (i.e. equal to the given value or greater than it) + + * ```lt({property},{value})``` (i.e. lower than the given value or equal to it) + + * ```le({property},{value})``` (i.e. lower than the given value) + + * ```in({property},{value},{value},...)``` (i.e. contains at least one of the values listed) + + * ```like({property},{value})``` (i.e. contains values similar to the expressions listed) + + * ```ilike({property},{value})``` (i.e. contains values similar and case insensitive to the expressions listed) + + * ```exists({property})``` (i.e. all things in which the given path exists) + + * ```empty({property})``` (i.e. all things in which the given path is absent, null, an empty array, an empty object or an empty string) + + + Note: When using filter operations, only things with the specified properties are returned. + For example, the filter `ne(attributes/owner, "SID123")` will only return things that do have + the `owner` attribute. + + + #### Logical operations: + + + * ```and({query},{query},...)``` + + * ```or({query},{query},...)``` + + * ```not({query})``` + + + #### Examples: + + * ```eq(attributes/location,"kitchen")``` + + * ```ge(thingId,"myThing1")``` + + * ```gt(_created,"2020-08-05T12:17")``` + + * ```exists(features/featureId)``` + + * ```empty(attributes/tags)``` + + * ```and(eq(attributes/location,"kitchen"),eq(attributes/color,"red"))``` + + * ```or(eq(attributes/location,"kitchen"),eq(attributes/location,"living-room"))``` + + * ```like(attributes/key1,"known-chars-at-start*")``` + + * ```like(attributes/key1,"*known-chars-at-end")``` + + * ```like(attributes/key1,"*known-chars-in-between*")``` + + * ```like(attributes/key1,"just-som?-char?-unkn?wn")``` + + The `like` filters with the wildcard `*` at the beginning can slow down your search request. + type: string + NamespaceProperty: + description: |- + A comma-separated list of namespaces. This list is used to limit the query to things in the given namespaces + only. + + + #### Examples: + + * `?namespaces=com.example.namespace` + + * `?namespaces=com.example.namespace1,com.example.namespace2` + type: string + ConfigOverrides: + type: object + description: Config overrides for a dynamic config section. + properties: + enabled: + type: boolean + log-warning-instead-of-failing-api-calls: + type: boolean + thing: + $ref: '#/components/schemas/ThingValidationConfig' + feature: + $ref: '#/components/schemas/FeatureValidationConfig' + required: + - enabled + - thing + - feature + ValidationContext: + type: object + description: Validation context for dynamic config section. + properties: + ditto-headers-patterns: + type: array + items: + type: object + additionalProperties: + type: string + thing-definition-patterns: + type: array + items: + type: string + feature-definition-patterns: + type: array + items: + type: string + scope-id: + type: string + required: + - scope-id + ThingValidationConfig: + type: object + description: Thing validation config. + properties: + enforce: + type: object + properties: + enforce-thing-description-modification: + type: boolean + attributes: + type: boolean + inbox-messages-input: + type: boolean + inbox-messages-output: + type: boolean + outbox-messages: + type: boolean + forbid: + type: object + properties: + thing-description-deletion: + type: boolean + non-modeled-attributes: + type: boolean + non-modeled-inbox-messages: + type: boolean + non-modeled-outbox-messages: + type: boolean + FeatureValidationConfig: + type: object + description: Feature validation config. + properties: + enforce: + type: object + properties: + featureDescriptionModification: + type: boolean + presenceOfModeledFeatures: + type: boolean + properties: + type: boolean + desiredProperties: + type: boolean + inbox-messages-input: + type: boolean + inbox-messages-output: + type: boolean + outbox-messages: + type: boolean + forbid: + type: object + properties: + featureDescriptionDeletion: + type: boolean + nonModeledFeatures: + type: boolean + nonModeledProperties: + type: boolean + nonModeledDesiredProperties: + type: boolean + non-modeled-inbox-messages: + type: boolean + non-modeled-outbox-messages: + type: boolean + DynamicValidationConfig: + type: object + description: Dynamic config section for request/response. + properties: + scope-id: + type: string + validation-context: + $ref: '#/components/schemas/ValidationContext' + config-overrides: + $ref: '#/components/schemas/ConfigOverrides' + required: + - scope-id + - validation-context + - config-overrides + WotValidationConfig: + type: object + description: WoT validation configuration object. + properties: + configId: + type: string + description: The unique ID of the config. + enabled: + type: boolean + description: Whether WoT validation is enabled globally. Defaults to true if not specified. + log-warning-instead-of-failing-api-calls: + type: boolean + thing: + $ref: '#/components/schemas/ThingValidationConfig' + feature: + $ref: '#/components/schemas/FeatureValidationConfig' + dynamic-config: + type: array + items: + $ref: '#/components/schemas/DynamicValidationConfig' + revision: + type: integer + format: int64 + created: + type: string + format: date-time + modified: + type: string + format: date-time + deleted: + type: boolean + metadata: + type: object + required: + - configId + - thing + - feature + securitySchemes: + NginxBasic: + type: http + description: Eclipse Ditto sandbox demo user (demo1 ... demo9) + password (demo) + scheme: basic + Bearer: + type: http + scheme: bearer + bearerFormat: JWT + description: A JSON Web Token issued by a supported OAuth 2.0 Identity Provider. + OpenIDConnect: + type: openIdConnect + description: OpenID Connect Discovery URL. The placeholder is replaced by Swagger UI when configured. + openIdConnectUrl: __OIDC_DISCOVERY_URL__ + DevOpsBasic: + type: http + description: Eclipse Ditto devops user (devops) + password (foobar) + scheme: basic + DevOpsBearer: + type: http + scheme: bearer + bearerFormat: JWT + description: A JSON Web Token issued by a supported OAuth 2.0 Identity Provider for the Eclipse Ditto devops user. diff --git a/mcp/package-lock.json b/mcp/package-lock.json index 72161e55b2..f497e383f4 100644 --- a/mcp/package-lock.json +++ b/mcp/package-lock.json @@ -14,6 +14,7 @@ "express": "^4.21.2", "pg": "^8.22.0", "sqlite-vec": "^0.1.9", + "yaml": "^2.9.0", "zod": "^3.23.8" }, "bin": { @@ -2517,7 +2518,6 @@ "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "dev": true, "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6990,7 +6990,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/mcp/package.json b/mcp/package.json index bbd35b0559..25475ca240 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -28,6 +28,7 @@ "express": "^4.21.2", "pg": "^8.22.0", "sqlite-vec": "^0.1.9", + "yaml": "^2.9.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/mcp/src/bin/stdio.ts b/mcp/src/bin/stdio.ts index 505fe18d97..3c66253f72 100644 --- a/mcp/src/bin/stdio.ts +++ b/mcp/src/bin/stdio.ts @@ -7,7 +7,7 @@ import { buildKnowledgeService } from "../knowledge/build.js"; async function main(): Promise { const config = loadConfig(process.env.DITTO_MCP_CONFIG); const knowledge = await buildKnowledgeService(config); - const registry = registerTools(config, knowledge); + const registry = await registerTools(config, knowledge); const server = buildServer(registry, config); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts index b58aae15a2..a19fd7c16c 100644 --- a/mcp/src/config/load.test.ts +++ b/mcp/src/config/load.test.ts @@ -22,6 +22,9 @@ describe("loadConfig", () => { expect(cfg.knowledge.embedding.batchSize).toBe(32); expect(cfg.knowledge.localDir.enabled).toBe(false); expect(cfg.knowledge.store.kind).toBe("sqlite"); + expect(cfg.ditto.enabled).toBe(false); + expect(cfg.ditto.credential.kind).toBe("basic"); + expect(cfg.ditto.policy.allowMethods).toEqual(["GET"]); }); it("accepts a pgvector store config", () => { @@ -52,4 +55,16 @@ describe("loadConfig", () => { writeFileSync(file, JSON.stringify({ server: { http: { port: "nope" } } })); expect(() => loadConfig(file)).toThrow(); }); + + it("accepts an oidc credential config", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ + ditto: { enabled: true, credential: { kind: "oidc", tokenUrl: "https://idp/token", clientId: "c", clientSecret: "s", scope: "ditto", devops: true } }, + })); + const cfg = loadConfig(file); + expect(cfg.ditto.credential.kind).toBe("oidc"); + expect(cfg.ditto.credential.tokenUrl).toBe("https://idp/token"); + expect(cfg.ditto.credential.devops).toBe(true); + }); }); diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index 7d201afba3..cb2f36dd5b 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -79,6 +79,39 @@ export const AppConfigSchema = z publicSource: { enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }, store: { kind: "sqlite", sqlite: {}, pgvector: { table: "ditto_kn" } }, }), + ditto: z + .object({ + enabled: z.boolean().default(false), + baseUrl: z.string().optional(), + openApi: z + .object({ path: z.string().optional(), url: z.string().optional() }) + .default({}), + credential: z + .object({ + kind: z.enum(["basic", "devops", "oidc"]).default("basic"), + username: z.string().optional(), + password: z.string().optional(), + tokenUrl: z.string().optional(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), + scope: z.string().optional(), + devops: z.boolean().optional(), + }) + .default({ kind: "basic" }), + policy: z + .object({ + allowMethods: z.array(z.string()).default(["GET"]), + writeAllowlist: z.array(z.string()).default([]), + sudoAllowlist: z.array(z.string()).default([]), + }) + .default({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }), + }) + .default({ + enabled: false, + openApi: {}, + credential: { kind: "basic" }, + policy: { allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }, + }), }) .default({}); diff --git a/mcp/src/ditto/action-tool.test.ts b/mcp/src/ditto/action-tool.test.ts new file mode 100644 index 0000000000..3fb5c9c2cf --- /dev/null +++ b/mcp/src/ditto/action-tool.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { operationToTool } from "./action-tool.js"; +import { HttpDittoClient } from "./client.js"; +import { startFakeDitto } from "./fake-ditto.js"; +import { AppConfigSchema } from "../config/schema.js"; +import { createConfigCredential } from "./credential.js"; +import type { DittoOperation } from "./openapi.js"; + +const cfg = (ditto: object) => AppConfigSchema.parse({ ditto: { enabled: true, ...ditto } }); +const op = (o: Partial): DittoOperation => ({ + operationId: "getThingById", method: "GET", path: "/things/{thingId}", summary: "Retrieve a thing", + description: "Retrieve a thing", params: [{ name: "thingId", in: "path", required: true, type: "string" }], + hasBody: false, securitySchemes: [], ...o, +}); + +let fake: Awaited>; +afterEach(async () => { await fake?.stop(); }); + +describe("operationToTool", () => { + it("builds a tool that calls Ditto with the config credential and returns the response", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: JSON.stringify({ thingId: "ns:1" }) })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config); + const tool = operationToTool(op({}), client, configCredential); + const res = await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text).toContain("ns:1"); + expect(fake.requests[0].auth).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); + }); + + it("refuses a sudo op without a devops credential (does not call Ditto)", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "should not be called" })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config); + const tool = operationToTool(op({ operationId: "sudoRetrieveThing", path: "/sudo/things/{thingId}" }), client, configCredential); + const res = await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text.toLowerCase()).toContain("devops"); + expect(fake.requests).toHaveLength(0); + }); + + it("produces a typed body schema when bodySchema has props", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config); + const withBodySchema = op({ + operationId: "postThing", method: "POST", hasBody: true, + bodySchema: { props: [ + { name: "thingId", type: "string", required: true }, + { name: "counter", type: "number", required: false }, + ]}, + }); + const tool = operationToTool(withBodySchema, client, configCredential); + const schema = tool.inputSchema; + expect(Object.keys(schema)).toContain("body"); + expect(Object.keys(schema)).toContain("thingId"); + // zod shape should include typed body props + const bodyShape = (schema.body as any)?._def; + expect(bodyShape).toBeDefined(); + }); + + it("exposes a body arg for hasBody even without bodySchema", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config); + const noBodySchema = op({ operationId: "postAnything", method: "POST", hasBody: true }); + const tool = operationToTool(noBodySchema, client, configCredential); + const schema = tool.inputSchema; + expect(Object.keys(schema)).toContain("body"); + }); + + it("accepts an object value for a $ref/unknown body prop (z.any, not z.string)", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config); + const withUnknownProp = op({ + operationId: "putThing", method: "PUT", hasBody: true, + bodySchema: { props: [ + { name: "policyId", type: "string", required: true }, + { name: "attributes", type: "unknown", required: false }, + ]}, + }); + const tool = operationToTool(withUnknownProp, client, configCredential); + const schema = tool.inputSchema; + // Validate that an object value is accepted for 'attributes' (proves it's z.any, not z.string) + const bodySchema = (schema.body as any); + expect(() => bodySchema.parse({ body: { attributes: { color: "blue" } } })).not.toThrow(); + expect(() => bodySchema.parse({ body: { policyId: "ns:p", attributes: { nested: { deep: true } } } })).not.toThrow(); + }); + + it("makes all body props optional (even required props)", () => { + const client = new HttpDittoClient("http://fake"); + const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); + const configCredential = createConfigCredential(config); + const withRequiredProp = op({ + operationId: "postThing", method: "POST", hasBody: true, + bodySchema: { props: [ + { name: "thingId", type: "string", required: true }, + ]}, + }); + const tool = operationToTool(withRequiredProp, client, configCredential); + const schema = tool.inputSchema; + const bodySchema = (schema.body as any); + // The tool accepts a body without the "required" prop + expect(() => bodySchema.parse({ body: {} })).not.toThrow(); + expect(() => bodySchema.parse({ body: { otherField: "x" } })).not.toThrow(); + }); +}); diff --git a/mcp/src/ditto/action-tool.ts b/mcp/src/ditto/action-tool.ts new file mode 100644 index 0000000000..d1aad01937 --- /dev/null +++ b/mcp/src/ditto/action-tool.ts @@ -0,0 +1,59 @@ +import { z, type ZodRawShape } from "zod"; +import type { ToolDef, ToolResult, RequestCtx } from "../core/types.js"; +import type { DittoOperation } from "./openapi.js"; +import type { DittoClient } from "./client.js"; +import type { DittoCredential } from "./credential.js"; +import { resolveCredential } from "./credential.js"; +import { isSudo } from "./tool-policy.js"; + +function sanitizeName(id: string): string { + return id.replace(/[^A-Za-z0-9_]/g, "_").slice(0, 64); +} + +function inputSchema(op: DittoOperation): ZodRawShape { + const shape: ZodRawShape = {}; + for (const p of op.params) { + const base = p.type === "number" ? z.number() : p.type === "boolean" ? z.boolean() : z.string(); + shape[p.name] = (p.required ? base : base.optional()).describe(`${p.in} parameter ${p.name}`); + } + if (op.bodySchema?.props.length) { + const bodyShape: ZodRawShape = {}; + for (const bp of op.bodySchema.props) { + const base = bp.type === "number" ? z.number() + : bp.type === "boolean" ? z.boolean() + : bp.type === "object" ? z.record(z.any()) + : bp.type === "array" ? z.array(z.any()) + : bp.type === "unknown" ? z.any() + : z.string(); + const desc = `body.${bp.name}` + (bp.required ? " (required)" : ""); + bodyShape[bp.name] = base.optional().describe(desc); + } + shape.body = z.object(bodyShape).passthrough().optional().describe("JSON request body"); + } else if (op.hasBody) { + shape.body = z.any().optional().describe("JSON request body"); + } + return shape; +} + +function text(t: string): ToolResult { + return { content: [{ type: "text", text: t }] }; +} + +export function operationToTool(op: DittoOperation, client: DittoClient, configCredential: DittoCredential): ToolDef { + const sudo = isSudo(op); + return { + name: sanitizeName(op.operationId), + description: + `${op.method} ${op.path} — ${op.description || op.summary}` + + (sudo ? " [sudo — requires a devops credential]" : ""), + inputSchema: inputSchema(op), + handler: async (args: unknown, ctx: RequestCtx): Promise => { + const credential = resolveCredential(configCredential, { headers: ctx.headers }); + if (sudo && !credential.isDevops) { + return text(`Refused: "${op.operationId}" is a sudo operation and requires a devops credential.`); + } + const res = await client.execute(op, (args ?? {}) as Record, credential, ctx.signal); + return text(`HTTP ${res.status}\n${res.body}`); + }, + }; +} diff --git a/mcp/src/ditto/action-tools.test.ts b/mcp/src/ditto/action-tools.test.ts new file mode 100644 index 0000000000..d00b1fbb97 --- /dev/null +++ b/mcp/src/ditto/action-tools.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { makeActionTools } from "./action-tools.js"; +import { HttpDittoClient } from "./client.js"; +import { startFakeDitto } from "./fake-ditto.js"; +import { AppConfigSchema } from "../config/schema.js"; + +const SPEC = { + paths: { + "/things/{thingId}": { + get: { operationId: "getThingById", summary: "get", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }] }, + put: { operationId: "putThing", summary: "put", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }], requestBody: {} }, + }, + "/sudo/things/{thingId}": { + get: { operationId: "sudoRetrieveThing", summary: "sudo get", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }] }, + }, + }, +}; + +let fake: Awaited>; +afterEach(async () => { await fake?.stop(); }); + +async function tools(policy: object, credKind = "basic") { + fake = await startFakeDitto((req) => ({ status: 200, body: JSON.stringify({ url: req.url }) })); + const config = AppConfigSchema.parse({ + ditto: { enabled: true, baseUrl: fake.baseUrl, credential: { kind: credKind, username: "u", password: "p" }, policy }, + }); + const list = await makeActionTools(config, { loadSpec: async () => SPEC, client: new HttpDittoClient(fake.baseUrl) }); + return { config, byName: Object.fromEntries(list.map((t) => [t.name, t])) }; +} + +describe("makeActionTools", () => { + it("registers only GET (read-only) by default", async () => { + const { byName } = await tools({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }); + expect(byName.getThingById).toBeDefined(); + expect(byName.putThing).toBeUndefined(); // write blocked + expect(byName.sudoRetrieveThing).toBeUndefined(); // sudo blocked + }); + + it("includes a write when allowlisted and a sudo when sudoAllowlisted", async () => { + const { byName } = await tools({ allowMethods: ["GET"], writeAllowlist: ["putThing"], sudoAllowlist: ["sudoRetrieveThing"] }); + expect(byName.putThing).toBeDefined(); + expect(byName.sudoRetrieveThing).toBeDefined(); + }); + + it("a registered GET tool calls the fake Ditto", async () => { + const { byName, config } = await tools({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] }); + const res = await byName.getThingById.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text).toContain("/things/ns:1"); + }); + + it("de-duplicates colliding tool names by appending _2, _3, ...", async () => { + const specWithCollision = { + paths: { + "/a": { get: { operationId: "foo-bar", summary: "a" } }, + "/b": { get: { operationId: "foo/bar", summary: "b" } }, + "/c": { get: { operationId: "foo.bar", summary: "c" } }, + }, + }; + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const config = AppConfigSchema.parse({ + ditto: { enabled: true, baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" }, policy: { allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] } }, + }); + const list = await makeActionTools(config, { loadSpec: async () => specWithCollision, client: new HttpDittoClient(fake.baseUrl) }); + const names = list.map((t) => t.name); + expect(names).toContain("foo_bar"); + expect(names).toContain("foo_bar_2"); + expect(names).toContain("foo_bar_3"); + expect(names.length).toBe(3); + }); +}); diff --git a/mcp/src/ditto/action-tools.ts b/mcp/src/ditto/action-tools.ts new file mode 100644 index 0000000000..8e110aa666 --- /dev/null +++ b/mcp/src/ditto/action-tools.ts @@ -0,0 +1,66 @@ +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import YAML from "yaml"; +import type { AppConfig } from "../config/schema.js"; +import type { ToolDef } from "../core/types.js"; +import type { DittoClient } from "./client.js"; +import { HttpDittoClient } from "./client.js"; +import { parseOperations } from "./openapi.js"; +import { isAllowed } from "./tool-policy.js"; +import { operationToTool } from "./action-tool.js"; +import { createConfigCredential } from "./credential.js"; + +export interface ActionToolDeps { + loadSpec?: () => Promise; + client?: DittoClient; +} + +// Pinned Ditto OpenAPI bundled with the server. Resolves to mcp/assets/ from +// both src (tsx) and dist (tsc): dirname is src/ditto or dist/ditto → ../../assets. +const BUNDLED_SPEC = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "ditto-openapi.yml"); + +// YAML.parse also parses JSON, so this handles .yml, .yaml, and .json specs. +async function defaultLoadSpec(config: AppConfig): Promise { + const { path, url } = config.ditto.openApi; + if (path) return YAML.parse(await readFile(path, "utf8")); + if (url) { + const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); + if (!res.ok) throw new Error(`openapi fetch ${url} -> ${res.status}`); + return YAML.parse(await res.text()); + } + // Fallback: the pinned Ditto spec shipped with the server. + return YAML.parse(await readFile(BUNDLED_SPEC, "utf8")); +} + +export async function makeActionTools(config: AppConfig, deps: ActionToolDeps = {}): Promise { + const client = + deps.client ?? + (config.ditto.baseUrl + ? new HttpDittoClient(config.ditto.baseUrl) + : undefined); + if (!client) { + process.stderr.write("[ditto-mcp] action tools disabled: ditto.baseUrl is required\n"); + return []; + } + let spec: unknown; + try { + spec = deps.loadSpec ? await deps.loadSpec() : await defaultLoadSpec(config); + } catch (err) { + process.stderr.write(`[ditto-mcp] action tools disabled: ${String(err)}\n`); + return []; + } + const configCredential = createConfigCredential(config); + const tools = parseOperations(spec) + .filter((op) => isAllowed(op, config.ditto.policy)) + .map((op) => operationToTool(op, client, configCredential)); + // De-duplicate tool names: on collision, append _2, _3, ... + const seen = new Map(); + for (const tool of tools) { + const base = tool.name; + const count = seen.get(base) ?? 0; + seen.set(base, count + 1); + if (count > 0) tool.name = `${base}_${count + 1}`; + } + return tools; +} diff --git a/mcp/src/ditto/bundled-spec.test.ts b/mcp/src/ditto/bundled-spec.test.ts new file mode 100644 index 0000000000..fe422278d1 --- /dev/null +++ b/mcp/src/ditto/bundled-spec.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import YAML from "yaml"; +import { parseOperations } from "./openapi.js"; +import { isSudo } from "./tool-policy.js"; + +const specPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "ditto-openapi.yml"); + +describe("bundled Ditto spec", () => { + it("parses and yields operations incl. a things GET with a resolved path param", () => { + const spec = YAML.parse(readFileSync(specPath, "utf8")); + const ops = parseOperations(spec); + expect(ops.length).toBeGreaterThan(20); + const getThing = ops.find((o) => o.method === "GET" && o.path.includes("/things/{thingId}")); + expect(getThing).toBeDefined(); + expect(getThing!.params.some((p) => p.name === "thingId" && p.in === "path")).toBe(true); + }); + + it("classifies /api/2/connections GET as isSudo (DevOpsBasic security)", () => { + const spec = YAML.parse(readFileSync(specPath, "utf8")); + const ops = parseOperations(spec); + const getConnections = ops.find((o) => o.method === "GET" && o.path.startsWith("/api/2/connections")); + expect(getConnections).toBeDefined(); + expect(isSudo(getConnections!)).toBe(true); + }); + + it("resolves bodySchema for PUT /api/2/things/{thingId}", () => { + const spec = YAML.parse(readFileSync(specPath, "utf8")); + const ops = parseOperations(spec); + const putThing = ops.find((o) => o.method === "PUT" && o.path === "/api/2/things/{thingId}"); + expect(putThing).toBeDefined(); + expect(putThing!.hasBody).toBe(true); + // The NewThing schema is an object with properties, so bodySchema should be defined + expect(putThing!.bodySchema).toBeDefined(); + expect(putThing!.bodySchema!.props.length).toBeGreaterThan(0); + // Verify some expected properties are present (policyId, definition, attributes, features, _policy, _copyPolicyFrom) + const propNames = putThing!.bodySchema!.props.map(p => p.name); + expect(propNames).toContain("policyId"); + }); +}); diff --git a/mcp/src/ditto/client.test.ts b/mcp/src/ditto/client.test.ts new file mode 100644 index 0000000000..751d194c6e --- /dev/null +++ b/mcp/src/ditto/client.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { HttpDittoClient } from "./client.js"; +import { startFakeDitto } from "./fake-ditto.js"; +import type { DittoOperation } from "./openapi.js"; + +const op = (over: Partial): DittoOperation => ({ + operationId: "op", method: "GET", path: "/x", summary: "", description: "", params: [], hasBody: false, ...over, +}); +const cred = (h?: string) => ({ isDevops: false, authHeader: async () => h }); + +let fake: Awaited>; +afterEach(async () => { await fake?.stop(); }); + +describe("HttpDittoClient", () => { + it("substitutes path params, sends query, forwards auth", async () => { + fake = await startFakeDitto((req) => ({ status: 200, body: JSON.stringify({ ok: req.url }) })); + const client = new HttpDittoClient(fake.baseUrl); + const res = await client.execute( + op({ path: "/things/{thingId}", params: [ + { name: "thingId", in: "path", required: true, type: "string" }, + { name: "fields", in: "query", required: false, type: "string" }] }), + { thingId: "ns:1", fields: "attributes" }, + cred("Basic abc"), + ); + expect(res.status).toBe(200); + expect(fake.requests[0].url).toBe("/things/ns:1?fields=attributes"); + expect(fake.requests[0].auth).toBe("Basic abc"); + }); + + it("sends a JSON body for write ops", async () => { + fake = await startFakeDitto(() => ({ status: 201, body: "" })); + const client = new HttpDittoClient(fake.baseUrl); + const res = await client.execute( + op({ method: "PUT", path: "/things/{id}", hasBody: true, + params: [{ name: "id", in: "path", required: true, type: "string" }] }), + { id: "ns:1", body: { attributes: { a: 1 } } }, + cred(), + ); + expect(res.status).toBe(201); + expect(JSON.parse(fake.requests[0].body!)).toEqual({ attributes: { a: 1 } }); + }); + + it("encodes unsafe path-param chars but keeps the Ditto namespace colon", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "" })); + const client = new HttpDittoClient(fake.baseUrl); + await client.execute( + op({ path: "/things/{thingId}", params: [{ name: "thingId", in: "path", required: true, type: "string" }] }), + { thingId: "ns:a b/c" }, + cred(), + ); + expect(fake.requests[0].url).toBe("/things/ns:a%20b%2Fc"); // colon kept; space+slash encoded + }); +}); diff --git a/mcp/src/ditto/client.ts b/mcp/src/ditto/client.ts new file mode 100644 index 0000000000..243890489a --- /dev/null +++ b/mcp/src/ditto/client.ts @@ -0,0 +1,55 @@ +import type { DittoOperation } from "./openapi.js"; +import type { DittoCredential } from "./credential.js"; + +export interface DittoResponse { + status: number; + body: string; +} + +export interface DittoClient { + execute( + op: DittoOperation, + args: Record, + credential: DittoCredential, + signal?: AbortSignal, + ): Promise; +} + +export class HttpDittoClient implements DittoClient { + constructor( + private readonly baseUrl: string, + private readonly fetchFn: typeof fetch = fetch, + ) {} + + async execute( + op: DittoOperation, + args: Record, + credential: DittoCredential, + signal?: AbortSignal, + ): Promise { + let path = op.path; + const query = new URLSearchParams(); + for (const p of op.params) { + const v = args[p.name]; + if (p.in === "path") { + path = path.replace(`{${p.name}}`, encodeURIComponent(String(v ?? "")).replace(/%3A/g, ":")); + } else if (v !== undefined) { + query.set(p.name, String(v)); + } + } + const qs = query.toString(); + const url = `${this.baseUrl}${path}${qs ? `?${qs}` : ""}`; + + const headers: Record = {}; + const auth = await credential.authHeader(signal); + if (auth) headers["authorization"] = auth; + let body: string | undefined; + if (op.hasBody && args.body !== undefined) { + headers["content-type"] = "application/json"; + body = JSON.stringify(args.body); + } + + const res = await this.fetchFn(url, { method: op.method, headers, body, signal }); + return { status: res.status, body: await res.text() }; + } +} diff --git a/mcp/src/ditto/credential.test.ts b/mcp/src/ditto/credential.test.ts new file mode 100644 index 0000000000..55c696414a --- /dev/null +++ b/mcp/src/ditto/credential.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { AppConfigSchema } from "../config/schema.js"; +import { createConfigCredential, resolveCredential } from "./credential.js"; +import { startFakeOidc } from "./fake-oidc.js"; + +const cfg = (ditto: object) => AppConfigSchema.parse({ ditto: { enabled: true, ...ditto } }); + +describe("credentials", () => { + it("config basic → async Basic header", async () => { + const c = createConfigCredential(cfg({ credential: { kind: "basic", username: "u", password: "p" } })); + expect(await c.authHeader()).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); + expect(c.isDevops).toBe(false); + }); + + it("devops flag marks isDevops regardless of kind", async () => { + expect(createConfigCredential(cfg({ credential: { kind: "devops", username: "d", password: "s" } })).isDevops).toBe(true); + expect(createConfigCredential(cfg({ credential: { kind: "oidc", tokenUrl: "x", clientId: "c", clientSecret: "s", devops: true } })).isDevops).toBe(true); + }); + + it("session Authorization overrides config, inherits config isDevops", async () => { + const cc = createConfigCredential(cfg({ credential: { kind: "devops", username: "d", password: "s" } })); + const c = resolveCredential(cc, { headers: { Authorization: "Bearer sess" } }); + expect(await c.authHeader()).toBe("Bearer sess"); + expect(c.isDevops).toBe(true); + }); + + it("oidc fetches a bearer token and caches it (one token call for two uses)", async () => { + const oidc = await startFakeOidc({ access_token: "tok123", expires_in: 3600 }); + try { + const c = createConfigCredential(cfg({ credential: { kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "c", clientSecret: "s" } })); + expect(await c.authHeader()).toBe("Bearer tok123"); + expect(await c.authHeader()).toBe("Bearer tok123"); + expect(oidc.calls).toBe(1); // cached + } finally { await oidc.stop(); } + }); + + it("oidc sends grant_type=client_credentials, Basic auth, and scope (when set)", async () => { + const oidc = await startFakeOidc({ access_token: "tok", expires_in: 3600 }); + try { + const c = createConfigCredential(cfg({ credential: { kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "myClient", clientSecret: "mySecret", scope: "scope1 scope2" } })); + await c.authHeader(); + expect(oidc.requests).toHaveLength(1); + const req = oidc.requests[0]; + expect(req.method).toBe("POST"); + expect(req.headers.authorization).toBe(`Basic ${Buffer.from("myClient:mySecret").toString("base64")}`); + expect(req.headers["content-type"]).toBe("application/x-www-form-urlencoded"); + expect(req.body).toContain("grant_type=client_credentials"); + expect(req.body).toContain("scope=scope1+scope2"); + } finally { await oidc.stop(); } + }); +}); diff --git a/mcp/src/ditto/credential.ts b/mcp/src/ditto/credential.ts new file mode 100644 index 0000000000..df1ad277fc --- /dev/null +++ b/mcp/src/ditto/credential.ts @@ -0,0 +1,95 @@ +import type { AppConfig } from "../config/schema.js"; + +export interface DittoCredential { + readonly isDevops: boolean; + authHeader(signal?: AbortSignal): Promise; +} + +function first(h: string | string[] | undefined): string | undefined { + return Array.isArray(h) ? h[0] : h; +} + +class StaticCredential implements DittoCredential { + constructor(readonly isDevops: boolean, private readonly header: string | undefined) {} + async authHeader(): Promise { + return this.header; + } +} + +interface OidcOptions { + tokenUrl: string; + clientId: string; + clientSecret: string; + scope?: string; + isDevops: boolean; +} + +export class OidcClientCredential implements DittoCredential { + readonly isDevops: boolean; + private token?: string; + private expiresAt = 0; + private inflight?: Promise; + + constructor(private readonly opts: OidcOptions, private readonly fetchFn: typeof fetch = fetch) { + this.isDevops = opts.isDevops; + } + + async authHeader(signal?: AbortSignal): Promise { + const now = Date.now(); + if (this.token && now < this.expiresAt) return `Bearer ${this.token}`; + if (!this.inflight) this.inflight = this.fetchToken(signal).finally(() => { this.inflight = undefined; }); + const token = await this.inflight; + return `Bearer ${token}`; + } + + private async fetchToken(signal?: AbortSignal): Promise { + const body = new URLSearchParams({ grant_type: "client_credentials" }); + if (this.opts.scope) body.set("scope", this.opts.scope); + const basic = Buffer.from(`${this.opts.clientId}:${this.opts.clientSecret}`).toString("base64"); + const timeoutSignal = AbortSignal.timeout(15000); + const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal; + const res = await this.fetchFn(this.opts.tokenUrl, { + method: "POST", + headers: { authorization: `Basic ${basic}`, "content-type": "application/x-www-form-urlencoded" }, + body: body.toString(), + signal: combinedSignal, + }); + if (!res.ok) throw new Error(`oidc token endpoint ${this.opts.tokenUrl} -> ${res.status}`); + const json = (await res.json()) as { access_token?: string; expires_in?: number }; + if (!json.access_token) throw new Error("oidc token response missing access_token"); + this.token = json.access_token; + this.expiresAt = Date.now() + Math.max(0, (json.expires_in ?? 300) - 30) * 1000; + return this.token; + } +} + +export function createConfigCredential(config: AppConfig, fetchFn: typeof fetch = fetch): DittoCredential { + const c = config.ditto.credential; + const isDevops = c.devops ?? c.kind === "devops"; + if (c.kind === "oidc") { + if (!c.tokenUrl || !c.clientId || !c.clientSecret) { + process.stderr.write("[ditto-mcp] oidc credential missing tokenUrl/clientId/clientSecret; using no credential\n"); + return new StaticCredential(isDevops, undefined); + } + return new OidcClientCredential( + { tokenUrl: c.tokenUrl, clientId: c.clientId, clientSecret: c.clientSecret, scope: c.scope, isDevops }, + fetchFn, + ); + } + if (c.username !== undefined) { + const header = `Basic ${Buffer.from(`${c.username}:${c.password ?? ""}`).toString("base64")}`; + return new StaticCredential(isDevops, header); + } + return new StaticCredential(false, undefined); +} + +export function resolveCredential( + configCredential: DittoCredential, + ctx: { headers?: Record }, +): DittoCredential { + const headers = ctx.headers ?? {}; + const key = Object.keys(headers).find((k) => k.toLowerCase() === "authorization"); + const sessionAuth = first(key ? headers[key] : undefined); + if (sessionAuth) return new StaticCredential(configCredential.isDevops, sessionAuth); + return configCredential; +} diff --git a/mcp/src/ditto/fake-ditto.ts b/mcp/src/ditto/fake-ditto.ts new file mode 100644 index 0000000000..5057ed1221 --- /dev/null +++ b/mcp/src/ditto/fake-ditto.ts @@ -0,0 +1,35 @@ +import { createServer, type Server } from "node:http"; + +export interface RecordedRequest { method: string; url: string; auth?: string; body?: string } +export interface FakeReply { status: number; body: string } + +export async function startFakeDitto( + handler: (req: RecordedRequest) => FakeReply, +): Promise<{ baseUrl: string; stop: () => Promise; requests: RecordedRequest[] }> { + const requests: RecordedRequest[] = []; + const server: Server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (c) => chunks.push(c as Buffer)); + req.on("end", () => { + const rec: RecordedRequest = { + method: req.method ?? "GET", + url: req.url ?? "/", + auth: req.headers["authorization"] as string | undefined, + body: chunks.length ? Buffer.concat(chunks).toString("utf8") : undefined, + }; + requests.push(rec); + const reply = handler(rec); + res.statusCode = reply.status; + res.setHeader("content-type", "application/json"); + res.end(reply.body); + }); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + return { + baseUrl: `http://127.0.0.1:${port}`, + stop: () => new Promise((r) => server.close(() => r())), + requests, + }; +} diff --git a/mcp/src/ditto/fake-oidc.ts b/mcp/src/ditto/fake-oidc.ts new file mode 100644 index 0000000000..1bcb6a48ea --- /dev/null +++ b/mcp/src/ditto/fake-oidc.ts @@ -0,0 +1,33 @@ +import { createServer, type Server } from "node:http"; + +export interface FakeOidcRequest { + method: string; + headers: Record; + body: string; +} + +export async function startFakeOidc( + token: { access_token: string; expires_in: number }, +): Promise<{ tokenUrl: string; stop: () => Promise; calls: number; requests: FakeOidcRequest[] }> { + let calls = 0; + const requests: FakeOidcRequest[] = []; + const server: Server = createServer((req, res) => { + calls++; + let body = ""; + req.on("data", (chunk) => { body += chunk; }); + req.on("end", () => { + requests.push({ method: req.method ?? "", headers: req.headers as Record, body }); + res.setHeader("content-type", "application/json"); + res.end(JSON.stringify(token)); + }); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + const addr = server.address(); + const port = typeof addr === "object" && addr ? addr.port : 0; + return { + tokenUrl: `http://127.0.0.1:${port}/token`, + stop: () => new Promise((r) => server.close(() => r())), + get calls() { return calls; }, + requests, + }; +} diff --git a/mcp/src/ditto/openapi.test.ts b/mcp/src/ditto/openapi.test.ts new file mode 100644 index 0000000000..ff1fb62380 --- /dev/null +++ b/mcp/src/ditto/openapi.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect } from "vitest"; +import { parseOperations } from "./openapi.js"; + +// Mirrors Ditto's real shape: a $ref'd component parameter + a path-item-level parameter. +const SPEC = { + components: { + parameters: { + ThingIdPathParam: { name: "thingId", in: "path", required: true, schema: { type: "string" } }, + }, + }, + paths: { + "/things/{thingId}": { + parameters: [{ $ref: "#/components/parameters/ThingIdPathParam" }], // path-item level, shared + get: { + operationId: "getThingById", + summary: "Retrieve a thing", + parameters: [{ name: "fields", in: "query", required: false, schema: { type: "string" } }], + }, + put: { + operationId: "putThing", + summary: "Create or update a thing", + requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/NewThing" } } } }, + }, + }, + "/search/things": { + get: { + operationId: "searchThings", + summary: "Search things", + parameters: [{ name: "filter", in: "query", required: false, schema: { type: "string" } }], + }, + }, + }, +}; + +describe("parseOperations", () => { + it("merges path-item params, resolves $ref params, extracts body existence", () => { + const ops = parseOperations(SPEC); + const get = ops.find((o) => o.operationId === "getThingById")!; + expect(get.method).toBe("GET"); + expect(get.path).toBe("/things/{thingId}"); + // path-item $ref param (thingId) merged with op-level query param (fields): + expect(get.params).toContainEqual({ name: "thingId", in: "path", required: true, type: "string" }); + expect(get.params).toContainEqual({ name: "fields", in: "query", required: false, type: "string" }); + expect(get.hasBody).toBe(false); + + const put = ops.find((o) => o.operationId === "putThing")!; + expect(put.method).toBe("PUT"); + expect(put.params).toContainEqual({ name: "thingId", in: "path", required: true, type: "string" }); // inherited + expect(put.hasBody).toBe(true); // requestBody exists (schema $ref not resolved) + + const search = ops.find((o) => o.operationId === "searchThings")!; + expect(search.params[0]).toEqual({ name: "filter", in: "query", required: false, type: "string" }); + }); + + it("synthesizes an operationId when missing", () => { + const ops = parseOperations({ paths: { "/x": { get: {} } } }); + expect(ops[0].operationId).toBe("GET_/x"); + }); + + it("captures securitySchemes from op.security", () => { + const spec = { + paths: { + "/api/2/connections": { + get: { + operationId: "getConnections", + summary: "list connections", + security: [{ DevOpsBasic: [] }], + }, + }, + }, + }; + const ops = parseOperations(spec); + expect(ops[0].securitySchemes).toEqual(["DevOpsBasic"]); + }); + + it("captures securitySchemes from spec-level security when op.security is missing", () => { + const spec = { + security: [{ ApiKeyAuth: [] }], + paths: { + "/things": { + get: { operationId: "getThings", summary: "list things" }, + }, + }, + }; + const ops = parseOperations(spec); + expect(ops[0].securitySchemes).toEqual(["ApiKeyAuth"]); + }); + + it("returns empty securitySchemes when no security is defined", () => { + const spec = { + paths: { + "/public": { + get: { operationId: "getPublic", summary: "public endpoint" }, + }, + }, + }; + const ops = parseOperations(spec); + expect(ops[0].securitySchemes).toEqual([]); + }); + + it("resolves a requestBody object schema ($ref) into a shallow BodySchema", () => { + const spec = { + components: { schemas: { NewThing: { type: "object", required: ["thingId"], properties: { + thingId: { type: "string" }, attributes: { type: "object" }, counter: { type: "integer" } } } } }, + paths: { "/things": { post: { operationId: "postThing", + requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/NewThing" } } } } } } }, + }; + const ops = parseOperations(spec); + const postOp = ops.find((o) => o.operationId === "postThing")!; + expect(postOp.hasBody).toBe(true); + expect(postOp.bodySchema?.props).toContainEqual({ name: "thingId", type: "string", required: true }); + expect(postOp.bodySchema?.props).toContainEqual({ name: "counter", type: "number", required: false }); + expect(postOp.bodySchema?.props).toContainEqual({ name: "attributes", type: "object", required: false }); + }); + + it("marks a $ref or allOf/oneOf/anyOf body property as type 'unknown' (not string)", () => { + const spec = { + components: { schemas: { + ComplexThing: { type: "object", required: ["policyId"], properties: { + policyId: { type: "string" }, + attributes: { $ref: "#/components/schemas/Attributes" }, + _policy: { allOf: [{ $ref: "#/components/schemas/Policy" }] }, + features: { oneOf: [{ type: "object" }, { type: "null" }] }, + }}, + }}, + paths: { "/things": { put: { operationId: "putComplexThing", + requestBody: { content: { "application/json": { schema: { $ref: "#/components/schemas/ComplexThing" } } } } } } }, + }; + const ops = parseOperations(spec); + const putOp = ops.find((o) => o.operationId === "putComplexThing")!; + expect(putOp.bodySchema?.props).toContainEqual({ name: "policyId", type: "string", required: true }); + expect(putOp.bodySchema?.props).toContainEqual({ name: "attributes", type: "unknown", required: false }); + expect(putOp.bodySchema?.props).toContainEqual({ name: "_policy", type: "unknown", required: false }); + expect(putOp.bodySchema?.props).toContainEqual({ name: "features", type: "unknown", required: false }); + }); +}); diff --git a/mcp/src/ditto/openapi.ts b/mcp/src/ditto/openapi.ts new file mode 100644 index 0000000000..6f9d021e4b --- /dev/null +++ b/mcp/src/ditto/openapi.ts @@ -0,0 +1,131 @@ +export interface OpParam { + name: string; + in: "path" | "query"; + required: boolean; + type: "string" | "number" | "boolean"; +} + +export interface BodyProp { + name: string; + type: "string" | "number" | "boolean" | "object" | "array" | "unknown"; + required: boolean; +} + +export interface BodySchema { + props: BodyProp[]; +} + +export interface DittoOperation { + operationId: string; + method: string; + path: string; + summary: string; + description: string; + params: OpParam[]; + hasBody: boolean; + bodySchema?: BodySchema; + securitySchemes: string[]; +} + +const METHODS = ["get", "put", "post", "delete", "patch"]; + +interface RawParam { name?: string; in?: string; required?: boolean; schema?: unknown; $ref?: string } + +function paramType(schema: unknown): OpParam["type"] { + const t = (schema as { type?: string } | undefined)?.type; + return t === "number" || t === "integer" ? "number" : t === "boolean" ? "boolean" : "string"; +} + +function bodyPropType(schema: unknown): BodyProp["type"] { + const s = schema as { type?: string; $ref?: string; allOf?: unknown; oneOf?: unknown; anyOf?: unknown } | undefined; + // If the schema is a $ref or has composition keywords (allOf/oneOf/anyOf), or has no recognizable type, return "unknown" + if (s?.$ref || s?.allOf || s?.oneOf || s?.anyOf) return "unknown"; + const t = s?.type; + if (t === "integer" || t === "number") return "number"; + if (t === "boolean") return "boolean"; + if (t === "object") return "object"; + if (t === "array") return "array"; + if (t === "string") return "string"; + return "unknown"; // no recognizable primitive type +} + +function resolveSchemaRef(s: SpecShape, schema: unknown): unknown { + const ref = (schema as { $ref?: string } | undefined)?.$ref; + if (ref) return (s.components?.schemas as Record | undefined)?.[ref.split("/").pop() ?? ""]; + return schema; +} + +function bodySchemaOf(s: SpecShape, op: { requestBody?: unknown }): BodySchema | undefined { + const schemaRef = (op.requestBody as { content?: Record } | undefined) + ?.content?.["application/json"]?.schema; + const schema = resolveSchemaRef(s, schemaRef) as + | { type?: string; properties?: Record; required?: string[] } | undefined; + if (!schema || schema.type !== "object" || !schema.properties) return undefined; + const required = new Set(schema.required ?? []); + const props: BodyProp[] = Object.entries(schema.properties).map(([name, p]) => ({ + name, + type: bodyPropType(p), // pass the full property schema + required: required.has(name), + })); + return props.length ? { props } : undefined; +} + +/** Resolve a `{$ref: '#/components/parameters/Name'}` param against the spec; pass others through. */ +function resolveParam(spec: SpecShape, p: RawParam): RawParam { + if (p.$ref) { + const name = p.$ref.split("/").pop() ?? ""; + return (spec.components?.parameters?.[name] as RawParam | undefined) ?? {}; + } + return p; +} + +interface SpecShape { + paths?: Record>; + components?: { parameters?: Record; schemas?: Record }; + security?: Array>; +} + +export function parseOperations(spec: unknown): DittoOperation[] { + const s = (spec ?? {}) as SpecShape; + const paths = s.paths ?? {}; + const ops: DittoOperation[] = []; + for (const [path, item] of Object.entries(paths)) { + // Path-item-level parameters apply to every operation under this path. + const pathParams = ((item.parameters as RawParam[] | undefined) ?? []).map((p) => resolveParam(s, p)); + for (const method of METHODS) { + const op = item[method] as + | { operationId?: string; summary?: string; description?: string; parameters?: RawParam[]; requestBody?: unknown; security?: Array> } + | undefined; + if (!op) continue; + const opParams = (op.parameters ?? []).map((p) => resolveParam(s, p)); + const seen = new Set(); + const params: OpParam[] = [...opParams, ...pathParams] // op-level wins on name clash + .filter((p) => p.in === "path" || p.in === "query") + .filter((p) => (seen.has(`${p.in}:${p.name}`) ? false : seen.add(`${p.in}:${p.name}`))) + .map((p) => ({ + name: String(p.name), + in: p.in as "path" | "query", + required: p.required === true || p.in === "path", + type: paramType(p.schema), + })); + // Effective security = op.security ?? spec.security ?? []; flatten to scheme names + const effectiveSecurity = op.security ?? s.security ?? []; + const securitySchemes = Array.from( + new Set(effectiveSecurity.flatMap((req) => Object.keys(req))) + ); + const hasBody = op.requestBody !== undefined; + ops.push({ + operationId: op.operationId ?? `${method.toUpperCase()}_${path}`, + method: method.toUpperCase(), + path, + summary: op.summary ?? "", + description: op.description ?? op.summary ?? "", + params, + hasBody, + bodySchema: hasBody ? bodySchemaOf(s, op) : undefined, + securitySchemes, + }); + } + } + return ops; +} diff --git a/mcp/src/ditto/tool-policy.test.ts b/mcp/src/ditto/tool-policy.test.ts new file mode 100644 index 0000000000..49700ed683 --- /dev/null +++ b/mcp/src/ditto/tool-policy.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { isSudo, isAllowed } from "./tool-policy.js"; +import type { DittoOperation } from "./openapi.js"; + +const op = (o: Partial): DittoOperation => ({ + operationId: "x", method: "GET", path: "/x", summary: "", description: "", params: [], hasBody: false, securitySchemes: [], ...o, +}); +const policy = (p: object) => ({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [], ...p }); + +describe("tool-policy", () => { + it("allows GET by default, blocks writes", () => { + expect(isAllowed(op({ method: "GET" }), policy({}))).toBe(true); + expect(isAllowed(op({ operationId: "putThing", method: "PUT" }), policy({}))).toBe(false); + }); + it("allows a write only when allowlisted", () => { + expect(isAllowed(op({ operationId: "putThing", method: "PUT" }), policy({ writeAllowlist: ["putThing"] }))).toBe(true); + }); + it("treats sudo ops specially: only via sudoAllowlist", () => { + const s = op({ operationId: "sudoRetrieveThing", method: "GET" }); + expect(isSudo(s)).toBe(true); + expect(isAllowed(s, policy({}))).toBe(false); // GET but sudo -> not auto-allowed + expect(isAllowed(s, policy({ sudoAllowlist: ["sudoRetrieveThing"] }))).toBe(true); + }); + + it("treats /devops paths as devops-privileged (sudo-gated)", () => { + const d = op({ operationId: "getLogging", method: "GET", path: "/devops/logging" }); + expect(isSudo(d)).toBe(true); + expect(isAllowed(d, policy({}))).toBe(false); // GET but /devops -> not auto-allowed + expect(isAllowed(d, policy({ sudoAllowlist: ["getLogging"] }))).toBe(true); + }); + + it("treats ops with DevOpsBasic/DevOpsBearer security as sudo", () => { + const conn = op({ + operationId: "getConnections", + method: "GET", + path: "/api/2/connections", + securitySchemes: ["DevOpsBasic"], + }); + expect(isSudo(conn)).toBe(true); + expect(isAllowed(conn, policy({}))).toBe(false); // GET but devops-secured -> not auto-allowed + expect(isAllowed(conn, policy({ sudoAllowlist: ["getConnections"] }))).toBe(true); + }); + + it("treats ops with DevOpsBearer (case-insensitive) as sudo", () => { + const op2 = op({ securitySchemes: ["DevOpsBearer"], path: "/api/2/connections/foo" }); + expect(isSudo(op2)).toBe(true); + }); +}); diff --git a/mcp/src/ditto/tool-policy.ts b/mcp/src/ditto/tool-policy.ts new file mode 100644 index 0000000000..59bdcb64f5 --- /dev/null +++ b/mcp/src/ditto/tool-policy.ts @@ -0,0 +1,18 @@ +import type { AppConfig } from "../config/schema.js"; +import type { DittoOperation } from "./openapi.js"; + +export function isSudo(op: DittoOperation): boolean { + const p = op.path.toLowerCase(); + const hasDevopsSecurity = op.securitySchemes.some((s) => s.toLowerCase().includes("devops")); + return ( + hasDevopsSecurity || + op.operationId.toLowerCase().startsWith("sudo") || + p.includes("/sudo") || + p.startsWith("/devops") + ); +} + +export function isAllowed(op: DittoOperation, policy: AppConfig["ditto"]["policy"]): boolean { + if (isSudo(op)) return policy.sudoAllowlist.includes(op.operationId); + return policy.allowMethods.includes(op.method) || policy.writeAllowlist.includes(op.operationId); +} diff --git a/mcp/src/server/build-server.test.ts b/mcp/src/server/build-server.test.ts index b77842dad0..facef634e5 100644 --- a/mcp/src/server/build-server.test.ts +++ b/mcp/src/server/build-server.test.ts @@ -8,7 +8,7 @@ import { buildServer } from "./build-server.js"; async function connectedClient() { // knowledge disabled to keep this transport test hermetic; knowledge covered in knowledge tests. const config = AppConfigSchema.parse({ knowledge: { enabled: false } }); - const registry = registerTools(config); + const registry = await registerTools(config); const server = buildServer(registry, config); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -40,7 +40,7 @@ describe("buildServer + ping (in-memory e2e)", () => { tools: { ping: false }, knowledge: { enabled: false }, }); - const registry = registerTools(config); + const registry = await registerTools(config); const server = buildServer(registry, config); const [ct, st] = InMemoryTransport.createLinkedPair(); await server.connect(st); diff --git a/mcp/src/server/http-app.ts b/mcp/src/server/http-app.ts index fcb04f133b..7552f2cf12 100644 --- a/mcp/src/server/http-app.ts +++ b/mcp/src/server/http-app.ts @@ -52,7 +52,7 @@ export function createHttpApp( transport.onclose = () => { if (transport?.sessionId) transports.delete(transport.sessionId); }; - const registry = registerTools(config, knowledgeService); + const registry = await registerTools(config, knowledgeService); const server = buildServer(registry, config); await server.connect(transport); } diff --git a/mcp/src/tools/index.test.ts b/mcp/src/tools/index.test.ts index d74900db44..1dc73671e5 100644 --- a/mcp/src/tools/index.test.ts +++ b/mcp/src/tools/index.test.ts @@ -18,23 +18,28 @@ let store: SqliteKnowledgeStore | undefined; afterEach(() => store?.close()); describe("registerTools wiring", () => { - it("registers ping by default", () => { - const reg = registerTools( + it("registers ping by default", async () => { + const reg = await registerTools( AppConfigSchema.parse({ knowledge: { enabled: false } }), ); expect(reg.get("ping")).toBeDefined(); }); - it("omits knowledge tools when knowledge disabled", () => { - const reg = registerTools( + it("omits action tools when ditto disabled (default)", async () => { + const reg = await registerTools(AppConfigSchema.parse({})); + expect(reg.list().some((t) => t.name === "getThingById")).toBe(false); + }); + + it("omits knowledge tools when knowledge disabled", async () => { + const reg = await registerTools( AppConfigSchema.parse({ knowledge: { enabled: false } }), ); expect(reg.get("search")).toBeUndefined(); expect(reg.get("get_chunk")).toBeUndefined(); }); - it("omits knowledge tools when service not provided", () => { - const reg = registerTools( + it("omits knowledge tools when service not provided", async () => { + const reg = await registerTools( AppConfigSchema.parse({ knowledge: { enabled: true } }), ); expect(reg.get("search")).toBeUndefined(); @@ -46,7 +51,7 @@ describe("registerTools wiring", () => { await store.addChunks([chunk("a", "test content")]); const retriever = new FtsRetriever(store); const service = new KnowledgeService(store, retriever); - const reg = registerTools( + const reg = await registerTools( AppConfigSchema.parse({ knowledge: { enabled: true } }), service, ); diff --git a/mcp/src/tools/index.ts b/mcp/src/tools/index.ts index 9668977fc4..77bf18e3a8 100644 --- a/mcp/src/tools/index.ts +++ b/mcp/src/tools/index.ts @@ -4,15 +4,19 @@ import { pingTool } from "./ping.js"; import { makeKnowledgeTools } from "./knowledge.js"; import type { KnowledgeService } from "../knowledge/knowledge-service.js"; -export function registerTools( +export async function registerTools( config: AppConfig, knowledgeService?: KnowledgeService, -): ToolRegistry { +): Promise { const registry = new ToolRegistry(); if (config.tools.ping) registry.register(pingTool); if (config.knowledge.enabled && knowledgeService) { for (const tool of makeKnowledgeTools(knowledgeService)) registry.register(tool); } + if (config.ditto.enabled) { + const { makeActionTools } = await import("../ditto/action-tools.js"); + for (const tool of await makeActionTools(config)) registry.register(tool); + } return registry; } From d4c8d4b5636c91a72a149ce9c37ff7c4278f5fe0 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Mon, 10 Aug 2026 14:30:19 +0300 Subject: [PATCH 06/11] docs(mcp): documentation + OSS packaging Consolidated README, example configs, hardened .gitignore; docs for HTTP/config, knowledge/persistence/pgvector, action tools/passthrough/policy, OIDC + typed bodies. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/.gitignore | 13 + mcp/README.md | 537 +++++++++++++---------------- mcp/examples/ditto-oidc-write.json | 18 + mcp/examples/ditto-readonly.json | 18 + mcp/examples/hybrid-local.json | 24 ++ mcp/examples/pgvector.json | 16 + mcp/examples/public-fts.json | 14 + mcp/src/config/examples.test.ts | 24 ++ 8 files changed, 369 insertions(+), 295 deletions(-) create mode 100644 mcp/examples/ditto-oidc-write.json create mode 100644 mcp/examples/ditto-readonly.json create mode 100644 mcp/examples/hybrid-local.json create mode 100644 mcp/examples/pgvector.json create mode 100644 mcp/examples/public-fts.json create mode 100644 mcp/src/config/examples.test.ts diff --git a/mcp/.gitignore b/mcp/.gitignore index 3c25e1e49c..763b596b9f 100644 --- a/mcp/.gitignore +++ b/mcp/.gitignore @@ -1,3 +1,16 @@ node_modules/ dist/ *.log + +# Built knowledge index (SQLite) — DO NOT commit +*.db +*.db-wal +*.db-shm +*.db.tmp + +# Downloaded embedding models / cache +/models/ +.cache/ + +# Local config with secrets +config.local.json diff --git a/mcp/README.md b/mcp/README.md index badc984455..d43533f14e 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -1,262 +1,234 @@ -# ditto-mcp-server +# Ditto MCP Server -Extensible MCP server for Ditto knowledge and tools. P1 = foundation -(config, plugin registry, server factory, stdio + streamable-HTTP transports, -`ping` tool). P2a = knowledge: `search` and `get_chunk` tools backed by a -pluggable `KnowledgeSource` → `Retriever` core. See the design spec and plans -under `docs/superpowers/`. +Model Context Protocol (MCP) server for Eclipse Ditto. Provides two classes of tools: -## Requirements -- Node >= 22 - -## Develop - npm install - npm test # vitest - npm run typecheck # tsc --noEmit - npm run dev:stdio # run stdio transport - npm run dev:http # run streamable-HTTP transport on :3000/mcp - -## Configuration +1. **Knowledge tools** (`search`, `get_chunk`) — semantic/keyword search over Ditto documentation (public llms.txt + optional local markdown corpus) +2. **Action tools** (dynamically generated from OpenAPI) — query and manage Things, Policies, Connections, and other Ditto resources -Optional JSON config via `DITTO_MCP_CONFIG=/path/to/config.json`. All fields have defaults; see `src/config/schema.ts`. - -### Knowledge +## Requirements -The server exposes `search` and `get_chunk` tools backed by a pluggable -`KnowledgeSource` → `Retriever` core. You can index the public Ditto docs -(`llms.txt`), a local markdown directory, or both, and choose from three -retriever modes: keyword FTS (default), semantic vector search, or hybrid (RRF -fusion of both). +- Node >= 22 -**On startup**, the server fetches the public docs and/or indexes the local -directory (if enabled). To disable this, set `knowledge.enabled=false` or -disable individual sources. +## Quickstart -#### Retriever Modes +### Install & Build -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `knowledge.retriever` | `"fts" \| "vector" \| "hybrid"` | `"fts"` | Retriever mode: `fts` = SQLite FTS5 keyword search; `vector` = semantic vector search; `hybrid` = RRF fusion of FTS + vector | +```bash +cd mcp/ +npm install +npm run build +``` -**Default (`fts`)**: fast keyword search, no model download, works offline. +### Run (stdio transport) -**Vector / Hybrid**: embed the entire corpus in memory at server startup and -download the BGE embedding model (~80MB, ONNX) on first run unless -`allowRemoteModels: false` + `modelPath` are set. The default `fts` mode loads -no embedding stack. See `embedding` config below. +The MCP server runs in stdio mode by default (for Claude Desktop, Cline, and other MCP clients): -#### Sources +```bash +# Development (with tsx) +npm run dev:stdio -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `knowledge.enabled` | `boolean` | `true` | Enable knowledge tools (`search`, `get_chunk`) | -| `knowledge.publicSource.enabled` | `boolean` | `true` | Enable PublicSource (Ditto `llms.txt`) | -| `knowledge.publicSource.url` | `string` | `"https://eclipse.dev/ditto/llms.txt"` | URL to the `llms.txt` index | -| `knowledge.publicSource.maxDocs` | `number?` | `undefined` | Optional limit on the number of docs to fetch | -| `knowledge.localDir.enabled` | `boolean` | `false` | Enable LocalDirSource (index a local markdown directory) | -| `knowledge.localDir.path` | `string?` | `undefined` | Path to a local directory containing `.md` files | -| `knowledge.localDir.id` | `string` | `"local"` | Source ID for local chunks | +# Production (built) +node dist/bin/stdio.js +``` -#### Embedding Config (for `vector` / `hybrid`) +### Connect to Claude Desktop -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `knowledge.embedding.model` | `string` | `"Xenova/bge-small-en-v1.5"` | Hugging Face model ID | -| `knowledge.embedding.dim` | `number` | `384` | Embedding dimension (must match the model) | -| `knowledge.embedding.modelPath` | `string?` | `undefined` | Local path to the ONNX model (offline mode) | -| `knowledge.embedding.allowRemoteModels` | `boolean` | `true` | Allow model download from Hugging Face | -| `knowledge.embedding.cacheDir` | `string?` | `undefined` | Custom cache directory for downloaded models | +Add the server to Claude Desktop's MCP config: -**Offline vector search**: set `allowRemoteModels: false` and provide -`modelPath` pointing to a pre-downloaded ONNX model directory. +```bash +claude mcp add ditto \ + -e DITTO_MCP_CONFIG=/absolute/path/to/config.json \ + -- node /absolute/path/to/ditto/mcp/dist/bin/stdio.js +``` -#### Examples +Or edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) / `%APPDATA%\Claude\claude_desktop_config.json` (Windows) directly: -**Hybrid retriever + local dir:** ```json { - "knowledge": { - "retriever": "hybrid", - "publicSource": { "enabled": true }, - "localDir": { "enabled": true, "path": "/home/user/my-docs" } + "mcpServers": { + "ditto": { + "command": "node", + "args": ["/absolute/path/to/ditto/mcp/dist/bin/stdio.js"], + "env": { + "DITTO_MCP_CONFIG": "/absolute/path/to/config.json" + } + } } } ``` -**Offline vector search:** -```json -{ - "knowledge": { - "retriever": "vector", - "embedding": { - "allowRemoteModels": false, - "modelPath": "/opt/models/bge-small-en-v1.5" - } - } -} +### Run (HTTP transport) + +The server can also run over HTTP (streamable SSE transport): + +```bash +# Development +npm run dev:http + +# Production +node dist/bin/http.js ``` -**Disable knowledge:** -```json -{ - "knowledge": { - "enabled": false - } -} +By default, the HTTP server binds to `127.0.0.1:3000` and serves at `/mcp`. Configure via `server.http` in the config (see below). + +## Two Processes: Ingest vs. Server + +The MCP server supports **persistent knowledge indexes** (SQLite or Postgres). The index must be built **before** the server starts (or the server falls back to building it in memory at startup). + +- **Ingest process** (`ditto-mcp-ingest` or `npm run dev:ingest`) — builds and persists the knowledge index (fetch → chunk → embed → write to store). Run this once, or whenever your corpus changes. +- **Server process** (`ditto-mcp-stdio` / `ditto-mcp-http`) — serves MCP tools. Reads the prebuilt index if it exists and is valid; otherwise builds in memory (SQLite) or refuses to start (Postgres). + +**When to run ingest:** + +- After changing `knowledge.retriever`, `knowledge.embedding.model`, or `knowledge.embedding.dim` (metadata mismatch requires a rebuild). +- After adding/removing sources (`publicSource`, `localDir`). +- When you want to persist the index to disk (SQLite) or Postgres (pgvector). + +**Ingest command:** + +```bash +DITTO_MCP_CONFIG=/path/to/config.json npm run dev:ingest # development +DITTO_MCP_CONFIG=/path/to/config.json ditto-mcp-ingest # production ``` -### Persistence & Ingestion (P2b-2) +The ingest command reads `knowledge.store` from your config and writes the index to the configured location (SQLite file path or Postgres connection). If the config specifies `kind: "sqlite"` but no `sqlite.path`, ingest will error (it requires an explicit path to write to). + +## Configuration -The knowledge index (chunks, FTS, and vectors) lives in a pluggable -`KnowledgeStore` backend. Currently, `SqliteKnowledgeStore` persists everything -to a single `.db` file; `PgKnowledgeStore` (pgvector + Postgres FTS) is next -(P2c). +All configuration is optional. The server uses sensible defaults when no config is provided. Pass a JSON config file via the `DITTO_MCP_CONFIG` environment variable. -#### Store Configuration +See `examples/` for reference configs. All examples are parse-tested in CI and won't rot. + +### Server (HTTP transport options) | Field | Type | Default | Description | |-------|------|---------|-------------| -| `knowledge.store.kind` | `"sqlite" \| "pgvector"` | `"sqlite"` | Store backend: `sqlite` (file-based, default) or `pgvector` (Postgres + pgvector + FTS) | -| `knowledge.store.sqlite.path` | `string?` | `undefined` | Path to the SQLite file. If unset or missing, the server builds an in-memory index at startup. | -| `knowledge.store.pgvector.connectionString` | `string?` | `undefined` | Postgres connection string (e.g., `postgresql://user:pass@host:5432/ditto`). Required when `kind: "pgvector"`. | -| `knowledge.store.pgvector.table` | `string` | `"ditto_kn"` | Table name prefix for Postgres tables (chunks, FTS, vectors). | +| `server.name` | `string` | `"ditto-mcp"` | Server name exposed to MCP clients | +| `server.http.port` | `number` | `3000` | HTTP server port | +| `server.http.host` | `string` | `"127.0.0.1"` | Bind address (loopback by default) | +| `server.http.enableDnsRebindingProtection` | `boolean` | `true` | Enable DNS rebinding protection (rejects requests with invalid Host/Origin headers) | +| `server.http.allowedHosts` | `string[]?` | `undefined` | Allowed Host header values (e.g., `["mcp.example.com:3000"]`). When undefined and protection is enabled, a loopback allowlist is derived: `["127.0.0.1:port", "localhost:port", "[::1]:port", "host:port"]` | +| `server.http.allowedOrigins` | `string[]?` | `undefined` | Allowed Origin header values (optional) | -**Example (SQLite persistent store):** -```json -{ - "knowledge": { - "retriever": "fts", - "store": { "kind": "sqlite", "sqlite": { "path": "/var/lib/ditto-mcp/index.db" } } - } -} -``` +**Remote deployments:** When binding a non-loopback host (e.g., `0.0.0.0` or a public IP), you **MUST** set `allowedHosts` explicitly. The SDK matches the full `Host` header (e.g., `mcp.example.com:3000`), so include the exact `host:port` values your clients will send. -**Example (Postgres / pgvector store — AWS RDS):** +Example: ```json { - "knowledge": { - "retriever": "hybrid", - "store": { - "kind": "pgvector", - "pgvector": { - "connectionString": "postgresql://user:password@my-rds.c9akciq32.us-east-1.rds.amazonaws.com:5432/ditto", - "table": "ditto_kn" - } + "server": { + "http": { + "host": "0.0.0.0", + "port": 3000, + "allowedHosts": ["mcp.example.com:3000"] } } } ``` -#### Postgres / pgvector (P2c-2) +### Knowledge Tools -When `knowledge.store.kind: "pgvector"`, the server uses Postgres for persistence: -- **Vectors**: stored in a `pgvector` column (requires the `vector` extension) -- **Keyword index**: built using Postgres `tsvector` FTS -- **Chunks**: stored in a text table +The server exposes `search` and `get_chunk` tools backed by a pluggable knowledge core. You can index: +- **Public Ditto docs** (fetched from `llms.txt` at startup, enabled by default) +- **Local markdown directory** (disabled by default) +- **Both** (merged corpus) -**Setup:** -1. Create a Postgres database (e.g., `ditto`). -2. Ensure the `vector` extension is available. For **AWS RDS**: - - Create a custom parameter group with `rds.extensions = 'vector'` - - Apply it to your Postgres instance - - Connect and run `CREATE EXTENSION IF NOT EXISTS vector;` -3. Configure the connection string in your config JSON. +Choose a retriever mode: +- `fts` (default) — fast keyword search, no model download, works offline +- `vector` — semantic vector search (downloads BGE embedding model ~80MB on first run) +- `hybrid` — RRF fusion of FTS + vector (best recall) -**Ingest:** The `ingest` command fetches the corpus, embeds vectors, and writes all chunks/FTS/vectors to Postgres: -```bash -DITTO_MCP_CONFIG=/path/to/config.json npm run dev:ingest -``` +The index can be stored in **SQLite** (file-based, default) or **Postgres** (pgvector + FTS). -On the first run, the server validates index metadata (retriever, embedding model/dim, schema version). On re-ingest, the server calls `reset()` (drops the vec table and clears chunks/meta), rebuilds the index, and sets the completion flag — this is an **offline/maintenance operation** (not zero-downtime for a live instance). Re-ingest always replaces the full index from scratch, and can survive embedding-dim changes (reset() drops and recreates the vec table). +#### Knowledge Config -**Server Load-or-Build Behavior:** -- **Index exists & metadata matches**: server connects and serves immediately (no rebuild). -- **Index missing or metadata mismatch**: server logs a warning and disables knowledge tools (pgvector backend does NOT fall back to in-memory build — you MUST run `ingest` to populate Postgres before the server can serve knowledge). -- **Connection failure**: server exits with an error (Postgres backend requires a live database). +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.enabled` | `boolean` | `true` | Enable knowledge tools | +| `knowledge.retriever` | `"fts" \| "vector" \| "hybrid"` | `"fts"` | Retriever mode | -**Integration Tests:** -Postgres integration tests run via `npm run test:pg` (requires Docker and testcontainers): -```bash -npm run test:pg -``` -These tests are **excluded** from the default `npm test` suite to keep the default test run hermetic (no Docker, no network, no external dependencies). +#### Sources -#### Ingest Command +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.publicSource.enabled` | `boolean` | `true` | Enable PublicSource (Ditto llms.txt) | +| `knowledge.publicSource.url` | `string` | `"https://eclipse.dev/ditto/llms.txt"` | llms.txt URL | +| `knowledge.publicSource.maxDocs` | `number?` | `undefined` | Optional doc limit | +| `knowledge.localDir.enabled` | `boolean` | `false` | Enable LocalDirSource (index a local markdown directory) | +| `knowledge.localDir.path` | `string?` | `undefined` | Path to local markdown directory | +| `knowledge.localDir.id` | `string` | `"local"` | Source ID for local chunks | -To pre-build the index and persist it to a file (SQLite) or database (Postgres), use the `ingest` command: +#### Embedding (for vector/hybrid) -```bash -# Development -DITTO_MCP_CONFIG=/path/to/config.json npm run dev:ingest +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.embedding.model` | `string` | `"Xenova/bge-small-en-v1.5"` | Hugging Face model ID | +| `knowledge.embedding.dim` | `number` | `384` | Embedding dimension (must match the model) | +| `knowledge.embedding.modelPath` | `string?` | `undefined` | Local path to ONNX model (offline mode) | +| `knowledge.embedding.allowRemoteModels` | `boolean` | `true` | Allow model download from Hugging Face | +| `knowledge.embedding.cacheDir` | `string?` | `undefined` | Custom cache directory for downloaded models | +| `knowledge.embedding.batchSize` | `number` | `32` | Embedding batch size | -# Built -DITTO_MCP_CONFIG=/path/to/config.json ditto-mcp-ingest -``` +**Offline vector search:** Set `allowRemoteModels: false` and provide `modelPath` pointing to a pre-downloaded ONNX model directory. -The `ingest` command reads the config, fetches/indexes the corpus, and writes the store (file path for SQLite, or Postgres for pgvector). The config must specify a valid store location, or `ingest` will error. +#### Store (persistence) -#### Server Load-or-Build Behavior +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.store.kind` | `"sqlite" \| "pgvector"` | `"sqlite"` | Store backend | +| `knowledge.store.sqlite.path` | `string?` | `undefined` | Path to SQLite file. If unset, the server builds an in-memory index at startup. | +| `knowledge.store.pgvector.connectionString` | `string?` | `undefined` | Postgres connection string (required when `kind: "pgvector"`) | +| `knowledge.store.pgvector.table` | `string` | `"ditto_kn"` | Table name prefix for Postgres tables | -When the server starts: -- **Populated store exists & metadata matches**: opens the prebuilt store and validates index metadata (retriever mode, embedding model/dim, schema version, and completion flag). If metadata matches the config, the store is served instantly (no fetch/embed). -- **SQLite — missing/empty/mismatched store**: builds the index in memory (fallback mode). The server never writes the file automatically — use `ingest` to persist. A corrupt or mismatched file at `knowledge.store.sqlite.path` never crashes the server or disables knowledge — it triggers the same in-memory fallback as a missing file. -- **Postgres — missing/empty/mismatched store**: server logs a warning and disables knowledge tools (pgvector backend does NOT fall back to in-memory build — you MUST run `ingest` to populate Postgres before the server can serve knowledge). -- **Postgres connection failure**: exits with an error. Postgres backend requires a live database. +**SQLite (default):** File-based index. If `sqlite.path` is set and the file exists, the server loads it instantly (no fetch/embed). If missing/corrupt/mismatched, the server builds the index in memory (fallback mode). The server never writes the file automatically — use `ditto-mcp-ingest` to persist. -The `ingest` command uses backend-specific atomic writes: -- **SQLite**: temp file + rename on success (zero-downtime, crash-safe). -- **Postgres**: `reset()` → rebuild → set completion flag (offline operation; re-ingest requires downtime, but can survive embedding-dim changes since reset() drops and recreates the vec table). +**Postgres (pgvector):** Requires the `vector` extension. If the index exists and metadata matches, the server loads instantly. If missing/mismatched, the server **refuses to start** (no in-memory fallback). You **must** run `ditto-mcp-ingest` to populate Postgres before starting the server. -The async `KnowledgeStore` lifecycle (`isPopulated()`, `getMeta()`, `setMeta()`, `reset()`, `close()`) enables both `SqliteKnowledgeStore` and `PgKnowledgeStore` to plug in behind the same interface with no churn to `build.ts` or `build-index.ts`. Both backends validate metadata and support offline re-ingest. +**Postgres setup (AWS RDS example):** +1. Create a Postgres database. +2. Enable the `vector` extension: + - Create a custom parameter group with `rds.extensions = 'vector'` + - Apply it to your instance + - Connect and run `CREATE EXTENSION IF NOT EXISTS vector;` +3. Configure `knowledge.store.pgvector.connectionString` in your config. +4. Run `ditto-mcp-ingest` to build the index. -## Action tools (P3-1) +**Testing Postgres:** Run `npm run test:pg` (requires Docker + testcontainers). These tests are excluded from the default `npm test` suite to keep the default test run hermetic. -The server exposes action tools dynamically generated from a Ditto OpenAPI specification. -Each action tool makes HTTP calls to a Ditto instance, enforcing a configurable **ToolPolicy** -(read-only by default) and **credential passthrough** (the MCP forwards credentials to Ditto, -which enforces authorization; the MCP never decides access beyond policy gating). +### Ditto Action Tools -### Enable Action Tools +The server can expose action tools dynamically generated from a Ditto OpenAPI spec. Each tool makes HTTP calls to a Ditto instance. Credentials are passed through to Ditto (the MCP never decides authorization beyond policy gating). The default policy is **read-only** (`GET` only). -Set `ditto.enabled: true` and provide either a `ditto.baseUrl` or fetch the OpenAPI spec from a custom location: +#### Ditto Config | Field | Type | Default | Description | |-------|------|---------|-------------| | `ditto.enabled` | `boolean` | `false` | Enable action tools | -| `ditto.baseUrl` | `string` | (required if enabled) | Base URL to the Ditto instance (e.g., `http://localhost:8080`) | -| `ditto.openApi.path` | `string?` | `undefined` | Path to the OpenAPI spec file (reads locally). If unset, falls back to bundled spec. | -| `ditto.openApi.url` | `string?` | `undefined` | URL to fetch the OpenAPI spec from (remote fetch). If unset, falls back to bundled spec. | +| `ditto.baseUrl` | `string` | (required if enabled) | Base URL to Ditto instance (e.g., `http://localhost:8080`) | +| `ditto.openApi.path` | `string?` | `undefined` | Path to local OpenAPI spec file. If unset, uses bundled spec. | +| `ditto.openApi.url` | `string?` | `undefined` | URL to fetch OpenAPI spec from. If unset, uses bundled spec. | -If both `path` and `url` are unset, the server uses the bundled pinned Ditto OpenAPI spec -(`mcp/assets/ditto-openapi.yml`), which is a snapshot of a known Ditto release and works offline. +If both `path` and `url` are unset, the server uses the bundled pinned Ditto OpenAPI spec (`mcp/assets/ditto-openapi.yml`), which is a snapshot of a known Ditto release and works offline. -### Credential Passthrough +#### Credentials -Action tools support two credential modes: +Action tools support three credential modes: -1. **Session-level credentials** (per tool call): The caller can pass an `Authorization` header - with each tool invocation. The header is forwarded to the Ditto backend. +| Kind | Description | +|------|-------------| +| `basic` | Username + password (sent as `Authorization: Basic `) | +| `devops` | Static bearer token (sent as `Authorization: Bearer `) | +| `oidc` | OAuth2 client-credentials flow (exchanges `clientId` + `clientSecret` for an access token, auto-refreshes ~30s before expiry) | -2. **Config-level credentials** (`ditto.credential`): A static credential (basic auth or devops token) - configured at server startup, forwarded to every action-tool call unless overridden by a per-session - `Authorization` header. +**Config-level credentials** (`ditto.credential`) are forwarded to every action-tool call unless overridden by a per-session `Authorization` header (session-level credentials). -**Credential types:** -- `basic` — username + password (sent as `Authorization: Basic `) -- `devops` — a static token (sent as `Authorization: Bearer `) -- `oidc` — OAuth2 client-credentials flow (sends `Authorization: Bearer `) - -**OIDC client-credentials:** Set `ditto.credential.kind: "oidc"` to enable OAuth2 client-credentials. -The MCP exchanges `clientId` + `clientSecret` for an access token on the first action-tool call, caches it, -and auto-refreshes ~30 seconds before expiry. The access token is forwarded as `Authorization: Bearer `. - -OIDC credential fields: -- `tokenUrl` (required) — OAuth2 token endpoint (e.g., `https://auth.example.com/oauth/token`) +**OIDC credential fields:** +- `tokenUrl` (required) — OAuth2 token endpoint - `clientId` (required) — OAuth2 client identifier - `clientSecret` (required) — OAuth2 client secret (never logged) -- `scope` (optional) — OAuth2 scopes (space-separated; e.g., `"scope1 scope2"`) +- `scope` (optional) — OAuth2 scopes (space-separated) +- `devops` (optional) — Operator assertion that the credential is devops-capable (required for sudo operations) -Example OIDC config: +Example: ```json { "ditto": { @@ -273,141 +245,116 @@ Example OIDC config: } ``` -**Authorization enforcement:** The MCP never decides authorization. It forwards the credential -(or `Authorization` header) and lets the Ditto backend enforce access control. Credentials are -never logged by the server. +**Authorization enforcement:** The MCP never decides authorization. It forwards the credential (or `Authorization` header) and lets Ditto enforce access control. Credentials are never logged. -### ToolPolicy +#### Policy -By default, action tools only expose read (`GET`) operations. Write and privileged operations -require explicit allowlisting: +By default, action tools only expose **read** (`GET`) operations. Write and privileged operations require explicit allowlisting: | Field | Type | Default | Description | |-------|------|---------|-------------| -| `ditto.policy.allowMethods` | `string[]` | `["GET"]` | Wholesale HTTP method allowlist (applies to all non-sudo operations). Keep this `["GET"]` for read-only; expand for writes. | -| `ditto.policy.writeAllowlist` | `string[]` | `[]` | Per-operation granular allowlist for enabling specific write operations (operationIds). Use this to enable individual writes when `allowMethods` includes write verbs. | -| `ditto.policy.sudoAllowlist` | `string[]` | `[]` | Per-operation allowlist for sudo/devops-privileged operations (operationIds). Required for `/api/2/connections*`, `/devops/*`, and `sudo*` operations. | +| `ditto.policy.allowMethods` | `string[]` | `["GET"]` | Wholesale HTTP method allowlist (applies to all non-sudo operations) | +| `ditto.policy.writeAllowlist` | `string[]` | `[]` | Per-operation granular allowlist for enabling specific write operations (operationIds) | +| `ditto.policy.sudoAllowlist` | `string[]` | `[]` | Per-operation allowlist for sudo/devops-privileged operations (operationIds) | **Sudo operations & devops flag:** -Ditto secures `/api/2/connections*` (secret-bearing) with `DevOpsBasic`/`DevOpsBearer` security, -and `/devops/*` paths are devops-privileged. These operations are classified as "sudo" and: -- Must be explicitly listed in `sudoAllowlist` (by operationId). + +Ditto secures `/api/2/connections*` (secret-bearing) with `DevOpsBasic`/`DevOpsBearer` security, and `/devops/*` paths are devops-privileged. These operations are classified as "sudo" and: +- Must be explicitly listed in `sudoAllowlist` (by operationId) - Require a devops-capable credential. If a devops credential is not present, the operation is refused and never sent to Ditto. - Are NOT auto-allowed even if the method is `GET` and in `allowMethods`. -Set `credential.devops: true` as an **operator assertion** that the credential is devops-capable. -This gates `sudo*`/`/devops`/`/connections` tools at the MCP layer — Ditto still enforces the real -authorization. Deriving `devops` from token introspection/claims is a future enhancement (currently, -only `basic` and `devops` kinds are implicitly devops-capable; `oidc` requires explicit `devops: true`). +Set `credential.devops: true` as an **operator assertion** that the credential is devops-capable. This gates `sudo*`/`/devops`/`/connections` tools at the MCP layer — Ditto still enforces the real authorization. Deriving `devops` from token introspection/claims is a future enhancement (currently, `basic` and `devops` kinds are implicitly devops-capable; `oidc` requires explicit `devops: true`). **Examples:** -- Read-only (default): `{ "allowMethods": ["GET"] }` — only non-sudo GET operations are allowed. -- Enable specific writes: `{ "allowMethods": ["GET", "POST", "PATCH"], "writeAllowlist": ["putThing", "modifyThing"] }` — enables specific write operations. -- Enable sudo: `{ "allowMethods": ["GET"], "sudoAllowlist": ["getConnections", "getLogging"] }` — enables specific devops-privileged operations (requires devops credential). -### Typed Request Bodies +- **Read-only (default):** `{ "allowMethods": ["GET"] }` — only non-sudo GET operations are allowed. +- **Enable specific writes:** `{ "allowMethods": ["GET", "POST", "PATCH"], "writeAllowlist": ["putThing", "modifyThing"] }` — enables specific write operations. +- **Enable sudo:** `{ "allowMethods": ["GET"], "sudoAllowlist": ["getConnections", "getLogging"] }` — enables specific devops-privileged operations (requires devops credential). -Write tools (POST, PATCH, PUT) expose their top-level request body fields in the tool schema, -allowing clients to discover and validate the shape of the request. Nested objects are passed through -as freeform JSON (no further schema introspection). This surfaces the Ditto OpenAPI operation's -request body schema to the MCP tool layer without deeply traversing `allOf`, `oneOf`, or nested `$ref`s. +### Tools Exposed -For example, a `createThing` operation with a top-level `body.attributes` field will expose -`attributes` as a schema input field; callers can then pass nested objects like `{ "color": "blue" }` -within that field. +When running, the server exposes: -### Action Tools Configuration Examples +**Core tools:** +- `ping` — health check (enabled by default via `tools.ping`; independent of `knowledge`/`ditto`) -**Example 1: Read-only access (default policy):** -```json -{ - "ditto": { - "enabled": true, - "baseUrl": "http://localhost:8080", - "credential": { - "type": "basic", - "username": "ditto", - "password": "ditto" - } - } -} -``` -This uses the bundled spec (offline) and forwards basic auth to Ditto. Only `GET` operations are available. +**Knowledge tools (if `knowledge.enabled: true`):** +- `search` — semantic/keyword search over the knowledge corpus +- `get_chunk` — retrieve a specific chunk by ID -**Example 2: Write + sudo operations with devops credential:** -```json -{ - "ditto": { - "enabled": true, - "baseUrl": "http://localhost:8080", - "credential": { - "type": "devops", - "token": "my-devops-secret" - }, - "policy": { - "allowMethods": ["GET", "POST", "PATCH", "DELETE"], - "writeAllowlist": ["/api/2/things", "/api/2/things/{thingId}"], - "sudoAllowlist": ["/devops/piggyback/send"] - } - } -} -``` -This enables write operations on things and `/devops/piggyback/send` (sudo). The devops credential -is forwarded to Ditto for all requests. +**Action tools (if `ditto.enabled: true`):** -**Example 3: Custom OpenAPI spec from URL:** -```json -{ - "ditto": { - "enabled": true, - "baseUrl": "http://localhost:8080", - "openApi": { - "url": "http://my-ditto:8080/openapi.json" - }, - "credential": { - "type": "basic", - "username": "user", - "password": "pass" - } - } -} -``` -This fetches the OpenAPI spec from a remote URL instead of using the bundled spec. +Dynamically generated from the Ditto OpenAPI spec. Examples: +- `getThing`, `putThing`, `modifyThing`, `deleteThing` +- `getPolicy`, `putPolicy`, `modifyPolicy`, `deletePolicy` +- `getConnections`, `createConnection`, `modifyConnection`, `deleteConnection` (sudo, requires devops credential + `sudoAllowlist`) +- `sudoRetrieveThing`, `piggybackSend` (sudo) -### HTTP Server Options (`server.http`) +Write tools (POST, PATCH, PUT) expose their top-level request body fields in the tool schema, allowing clients to discover and validate the shape of the request. Nested objects are passed through as freeform JSON. -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `host` | `string` | `"127.0.0.1"` | Bind address (loopback by default) | -| `port` | `number` | `3000` | Port to listen on | -| `enableDnsRebindingProtection` | `boolean` | `true` | Enable DNS rebinding protection (rejects requests with invalid Host/Origin headers) | -| `allowedHosts` | `string[]?` | `undefined` | Allowed Host header values (e.g., `["mcp.example.com:3000"]`). When undefined and protection is enabled, a loopback allowlist is derived: `["127.0.0.1:port", "localhost:port", "[::1]:port", "host:port"]` | -| `allowedOrigins` | `string[]?` | `undefined` | Allowed Origin header values (optional) | +## Security & OSS Notes -### Remote Deployments +**Commit the engine, NOT your corpus/built index/secrets:** -When binding a **non-loopback** host (e.g., `0.0.0.0` or a public IP), you **MUST** set `allowedHosts` explicitly. The SDK matches the full `Host` header (e.g., `mcp.example.com:3000`), so include the exact `host:port` values your clients will send. Without explicit `allowedHosts`, DNS-rebinding protection will reject all remote requests with HTTP 403. +- The built knowledge index (`.db` files) is gitignored. Commit the code, not the index. +- Local corpus directories (markdown files) may contain proprietary content — do NOT commit them to public repos unless intended. +- Credentials in `config.local.json` or other configs may contain secrets — do NOT commit them. Use environment variables or secret managers in production. +- Downloaded embedding models (`models/`, `.cache/`) are gitignored. -Example config for remote deployment: -```json -{ - "server": { - "http": { - "host": "0.0.0.0", - "port": 3000, - "allowedHosts": ["mcp.example.com:3000"] - } - } -} +**Credential passthrough:** + +The MCP forwards credentials to Ditto, which enforces authorization. The MCP never decides access beyond policy gating (allowMethods, writeAllowlist, sudoAllowlist). Credentials are never logged by the server. + +**Read-only default:** + +The default policy (`allowMethods: ["GET"]`) ensures action tools are read-only unless you explicitly enable writes. Sudo operations (connections, devops) require explicit `sudoAllowlist` + devops credential. + +**Sudo/devops/connections gating:** + +Connections and devops endpoints are gated at the MCP layer (sudo policy) to prevent accidental exposure of secret-bearing APIs. Ditto still enforces the real authorization. + +## Testing + +```bash +npm test # hermetic unit tests (no Docker, no network) +npm run test:pg # Postgres integration tests (requires Docker + testcontainers) +npm run typecheck # TypeScript type check ``` -## Layout +The examples are parse-tested in `src/config/examples.test.ts` to ensure they stay valid. + +## Examples + +See `examples/` for reference configs: + +- `public-fts.json` — simplest public quickstart (FTS, SQLite, llms.txt only) +- `hybrid-local.json` — hybrid retriever + local markdown directory + llms.txt +- `pgvector.json` — Postgres (pgvector) store for persistent index +- `ditto-readonly.json` — Ditto action tools with read-only policy (basic auth) +- `ditto-oidc-write.json` — Ditto action tools with OIDC client-credentials + write/sudo operations + +All examples use placeholders (e.g., `REPLACE_ME`, `/path/to/...`) for secrets and paths. Replace these with your own values before use. + +## Project Layout + - `src/core/` — shared types (`ToolDef`, `RequestCtx`) - `src/registry/` — `ToolRegistry` - `src/config/` — zod schema + loader -- `src/tools/` — tool implementations (`ping`, knowledge tools) + wiring -- `src/knowledge/` — corpus/retrieval core (`KnowledgeSource`, `Retriever`, `PublicSource`, `FtsRetriever`) +- `src/tools/` — tool implementations (`ping`, `search`, `get_chunk`) + wiring +- `src/knowledge/` — corpus/retrieval core (`KnowledgeSource`, `Retriever`, `PublicSource`, `LocalDirSource`, `FtsRetriever`, `VectorRetriever`, `HybridRetriever`) +- `src/ditto/` — action tools (OpenAPI → MCP tool schema, credential handling, policy enforcement) - `src/server/` — `buildServer`, `createHttpApp` -- `src/bin/` — `stdio` and `http` entrypoints - -Dependencies: `@modelcontextprotocol/sdk`, `express`, `zod`, `better-sqlite3`, `@huggingface/transformers`, `sqlite-vec` +- `src/bin/` — `stdio`, `http`, `ingest` entrypoints +- `examples/` — reference configs (parse-tested) + +## Dependencies + +- `@modelcontextprotocol/sdk` — MCP protocol +- `express` — HTTP server +- `zod` — config validation +- `better-sqlite3` — SQLite store +- `pg` — Postgres client (pgvector store) +- `sqlite-vec` — SQLite vector extension +- `@huggingface/transformers` — ONNX embedding models +- `yaml` — OpenAPI spec parsing diff --git a/mcp/examples/ditto-oidc-write.json b/mcp/examples/ditto-oidc-write.json new file mode 100644 index 0000000000..6733ed8d91 --- /dev/null +++ b/mcp/examples/ditto-oidc-write.json @@ -0,0 +1,18 @@ +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "kind": "oidc", + "tokenUrl": "https://idp.example/token", + "clientId": "REPLACE_ME", + "clientSecret": "REPLACE_ME", + "devops": true + }, + "policy": { + "allowMethods": ["GET"], + "writeAllowlist": ["putThing"], + "sudoAllowlist": ["sudoRetrieveThing"] + } + } +} diff --git a/mcp/examples/ditto-readonly.json b/mcp/examples/ditto-readonly.json new file mode 100644 index 0000000000..4070b1f681 --- /dev/null +++ b/mcp/examples/ditto-readonly.json @@ -0,0 +1,18 @@ +{ + "knowledge": { + "enabled": true, + "retriever": "fts" + }, + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { + "kind": "basic", + "username": "ditto", + "password": "REPLACE_ME" + }, + "policy": { + "allowMethods": ["GET"] + } + } +} diff --git a/mcp/examples/hybrid-local.json b/mcp/examples/hybrid-local.json new file mode 100644 index 0000000000..edf2b83307 --- /dev/null +++ b/mcp/examples/hybrid-local.json @@ -0,0 +1,24 @@ +{ + "knowledge": { + "retriever": "hybrid", + "publicSource": { + "enabled": true + }, + "localDir": { + "enabled": true, + "path": "/path/to/your/markdown" + }, + "embedding": { + "model": "Xenova/bge-small-en-v1.5", + "dim": 384, + "allowRemoteModels": true, + "batchSize": 32 + }, + "store": { + "kind": "sqlite", + "sqlite": { + "path": "./ditto-index.db" + } + } + } +} diff --git a/mcp/examples/pgvector.json b/mcp/examples/pgvector.json new file mode 100644 index 0000000000..0b2d6847f7 --- /dev/null +++ b/mcp/examples/pgvector.json @@ -0,0 +1,16 @@ +{ + "knowledge": { + "retriever": "hybrid", + "embedding": { + "model": "Xenova/bge-small-en-v1.5", + "dim": 384 + }, + "store": { + "kind": "pgvector", + "pgvector": { + "connectionString": "postgres://USER:PASSWORD@HOST:5432/ditto", + "table": "ditto_kn" + } + } + } +} diff --git a/mcp/examples/public-fts.json b/mcp/examples/public-fts.json new file mode 100644 index 0000000000..336f1ed2e4 --- /dev/null +++ b/mcp/examples/public-fts.json @@ -0,0 +1,14 @@ +{ + "knowledge": { + "retriever": "fts", + "publicSource": { + "enabled": true + }, + "store": { + "kind": "sqlite", + "sqlite": { + "path": "./ditto-index.db" + } + } + } +} diff --git a/mcp/src/config/examples.test.ts b/mcp/src/config/examples.test.ts new file mode 100644 index 0000000000..5aaba73a57 --- /dev/null +++ b/mcp/src/config/examples.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { readdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { loadConfig } from "./load.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const examplesDir = join(__dirname, "../../examples"); + +describe("example configs", () => { + const exampleFiles = readdirSync(examplesDir).filter((f) => f.endsWith(".json")); + + it("should have at least one example", () => { + expect(exampleFiles.length).toBeGreaterThan(0); + }); + + exampleFiles.forEach((file) => { + it(`should parse ${file} without error`, () => { + const path = join(examplesDir, file); + expect(() => loadConfig(path)).not.toThrow(); + }); + }); +}); From 44c85c0276db8699e69b8f9d704e9593dfd94085 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Tue, 11 Aug 2026 17:09:10 +0300 Subject: [PATCH 07/11] feat(mcp): devops credential + dynamic openapi spec source Squashes the devops-credential and openapi-spec-source work. Devops credential: - Add optional `ditto.devopsCredential` used exclusively for sudo-classified operations (/devops/*, sudo*, and the secret-bearing /api/2/connections* API); all other operations use `ditto.credential`. - Sudo ops are refused at the MCP layer when no devopsCredential is set. Connectivity is classified sudo via a spec-independent path rule so it is always devops-gated. - Remove the `devops` credential kind and the `isDevops` flag: devops capability is now positional (which credential slot). A per-session Authorization header overrides the selected credential without inheriting devops status. Credential kind is now `basic | oidc` only. OpenAPI spec source: - Resolve the spec by precedence path > url > version > in-repo canonical; add `openApi.version` (git tag/ref) and `openApi.versionUrlTemplate`. - Default fallback reads the canonical in-repo spec (documentation/src/main/resources/openapi/ditto-api-2.yml) instead of a committed duplicate; drop mcp/assets/ditto-openapi.yml. BREAKING CHANGE: kind:"devops" and the devops:true flag are removed; move a devops credential into ditto.devopsCredential. Sudo operations now require ditto.devopsCredential to be set. Co-Authored-By: Claude Opus 4.8 --- mcp/README.md | 51 +- mcp/assets/ditto-openapi.yml | 12377 --------------------------- mcp/examples/ditto-oidc-write.json | 13 +- mcp/src/config/load.test.ts | 38 +- mcp/src/config/schema.ts | 35 +- mcp/src/ditto/action-tool.test.ts | 58 +- mcp/src/ditto/action-tool.ts | 15 +- mcp/src/ditto/action-tools.test.ts | 22 +- mcp/src/ditto/action-tools.ts | 49 +- mcp/src/ditto/bundled-spec.test.ts | 6 +- mcp/src/ditto/client.test.ts | 2 +- mcp/src/ditto/credential.test.ts | 36 +- mcp/src/ditto/credential.ts | 32 +- mcp/src/ditto/tool-policy.test.ts | 7 + mcp/src/ditto/tool-policy.ts | 3 +- 15 files changed, 267 insertions(+), 12477 deletions(-) delete mode 100644 mcp/assets/ditto-openapi.yml diff --git a/mcp/README.md b/mcp/README.md index d43533f14e..2caf8f73c4 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -204,29 +204,27 @@ The server can expose action tools dynamically generated from a Ditto OpenAPI sp |-------|------|---------|-------------| | `ditto.enabled` | `boolean` | `false` | Enable action tools | | `ditto.baseUrl` | `string` | (required if enabled) | Base URL to Ditto instance (e.g., `http://localhost:8080`) | -| `ditto.openApi.path` | `string?` | `undefined` | Path to local OpenAPI spec file. If unset, uses bundled spec. | -| `ditto.openApi.url` | `string?` | `undefined` | URL to fetch OpenAPI spec from. If unset, uses bundled spec. | +| `ditto.openApi.path` | `string?` | `undefined` | Path to a local OpenAPI spec file. | +| `ditto.openApi.url` | `string?` | `undefined` | URL to fetch the OpenAPI spec from. | +| `ditto.openApi.version` | `string?` | `undefined` | Ditto git tag/ref (e.g. `3.6.0`) to fetch the matching spec for, via `versionUrlTemplate`. | +| `ditto.openApi.versionUrlTemplate` | `string` | eclipse-ditto raw URL | URL template with a `${version}` placeholder; override for forks/mirrors. | -If both `path` and `url` are unset, the server uses the bundled pinned Ditto OpenAPI spec (`mcp/assets/ditto-openapi.yml`), which is a snapshot of a known Ditto release and works offline. +Spec resolution precedence (first match wins): `path` > `url` > `version` > the in-repo canonical spec (`documentation/src/main/resources/openapi/ditto-api-2.yml`), which matches the checked-out Ditto version and works offline. Set `version` to target a different Ditto release at runtime (requires network). #### Credentials -Action tools support three credential modes: +Action tools support two credential modes: | Kind | Description | |------|-------------| | `basic` | Username + password (sent as `Authorization: Basic `) | -| `devops` | Static bearer token (sent as `Authorization: Bearer `) | -| `oidc` | OAuth2 client-credentials flow (exchanges `clientId` + `clientSecret` for an access token, auto-refreshes ~30s before expiry) | - -**Config-level credentials** (`ditto.credential`) are forwarded to every action-tool call unless overridden by a per-session `Authorization` header (session-level credentials). +| `oidc` | OAuth2 client-credentials flow: requests a token from `tokenUrl` using `clientId` + `clientSecret`, sends it as `Authorization: Bearer `, auto-refreshes ~30s before expiry | **OIDC credential fields:** - `tokenUrl` (required) — OAuth2 token endpoint - `clientId` (required) — OAuth2 client identifier - `clientSecret` (required) — OAuth2 client secret (never logged) - `scope` (optional) — OAuth2 scopes (space-separated) -- `devops` (optional) — Operator assertion that the credential is devops-capable (required for sudo operations) Example: ```json @@ -245,6 +243,30 @@ Example: } ``` +**Standard vs devops credentials:** + +`ditto.credential` authenticates standard operations. `ditto.devopsCredential` (optional, same shape) authenticates **sudo** operations — `/devops/*`, `sudo*`, and the secret-bearing connectivity API (`/api/2/connections*`). + +- Sudo operations use `devopsCredential` **exclusively**. If `devopsCredential` is not set, every sudo tool is refused at the MCP layer (even if `credential` could reach it). +- `devopsCredential` may be `basic` (Ditto `DevOpsBasic`: a devops user's username/password) or `oidc` (Ditto `DevOpsBearer`: OAuth2). For a separate devops OIDC identity, give it its own `clientId`/`clientSecret`. +- A per-session `Authorization` header overrides whichever credential the operation selected (standard for normal ops, devops for sudo ops). + +Example (separate OIDC identities): + +```json +{ + "ditto": { + "enabled": true, + "baseUrl": "http://localhost:8080", + "credential": { "kind": "oidc", "tokenUrl": "https://idp/token", "clientId": "app", "clientSecret": "..." }, + "devopsCredential": { "kind": "oidc", "tokenUrl": "https://idp/token", "clientId": "devops", "clientSecret": "..." }, + "policy": { "sudoAllowlist": ["getConnections"] } + } +} +``` + +**Migration from earlier configs:** the `devops` credential kind and the `devops: true` flag are removed. Move a devops credential into `ditto.devopsCredential` (use `kind: "basic"` for a devops username/password). + **Authorization enforcement:** The MCP never decides authorization. It forwards the credential (or `Authorization` header) and lets Ditto enforce access control. Credentials are never logged. #### Policy @@ -257,14 +279,19 @@ By default, action tools only expose **read** (`GET`) operations. Write and priv | `ditto.policy.writeAllowlist` | `string[]` | `[]` | Per-operation granular allowlist for enabling specific write operations (operationIds) | | `ditto.policy.sudoAllowlist` | `string[]` | `[]` | Per-operation allowlist for sudo/devops-privileged operations (operationIds) | -**Sudo operations & devops flag:** +**Sudo operations & devops credential:** Ditto secures `/api/2/connections*` (secret-bearing) with `DevOpsBasic`/`DevOpsBearer` security, and `/devops/*` paths are devops-privileged. These operations are classified as "sudo" and: - Must be explicitly listed in `sudoAllowlist` (by operationId) -- Require a devops-capable credential. If a devops credential is not present, the operation is refused and never sent to Ditto. +- Require `ditto.devopsCredential` to be configured. - Are NOT auto-allowed even if the method is `GET` and in `allowMethods`. -Set `credential.devops: true` as an **operator assertion** that the credential is devops-capable. This gates `sudo*`/`/devops`/`/connections` tools at the MCP layer — Ditto still enforces the real authorization. Deriving `devops` from token introspection/claims is a future enhancement (currently, `basic` and `devops` kinds are implicitly devops-capable; `oidc` requires explicit `devops: true`). +Connectivity is always devops-gated (classified sudo regardless of the OpenAPI spec's declared security), but policy granularity is unchanged — `sudoAllowlist` is a per-`operationId` opt-in (default `[]` = all sudo blocked). Allow connections while blocking direct-actor/devops commands by listing only the connection operationIds: + +- Connections read only: `"sudoAllowlist": ["getConnections", "getConnection"]` +- Full connections CRUD, still blocking piggyback/devops: `"sudoAllowlist": ["getConnections","getConnection","createConnection","modifyConnection","deleteConnection"]` + +All sudo operations still require `ditto.devopsCredential` to be set. **Examples:** diff --git a/mcp/assets/ditto-openapi.yml b/mcp/assets/ditto-openapi.yml deleted file mode 100644 index 00c078bc97..0000000000 --- a/mcp/assets/ditto-openapi.yml +++ /dev/null @@ -1,12377 +0,0 @@ -openapi: 3.0.0 -info: - title: Eclipse Ditto™ HTTP API - version: '2' - description: |- - JSON-based, REST-like API for Eclipse Ditto - - The Eclipse Ditto HTTP API uses response status codes (see [RFC 7231](https://tools.ietf.org/html/rfc7231#section-6)) - to indicate whether a specific request has been successfully completed, or not. - - The information Ditto provides additionally to the status code (e.g. in API docs, or error codes like. "things:thing.tooLarge") might change without advance notice. - These are not be considered as official API, and must therefore not be applied in your applications or tests. -servers: - - url: 'https://ditto.eclipseprojects.io/' - description: online Ditto Sandbox - - url: / - description: local Ditto -tags: - - name: Things - description: Manage every thing - - name: Features - description: Structure the features of your things - - name: Policies - description: Control access to your things - - name: Things-Search - description: Find every thing - - name: Messages - description: Talk with your things - - name: CloudEvents - description: Process CloudEvents in Ditto - - name: Connections - description: Manage connections - - name: WoT - description: WoT (Web of Things) Discovery endpoints - - name: Devops - description: Devops APIs to manage log levels and configuration in runtime and send piggyback command -security: - - OpenIDConnect: [] - - NginxBasic: [] - - Bearer: [] -paths: - /api/2/things: - get: - summary: Retrieve visible things or things with specified IDs - description: |- - Returns all visible things or things passed in by the required parameter `ids`, which you (the authorized subject) are allowed to read. - - Optionally, if you want to retrieve only some of the thing's fields, you can use the specific field selectors (see parameter `fields`) . - - Tip: In order to formulate a `filter` which things to search for, take a look at the `/search` resource. - tags: - - Things - parameters: - - name: ids - in: query - description: Contains a comma-separated list of `thingId`s to retrieve in one single request. - required: false - schema: - type: string - - $ref: '#/components/parameters/ThingFieldsQueryParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: |- - The successfully completed request contains a list of the for the user available Things, or the Things asked for via the `ids` paramter. - The Things are sorted either by their ID, or in the same order as the Thing IDs were provided in the `ids` parameter. - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Thing' - application/td+json: - schema: - type: array - items: - $ref: '#/components/schemas/WotThingDescription' - '400': - description: The request could not be completed. At least one of the defined query parameters was invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '414': - description: The request could not be completed due to an URI length exceeding 8k characters. - post: - summary: Create a new thing - description: |- - Creates a thing with a default `thingId` and a default `policyId`. - - The thing will be empty, i.e. no features, definition, attributes etc. by default. - - The default `thingId` consists of your default namespace and a UUID. - - The default `policyId` is identical with the default `thingId`, and allows the currently authorized subject all permissions. - - In case you need to create a thing with a specific ID, use a *PUT* request instead, as any `thingId` specified in the request body will be ignored. - - The field `_created` is filled automatically with the timestamp of the creation. The field is read-only and can - be retrieved later by explicitly selecting it or used in search filters. - tags: - - Things - parameters: - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/AllowPolicyLockoutParam' - - $ref: '#/components/parameters/Namespace' - responses: - '201': - description: The thing was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created thing resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Thing' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` must not be set in the request body - * the JSON body of the thing to be created is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. - Possible reasons: - * the caller would not have access to the thing after creating it with the given policy. - * the caller has insufficient permissions. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller had insufficient permissions to read the referenced thing. - * the policy that should be copied does not exist. - * the caller had insufficient permissions to read the policy that should be copied. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/NewThing' - example: - definition: 'com.acme:coffeebrewer:0.1.0' - attributes: - manufacturer: ACME demo corp. - location: 'Berlin, main floor' - serialno: '42' - model: Speaking coffee machine - features: - coffee-brewer: - definition: - - 'com.acme:coffeebrewer:0.1.0' - properties: - brewed-coffees: 0 - water-tank: - properties: - configuration: - smartMode: true - brewingTemp: 87 - tempToHold: 44 - timeoutSeconds: 6000 - status: - waterAmount: 731 - temperature: 44 - description: 'JSON representation of the thing to be created. Use ''{}'' to create an empty thing with a default policy.' - '/api/2/things/{thingId}': - get: - summary: Retrieve a specific thing - description: |- - Returns the thing identified by the `thingId` path parameter. The response includes details about the thing, - including the `policyId`, attributes, definition and features. - - Optionally, you can use the field selectors (see parameter `fields`) to only get specific fields, - which you are interested in. - - ### Example: - Use the field selector `_policy` to retrieve the content of the policy. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/ThingFieldsQueryParam' - - $ref: '#/components/parameters/IfMatchHeaderParam' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParamPutDescription' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The request successfully returned the specific thing. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Thing' - application/td+json: - schema: - $ref: '#/components/schemas/WotThingDescription' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * at least one of the defined query parameters is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update a thing with a specified ID - description: |- - Create or update the thing specified by the `thingId` path parameter and the optional JSON body. - - * If you set a new `thingId` in the path, a thing will be created. - * If you set an existing `thingId` in the path, the thing will be updated. - - - ### Create a new thing - At the initial creation of a thing, only a valid `thingId` is required. - However, you can create a full-fledged thing all at once. - - ### Example: - To create a coffee maker thing, set the `thingId` in the path, e.g. to "com.acme.coffeemaker:BE-42" - and the body part, like in the following snippet. - - ``` - { - "definition": "com.acme:coffeebrewer:0.1.0", - "attributes": { - "manufacturer": "ACME demo corp.", - "location": "Berlin, main floor", - "serialno": "42", - "model": "Speaking coffee machine" - }, - "features": { - "coffee-brewer": { - "definition": [ "com.acme:coffeebrewer:0.1.0" ], - "properties": { - "brewed-coffees": 0 - } - }, - "water-tank": { - "properties": { - "configuration": { - "smartMode": true, - "brewingTemp": 87, - "tempToHold": 44, - "timeoutSeconds": 6000 - }, - "status": { - "waterAmount": 731, - "temperature": 44 - } - } - } - } - } - ``` - As the example does not set a policy in the request body, but the thing concept requires one, - the service will create a default policy. The default policy, has the exactly same id - as the thing, and grants ALL permissions to the authorized subject. - - In case you need to associate the new thing to an already existing policy you can additionally - set a policy e.g. "policyId": "com.acme.coffeemaker:policy-1" as the first element in the body part. - Keep in mind, that you can also change the assignment to another policy anytime, - with a request on the sub-resource "PUT /things/{thingId}/policyId" - - The field `_created` is filled automatically with the timestamp of the creation. The field is read-only and can - be retrieved later by explicitly selecting it or used in search filters. - - ### Update an existing thing - - For updating an existing thing, the authorized subject needs **WRITE** permission on the thing's root resource. - - The ID of a thing cannot be changed after creation. Any `thingId` - specified in the request body is therefore ignored. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParam' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The thing was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created thing resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Thing' - '204': - description: The thing was successfully modified. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the thing to be created/modified is invalid - * the JSON body of the thing to be created/modified contains a `thingId` - which does not match the ID in the path - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller would not have access to the thing after creating it with the given policy - * the caller has insufficient permissions. - For modifying an existing thing, an unrestricted `WRITE` permission on the thing's root resource is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller has insufficient permissions to read the referenced thing. - * the policy that should be copied does not exist. - * the caller has insufficient permissions to read the policy that should be copied. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/NewThing' - example: - definition: 'com.acme:coffeebrewer:0.1.0' - attributes: - manufacturer: ACME demo corp. - location: 'Berlin, main floor' - serialno: '42' - model: Speaking coffee machine - features: - coffee-brewer: - definition: - - 'com.acme:coffeebrewer:0.1.0' - properties: - brewed-coffees: 0 - water-tank: - properties: - configuration: - smartMode: true - brewingTemp: 87 - tempToHold: 44 - timeoutSeconds: 6000 - status: - waterAmount: 731 - temperature: 44 - description: JSON representation of the thing to be modified. - patch: - summary: Create or patch a thing with a specified ID - description: |- - Create or patch an existing thing specified by the `thingId` path parameter. - - If the thing did not yet exist, it will be created. - For an existing thing, patching a thing will merge the provided request body with the existing thing values. - This makes it possible to change only some parts of a thing in single request without providing the full thing - structure in the request body. - - - ### Patch a thing - - With this resource it is possible to add, update or delete parts of an existing thing or to create the thing if it - does not yet exist. - The request body provided in *JSON merge patch* (RFC-7396) format will be merged with the existing thing. - Notice that the `null` value in the JSON body will delete the specified JSON key from the thing. - For further documentation of JSON merge patch see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - - ### Example - A Thing already exists with the following content: - - ``` - { - "definition": "com.acme:coffeebrewer:0.1.0", - "attributes": { - "manufacturer": "ACME demo corp.", - "location": "Berlin, main floor", - "serialno": "42", - "model": "Speaking coffee machine" - }, - "features": { - "coffee-brewer": { - "definition": ["com.acme:coffeebrewer:0.1.0"], - "properties": { - "brewed-coffees": 0 - } - }, - "water-tank": { - "properties": { - "configuration": { - "smartMode": true, - "brewingTemp": 87, - "tempToHold": 44, - "timeoutSeconds": 6000 - }, - "status": { - "waterAmount": 731, - "temperature": 44 - } - } - } - } - } - ``` - - To make changes that only affect parts of the existing thing, e.g. add some attribute and delete a - specific feature property, the content of the request body could look like this: - - ``` - { - "attributes": { - "manufacturingYear": "2020" - }, - "features": { - "water-tank": { - "properties": { - "configuration": { - "smartMode": null, - "tempToHold": 50, - } - } - } - } - } - ``` - - The request body will be merged with the existing thing and the result will be the following thing: - - ``` - { - "definition": "com.acme:coffeebrewer:0.1.0", - "attributes": { - "manufacturer": "ACME demo corp.", - "manufacturingYear": "2020", - "location": "Berlin, main floor", - "serialno": "42", - "model": "Speaking coffee machine" - }, - "features": { - "coffee-brewer": { - "definition": ["com.acme:coffeebrewer:0.1.0"], - "properties": { - "brewed-coffees": 0 - } - }, - "water-tank": { - "properties": { - "configuration": { - "brewingTemp": 87, - "tempToHold": 50, - "timeoutSeconds": 6000 - }, - "status": { - "waterAmount": 731, - "temperature": 44 - } - } - } - } - } - ``` - - ### Permissions for patching an existing Thing - - For updating an existing thing, the authorized subject needs **WRITE** permission on those parts of the thing - that are affected by the merge update. - - For example, to successfully execute the above example the authorized subject needs to have unrestricted - *WRITE* permissions on all affected paths of the JSON merge patch: `attributes/manufacturingYear`, - `features/water-tank/properties/configuration/smartMode`, - `features/water-tank/properties/configuration/tempToHold`. The *WRITE* permission must not be revoked on any - level further down the hierarchy. Consequently it is also sufficient for the authorized subject to have - unrestricted *WRITE* permission at root level or unrestricted *WRITE* permission at `/attributes` and - `/features` etc. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParam' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The thing was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created thing resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Thing' - '204': - description: The thing was successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the JSON body of the thing to be patched is invalid - * the JSON body of the thing to be patched contains a `thingId` which does not match the ID in the path - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller would not have access to the thing after creating it with the given policy - * the caller has insufficient permissions. - For modifying an existing thing, an unrestricted `WRITE` permission on the thing's root resource is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller has insufficient permissions to read the referenced thing. - * the policy that should be copied does not exist. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/PatchThing' - example: - attributes: - manufacturingYear: '2020' - features: - water-tank: - properties: - configuration: - smartMode: null - tempToHold: 50 - description: JSON representation of the thing to be patched. - delete: - summary: Delete a specific thing - description: |- - Deletes the thing identified by the `thingId` path parameter. - - This will not delete the policy, which is used for controlling access to this thing. - - You can delete the policy afterwards via DELETE `/policies/{policyId}` if you don't need it for other things. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParam' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The thing was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller had insufficient permissions. - For deleting an existing thing, an unrestricted `WRITE` permission on the thing's root resource is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/migrateDefinition': - post: - summary: Update the definition of an existing Thing - description: |- - Updates the definition of the specified thing by providing a new definition URL along with an optional migration payload. - - The request body allows specifying: - - A new Thing definition URL. - - A migration payload containing updates to attributes and features. - - Patch conditions to ensure consistent updates. - - Whether properties should be initialized if missing. - - **Placeholders in migration payload:** String values in `migrationPayload` may use the thing-json - placeholder. Both brace `{{ thing-json: }}` and legacy `${ thing-json: }` - are supported. - - If the `dry-run` query parameter or header is set to `true`, the request will return the calculated migration result without applying any changes. - - ### Example: - ```json - { - "thingDefinitionUrl": "https://example.com/new-thing-definition.json", - "migrationPayload": { - "attributes": { - "manufacturer": "New Corp" - }, - "features": { - "sensor": { - "properties": { - "status": { - "temperature": { - "value": 25.0 - } - } - } - } - } - }, - "patchConditions": { - "thing:/features/sensor": "not(exists(/features/sensor))" - }, - "initializeMissingPropertiesFromDefaults": true - } - ``` - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - name: dry-run - in: query - description: 'If set to `true`, performs a dry-run and returns the migration result without applying changes.' - required: false - schema: - type: boolean - default: false - requestBody: - $ref: '#/components/requestBodies/MigrateThingDefinitionRequest' - responses: - '200': - $ref: '#/components/responses/MigrateThingDefinitionResponse' - '202': - description: Dry-run successful. The migration result is returned without applying changes. - content: - application/json: - schema: - $ref: '#/components/schemas/MigrateThingDefinitionResponse' - '400': - description: The request could not be processed due to invalid input. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: Unauthorized request due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The specified thing could not be found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - description: The update conditions were not met. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '/api/2/things/{thingId}/definition': - get: - summary: Retrieve the definition of a specific thing - description: Returns the definition of the thing identified by the `thingId` path parameter. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The request successfully returned the definition of the specific thing. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Definition' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation - (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the definition of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update the definition of a specific thing - description: |- - * If the thing does not have a definition yet, this request will create it. - * If the thing already has a definition you can assign it to a new one by setting the new definition in the request body. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The definition was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created definition resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Definition' - '204': - description: The definition was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation - (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON was invalid - * the request body was not a JSON object. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying a definition of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Definition' - patch: - summary: Patch the definition of a specific thing - description: |- - * If the thing does not have a definition yet, this request will create it. - * If the thing already has a definition you can replace it by providing the new definition in the request body. - * If the request body is set to `null` then the defintion will be deleted. - - Notice that the `null` value in the JSON body has a special meaning and will delete the definition from the thing. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The definition was successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation - (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON was invalid - * the request body was not a JSON object. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying a definition of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/Definition' - example: '"example:test:definition"' - description: |- - JSON string representation of the definition to be patched. - - Consider that the value has to be a JSON string. - - Examples: - - * a string: `"value"` - Currently the definition should follow the pattern: [_a-zA-Z0-9\-]:[_a-zA-Z0-9\-]:[_a-zA-Z0-9\-] - * an empty string: `""` - * `null`: the definition will be deleted - delete: - summary: Delete the definition of a specific thing - description: Deletes the definition of the thing identified by the `thingId`. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The definition was successfully deleted. - '400': - description: |- - The request could not be completed. The `thingId` does not conform to the namespaced entity ID notation - (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying a definition of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID or its definition was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/policyId': - get: - summary: Retrieve the policy ID of a thing - description: Returns the policy ID of the thing identified by the `thingId` path parameter. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The request successfully returned the policy ID. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - type: string - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Update the policy ID of a thing - description: |- - Update the policy ID of the thing identified by the `thingId` path parameter. - - ### Update - If the thing already has a `policyId` you can assign it to an existing policy by setting the new `policyId` - in the request body. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The policy ID was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The thing with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - type: string - example: '"your.namespace:your-policy-name"' - description: |- - The policy is used for controlling access to this thing. It is managed by - resource `/policies/{policyId}`. - - The ID of a policy needs to conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - required: true - patch: - summary: Patch the policy ID of a thing - description: |- - Patch the policy ID of the thing identified by the `thingId` path parameter. - - The `policyId` of the thing will be updated. - Notice that for this resource it is not possible to remove the `policyId` from the thing by setting the - payload to `null`. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: 'The policy ID was successfully patched. Note: You will need to create the policy content separately.' - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the `policyId` can not be removed from a thing. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - type: string - example: '"your.namespace:your-policy-name"' - description: |- - The policy is used for controlling access to this thing. It is managed by resource `/policies/{policyId}`. - - The ID of a policy needs to conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - required: true - '/api/2/things/{thingId}/attributes': - get: - summary: List all attributes of a specific thing - description: Returns all attributes of the thing identified by the `thingId` path parameter. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/AttributesFieldsQueryParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The attributes of the specific thing were successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Attributes' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update all attributes of a specific thing at once - description: |- - Create or update the attributes of a thing identified by the `thingId` - path parameter. The attributes will be overwritten - all at once - with the - content (JSON) set in the request body. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The attributes were successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created attribute resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Attributes' - '204': - description: The attributes were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the attributes to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the attributes of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Attributes' - patch: - summary: Patch all attributes of a specific thing - description: |- - Patch the attributes of a thing identified by the `thingId` path parameter. - The existing attributes will be merged with the JSON content set in the request body. - - Notice that the `null` value has a special meaning and can be used to delete all or specific attributes from a thing. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - **Note**: In contrast to the "PUT things/{thingId}/attributes" request, - a partial update is supported here and request body is merged with the existing attributes. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The attributes were successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the attributes to be patched is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the attributes of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/Attributes' - example: - manufacturer: - name: ACME demo corp. - location: 'Berlin, main floor' - coffeemaker: - serialno: '42' - model: Speaking coffee machine - description: |- - JSON object of all attributes to be patched. Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). - - Examples: - * a simple object: `{ "key": "value"}` - We strongly recommend to use a restricted set of characters for the key (identifier), as the key might be needed for the (URL) path later.
Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - * a nested object as shown in the example value - * `null`: deletes all attributes - required: true - delete: - summary: Delete all attributes of a specific thing at once - description: Deletes all attributes of the thing identified by the `thingId` path parameter. - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The attributes were successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting all attributes of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID or its attributes were not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/attributes/{attributePath}': - get: - summary: Retrieve a specific attribute of a specific thing - description: |- - Returns a specific attribute of the thing identified by the `thingId` path parameter. - - The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). - - ### Example: - - In order to retrieve the `name` field of an `manufacturer` attribute, the full path would be - `/things/{thingId}/attributes/manufacturer/name` - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/AttributesPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The attribute was successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The thing with the given ID or - the attribute at the specified path was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update a specific attribute of a specific thing - description: |- - Create or update a specific attribute of the thing identified by the `thingId` path parameter. - - * If you specify a new attribute path, this will be created - * If you specify an existing attribute path, this will be updated - - The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). - - ### Example: - - In order to put the `name` field of an `manufacturer` attribute, the full path would be - `/things/{thingId}/attributes/manufacturer/name` - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/AttributesPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The attribute was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created attribute resource - schema: - type: string - '204': - description: The attribute was successfully modified. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying an attribute of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Value' - patch: - summary: Patch a specific attribute of a specific thing - description: |- - Patch a specific attribute of a thing identified by the `thingId` path parameter. - - * If you specify a new attribute path, this will be created - * If you specify an existing attribute path, this will be merged - * If you set the request body to `null` for an existing attribute path then the attribute will be deleted. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). - - ### Example: - - In order to patch the `name` field of an `manufacturer` attribute, the full path would be - `/things/{thingId}/attributes/manufacturer/name` - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/AttributesPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The attribute was successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying an attribute of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/PatchValue' - delete: - summary: Delete a specific attribute of a specific thing - description: |- - Deletes a specific attribute of the thing identified by the `thingId` path parameter. - - The attribute (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). - - ### Example: - In order to delete the `name` field of an `manufacturer` attribute, the full path would be - `/things/{thingId}/attributes/manufacturer/name` - tags: - - Things - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/AttributesPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The attribute was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting a single attribute of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID or the attribute at the specified path was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/features': - get: - summary: List all features of a specific thing - description: Returns all features of the thing identified by the `thingId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeaturesFieldsQueryParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: |- - The list of features of the specific thing were successfully - retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Features' - example: - featureId1: - definition: - - 'namespace:definition1:v1.0' - properties: - property1: value1 - featureId2: - definition: - - 'namespace:definition2:v1.0' - properties: - property2: value2 - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined query parameters is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The thing with the given ID was - not found or the features have not been defined. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify all features of a specific thing at once - description: |- - Create or modify all features of a thing identified by the `thingId` path parameter. - - ### Create all features at once - In case at the initial creation of your thing you have not specified any features, these can be created here. - - ### Update all features at once - To update all features at once prepare the JSON body accordingly. - - Note: In contrast to the "PUT thing" request, a partial update is not supported here, - but the content will be **overwritten**. - If you need to update single features or their paths, please use the sub-resources instead. - - ### Example: - - ``` - { - "coffee-brewer": { - "definition": ["com.acme:coffeebrewer:0.1.0"], - "properties": { - "brewed-coffees": 0 - } - }, - "water-tank": { - "properties": { - "configuration": { - "smartMode": true, - "brewingTemp": 87, - "tempToHold": 44, - "timeoutSeconds": 6000 - }, - "status": { - "waterAmount": 731, - "temperature": 44 - } - } - } - } - ``` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The features were successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created features resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Features' - example: {} - '204': - description: The features were successfully modified. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the feature to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying all features of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Features' - example: - coffee-brewer: - properties: - definition: - - 'com.acme:coffeebrewer:0.1.0' - brewed-coffees: 0 - water-tank: - properties: - configuration: - smartMode: true - brewingTemp: 87 - tempToHold: 44 - timeoutSeconds: 6000 - status: - waterAmount: 731 - temperature: 44 - description: |- - JSON object of all features to be modified at once. Consider that the value has to be a JSON object or null. - - Examples: - * an empty object: {} - would just delete all old features - * an empty feature: { "featureId": {} } - We strongly recommend to use a restricted set of characters - for the `featureId`, as it might be needed for the (URL) path later. - - Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9-]* - - * a nested object with multiple features as shown in the example value field - required: true - patch: - summary: Patch all features of a specific thing - description: |- - Patch all features of a thing identified by the `thingId` path parameter. - - The existing features will be merged with the JSON content set in the request body. - - Notice that the `null` value has a special meaning and can be used to delete specific features from the thing. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - **Note**: In contrast to the "PUT thing/{thingId}/features" request, a partial update is supported here - and request body is merged with the existing features. - - ### Example - - The following example will add/update the properties `brewed-coffees`, `tempToHold` and `failState`. - The configuration property `smartMode` will be deleted from the thing. - - - ``` - { - "coffee-brewer": { - "properties": { - "brewed-coffees": 10 - } - }, - "water-tank": { - "properties": { - "configuration": { - "smartMode": null, - "tempToHold": 50, - }, - "status": { - "failState": true - } - } - } - } - ``` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The features were successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the feature to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying all features of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/Features' - example: - coffee-brewer: - properties: - brewed-coffees: 10 - water-tank: - properties: - configuration: - smartMode: null - tempToHold: 50 - status: - failState: true - description: |- - JSON object of all features to be patched. Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). - - Examples: - * a nested object with multiple features as shown in the example value field - - * **Note**: To delete certain entries of a feature the `null` value can be used. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - required: true - delete: - summary: Delete all features of a specific thing - description: Deletes all features of the thing identified by the `thingId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The features were successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting all features of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The thing with the given ID was - not found or the features have not been defined. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/features/{featureId}': - get: - summary: Retrieve a specific feature of a specific thing - description: |- - Returns a specific feature identified by the `featureId` path parameter of the thing - identified by the `thingId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/FeatureFieldsQueryParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The feature was successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Feature' - application/td+json: - schema: - $ref: '#/components/schemas/WotThingDescription' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined query parameters is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The thing with the given ID or - the feature with the specified `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify a specific feature of a specific thing - description: |- - Create or modify a specific feature identified by the `featureId` path - parameter of the thing identified by the `thingId` path parameter. - - ### Create feature - If the feature ID is new, the feature and all content from the JSON body will be created - - ### Update feature - If the feature ID is used already in this thing, the feature will be overwrittern - with the content from the JSON body. - - ### Example: - Set the `featureId` to **coffee-brewer** and all properties in the body part. - - ``` - { - "definition": ["com.acme:coffeebrewer:0.1.0"], - "properties": { - "brewed-coffees": 42 - } - } - ``` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The feature was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created feature resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Feature' - '204': - description: The feature was successfully modified. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the feature to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying a single feature of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Feature' - example: - definition: - - 'com.acme:coffeemaker:0.1.0' - - 'com.acme:coffeemaker:1.1.0' - properties: - connected: true - brewed-coffees: 0 - description: |- - JSON representation of the feature to be created/modified. - Consider that the value has to be a JSON object or null. - - Examples: - * an empty object: {} - would just create the featureID but would delete all content of the feature - * a nested object with multiple model definitions and multiple properties as shown in the example value field - required: true - patch: - summary: Patch a specific feature of a specific thing - description: |- - Patch a specific feature identified by the `featureId` path parameter of a thing identified by the `thingId` path parameter. - - The existing feature will be merged with the JSON content set in the request body. - - Notice that the `null` value can be used to delete the whole feature or specific parts of it. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - **Note**: In contrast to the "PUT things/{thingId}/features/{featureId}" request, - a partial update is supported here and request body is merged with the existing feature. - - ### Example - - Set the `featureId` to **coffee-brewer** and all properties in the body part - to update the `brewed-coffees` property and delete the definition. - - ``` - { - "definition": null, - "properties": { - "brewed-coffees": 42 - } - } - ``` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The feature was successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the feature to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying a single feature of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/Feature' - example: - definition: null - properties: - connected: true - brewed-coffees: 0 - description: |- - JSON representation of the feature to be patched. Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). - - Examples: - * a nested object with multiple model definitions and multiple properties as shown in the example value field - * **Note**: To delete certain properties of a feature the `null` value can be used. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - required: true - delete: - summary: Delete a specific feature of a specific thing - description: |- - Deletes a specific feature identified by the `featureId` path parameter - of the thing identified by the `thingId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The feature was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting a single feature of an existing thing, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing with the given ID or the feature at the specified path was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/features/{featureId}/definition': - get: - summary: List the definition of a feature - description: Returns the complete definition field of the feature identified by the `thingId` and `featureId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The definition was successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureDefinition' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined query parameters is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified feature has no - definition or the thing with the specified `thingId` or the feature - with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update the definition of a feature - description: |- - Create or update the complete definition of a feature identified by the `thingId` and `featureId` path parameter. - - The definition field will be overwritten with the JSON array set in the request body - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The definition was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created definition resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureDefinition' - '204': - description: The definition was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the definition of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureDefinition' - example: - - 'com.acme:coffeebrewer:0.1.0' - - 'com.acme:coffeebrewer:1.0.0' - description: |- - JSON array of the complete definition to be updated. - - Consider that the value has to be a JSON array or `null`. - - The content of the JSON array are strings in the format `"::"` or a valid HTTP(s) URL, which is enforced. - required: true - patch: - summary: Patch the definition of a feature - description: |- - Patch the definition of a feature identified by the `thingId` and `featureId` path parameter. - - The existing definition field will be overwritten with the JSON array set in the request body. - - Notice that the `null` value can be used to delete the definition of a feature. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The definition was successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the definition of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/FeatureDefinition' - example: - - 'com.acme:coffeebrewer:0.1.0' - - 'com.acme:coffeebrewer:1.1.0' - description: |- - JSON array of the complete definition to be patched. Consider that the value has to be a JSON array. - - The content of the JSON array are strings in the format `"::"` or a valid HTTP(s) URL, which is enforced. - To delete the definition use `null` as content in the request body. - required: true - delete: - summary: Delete the definition of a feature - description: Deletes the complete definition of the feature identified by the `thingId` and `featureId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The definition was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting the definition of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified feature has no definition or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/features/{featureId}/properties': - get: - summary: List all properties of a feature - description: Returns all properties of the feature identified by the `thingId` and `featureId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertiesFieldsQueryParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The properties were successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureProperties' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined query parameters is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified feature has no properties or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update all properties of a feature at once - description: |- - Create or update the properties of a feature identified by the `thingId` and `featureId` path parameter. - - The properties will be overwritten with the JSON content from the request body. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The properties were successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureProperties' - '204': - description: The properties were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the feature properties to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureProperties' - example: - configuration: - smartMode: true - brewingTemp: 87 - tempToHold: 44 - timeoutSeconds: 6000 - status: - waterAmount: 731 - temperature: 44 - description: |- - JSON object of all properties to be updated at once. - - Consider that the value has to be a JSON object or `null`. We strongly recommend to use - a restricted set of characters for the key (identifier). - - Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - required: true - patch: - summary: Patch all properties of a feature - description: |- - Patch the properties of a feature identified by the `thingId` and `featureId` path parameter. - - The existing properties will be merged with the JSON content set in the request body. - - Notice that the `null` value can be used to delete specific feature properties. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - **Note**: In contrast to the "PUT things/{thingId}/features/{featureId}/properties" request, - a partial update is supported here and request body is merged with the existing properties. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The properties were successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the feature properties to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/FeatureProperties' - example: - configuration: - smartMode: null - brewingTemp: 87 - tempToHold: 44 - timeoutSeconds: 6000 - status: - waterAmount: 731 - temperature: 44 - description: |- - JSON object of all properties to be patched. - - Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). - We strongly recommend to use a restricted set of characters for the key (identifier). - - Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - required: true - delete: - summary: Delete all properties of a feature - description: Deletes all properties of the feature identified by the `thingId` and `featureId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The properties were successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting the properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified feature has no properties or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/features/{featureId}/properties/{propertyPath}': - get: - summary: Retrieve a specific property of a feature - description: |- - Returns the a specific property path of the feature identified by the `thingId` and `featureId` path parameter. - - The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901) - - ### Example - To retrieve the value of the `brewingTemp` in the `water-tank` of our coffeemaker example the full path is: - `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The property was successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified property or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update a specific property of a feature - description: |- - Create or update a specific property of a feature identified by the `thingId` and `featureId` path parameter. - - The property will be created if it doesn't exist or else updated. - - The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901), - - ### Example - To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: - `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The property was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '204': - description: The property was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For creating/updating a property of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Value' - patch: - summary: Patch a specific property of a feature - description: |- - Patch a specific property of a feature identified by the `thingId` and `featureId` path parameter. - - The existing property will be merged with the existing one of the thing. - - Notice that the `null` value can be used to delete the specified propertyPath. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). - - ### Example - To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: - - `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The property was successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For creating/updating a property of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/PatchValue' - delete: - summary: Delete a specific property of a feature - description: |- - Deletes a specific property of the feature identified by the `thingId` and `featureId` path parameter. - - The property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901) - - ### Example - To delete the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: - `/things/{thingId}/features/water-tank/properties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The property was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting the properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified property or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/features/{featureId}/desiredProperties': - get: - summary: List all desired properties of a feature - description: Returns all desired properties of the feature identified by the `thingId` and `featureId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/DesiredPropertiesFieldsQueryParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The desired properties were successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureProperties' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined query parameters is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified feature has no desired properties or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update all desired properties of a feature at once - description: |- - Create or update the desired properties of a feature identified by the `thingId` and `featureId` path parameter. - - The desired properties will be overwritten with the JSON content from the request body. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The desired properties were successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureProperties' - '204': - description: The desired properties were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the desired feature roperties to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the desired properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/FeatureProperties' - example: - configuration: - smartMode: true - brewingTemp: 87 - tempToHold: 44 - timeoutSeconds: 6000 - status: - waterAmount: 731 - temperature: 44 - description: |- - JSON object of all desried properties to be updated at once. - - Consider that the value has to be a JSON object or `null`. We strongly recommend to use - a restricted set of characters for the key (identifier). - - Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - required: true - patch: - summary: Patch all desired properties of a feature - description: |- - Patch the desired properties of a feature identified by the `thingId` and `featureId` path parameter. - - The existing desired properties will be merged with the JSON content set in the request body. - - Notice that the `null` value can be used to delete the whole feature or specific parts of it. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - **Note**: In contrast to the "PUT things/{thingId}/features/{featureId}/desiredProperties" request, - a partial update is supported here and request body is merged with the existing desired properties. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The desired properties were successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body of the desired feature roperties to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For modifying the desired properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/FeatureProperties' - example: - configuration: - smartMode: null - brewingTemp: 87 - tempToHold: 44 - timeoutSeconds: 6000 - status: - waterAmount: 731 - temperature: 44 - description: |- - JSON object of all desried properties to be patched. - - Consider that the value has to be a [JSON merge patch](https://tools.ietf.org/html/rfc7396). We strongly recommend to use - a restricted set of characters for the key (identifier). - - Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - required: true - delete: - summary: Delete all desired properties of a feature - description: Deletes all desired properties of the feature identified by the `thingId` and `featureId` path parameter. - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The desired properties were successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting the desired properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified feature has no desired properties or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/features/{featureId}/desiredProperties/{propertyPath}': - get: - summary: Retrieve a specific desired property of a feature - description: |- - Returns the a specific desired property path of the feature identified by the `thingId` and `featureId` path parameter. - - The desired property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901) - - ### Example - To retrieve the value of the `brewingTemp` in the `water-tank` of our coffeemaker example the full path is: - - `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/GetMetadataParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - - $ref: '#/components/parameters/LiveChannelConditionParam' - - $ref: '#/components/parameters/LiveChannelTimeoutStrategyParam' - responses: - '200': - description: The desired property was successfully retrieved. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - live-channel-condition-matched: - description: Whether or not the live-channel-condition did match and the thing was retrieved from the device. - schema: - type: boolean - channel: - description: The cannel which was used to retrieve the thing. - schema: - type: string - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified desired property or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update a specific desired property of a feature - description: |- - Create or update a specific desired property of a feature identified by the `thingId` and `featureId` path parameter. - - The desired property will be created if it doesn't exist or else updated. - - The desired property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901), - - ### Example - To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: - - `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '201': - description: The desired property was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '204': - description: The desired property was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For creating/updating a desired property of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Value' - patch: - summary: Patch a specific desired property of a feature - description: |- - Patch a specific desired property of a feature identified by the `thingId` and `featureId` path parameter. - - The exisiting desired property of a feature will be merged with the JSON content set in the request body. - - Notice that the `null` value can be used to delete the specified propertyPath. - For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - - The desired property (JSON) can be referenced hierarchically, by applying JSON Pointer notation (RFC-6901). - - ### Example - To set the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: - `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/PutMetadataParam' - - $ref: '#/components/parameters/DeleteMetadataParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The desired property was successfully patched. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * the JSON body is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For creating/updating a desired property of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The thing or the feature with the given ID was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/PatchValue' - delete: - summary: Delete a specific desired property of a feature - description: |- - Deletes a specific desired property of the feature identified by the `thingId` - and `featureId` path parameter. - - The desired property (JSON) can be referenced - hierarchically, by applying JSON Pointer notation (RFC-6901) - - ### Example - To delete the value of the brewingTemp in the water-tank of our coffeemaker example the full path is: - - `/things/{thingId}/features/water-tank/desiredProperties/configuration/brewingTemp` - tags: - - Features - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/PropertyPathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/RequestedAcksParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/ConditionParam' - - $ref: '#/components/parameters/ChannelParam' - responses: - '204': - description: The desired property was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - For deleting the properties of an existing feature, `WRITE` permission is required. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The specified desired property or - the thing with the specified `thingId` or the feature with `featureId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '424': - $ref: '#/components/responses/DependencyFailed' - '/api/2/things/{thingId}/inbox/claim': - post: - summary: Initiates claiming a specific thing in order to gain access - description: |- - ### Why - A claiming process may enable an end-user to claim things and proof ownership thereof. - Such a process is initially triggered via a claim message. - This message can be sent to the things service with the HTTP API or the things-client. - - ### How - At this resource you can send a "claim" message to the thing identified - by the `thingId` path parameter in order to gain access to it. The "claim" message is forwarded - together with the request body and `Content-Type` header to client(s) - which registered for Claim messages of the specific thing. - - The decision whether to grant access (by setting permissions) is - completely up to the client(s) which handle the "claim" message. - - The HTTP request blocks until all acknowledgement requests are fulfilled. - By default, it blocks until a response to the issued "claim" message is - available or until the `timeout` is expired. If many clients respond to - the issued message, the first response will complete the HTTP request. - - Note that the client chooses which HTTP status code it wants to return. Ditto - will forward the status code to you. (Also note that '204 - No Content' status code - will never return a body, even if the client responded with a body). - - ### Who - No special permission is required to issue a claim message. - - ### Example - See [Claiming](https://www.eclipse.dev/ditto/protocol-specification-things-messages.html#claim-messages) concept in detail and example in GitHub. - However, in that scenario, the policy should grant you READ and WRITE permission on - the "message:/" resource in order to be able to send the message and read the response. - Further, the things-client which handles the "claim" message, needs permission to change the policy itself - (i.e. READ and WRITE permission on the "policy:/" resource). - tags: - - Messages - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/MessageClaimTimeoutParam' - - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' - responses: - '200': - description: |- - The Claim message was processed successfully and the response body - contains the custom response. The response body may contain - arbitrary data chosen by the recipient. The response code defaults - to `200` but may be chosen by the recipient too. - '204': - description: |- - The Claim message was processed successfully and no custom response - body was set. The response code defaults to `204` but may be chosen - by the recipient. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined path parameters is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '408': - description: The request could not be completed due to timeout. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '413': - $ref: '#/components/responses/MessageTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - '429': - description: |- - The user has sent too many requests in a given amount of time ("rate - limiting"). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - requestBody: - $ref: '#/components/requestBodies/Payload' - '/api/2/things/{thingId}/inbox/messages/{messageSubject}': - post: - summary: Send a message TO a specific thing - description: |- - ### Why - A message can be sent to a thing or one of its features in order to invoke an operation on the device. - - ### How - Send a message with a `messageSubject` **to** the thing - identified by the `thingId` path parameter. The request body contains - the message payload and the `Content-Type` header defines its type. - - The HTTP request blocks until all acknowledgement requests are fulfilled. - By default, it blocks until a response to the message is available - or until the `timeout` is expired. If many clients respond to - the issued message, the first response will complete the HTTP request. - - In order to handle the message in a fire and forget manner, add - a query-parameter `timeout=0` to the request. - - Note that the client chooses which HTTP status code it wants to return. Ditto - will forward the status code to you. (Also note that '204 - No Content' status code - will never return a body, even if the client responded with a body). - - ### Who - You will need `WRITE` permission on the root "message:/" resource, or at least - the resource `message:/inbox/messages/messageSubject`. The receiving device needs `READ` permission on the resource. - Such permission is managed within the policy which controls the access on the thing. - - ### Example - Given you have a "coffemaker" thing as shown in the examples for the `things` resources. - The `messageSubject` understood by such a device would be "makeCoffee". - - Further, as in our example the "brewed-coffees" counter would increase as a response, you would need `WRITE` - permission for the things resource, at least at the respective path - - `/things/{thingId}/features/coffee-brewer/properties/brewed-coffees` - tags: - - Messages - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/MessageSubjectPathParam' - - $ref: '#/components/parameters/MessageTimeoutParam' - - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' - - $ref: '#/components/parameters/ConditionParam' - responses: - '202': - description: The message was sent but not necessarily received by the thing (fire and forget). - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined path parameters is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the resource `message:/inbox/messages/{messageSubject}`. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller has insufficient permissions to interact with the messages of referenced thing. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '408': - description: The request could not be completed due to timeout. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/MessageTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Payload' - '/api/2/things/{thingId}/outbox/messages/{messageSubject}': - post: - summary: Send a message FROM a specific thing - description: |- - Send a message with the subject `messageSubject` **from** the thing - identified by the `thingId` path parameter. The request body contains - the message payload and the `Content-Type` header defines its type. - - The HTTP request blocks until all acknowledgement requests are fulfilled. - By default, it blocks until a response to the message is available - or until the `timeout` is expired. If many clients respond to - the issued message, the first response will complete the HTTP request. - - In order to handle the message in a fire and forget manner, add - a query-parameter `timeout=0` to the request. - - Note that the client chooses which HTTP status code it wants to return. Ditto - will forward the status code to you. (Also note that '204 - No Content' status code - will never return a body, even if the client responded with a body). - - ### Who - You will need `WRITE` permission on the root "message:/" resource, or at least - the resource `message:/outbox/messages/messageSubject`. - Such permission is managed within the policy which controls the access on the thing. - tags: - - Messages - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/MessageSubjectPathParam' - - $ref: '#/components/parameters/MessageTimeoutParam' - - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' - - $ref: '#/components/parameters/ConditionParam' - responses: - '202': - description: The message was sent (fire and forget). - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined path parameters is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the resource `message:/outbox/messages/{messageSubject}`. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller has insufficient permissions to interact with the messages of referenced thing. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '408': - description: The request could not be completed due to timeout. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/MessageTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Payload' - '/api/2/things/{thingId}/features/{featureId}/inbox/messages/{messageSubject}': - post: - summary: Send a message TO a specific feature of a specific thing - description: |- - Send a message with the subject `messageSubject` **to** the feature - specified by the `featureId` and `thingId` path parameter. The request - body contains the message payload and the `Content-Type` header defines - its type. - - The HTTP request blocks until all acknowledgement requests are fulfilled. - By default, it blocks until a response to the message is available - or until the `timeout` is expired. If many clients respond to - the issued message, the first response will complete the HTTP request. - - In order to handle the message in a fire and forget manner, add - a query-parameter `timeout=0` to the request. - - Note that the client chooses which HTTP status code it wants to return. Ditto - will forward the status code to you. (Also note that '204 - No Content' status code - will never return a body, even if the client responded with a body). - - ### Who - You will need `WRITE` permission on the root "message:/" resource, or at least - the resource `message:/features/featureId/inbox/messages/messageSubject`. The receiving device needs `READ` permission on the resource. - Such permission is managed within the policy which controls the access on the thing. - tags: - - Messages - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/MessageSubjectPathParam' - - $ref: '#/components/parameters/MessageTimeoutParam' - - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' - - $ref: '#/components/parameters/ConditionParam' - responses: - '202': - description: |- - The message was sent but not necessarily received by the feature - (fire and forget). - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined path parameters is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the resource `message:/features/{featureId}/inbox/messages/{messageSubject}`. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller has insufficient permissions to interact with the messages of referenced thing. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '408': - description: The request could not be completed due to timeout. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/MessageTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Payload' - '/api/2/things/{thingId}/features/{featureId}/outbox/messages/{messageSubject}': - post: - summary: Send a message FROM a specific feature of a specific thing - description: |- - Send a message with the subject `messageSubject` **from** the feature - specified by the `featureId` and `thingId` path parameter. The request - body contains the message payload and the `Content-Type` header defines - its type. - - The HTTP request blocks until all acknowledgement requests are fulfilled. - By default, it blocks until a response to the message is available - or until the `timeout` is expired. If many clients respond to - the issued message, the first response will complete the HTTP request. - - In order to handle the message in a fire and forget manner, add - a query-parameter `timeout=0` to the request. - - Note that the client chooses which HTTP status code it wants to return. Ditto - will forward the status code to you. (Also note that '204 - No Content' status code - will never return a body, even if the client responded with a body). - - ### Who - You will need `WRITE` permission on the root "message:/" resource, or at least - the resource `message:/features/featureId/outbox/messages/messageSubject`. - Such permission is managed within the policy which controls the access on the thing. - tags: - - Messages - parameters: - - $ref: '#/components/parameters/ThingIdPathParam' - - $ref: '#/components/parameters/FeatureIdPathPathParam' - - $ref: '#/components/parameters/MessageSubjectPathParam' - - $ref: '#/components/parameters/MessageTimeoutParam' - - $ref: '#/components/parameters/LiveMessageRequestedAcksParam' - - $ref: '#/components/parameters/ConditionParam' - responses: - '202': - description: The message was sent (fire and forget). - '400': - description: |- - The request could not be completed. Possible reasons: - * the `thingId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - * at least one of the defined path parameters is valid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the resource `message:/features/{featureId}/outbox/messages/{messageSubject}`. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller has insufficient permissions to interact with the messages of referenced thing. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '408': - description: The request could not be completed due to timeout. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/MessageTooLarge' - '424': - $ref: '#/components/responses/DependencyFailed' - requestBody: - $ref: '#/components/requestBodies/Payload' - '/api/2/policies/{policyId}': - get: - summary: Retrieve a specific policy - description: |- - Returns the complete policy identified by the `policyId` path parameter. The - response contains the policy as JSON object. - - Tip: If you don't know the policy ID of a thing, request it via GET `/things/{thingId}`. - - Optionally, you can use the field selectors (see parameter `fields`) to only get specific fields, - which you are interested in. - - ### Example: - Use the field selector `_revision` to retrieve the revision of the policy. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/PolicyFieldsQueryParam' - - $ref: '#/components/parameters/IfMatchHeaderParam' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: |- - The request successfully returned completed and returned is the - policy. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Policy' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or update a policy with a specified ID - description: |- - Create or update the policy specified by the policyId path parameter. - * If you set a new policyId in the path, a new policy will be created. - * If you set an existing policyId in the path, the policy will be updated. - - ### Create a new policy - At the initial creation of a policy, at least one valid entry is required. However, you can create a full-fledged policy all at once. - - By default the authorized subject needs WRITE permission on the root resource of the created policy. You can - however omit this check by setting the parameter `allow-policy-lockout` to `true`. - - Example: To create a policy for multiple coffee maker things, - which gives **yourself** all permissions on all resources, set the policyId in the path, - e.g. to "com.acme.coffeemaker:policy-01" and the body part, like in the following snippet. - - ``` - { - "entries": { - "DEFAULT": { - "subjects": { - "{{ request:subjectId }}": { - "type": "the creator" - } - }, - "resources": { - "policy:/": { - "grant": [ - "READ", - "WRITE" - ], - "revoke": [] - }, - "thing:/": { - "grant": [ - "READ", - "WRITE" - ], - "revoke": [] - }, - "message:/": { - "grant": [ - "READ", - "WRITE" - ], - "revoke": [] - } - } - } - }, - "imports": { - "com.acme:importedPolicy" : { - "entries": [ "IMPORTED" ] - } - } - } - ``` - - ### Update an existing policy - For updating an existing policy, the authorized subject needs WRITE permission on the policy's root resource. - - The ID of a policy cannot be changed after creation. Any `policyId` specified in the request body is therefore ignored. - - ### Partially update an existing policy - Partial updates are not supported. - - If you need to create or update a specific label, resource, or subject, please use the respective sub-resources. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParam' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - - $ref: '#/components/parameters/AllowPolicyLockoutParam' - responses: - '201': - description: The policy was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created policy resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/NewPolicy' - '204': - description: The policy was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the policy to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the root `policy:/` resource, - without any revoke in a deeper path of the policy resource. - (You can omit this check by setting the `allow-policy-lockout` parameter.) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or policy referenced in a policy import was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/NewPolicy' - example: - entries: - DEFAULT: - subjects: - '{{ request:subjectId }}': - type: the creator - resources: - 'policy:/': - grant: - - READ - - WRITE - revoke: [] - 'thing:/': - grant: - - READ - - WRITE - revoke: [] - 'message:/': - grant: - - READ - - WRITE - revoke: [] - description: |- - JSON representation of the policy. - Use the placeholder `{{ request:subjectId }}` in order to let the - backend insert the authenticated subjectId of the HTTP request. - required: true - delete: - summary: Delete a specific policy - description: |- - Deletes the policy identified by the `policyId` path parameter. Deleting - a policy does not implicitly delete other entities (e.g. things) which - use this policy. - - Note: Delete the respective things **before** deleting the - policy, otherwise nobody has permission to read, update, or delete the things. - If you accidentally run into such a scenario, re-create the policy via - PUT `/policies/{policyId}`. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParam' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The policy was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the root `policy:/` resource, - without any revoke in a deeper path of the policy resource.having any revoke. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or policy referenced in a policy import was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '/api/2/policies/{policyId}/actions/activateTokenIntegration': - post: - summary: Activate subjects for this policy derived from the token - description: |- - **This action only works when authenticated with a Json Web Token (JWT).** - - Based on the authenticated token (JWT), **for each policy entry** matching those conditions: - * the authenticated token is granted the `EXECUTE` permission to perform the `activateTokenIntegration` action - * one of the subject IDs is contained in the authenticated token - * at least one `READ` permission to a `thing:/` resource path is granted - - a new subject is **injected into the matched policy entry** calculated with information extracted from the - authenticated JWT. - - The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) - specifies how long the specific subject will have access to the resource secured by the policy. - The subject will be automatically deleted from the policy once this timestamp is reached. - To give the subject a chance to prolong the access he can configure a connection to get announcements. - Policy announcements are published to websockets and connections that have the relevant subject ID. - - The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). - If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until - the acknowledgement requests under labels are fulfilled. - If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" - announcement was acknowledged, the "whenDeleted" announcement will not be triggered. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - responses: - '204': - description: The request was successful. Subjects were injected into authorized policy entries. - '400': - description: The request could not be completed because the authentication was not performed with a JWT. - '403': - description: |- - The request could not be completed because the authenticated JWT did not have the `EXECUTE` permission on any - entries of the policy. - '404': - description: |- - The request could not be completed because no policy entry matched the following conditions: - * containing a a subject ID matching the JWT's authenticated subject - * containing a `READ` permission granted to a `thing:/` resource path - requestBody: - $ref: '#/components/requestBodies/ActivateTokenIntegration' - '/api/2/policies/{policyId}/actions/deactivateTokenIntegration': - post: - summary: Deactivate subjects for this policy derived from the token - description: |- - **This action only works when authenticated with a Json Web Token (JWT).** - - Based on the authenticated token (JWT), **for each policy entry** matching those conditions: - * the authenticated token is granted the `EXECUTE` permission to perform the `deactivateTokenIntegration` action - * one of the subject IDs is contained in the authenticated token - - the calculated subject with information extracted from the authenticated JWT is **removed - from the matched policy entry**. - - The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) - specifies how long the specific subject will have access to the resource secured by the policy. - The subject will be automatically deleted from the policy once this timestamp is reached. - To give the subject a chance to prolong the access he can configure a connection to get announcements. - Policy announcements are published to websockets and connections that have the relevant subject ID. - - The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). - If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until - the acknowledgement requests under labels are fulfilled. - If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" - announcement was acknowledged, the "whenDeleted" announcement will not be triggered. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - responses: - '204': - description: The request was successful. Subjects were removed from authorized policy entries. - '400': - description: The request could not be completed because the authentication was not performed with a JWT. - '403': - description: |- - The request could not be completed because the authenticated JWT did not have the `EXECUTE` permission on any - entries of the policy. - '404': - description: |- - The request could not be completed because no policy entry matched the following conditions: - * containing a a subject ID matching the JWT's authenticated subject - '/api/2/policies/{policyId}/entries': - get: - summary: Retrieve the entries of a specific policy - description: |- - Returns all policy entries of the policy identified by the `policyId` - path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: |- - The request successfully returned completed and returned are the - policy entries. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyEntries' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the entries of a specific policy - description: |- - Modify the policy entries of the policy identified by the `policyId` - path parameter. - - Note: Take care to not lock yourself out. Use the placeholder {{ request:subjectId }} - in order to let the backend insert the authenticated subjectId of the HTTP request. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The policy entries were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the policy to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyEntries' - example: - DEFAULT: - subjects: - '{{ request:subjectId }}': - type: the creator - resources: - 'policy:/': - grant: - - READ - - WRITE - revoke: [] - 'thing:/': - grant: - - READ - - WRITE - revoke: [] - 'message:/': - grant: - - READ - - WRITE - revoke: [] - description: |- - JSON representation of the policy entries. - Use the placeholder `{{ request:subjectId }}` in order to let the - backend insert the authenticated subjectId of the HTTP request. - required: true - '/api/2/policies/{policyId}/entries/{label}': - get: - summary: Retrieve the entries of a specific Label of a specific policy - description: |- - Returns all entries (subjects, resources, etc.) of the policy identified by the `policyId` path - parameter, and by the `label` path parameter. - Example label: DEFAULT. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: |- - The request successfully returned completed and returned is the - policy entry. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyEntry' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify the entries of a specific Label of a specific policy - description: |- - Create or modify the policy entry identified by the - `policyId` path parameter and with the label identified by the `label` - path parameter. - * If you specify a new label, the respective policy entry will be created - * If you specify an existing label, the respective policy entry will be updated - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '201': - description: The policy entry was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created policy entry - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyEntry' - '204': - description: The policy entry was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the policy entry to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyEntry' - example: - subjects: - '{{ request:subjectId }}': - type: the creator - resources: - 'policy:/': - grant: - - READ - - WRITE - revoke: [] - 'thing:/': - grant: - - READ - - WRITE - revoke: [] - 'message:/': - grant: - - READ - - WRITE - revoke: [] - description: |- - JSON representation of the policy entry. - Use the placeholder `{{ request:subjectId }}` in order to let the - backend insert the authenticated subjectId of the HTTP request. - ### Example - Given your policy "com.acme.coffeemaker:policy-01" only has the - DEFAULT entry, and you want to add a "Consumer" section which additionally allows USER-01 - (managed within a Nginx reverse proxy) to - *read* the thing and to trigger a "makeCoffee" operation (i.e. POST such a message - see - POST /things/{thingId}/inbox/messages/{messageSubject}). - Set the label value to **Consumer** and the following request body: - ``` - { - "subjects": { - "nginx:USER-01": { - "type": "pre authenticated user from nginx" - } - }, - "resources": { - "thing:/": { - "grant": [ - "READ" - ], - "revoke": [] - }, - "message:/": { - "grant": [ - "WRITE" - ], - "revoke": [] - } - } - } - ``` - required: true - delete: - summary: Delete the entries of a specific Label of a specific policy - description: |- - Deletes the entry of the policy identified by the `policyId` path - parameter and with the label identified by the `label` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The policy entry was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '/api/2/policies/{policyId}/entries/{label}/actions/activateTokenIntegration': - post: - summary: Activate a subject for this policy entry derived from the token - description: |- - **This action only works when authenticated with a Json Web Token (JWT).** - - Based on the authenticated token (JWT), **this policy entry** is checked to match those conditions: - * the authenticated token is granted the `EXECUTE` permission to perform the `activateTokenIntegration` action - * one of the subject IDs is contained in the authenticated token - * at least one `READ` permission to a `thing:/` resource path is granted - - When all conditions match, a new subject is **injected into this policy entry** calculated with information - extracted from the authenticated JWT. - - The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) - specifies how long the specific subject will have access to the resource secured by the policy. - The subject will be automatically deleted from the policy once this timestamp is reached. - To give the subject a chance to prolong the access he can configure a connection to get announcements. - Policy announcements are published to websockets and connections that have the relevant subject ID. - - The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). - If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until - the acknowledgement requests under labels are fulfilled. - If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" - announcement was acknowledged, the "whenDeleted" announcement will not be triggered. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - responses: - '204': - description: The request was successful. The subject was injected. - '400': - description: The request could not be completed because the authentication was not performed with a JWT. - '403': - description: |- - The request could not be completed because the authenticated JWT did not have the `EXECUTE` permission on this - policy entry. - '404': - description: |- - The request could not be completed because this policy entry did not match the following conditions: - * containing a a subject ID matching the JWT's authenticated subject - * containing a `READ` permission granted to a `thing:/` resource path - requestBody: - $ref: '#/components/requestBodies/ActivateTokenIntegration' - '/api/2/policies/{policyId}/entries/{label}/actions/deactivateTokenIntegration': - post: - summary: Deactivate a subject for this policy entry derived from the token - description: |- - **This action only works when authenticated with a Json Web Token (JWT).** - - Based on the authenticated token (JWT), **this policy entry** is checked to match those conditions: - * the authenticated token is granted the `EXECUTE` permission to perform the `deactivateTokenIntegration` action - * one of the subject IDs is contained in the authenticated token - - When all conditions match, the calculated subject with information extracted from the authenticated JWT is **removed - from this policy entry**. - - The injected subjects expire when the JWT expires. The `expiry` timestamp (a string in ISO-8601 format) - specifies how long the specific subject will have access to the resource secured by the policy. - The subject will be automatically deleted from the policy once this timestamp is reached. - To give the subject a chance to prolong the access he can configure a connection to get announcements. - Policy announcements are published to websockets and connections that have the relevant subject ID. - - The settings under `announcement` control when a policy announcement is published (before expiry or when deleted). - If the field `requestedAcks` is set, then the announcements are published with at-least-once delivery until - the acknowledgement requests under labels are fulfilled. - If a "beforeExpiry" announcement was sent without acknowledgement requests, or the a "beforeExpiry" - announcement was acknowledged, the "whenDeleted" announcement will not be triggered. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - responses: - '204': - description: The request was successful. The subject was removed. - '400': - description: The request could not be completed because the authentication was not performed with a JWT. - '403': - description: The request could not be completed because the user did not have the `EXECUTE` permission on this policy entry. - '404': - description: |- - The request could not be completed because this policy entry did not match the following conditions: - * containing a a subject ID matching the JWT's authenticated subject - '/api/2/policies/{policyId}/entries/{label}/subjects': - get: - summary: Retrieve all Subjects for a specific Label of a specific policy - description: |- - Returns all subject entries of the policy identified by the - `policyId` path parameter, and by the `label` - path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The subjects are returned. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Subjects' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify all Subjects for a specific Label of a specific policy - description: |- - Create or modify at once ALL subjects of the policy entry identified - by the `policyId` path parameter, and by the `label` path parameter. - - ### Example - delete all subjects - To delete all subjects set an empty body { } - - ### Example - entities authenticated by nginx - To add a user authenticated via pre-authentication at nginx: - - ``` - { - "nginx:ID-user": { - "type": "pre authenticated user from nginx" - } - } - ``` - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The Subjects were successfully created or updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the policy subjects to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/subjects` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Subjects' - description: |- - JSON representation of the Subjects. - - - Use the placeholder `{{ request:subjectId }}` in order to let the - backend insert the authenticated subjectId of the HTTP request. - required: true - '/api/2/policies/{policyId}/entries/{label}/subjects/{subjectId}': - get: - summary: Retrieve one specific Subject for a specific Label of a specific policy - description: |- - Returns the subject with ID `subjectId` of the policy entry identified - by the `policyId` path parameter, and by the `label` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/SubjectIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: |- - The request successfully returned completed and returned is the - Subject. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/SubjectEntry' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID, - the policy entry or the Subject was not found in the context of the - authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify one specific Subject for a specific Label of a specific policy - description: |- - Create or modify the subject with ID `subjectId` of the policy identified - by the `policyId` path parameter, and by the `label` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/SubjectIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '201': - description: The Subject was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created Subject - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/SubjectEntry' - '204': - description: The Subject was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id))) - * the JSON body of the policy subject to be created/modified is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the root `policy:/entries/{label}/subjects/{subjectId}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/SubjectEntry' - description: JSON representation of the Subject - required: true - delete: - summary: Delete one specific Subject for a specific Label of a specific policy - description: |- - Deletes the subject with ID `subjectId` from the policy identified - by the `policyId` path parameter and - by the `label` path parameter. - - Note: If the subject is used in other labels, it will not be deleted there, - i.e. it will not lose those permissions, but only the permissions defined in the - label specified at this path. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/SubjectIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The Subject was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the root `policy:/entries/{label}/subjects/{subjectId}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID, - the policy entry or the Subject was not found in the context of the - authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '/api/2/policies/{policyId}/entries/{label}/resources': - get: - summary: Retrieve all Resources for a specific Label of a specific policy - description: |- - Returns all resource entries of the policy identified by - the `policyId` path parameter, - and by the `label` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The resources are returned. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Resources' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify all Resources for a specific Label of a specific policy - description: |- - Create or modify all resources of the policy - identified by the `policyId` path parameter, - and by the `label` path parameter. - - ### Delete all resource entries - - Set the empty body part, if you need to delete all resource entries: { } - - ### Set max permissions on all ressources - ``` - { - "policy:/": { - "grant": [ - "READ", - "WRITE" - ], - "revoke": [] - }, - "thing:/": { - "grant": [ - "READ", - "WRITE" - ], - "revoke": [] - }, - "message:/": { - "grant": [ - "READ", - "WRITE" - ], - "revoke": [] - } - } - ``` - ### Allow to read all parts of a thing except the "confidential" feature - ``` - { - "thing:/": { - "grant": [ - "READ" - ], - "revoke": [] - }, - "things:/features/confidential": { - "grant": [], - "revoke": [ - "READ" - ] - } - } - ``` - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The Resources were successfully created or updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON is invalid, or no valid Resources JSON object. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the root `policy:/entries/{label}/resources` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Resources' - description: JSON representation of the Resources - required: true - '/api/2/policies/{policyId}/entries/{label}/resources/{resourcePath}': - get: - summary: Retrieve one specific Resource for a specific Label of a specific policy - description: |- - Returns the resource with path `resourcePath` of the policy identified - by the `policyId` path parameter, and - by the `label` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/ResourcePathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: |- - The request successfully returned completed and returned is the - Resource. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/ResourceEntry' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID, - the policy entry or the Resource was not found in the context of the - authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify one specific Resource for a specific Label of a specific policy - description: |- - Create or modify the Resource with path `resourcePath` of the policy - entry identified by the `label` path parameter belonging to the policy - identified by the `policyId` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/ResourcePathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '201': - description: The Resource was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created Resource - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/ResourceEntry' - '204': - description: The Resource was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON is invalid, or no valid Resource JSON object. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/resources/{resourcePath}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ResourceEntry' - description: JSON representation of the Resource - required: true - delete: - summary: Delete one specific Resource for a specific Label of a specific policy - description: |- - Deletes the resource with path `resourcePath` from the policy - identified by the the `policyId` path parameter, and by the - `label` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/ResourcePathPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The Resource was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/resources/{resourcePath}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID, - the policy entry or the Resource was not found in the context of the - authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '/api/2/policies/{policyId}/entries/{label}/allowedAdditions': - get: - summary: Retrieve the allowed import additions for a specific policy entry - description: |- - Returns the allowed import additions of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - Allowed import additions control which types of additions (subjects, resources) are permitted - when this entry is referenced by other entries via `references`. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The allowed import additions are returned. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/AllowedAdditions' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the allowed import additions for a specific policy entry - description: |- - Modify the allowed import additions of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - Allowed import additions control which types of additions (subjects, resources) are permitted - when this entry is referenced by other entries via `references`. Setting an empty array - disables all additions for this entry. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The allowed import additions were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the allowed import additions is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/allowedAdditions` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/AllowedAdditions' - example: - - subjects - - resources - description: JSON array of allowed import addition types. - required: true - '/api/2/policies/{policyId}/entries/{label}/namespaces': - get: - summary: Retrieve the namespace patterns for a specific policy entry - description: |- - Returns the namespace patterns of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - Namespace patterns restrict which thing namespaces this entry applies to. - An empty list (or absent field) means the entry applies to all namespaces. - * `com.acme` matches only that exact namespace - * `com.acme.*` matches namespaces below `com.acme`, but not `com.acme` itself - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The namespace patterns are returned. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyEntry/properties/namespaces' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the namespace patterns for a specific policy entry - description: |- - Modify the namespace patterns of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - Namespace patterns restrict which thing namespaces this entry applies to. - Setting an empty array makes the entry apply to all namespaces (backward compatible default). - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The namespace patterns were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the namespace patterns is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/namespaces` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyEntry/properties/namespaces' - example: - - com.acme - - com.acme.* - description: JSON array of namespace patterns. - required: true - '/api/2/policies/{policyId}/entries/{label}/importable': - get: - summary: Retrieve the importable type for a specific policy entry - description: |- - Returns the importable type of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - The importable type controls whether and how a policy entry can be imported by other policies: - * `implicit` (default): the entry is imported without being listed individually - * `explicit`: the entry is only imported if it is listed in the importing policy - * `never`: the entry is not imported, regardless of being listed - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The importable type is returned. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Importable' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the importable type for a specific policy entry - description: |- - Modify the importable type of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - The importable type controls whether and how a policy entry can be imported by other policies: - * `implicit` (default): the entry is imported without being listed individually - * `explicit`: the entry is only imported if it is listed in the importing policy - * `never`: the entry is not imported, regardless of being listed - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The importable type was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the importable type is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/importable` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Importable' - example: explicit - description: 'The importable type value. Must be one of: "implicit", "explicit", "never".' - required: true - '/api/2/policies/{policyId}/imports': - get: - summary: Retrieve the imports of a specific policy - description: |- - Returns all policy imports of the policy identified by the `policyId` - path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned completed and returned are the policy imports. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyImports' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '402': - description: The request could not be completed due to exceeded data volume or exceeded transaction count. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to a missing or invalid API Token. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the imports of a specific policy - description: Modify the policy imports of the policy identified by the `policyId` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The policy imports were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the policy imports to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '402': - description: The request could not be completed due to exceeded data volume or exceeded transaction count. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/imports` resource, - without any revoke in a deeper path of the policy resource. - * the caller has insufficient permissions. - You need `READ` permission on the policy entries of the imported policies. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy (or an imported policy) with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyImports' - example: - 'com.acme:imported1': - entries: - - IMPORTED_ENTRY - 'com.acme:imported2': {} - description: JSON representation of the policy imports. - required: true - delete: - summary: Delete all imports of a specific policy - description: |- - Removes all imports from the policy identified by the `policyId` path parameter. - If any entry references point to an import, the deletion is rejected. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The policy imports were successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/imports` resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was not found in the - context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '409': - description: |- - The request could not be completed. An entry reference still points to one of the imports. - Remove the entry references first before deleting imports. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '/api/2/policies/{policyId}/imports/{importedPolicyId}': - get: - summary: Retrieve a specific policy import. - description: |- - Returns the policy import of the policy identified by the `policyId` path - parameter and imported policy identified by the `importedPolicyId` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/ImportedPolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned completed and returned is the policy import. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyImport' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '402': - description: The request could not be completed due to exceeded data volume or exceeded transaction count. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to a missing or invalid API Token. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy import was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Create or modify a specific policy import of a policy. - description: |- - Create or modify the policy import of a specific policy identified by the `policyId` path parameter - and the imported policy identified by the `importedPolicyId` path parameter. - - * If you specify a new policy import, the respective policy import will be created - * If you specify an existing policy import, the respective policy import will be updated - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/ImportedPolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '201': - description: The policy import was successfully created. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created policy import - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyImport' - '204': - description: The policy import was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the policy import to be created/modified is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '402': - description: The request could not be completed due to exceeded data volume or exceeded transaction count. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/imports/{importedPolicyId}` resource, - without any revoke in a deeper path of the policy resource. - * the caller has insufficient permissions. - You need `READ` permission on the `policy:/entries/{label}` resource of the *imported* policy, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/PolicyImport' - example: - entries: - - IMPORTED - description: JSON representation of the policy import. - required: true - delete: - summary: Delete a specific policy import. - description: |- - Deletes a specific policy import identified by the `policyId` path parameter - and the `importedPolicyId` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/ImportedPolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The policy import was successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '402': - description: The request could not be completed due to exceeded data volume or exceeded transaction count. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/imported/{importedPolicyId}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID was - not found in the context of the authenticated user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '/api/2/policies/{policyId}/imports/{importedPolicyId}/entries': - get: - summary: Retrieve the entries of a specific policy import - description: |- - Returns the entries (imported labels) of the policy import identified by the `policyId` path - parameter and the `importedPolicyId` path parameter. - - The entries define which policy entries from the imported policy should be imported, - identified by their labels. An empty array means all implicit entries are imported. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/ImportedPolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The entries are returned as a JSON array of label strings. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - type: array - items: - type: string - description: Label of a policy entry to import from the referenced policy. - example: - - default - - import - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy import was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the entries of a specific policy import - description: |- - Modify the entries (imported labels) of the policy import identified by the `policyId` path - parameter and the `importedPolicyId` path parameter. - - The entries define which policy entries from the imported policy should be imported, - identified by their labels. Provide an empty array to import all implicit entries. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/ImportedPolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The entries were successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the entries is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/imports/{importedPolicyId}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy import was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - type: array - items: - type: string - description: Label of a policy entry to import from the referenced policy. - example: - - default - - import - description: JSON array of policy entry labels to import. - required: true - '/api/2/policies/{policyId}/imports/{importedPolicyId}/transitiveImports': - get: - summary: Retrieve the transitive resolution policy IDs of a specific policy import - description: |- - Returns the "transitiveImports" array of the policy import identified by the `policyId` path - parameter and the `importedPolicyId` path parameter. - - The array lists policy IDs from the imported policy's own imports that should be resolved - transitively before extracting entries. This enables multi-level import chains. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/ImportedPolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The transitiveImports array is returned. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/TransitiveImports' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy import was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the transitive resolution policy IDs of a specific policy import - description: |- - Modify the "transitiveImports" array of the policy import identified by the `policyId` path - parameter and the `importedPolicyId` path parameter. - - The array lists policy IDs from the imported policy's own imports that should be resolved - transitively before extracting entries. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/ImportedPolicyIdPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The transitiveImports array was successfully updated. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` or the `importedPolicyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body is not a valid JSON array of policy ID strings - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/imports/{importedPolicyId}` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy import was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TransitiveImports' - example: - - 'org.eclipse.ditto:policy-template' - description: JSON array of policy IDs to resolve transitively. - required: true - '/api/2/policies/{policyId}/entries/{label}/references': - get: - summary: Retrieve the references of a specific policy entry - description: |- - Returns the references of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - References define links to other policy entries, optionally from imported policies. - Each reference object contains a required `entry` field (the label of the referenced entry) - and an optional `import` field (the policy ID of the import to reference from). - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - responses: - '200': - description: The request successfully returned. The references are returned. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/References' - '304': - $ref: '#/components/responses/NotModified' - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - put: - summary: Modify the references of a specific policy entry - description: |- - Sets the references of the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - - References define links to other policy entries, optionally from imported policies. - Each reference object contains a required `entry` field (the label of the referenced entry) - and an optional `import` field (the policy ID of the import to reference from). - Setting an empty array removes all references from this entry. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/IfEqualHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '201': - description: The references were successfully created (the entry had no references before). - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - Location: - description: The location of the created references resource. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/References' - '204': - description: The references were successfully updated (the entry already had references). - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - * the JSON body of the references is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/references` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - '413': - $ref: '#/components/responses/EntityTooLarge' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/References' - example: - - import: 'acme:fleet-roles' - entry: driver - - entry: shared-subjects - description: |- - JSON array of reference objects. Each object must contain a required `entry` field - (the label of the referenced policy entry) and may contain an optional `import` field - (the policy ID of the import to reference from). - required: true - delete: - summary: Remove all references from a specific policy entry - description: |- - Removes all references from the policy entry identified by the - `policyId` path parameter and the `label` path parameter. - tags: - - Policies - parameters: - - $ref: '#/components/parameters/PolicyIdPathParam' - - $ref: '#/components/parameters/LabelPathParam' - - $ref: '#/components/parameters/IfMatchHeaderParamHash' - - $ref: '#/components/parameters/IfNoneMatchHeaderParam' - - $ref: '#/components/parameters/TimeoutParam' - - $ref: '#/components/parameters/ResponseRequiredParam' - responses: - '204': - description: The references were successfully deleted. - '400': - description: |- - The request could not be completed. Possible reasons: - - * the `policyId` does not conform to the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)) - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - - * the caller has insufficient permissions. - You need `WRITE` permission on the `policy:/entries/{label}/references` resource, - without any revoke in a deeper path of the policy resource. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. The policy with the given ID or - the policy entry was not found in the context of the authenticated - user. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '412': - $ref: '#/components/responses/PreconditionFailed' - /api/2/whoami: - get: - summary: Retrieve information about the current caller - description: 'Get information about the current caller, e.g. the auth subjects that are generated for the caller.' - tags: - - Policies - responses: - '200': - description: The request successfully returned information about the caller. - content: - application/json: - schema: - $ref: '#/components/schemas/WhoAmI' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - /api/2/checkPermissions: - post: - summary: Check permissions for specified entities - description: This endpoint allows you to verify permissions for various entities on specific resources. - tags: - - Policies - requestBody: - $ref: '#/components/requestBodies/PermissionCheckRequest' - responses: - '200': - $ref: '#/components/responses/PermissionCheckResponse' - '401': - description: Unauthorized request due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - /api/2/search/things: - get: - summary: Search for things - description: |- - This resource can be used to search for things. - - * The query parameter `filter` is not mandatory. If it is not set, the - result contains all things which the logged in user is allowed to read. - - * The search is case sensitive. In case you don't know how exactly the - spelling of value of the namespace, name, attribute, feature etc. is, use the *like* - notation instead of *eq* for filtering. - - * The resource supports sorting and paging. If paging is not explicitly - specified by means of the `size` option, a default count of `25` - documents is returned. - - * The internal search index is "eventually consistent". Consistency with the latest - thing updates should recover within milliseconds. - parameters: - - $ref: '#/components/parameters/SearchFilter' - - $ref: '#/components/parameters/NamespacesFilter' - - $ref: '#/components/parameters/ThingFieldsQueryParam' - - $ref: '#/components/parameters/TimeoutParam' - - name: option - in: query - description: |- - Possible values for the parameter: - - #### Sort operations - - * ```sort([+|-]{property})``` - * ```sort([+|-]{property},[+|-]{property},...)``` - - #### Paging operations - - * ```size({page-size})``` Maximum allowed page size is `200`. Default page size is `25`. - * ```cursor({cursor-id})``` Start the search from the cursor location. Specify the cursor ID without - quotation marks. Cursor IDs are given in search responses and mark the position after the last entry of - the previous search. The meaning of cursor IDs is unspecified and may change without notice. - - The paging option `limit({offset},{count})` is deprecated. - It may result in slow queries or timeouts and will be removed eventually. - - #### Examples: - - * ```sort(+thingId)``` - * ```sort(-attributes/manufacturer)``` - * ```sort(+thingId,-attributes/manufacturer)``` - * ```size(10)``` return 10 results - * ```cursor(LOREMIPSUM)``` return results after the position of the cursor `LOREMIPSUM`. - - #### Combine: - - If you need to specify multiple options, when using the swagger UI just write each option in a new line. - When using the plain REST API programmatically, - you will need to separate the options using a comma (,) character. - - ```size(200),cursor(LOREMIPSUM)``` - - The deprecated paging option `limit` may not be combined with the other paging options `size` and `cursor`. - required: false - schema: - type: string - tags: - - Things-Search - responses: - '200': - description: An array of the matching things. - content: - application/json: - schema: - $ref: '#/components/schemas/SearchResultThings' - '400': - description: |- - The request could not be completed. A provided parameter is in a - wrong format. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to an invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '504': - description: The request ran out of time to execute on the the back-end. Optimize your query and try again. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - post: - summary: Search for things - description: |- - This resource can be used to search for things. - - * The parameter `filter` is not mandatory. If it is not set, the - result contains all things which the logged in user is allowed to read. - - * The search is case sensitive. In case you don't know how exactly the - spelling of value of the namespace, name, attribute, feature etc. is, use the *like* - notation instead of *eq* for filtering. - - * The resource supports sorting and paging. If paging is not explicitly - specified by means of the `size` option, a default count of `25` - documents is returned. - - * The internal search index is "eventually consistent". Consistency with the latest - thing updates should recover within milliseconds. - tags: - - Things-Search - responses: - '200': - description: An array of the matching things. - content: - application/json: - schema: - $ref: '#/components/schemas/SearchResultThings' - '400': - description: |- - The request could not be completed. A provided parameter is in a - wrong format. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to an invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '504': - description: The request ran out of time to execute on the the back-end. Optimize your query and try again. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - filter: - $ref: '#/components/schemas/SearchFilterProperty' - namespaces: - $ref: '#/components/schemas/NamespaceProperty' - fields: - description: |- - Contains a comma-separated list of fields to be included in the returned - JSON. attributes can be selected in the same manner. - - #### Selectable fields - - * `thingId` - * `policyId` - * `definition` - * `attributes` - - Supports selecting arbitrary sub-fields by using a comma-separated list: - * several attribute paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - - For example: - * `?fields=attributes/model` would select only `model` attribute value (if present) - * `?fields=attributes/model,attributes/location` would select only `model` and - `location` attribute values (if present) - - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) - separated with `/`) to select - - * sub-selectors can be used to request only specific sub-fields by placing expressions - in parentheses `( )` after a selected subfield - - For example: - * `?fields=attributes(model,location)` would select only `model` - and `location` attribute values (if present) - * `?fields=attributes(coffeemaker/serialno)` would select the `serialno` value - inside the `coffeemaker` object - * `?fields=attributes/address/postal(city,street)` would select the `city` and - `street` values inside the `postal` object inside the `address` object - - * `features` - - Supports selecting arbitrary fields in features similar to `attributes` (see also features documentation for more details) - - * `_namespace` - - Specifically selects the namespace also contained in the `thingId` - - * `_revision` - - Specifically selects the revision of the thing. The revision is a counter, which is incremented on each modification of a thing. - - * `_created` - - Specifically selects the created timestamp of the thing in ISO-8601 UTC format. The timestamp is set on creation of a thing. - - * `_modified` - - Specifically selects the modified timestamp of the thing in ISO-8601 UTC format. The timestamp is set on each modification of a thing. - - * `_metadata` - - Specifically selects the Metadata of the thing. The content is a JSON object having the Thing's JSON structure with the difference that the JSON leaves of the Thing are JSON objects containing the metadata. - - * `_policy` - - Specifically selects the content of the policy associated to the thing. (By default, only the policyId is returned.) - - #### Examples - - * `?fields=thingId,attributes,features` - * `?fields=attributes(model,manufacturer),features` - type: string - option: - description: |- - Possible values for the parameter: - - #### Sort operations - - * ```sort([+|-]{property})``` - * ```sort([+|-]{property},[+|-]{property},...)``` - - #### Paging operations - - * ```size({page-size})``` Maximum allowed page size is `200`. Default page size is `25`. - * ```cursor({cursor-id})``` Start the search from the cursor location. Specify the cursor ID without - quotation marks. Cursor IDs are given in search responses and mark the position after the last entry of - the previous search. The meaning of cursor IDs is unspecified and may change without notice. - - The paging option `limit({offset},{count})` is deprecated. - It may result in slow queries or timeouts and will be removed eventually. - - #### Examples: - - * ```sort(+thingId)``` - * ```sort(-attributes/manufacturer)``` - * ```sort(+thingId,-attributes/manufacturer)``` - * ```size(10)``` return 10 results - * ```cursor(LOREMIPSUM)``` return results after the position of the cursor `LOREMIPSUM`. - - #### Combine: - - If you need to specify multiple options, when using the swagger UI just write each option in a new line. - When using the plain REST API programmatically, - you will need to separate the options using a comma (,) character. - - ```size(200),cursor(LOREMIPSUM)``` - - The deprecated paging option `limit` may not be combined with the other paging options `size` and `cursor`. - type: string - condition: - description: |- - Similar to the `filter`, a `condition` may be passed to ensure strong consistency when querying things. - - This `condition` has the same syntax and semantics than the `filter` - it is however applied on the matched things - selected by the `filter` - on their current state. - - So combining this together with `filter` can provide strong consistency when performing a search. - type: string - encoding: - filter: - style: form - explode: false - namespaces: - style: form - explode: false - fields: - style: form - explode: false - option: - style: form - explode: false - example: - filter: 'and(like(definition,"*test*"))' - namespaces: 'org.eclipse.ditto,foo.bar' - fields: 'attributes/model,attributes/location' - option: 'limit(0,5)' - /api/2/search/things/count: - get: - summary: Count things - description: |- - This resource can be used to count things. - - The query parameter `filter` is not mandatory. If it is not set there is - returned the total amount of things which the logged in user is allowed - to read. - - To search for nested properties, we use JSON Pointer notation - (RFC-6901). See the following example how to search for the sub property - `location` of the parent property `attributes` with a forward slash as - separator: - - ```eq(attributes/location,"kitchen")``` - parameters: - - $ref: '#/components/parameters/SearchFilter' - - $ref: '#/components/parameters/NamespacesFilter' - - $ref: '#/components/parameters/TimeoutParam' - tags: - - Things-Search - responses: - '200': - description: A number indicating the amount of matched things - content: - application/json: - schema: - type: integer - '400': - description: |- - The request could not be completed. A provided parameter is in a - wrong format. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to an invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '504': - description: The request ran out of time to execute on the the back-end. Optimize your query and try again. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - post: - summary: Count things - description: |- - This resource can be used to count things. - - The parameter `filter` is not mandatory. If it is not set there is - returned the total amount of things which the logged in user is allowed - to read. - - To search for nested properties, we use JSON Pointer notation - (RFC-6901). See the following example how to search for the sub property - `location` of the parent property `attributes` with a forward slash as - separator: - - ```eq(attributes/location,"kitchen")``` - tags: - - Things-Search - responses: - '200': - description: A number indicating the amount of matched things - content: - application/json: - schema: - type: integer - '400': - description: |- - The request could not be completed. A provided parameter is in a - wrong format. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to an invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '504': - description: The request ran out of time to execute on the the back-end. Optimize your query and try again. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - requestBody: - content: - application/x-www-form-urlencoded: - schema: - type: object - properties: - filter: - $ref: '#/components/schemas/SearchFilterProperty' - namespaces: - $ref: '#/components/schemas/NamespaceProperty' - encoding: - filter: - style: form - explode: false - namespaces: - style: form - explode: false - example: - filter: 'and(like(definition,"*test*"))' - namespaces: 'org.eclipse.ditto,foo.bar' - /api/2/cloudevents: - post: - summary: Processes a CloudEvent sent in Ditto Protocol - description: |- - Provides an endpoint accepting [CloudEvents via its HTTP protocol binding](https://github.com/cloudevents/spec/blob/v1.0/http-protocol-binding.md) - in [Ditto Protocol JSON](https://www.eclipse.dev/ditto/protocol-specification.html). - - The endpoint can also directly be configured as a [Knative eventing](https://knative.dev/docs/eventing/) destination. - - Find more documentation on that [here](https://www.eclipse.dev/ditto/httpapi-protocol-bindings-cloudevents.html). - tags: - - CloudEvents - parameters: - - in: header - name: ce-specversion - description: The CloudEvents "specversion". - schema: - type: string - example: '1.0' - required: true - - in: header - name: ce-type - description: The CloudEvents event "type". - schema: - type: string - example: com.example.someevent - required: true - - in: header - name: ce-source - description: The CloudEvents event "source". - schema: - type: string - example: /mycontext - required: true - - in: header - name: ce-id - description: The CloudEvents event "id". - schema: - type: string - example: 1234-1234-1234 - required: true - - in: header - name: ce-time - description: The CloudEvents event "time". - schema: - type: string - format: date-time - example: '2020-12-31T23:59:59Z' - required: true - - in: header - name: ce-dataschema - description: 'The CloudEvents event "dataschema". If provided, this must start with `ditto:`.' - schema: - type: string - required: false - responses: - '202': - description: 'The Ditto Protocol CloudEvent was successfully parsed, the authentication was valid and also reached the persistence.' - '400': - description: |- - The request could not be completed. Possible reasons: - * the CloudEvent could not be parsed as some mandatory CloudEvent headers were missing from the request - * the payload was missing from the CloudEvent - * the [Ditto Protocol JSON](https://www.eclipse.dev/ditto/protocol-specification.html) message could not be parsed or was missing a required field - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: |- - The request could not be completed. Possible reasons: - * the caller has insufficient permissions. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: |- - The request could not be completed. Possible reasons: - * the referenced thing does not exist. - * the caller has insufficient permissions to perform the contained Ditto Protocol command. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '408': - description: The request could not be completed due to timeout. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '415': - description: The `Content-Type` of the request was not supported. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - requestBody: - content: - application/vnd.eclipse.ditto+json: - schema: - type: object - properties: - topic: - type: string - description: |- - Contains information about the contents of the payload: - * the affected Thing (namespace and Thing ID) - * the type of operation (command/event, create/retrieve/modify/delete) - example: org.eclipse.ditto/thing-1/things/twin/commands/modify - headers: - type: object - description: Additional headers. - properties: - correlation-id: - type: string - description: |- - The correlation-id header is used for linking one message with another. - It typically links a reply message with its requesting message. - example: - correlation-id: 1234-4321-1234 - path: - type: string - description: References the part of a Thing which is affected by this message. - example: /features/location/properties/longitude - value: - oneOf: - - type: object - - type: string - - type: number - - type: array - - type: boolean - description: The `value` field contains the actual payload e.g. a sensor value. - required: - - topic - - path - example: - topic: org.eclipse.ditto/thing-1/things/twin/commands/modify - path: / - value: - attributes: - foo: 42 - description: |- - The [Ditto Protocol JSON](https://www.eclipse.dev/ditto/protocol-specification.html) payload defining which - command to process. - /api/2/connections: - get: - summary: Retrieve all connections - description: Returns all connections. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionFieldsQueryParam' - - name: ids-only - in: query - description: 'When set to true, the request will return the registered ids only and not the whole connections objects.' - required: false - schema: - type: boolean - responses: - '200': - description: The request successfully returned the connections. - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Connection' - '400': - description: The request could not be completed. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to an invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. Connections not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - post: - summary: Create a new connection - description: |- - Creates the connection defined in the JSON body. - The ID of the connection will be **generated** by the backend. Any `ID` specified in the request body is therefore - prohibited. - Supported connection types are `amqp-091`, `amqp-10`, `mqtt`, `mqtt-5`, `kafka`, `hono` and `http-push`. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - name: dry-run - in: query - description: |- - When set to true, the request will not try to create the connection, but only try to connect it. - You can use this parameter to verify that the given connection is able to communicate with your external - system. - required: false - schema: - type: boolean - responses: - '200': - description: |- - Will be returned when a dry-run succeeded (see description of the dry-run query parameter for further - information). - '201': - description: The connection was successfully created. - headers: - Location: - description: The location of the created connection resource. - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/Connection' - '400': - description: |- - The request could not be completed. Possible reasons: - * an `ID` was set in the request body, but the ID will be generated by Ditto - * the JSON of the connection to be created is invalid - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. Connections not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/NewConnection' - example: - name: hono-example-connection-123 - connectionType: hono - connectionStatus: open - sources: - - addresses: - - telemetry - - event - - ... - authorizationContext: - - 'ditto:inbound-auth-subject' - - ... - consumerCount: 1 - enforcement: - input: '{{ header:device_id }}' - filters: - - '{{ thing:id }}' - payloadMapping: - - Ditto - - status - targets: - - address: command - topics: - - _/_/things/twin/events - authorizationContext: - - 'ditto:outbound-auth-subject' - - ... - headerMapping: {} - description: The example below shows a connection to Eclipse Hono. - required: true - '/api/2/connections/{connectionId}': - get: - summary: Retrieve a specific connection - description: Returns the connection identified by the `connectionId` path parameter. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionIdPathParam' - - $ref: '#/components/parameters/ConnectionFieldsQueryParam' - responses: - '200': - description: The request successfully returned the connection. - content: - application/json: - schema: - $ref: '#/components/schemas/Connection' - '400': - description: The request could not be completed. The `connectionId` must be an URI. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The connection with ID `connectionId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - put: - summary: Create or update a connection with a specified ID - description: Update the connection identified by the `connectionId` path parameter. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionIdPathParam' - responses: - '204': - description: The connection was successfully updated. - '400': - description: |- - The request could not be completed. Possible reasons: - * the `connectionId` must be an URI, - * the `ID` was wrongly set in the request body, - * the JSON of the connection to be created is invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The connection with ID `connectionId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/NewConnection' - example: - name: hono-example-connection-123 - connectionType: hono - connectionStatus: open - sources: - - addresses: - - telemetry - - event - - ... - authorizationContext: - - 'ditto:inbound-auth-subject' - - ... - consumerCount: 1 - enforcement: - input: '{{ header:device_id }}' - filters: - - '{{ thing:id }}' - payloadMapping: - - Ditto - - status - targets: - - address: command - topics: - - _/_/things/twin/events - authorizationContext: - - 'ditto:outbound-auth-subject' - - ... - headerMapping: {} - required: true - delete: - summary: Delete a specific connection - description: Delete the connection identified by the `connectionId` path parameter. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionIdPathParam' - responses: - '204': - description: The connection was successfully deleted. - '400': - description: The request could not be completed. The `connectionId` must be an URI. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to an invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The connection with ID `connectionId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '/api/2/connections/{connectionId}/command': - post: - summary: Send a command to a specific connection - description: |- - Sends the command specified in the body to the connection identified by the `connectionId` - path parameter. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionIdPathParam' - responses: - '200': - description: The command was sent to the connection successfully. - '400': - description: The request could not be completed. The `connectionId` must be an URI. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The connection with ID `connectionId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - requestBody: - content: - text/plain: - schema: - type: string - example: - description: |- - The command to send. Supported commands are - * `connectivity.commands:openConnection` - * `connectivity.commands:closeConnection` - * `connectivity.commands:resetConnectionMetrics` - * `connectivity.commands:enableConnectionLogs` - * `connectivity.commands:resetConnectionLogs` - required: true - '/api/2/connections/{connectionId}/status': - get: - summary: Retrieve status of a specific connection - description: Returns the status of the connection identified by the `connectionId` path parameter. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionIdPathParam' - responses: - '200': - description: The request successfully returned the connection status. - content: - application/json: - schema: - $ref: '#/components/schemas/ConnectionStatus' - '400': - description: The request could not be completed. The `connectionId` must be an URI. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The connection with ID `connectionId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '/api/2/connections/{connectionId}/metrics': - get: - summary: Retrieve metrics of a specific connection - description: Returns the metrics of the connection identified by the `connectionId` path parameter. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionIdPathParam' - responses: - '200': - description: The request successfully returned the connection metrics. - content: - application/json: - schema: - $ref: '#/components/schemas/ConnectionMetrics' - '400': - description: The request could not be completed. The `connectionId` must be an URI. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The connection with ID `connectionId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '/api/2/connections/{connectionId}/logs': - get: - summary: Retrieve logs of a specific connection - description: |- - Returns the logs of the connection identified by the `connectionId` path parameter. - **Before** log entries are generated and returned, logging needs be enabled with the `command` - `connectivity.commands:enableConnectionLogs`. When creating or opening an connection the logging is enabled per - default. This allows to log possible errors on connection establishing. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Connections - parameters: - - $ref: '#/components/parameters/ConnectionIdPathParam' - responses: - '200': - description: The request successfully returned the connection logs. - content: - application/json: - schema: - $ref: '#/components/schemas/ConnectionLogs' - '400': - description: The request could not be completed. The `connectionId` must be an URI. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '403': - description: The request could not be completed due to invalid authentication. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '404': - description: The request could not be completed. The connection with ID `connectionId` was not found. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - /devops/logging: - get: - summary: Retrieve all currently configured log levels - description: Return configured log level for all ditto cluster pod - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/LoggingFieldsQueryParam' - responses: - '200': - description: Return The current value of logging level - content: - application/json: - schema: - $ref: '#/components/schemas/RetrieveLoggingConfig' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - put: - summary: Update log levels - description: Modify log level for eatch pods menaged - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - requestBody: - description: Fields to update level log for each pods - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LoggingUpdateFields' - example: |- - { - "level": "info", - "logger": "org.apache.pekko.actor.CoordinatedShutdown" - } - responses: - '201': - $ref: '#/components/responses/SuccessUpdateLogLevel' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - '/devops/logging/{moduleName}': - get: - summary: Retrieve currently configured log levels for a specific module - description: Return the configured log - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/ModuleNamePathParam' - - $ref: '#/components/parameters/LoggingFieldsQueryParam' - responses: - '200': - description: Return The current value of logging level - content: - application/json: - schema: - $ref: '#/components/schemas/Module' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - put: - summary: Update log levels for a specific module - description: Return outcome modify log level for a specific module - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/ModuleNamePathParam' - requestBody: - description: Fields to update level log for module - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/LoggingUpdateFields' - example: |- - { - "level": "info", - "logger": "org.apache.pekko.actor.CoordinatedShutdown" - } - responses: - '201': - $ref: '#/components/responses/SuccessUpdateLogLevelSinglePod' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - /devops/config: - get: - summary: Retrieve the configuration at the specified path parameter - description: |- - It is recommended to not omit the query parameter path. - Otherwise, the full configurations of all services are aggregated in the response, which can become megabytes big. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/PathParam' - responses: - '200': - description: Return the configuration at the path - content: - application/json: - schema: - $ref: '#/components/schemas/RetrieveConfig' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - '/devops/config/{moduleName}/{podName}': - get: - summary: Retrieving the configuration of a specific service instance. - description: Return the configuration of a specific service instance. - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/ModuleNamePathParam' - - $ref: '#/components/parameters/NamePodParam' - - $ref: '#/components/parameters/PathParam' - responses: - '200': - description: Return The current value of specific service instance. - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/RetrieveConfigService' - example: |- - { - "gateway": { - "podName": { - "type": "common.responses:retrieveConfig", - "status": 200, - "config": { - "cluster": { - "cluster-status-roles-blocklist": [ - "cluster1", - "......" , - "clusterN" - ], - "number-of-shards": 20 - }, - "ddata": { - "vm arg1": "string", - ".............": "string", - "vm argn" : "string" - }, - "devops": { - "feature": { - "merge-things-enabled": true - }, - "namespace": { - "block-time": "string" - } - }, - "gateway": { - "authentication": { - "devops": { - "devops-authentication-method": "string", - "password": "string", - "secured": true, - "status-authentication-method": "string", - "statusPassword": "string" - }, - "http": { - "proxy": { - "enabled": false - } - }, - "oauth": { - "allowed-clock-skew": "string", - "openid-connect-issuers": { - "google": { - "issuer": "string" - } - }, - "protocol": "https", - "token-integration-subject": "string" - }, - "pre-authentication": { - "enabled": "true" - } - } - } - } - } - } - } - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - /devops/piggyback: - post: - summary: Send a piggyback command - description: Send a piggyback command to Pekko’s pub-sub-mediator - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/TimeoutParam' - requestBody: - description: Fields to send a command - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BasePiggybackCommandRequestSchema' - examples: - blockNamespace: - description: Block all messages to a namespace - value: |- - { - "targetActorSelection": "/system/distributedPubSubMediator", - "headers": { - "aggregate": false - }, - "piggybackCommand": { - "type": "namespaces.commands:blockNamespace", - "namespace": "namespaceToBlock" - } - } - shutdown: - description: Shutdown all actors in a namespace - value: |- - { - "targetActorSelection": "/system/distributedPubSubMediator", - "piggybackCommand": { - "type": "common.commands:shutdown", - "reason": { - "type": "purge-namespace", - "details": "namespaceToShutdown" - } - } - } - purgeNamespace: - description: Erase all data in a namespace from the persistence - value: |- - { - "targetActorSelection": "/system/distributedPubSubMediator", - "headers": { - "aggregate": true, - "is-group-topic": true - }, - "piggybackCommand": { - "type": "namespaces.commands:purgeNamespace", - "namespace": "namespaceToPurge" - } - } - unblockNamespace: - description: Unblock messages to a namespace - value: |- - { - "targetActorSelection": "/system/distributedPubSubMediator", - "headers": { - "aggregate": false - }, - "piggybackCommand": { - "type": "namespaces.commands:unblockNamespace", - "namespace": "namespaceToUnblock" - } - } - responses: - '200': - description: Response of command - content: - application/json: - schema: - $ref: '#/components/schemas/PiggybackManagingBackgroundCleanup' - examples: - blockNamespace: - value: |- - { - "type": "namespaces.responses:blockNamespace", - "status": 200, - "namespace": "namespaceToBlock", - "resourceType": "namespaces" - } - '400': - description: The request could not be completed. At least one of the defined query parameters was invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - '/devops/piggyback/{serviceName}': - post: - summary: Send a piggyback command to a specific service - description: Send a piggyback command to a specific service - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/ServiceNameParam' - - $ref: '#/components/parameters/TimeoutParam' - requestBody: - description: Fields to send a command - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BasePiggybackCommandRequestSchema' - examples: - persistenceCleanup: - description: Query background cleanup coordinator state - value: |- - { - "targetActorSelection": "/user/Root/persistenceCleanup", - "headers": {}, - "piggybackCommand": { - "type": "status.commands:retrieveHealth" - } - } - responses: - '200': - description: Return The current value of logging level - content: - application/json: - schema: - $ref: '#/components/schemas/PiggybackManagingBackgroundCleanup' - example: |- - { - "things": { - "ditto-things-65f6dd5d7-htkwt": { - "type": "status.responses:retrieveHealth", - "status": 200, - "statusInfo": { - "status": "UP", - "details": [ - { - "INFO": { - "state": "IN_QUIET_PERIOD", - "pid": "" - } - } - ] - } - } - } - } - '400': - description: The request could not be completed. At least one of the defined query parameters was invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - '/devops/piggyback/{serviceName}/{instanceIndex}': - post: - summary: Send a piggyback command to a specific instance of service - description: Send a piggyback command to a specific instance of service - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - tags: - - Devops - parameters: - - $ref: '#/components/parameters/ServiceNameParam' - - $ref: '#/components/parameters/InstanceIndex' - - $ref: '#/components/parameters/TimeoutParam' - requestBody: - description: Fields to send a command - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BasePiggybackCommandRequestSchema' - examples: - cleanup: - description: Cleanup events and snapshots of an entity - value: |- - { - "targetActorSelection": "/system/sharding/thing", - "headers": { - "aggregate": false - }, - "piggybackCommand": { - "type": "cleanup.sudo.commands:cleanupPersistence", - "entityId": "ditto:thing1" - } - } - responses: - '200': - description: response of command - content: - application/json: - example: |- - { - "type": "cleanup.sudo.responses:cleanupPersistence", - "status": 200, - "entityId": "thing:ditto:thing1" - } - '400': - description: The request could not be completed. At least one of the defined query parameters was invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - '401': - description: The request could not be completed due to missing authentication. - content: - text/plain: - schema: - $ref: '#/components/schemas/TextUnauthorizeError' - /.well-known/wot: - get: - summary: Retrieve WoT Thing Directory - description: |- - Returns a WoT (Web of Things) Thing Description of the Ditto Thing Directory, - as specified by the [WoT Discovery](https://www.w3.org/TR/wot-discovery/) specification. - - By default, this endpoint is publicly accessible without authentication. This can be configured - via the `GATEWAY_WOT_DIRECTORY_AUTHENTICATION_REQUIRED` environment variable. - - Both `GET` and `HEAD` methods are supported per the WoT Discovery specification. - tags: - - WoT - responses: - '200': - description: The WoT Thing Directory description was successfully retrieved. - content: - application/td+json: - schema: - $ref: '#/components/schemas/WotThingDescription' - example: - '@context': - - 'https://www.w3.org/2022/wot/td/v1.1' - - 'https://www.w3.org/2022/wot/discovery' - '@type': ThingDirectory - id: 'urn:ditto:wot:thing-directory' - title: Thing Description Directory (TDD) of Eclipse Ditto - version: - model: 1.0.0 - instance: 1.0.0 - '401': - description: The request could not be completed due to missing authentication (when authentication is required). - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - head: - summary: Retrieve WoT Thing Directory headers - description: |- - Returns headers for the WoT Thing Directory endpoint without a response body. - Supports the same authentication and configuration as the GET method. - tags: - - WoT - responses: - '200': - description: The WoT Thing Directory headers were successfully retrieved. - '401': - description: The request could not be completed due to missing authentication (when authentication is required). - /devops/wot/config: - get: - summary: Get the WoT validation config - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - responses: - '200': - $ref: '#/components/responses/WotValidationConfigResponse' - '404': - description: Not found - put: - summary: Update the WoT validation config - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/WotValidationConfig' - responses: - '204': - description: Updated config - delete: - summary: Delete the WoT validation config - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - responses: - '204': - description: Deleted successfully - '404': - description: Not found - /devops/wot/config/merged: - get: - summary: Get the merged WoT validation config - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - responses: - '200': - $ref: '#/components/responses/WotValidationConfigResponse' - '404': - description: Not found - /devops/wot/config/dynamicConfigs: - get: - summary: List all dynamic WoT validation config sections - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - responses: - '200': - description: List of dynamic config sections - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/DynamicValidationConfig' - '/devops/wot/config/dynamicConfigs/{scopeId}': - parameters: - - name: scopeId - in: path - required: true - schema: - type: string - get: - summary: Get a dynamic WoT validation config section by scopeId - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - responses: - '200': - $ref: '#/components/responses/DynamicValidationConfigResponse' - '404': - description: Not found - put: - summary: Create or update a dynamic WoT validation config section - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - requestBody: - $ref: '#/components/requestBodies/DynamicValidationConfigRequest' - responses: - '204': - description: Updated dynamic config section - delete: - summary: Delete a dynamic WoT validation config section - tags: - - Devops - security: - - DevOpsBasic: [] - - DevOpsBearer: [] - responses: - '204': - description: Deleted successfully - '404': - description: Not found -components: - requestBodies: - Attributes: - content: - application/json: - schema: - $ref: '#/components/schemas/Attributes' - example: - manufacturer: - name: ACME demo corp. - location: 'Berlin, main floor' - coffeemaker: - serialno: '42' - model: Speaking coffee machine - description: |- - JSON object of all attributes to be modified at once. Consider that the - value has to be a JSON object or `null`. - - Examples: - * an empty object: `{}` - would just delete all old attributes - * a simple object: `{ "key": "value"}` - We strongly recommend to use a restricted set of characters for the key (identifier), as the key might be needed for the (URL) path later.
Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - * a nested object as shown in the example value - required: true - Definition: - content: - application/json: - schema: - $ref: '#/components/schemas/Definition' - example: '"example:test:definition"' - description: |- - JSON string of the definition to be modified. Consider that the - value has to be a JSON string or `null`, examples: - - * a string: `"value"` - Currently the definition should follow the pattern: [_a-zA-Z0-9\-]:[_a-zA-Z0-9\-]:[_a-zA-Z0-9\-] - * an empty string: `""` - Payload: - content: - application/json: - schema: - type: string - example: '' - application/octet-stream: - schema: - type: string - example: '' - text/plain: - schema: - type: string - example: '' - description: |- - Payload of the message with max size of 250 kB. It can be any HTTP - supported content, including binary content. - Value: - content: - application/json: - schema: - type: object - example: {} - description: |- - JSON representation of the value to be created/updated. This may be as - well `null` or an empty object. - - Consider that the value has to be a JSON value, examples: - - * for a number, the JSON value is the number: `42` - - * for a string, the JSON value must be quoted: `"aString"` - - * for a boolean, the JSON value is the boolean: `true` - - * for an object, the JSON value is the object: `{ "key": "value"}` -} We strongly recommend to use a restricted set of characters for the key (identifier). Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - - * for an list, the JSON value is the list: `[ 1,2,3 ]` - required: true - PatchValue: - content: - application/merge-patch+json: - schema: - type: object - example: {} - description: |- - JSON representation of the value to be patched. This may be as well an empty object. - - Consider that the value has to be a JSON value. - - Examples: - * for a number, the JSON value is the number: `42` - * for a string, the JSON value must be quoted: `"aString"` - * for a boolean, the JSON value is the boolean: `true` - * for an object, the JSON value is the object: `{ "key": "value"}` -} We strongly recommend to use a restricted set of characters for the key (identifier). Currently these identifiers should follow the pattern: [_a-zA-Z][_a-zA-Z0-9\-]* - * for an list, the JSON value is the list: `[ 1,2,3 ]` - * special value `null` will delete the referenced key. For further documentation see [RFC 7396](https://tools.ietf.org/html/rfc7396). - required: true - ActivateTokenIntegration: - content: - application/json: - schema: - properties: - announcement: - $ref: '#/components/schemas/SubjectAnnouncement' - example: - announcement: - beforeExpiry: 5m - whenDeleted: true - requestedAcks: - labels: - - 'my-connection-id:my-issued-acknowledgement' - timeout: 30s - randomizationInterval: 5m - description: Optional request payload for `activateTokenIntegration` policy action. - required: false - MigrateThingDefinitionRequest: - content: - application/json: - schema: - $ref: '#/components/schemas/MigrateThingDefinitionRequest' - description: 'JSON payload containing the new definition URL, migration payload, patch conditions, and initialization flag.' - required: true - PermissionCheckRequest: - content: - application/json: - schema: - type: object - description: Request to check permissions for various entities and resources. - additionalProperties: - type: object - description: Details for a specific permission check request. - properties: - resource: - type: string - description: Resource path the permission check applies to. - entityId: - type: string - description: thingId of the entity performing the action. - hasPermissions: - type: array - items: - type: string - enum: - - READ - - WRITE - description: Required permissions on the resource. - description: 'JSON object containing permission check requests, keyed by an arbitrary identifier.' - required: true - DynamicValidationConfigRequest: - content: - application/json: - schema: - $ref: '#/components/schemas/DynamicValidationConfig' - ConfigOverridesRequest: - content: - application/json: - schema: - $ref: '#/components/schemas/ConfigOverrides' - responses: - EntityTooLarge: - description: The created or modified entity is larger than the accepted limit of 100 kB. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - MessageTooLarge: - description: The size of the sent message is larger than the accepted limit of 250 kB. - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - NotModified: - description: |- - The (sub-)resource has not been modified. This happens when you specified a If-None-Match header which - matches the current ETag of the (sub-)resource. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - PreconditionFailed: - description: |- - A precondition for reading or writing the (sub-)resource failed. This will happen for write requests, if you - specified an If-Match or If-None-Match header, which fails the precondition check against the current ETag of - the (sub-)resource. For read requests, this error may only happen for a failing If-Match header. In case of a - failing If-None-Match header for a read request, status 304 will be returned instead. - headers: - ETag: - description: |- - The (current server-side) ETag for this (sub-)resource. For top-level resources it is in the format - "rev:[revision]", for sub-resources it has the format "hash:[calculated-hash]". - schema: - type: string - content: - application/json: - schema: - $ref: '#/components/schemas/AdvancedError' - DependencyFailed: - description: |- - One or more acknowledgement requests in the parameter `requested-acks` - were not fulfilled. - content: - application/json: - schema: - properties: - acknowledgementLabel1: - properties: - status: - type: integer - description: The HTTP status of the acknowledgement - payload: - oneOf: - - type: object - - type: string - - type: number - - type: array - - type: boolean - description: The payload of the acknowledgement - required: - - status - example: - status: 200 - payload: OK - example: - acknowledgementLabel1: - status: 200 - payload: OK - acknnowledgementLabelN: - status: 403 - payload: Forbidden - SuccessUpdateLogLevel: - description: Return The summary of the outcome of all modified pods - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/ResultUpdateRequest' - SuccessUpdateLogLevelSinglePod: - description: Return The summary of the outcome of modified pod - content: - application/json: - schema: - $ref: '#/components/schemas/ModuleUpdatedLogLevel' - MigrateThingDefinitionResponse: - description: 'The thing definition was successfully updated, and the updated Thing is returned.' - content: - application/json: - schema: - $ref: '#/components/schemas/MigrateThingDefinitionResponse' - PermissionCheckResponse: - description: Response with permission check results for each entity. - content: - application/json: - schema: - type: object - description: Response with permission check results for each entity. - additionalProperties: - type: boolean - WotValidationConfigResponse: - description: The WoT validation configuration. - content: - application/json: - schema: - $ref: '#/components/schemas/WotValidationConfig' - DynamicValidationConfigResponse: - description: The dynamic WoT validation configuration. - content: - application/json: - schema: - $ref: '#/components/schemas/DynamicValidationConfig' - ConfigOverridesResponse: - description: The WoT validation configuration overrides. - content: - application/json: - schema: - $ref: '#/components/schemas/ConfigOverrides' - parameters: - AllowPolicyLockoutParam: - name: allow-policy-lockout - in: query - description: |- - Defines whether a subject is allowed to create a policy without having WRITE permission on the policy - resource of the created policy. - - The default (if ommited) is `false`. - required: false - schema: - type: boolean - AttributesPathPathParam: - name: attributePath - in: path - description: 'The path to the attribute, e.g. **manufacturer/name**' - required: true - schema: - type: string - AttributesFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields from the attributes to be - included in the returned JSON. - - #### Selectable fields - - Supports selecting arbitrary sub-fields as defined in the attributes by - using a comma-separated list: - * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - - For example: - * `?fields=model` would select only `model` attribute value (if present) - * `?fields=model,make` would select `model` and `make` attribute values (if present) - - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields - inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select - * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield - - For example: - * `?fields=location(longitude,latitude)` would select the `longitude` and `latitude` value inside the `location` attribute - - #### Examples - - * `?fields=model,make,location(longitude,latitude)` - - * `?fields=listOfAddresses/postal(city,street))` - required: false - schema: - type: string - ChannelParam: - name: channel - in: query - description: |- - Defines to which channel to route the command: `twin` (digital twin) or `live` (the device). - * If setting the channel parameter is omitted, the `twin` channel is set by default and the command is routed to the persisted representation of a thing in Eclipse Ditto. - * When using the `live` channel, the command/message is sent towards the device. - required: false - schema: - type: string - enum: - - twin - - live - ChannelParamPutDescription: - name: channel - in: query - description: |- - Defines to which channel to route the command: `twin` (digital twin) or `live` (the device). - * If setting the channel parameter is omitted, the `twin` channel is set by default and the command is routed to the persisted representation of a thing in Eclipse Ditto. - * When using the `live` channel, the command/message is sent towards the device. - - The option `live` is not available when a new thing should be created, only for updating an - existing thing. - required: false - schema: - type: string - enum: - - twin - - live - ConditionParam: - name: condition - in: query - description: |- - Defines that the request should only be processed if the given condition is met. The condition can be specified using RQL syntax. - #### Examples - E.g. if the temperature is not 23.9 update it to 23.9: - * ```PUT /api/2/things/{thingId}/features/temperature/properties/value?condition=ne(features/temperature/properties/value,23.9)``` - - `body: 23.9` - - Further example conditions: - * ```?condition=eq(features/temperature/properties/unit,"Celsius")``` - * ```?condition=ge(features/temperature/properties/lastModified,"2021-08-22T19:45:00Z")``` - * ```?condition=gt(_modified,"2021-08-05T12:17:00Z")``` - * ```?condition=exists(features/temperature/properties/value)``` - * ```?condition=empty(features/temperature/properties/value)``` - * ```?condition=and(gt(features/temperature/properties/value,18.5),lt(features/temperature/properties/value,25.2))``` - * ```?condition=or(gt(features/temperature/properties/value,18.5),not(exists(features/temperature/properties/value))``` - required: false - schema: - type: string - LiveChannelConditionParam: - name: live-channel-condition - in: query - description: |- - Defines that the request should fetch thing data via `live` channel if the given condition is met. The condition can be specified using RQL syntax. - #### Examples - - * ```?live-channel-condition=lt(_modified,"2021-12-24T12:23:42Z")``` - - * ```?live-channel-condition=ge(features/ConnectionStatus/properties/status/readyUntil,time:now)``` - required: false - schema: - type: string - LiveChannelTimeoutStrategyParam: - name: live-channel-timeout-strategy - in: query - description: Defines a strategy how to handle timeouts of a live response to a request sent via `channel=live` or with a matching live-channel-condition. - required: false - schema: - enum: - - fail - - use-twin - DesiredPropertiesFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields from the desiredProperties to be - included in the returned JSON. - - #### Selectable fields - - Supports selecting arbitrary sub-fields as defined in the desiredProperties by - using a comma-separated list: - * several desiredProperties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - - For example: - * `?fields=temperature` would select only `temperature` property value of desiredProperties (if present) - * `?fields=temperature,humidity` would select only `temperature` and `humidity` property values of desiredProperties (if present) - - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields - inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select - * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield - - For example: - * `?fields=location(longitude,latitude)` would select the `longitude` and `latitude` value inside the `location` property of desiredProperties - - #### Examples - - * `?fields=temperature,humidity,location(longitude,latitude)` - - * `?fields=configuration,status(powerConsumption/watts)` - required: false - schema: - type: string - FeatureFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields from the selected feature to be - included in the returned JSON. - - #### Selectable fields - - * `properties` - - Supports selecting arbitrary sub-fields by using a comma-separated list: - * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - - For example: - * `?fields=properties/color` would select only `color` property value (if present) - * `?fields=properties/color,properties/brightness` would select only `color` and `brightness` property values (if present) - - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select - * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield - - For example: - * `?fields=properties(color,brightness)` would select only `color` and `brightness` property values (if present) - * `?fields=properties(location/longitude)` would select the `longitude` value inside the `location` object - - #### Examples - - * `?fields=properties(color,brightness)` - required: false - schema: - type: string - FeatureIdPathPathParam: - name: featureId - in: path - description: The ID of the feature - has to conform to RFC-3986 (URI) - required: true - schema: - type: string - FeaturesFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields from one or more features to be - included in the returned JSON. - - #### Selectable fields - - * `{featureId}` The ID of the feature to select properties in - * `properties` - Supports selecting arbitrary sub-fields by using a comma-separated list: - * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - For example: - * `?fields={featureId}/properties/color` would select only `color` property value (if present) of the feature identified with `{featureId}` - * `?fields={featureId}/properties/color,properties/brightness` would select only `color` and `brightness` property values (if present) of the feature identified with `{featureId}` - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select - * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield - For example: - * `?fields={featureId}/properties(color,brightness)` would select only `color` and `brightness` property values (if present) of the feature identified with `{featureId}` - * `?fields={featureId}/properties(location/longitude)` would select the `longitude` value inside the `location` object of the feature identified with `{featureId}` - - - #### Examples - * `?fields=EnvironmentScanner/properties(temperature,humidity)` - * `?fields=EnvironmentScanner/properties(temperature,humidity),Vehicle/properties/configuration` - required: false - schema: - type: string - IfMatchHeaderParam: - name: If-Match - in: header - description: |- - The `If-Match` header, which has to conform to RFC-7232 (Conditional Requests). Common usages are: - * optimistic locking by specifying the `ETag` from a previous GET response, e.g. `If-Match: "rev:4711"` - * retrieving or modifying a resource only if it already exists, e.g. `If-Match: *` - required: false - schema: - type: string - IfMatchHeaderParamHash: - name: If-Match - in: header - description: |- - The `If-Match` header which has to conform to RFC-7232 (Conditional Requests). Common usages are: - * optimistic locking by specifying the `ETag` from a previous HTTP response, e.g. `If-Match: "hash:a75ece4e"` - * retrieving or modifying a resource only if it already exists, e.g. `If-Match: *` - required: false - schema: - type: string - IfNoneMatchHeaderParam: - name: If-None-Match - in: header - description: 'The `If-None-Match` header, which has to conform to RFC-7232 (Conditional Requests). A common usage scenario is to modify a resource only if it does not yet exist, thus to create it, by specifying `If-None-Match: *`.' - required: false - schema: - type: string - IfEqualHeaderParam: - name: if-equal - in: header - description: 'The `if-equal` header can take the values ''update'' (which is the default if omitted), ''skip'' or ''skip-minimizing-merge''. If ''update'' is defined, the entity will always be updated, even if it is equal before the update. If ''skip'' is defined, the entity not be updated if it is equal before the update. In this case a ''Precondition Failed'' 412 status is returned. If ''skip-minimizing-merge'' is defined, the entity will not be updated if it is equal before the update. In this case a ''Precondition Failed'' 412 status is returned. Additionally, merge/patch commands will be minimized to only the fields which actually changed, compared to the current state of the entity.' - required: false - schema: - type: string - enum: - - update - - skip - - skip-minimizing-merge - ImportedPolicyIdPathParam: - name: importedPolicyId - in: path - description: |- - The ID of the imported policy needs to follow the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - - The namespace needs to: - * conform to the reverse domain name notation - required: true - schema: - type: string - LabelPathParam: - name: label - in: path - description: The label of a policy entry - required: true - schema: - type: string - LiveMessageRequestedAcksParam: - name: requested-acks - in: query - description: |- - Contains the "requested acknowledgements" for this request as comma separated list. The HTTP call will - block until all requested acknowledgements were aggregated or will time out based on the specified `timeout` - parameter. - - The default (if omitted) requested acks is `requested-acks=live-response` which will block the - HTTP call until a subscriber of the live message sends a response. - required: false - schema: - type: string - MessageClaimTimeoutParam: - name: timeout - in: query - description: |- - Contains an optional timeout (in seconds) of how long to wait for the Claim response and therefore block the - HTTP request. Default value (if omitted): 60 seconds. Maximum value: 600 seconds. A value of 0 seconds applies - fire and forget semantics for the message. - required: false - schema: - type: integer - MessageSubjectPathParam: - name: messageSubject - in: path - description: The subject of the Message - has to conform to RFC-3986 (URI) - required: true - schema: - type: string - MessageTimeoutParam: - name: timeout - in: query - description: |- - Contains an optional timeout (in seconds) of how long to wait for the message response and therefore block the - HTTP request. Default value (if omitted): 10 seconds. Maximum value: 60 seconds. A value of 0 seconds applies - fire and forget semantics for the message. - required: false - schema: - type: integer - Namespace: - name: namespace - in: query - description: Defines a custom namespace for the thing while generating a new thing ID. - required: false - schema: - type: string - example: com.example.namespace - NamespacesFilter: - name: namespaces - in: query - description: |- - A comma-separated list of namespaces. This list is used to limit the query to things in the given namespaces - only. - - - #### Examples: - - * `?namespaces=com.example.namespace` - - * `?namespaces=com.example.namespace1,com.example.namespace2` - required: false - schema: - type: string - PolicyFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields to be included in the returned - JSON. - - #### Selectable fields - - * `policyId` - * `entries` - - Supports selecting arbitrary sub-fields by using a comma-separated list: - * several entry paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - - For example: - * `?fields=entries/ditto` would select only the `ditto` entry value(if present) - * `?fields=entries/ditto,entries/user` would select only `ditto` and - `user` entry values (if present) - - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) - separated with `/`) to select - - * sub-selectors can be used to request only specific sub-fields by placing expressions - in parentheses `( )` after a selected subfield - - For example: - * `?fields=entries(ditto,user)` would select only `ditto` - and `user` entry values (if present) - * `?fields=entries(ditto/subjects)` would select the `subjects` value - inside the `ditto` entry - * `?fields=entries/ditto/subjects(issuer:google,issuer:azure)` would select the `issuer:google` and - `issuer:azure` values inside the `subjects` object inside the `entries` object - - * `_namespace` - - Specifically selects the namespace also contained in the `policyId` - - * `_revision` - - Specifically selects the revision of the policy. The revision is a counter, which is incremented on each modification of a policy. - - * `_created` - - Specifically selects the created timestamp of the policy in ISO-8601 UTC format. The timestamp is set on creation of a policy. - - * `_modified` - - Specifically selects the modified timestamp of the policy in ISO-8601 UTC format. The timestamp is set on each modification of a policy. - - * `_metadata` - - Specifically selects the Metadata of the policy. The content is a JSON object having the policy's JSON structure with the difference that the JSON leaves of the policy are JSON objects containing the metadata. - - #### Examples - - * `?fields=policyId,entries,_revision` - * `?fields=entries(ditto,user),_namespace` - required: false - schema: - type: string - PolicyIdPathParam: - name: policyId - in: path - description: |- - The ID of the policy needs to follow the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)). - - The namespace needs to: - * conform to the reverse domain name notation - required: true - schema: - type: string - PropertiesFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields from the properties to be - included in the returned JSON. - - #### Selectable fields - - Supports selecting arbitrary sub-fields as defined in the properties by - using a comma-separated list: - * several properties paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - - For example: - * `?fields=temperature` would select only `temperature` property value (if present) - * `?fields=temperature,humidity` would select only `temperature` and `humidity` property values (if present) - - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields - inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) separated with `/`) to select - * sub-selectors can be used to request only specific sub-fields by placing expressions in parentheses `( )` after a selected subfield - - For example: - * `?fields=location(longitude,latitude)` would select the `longitude` and `latitude` value inside the `location` property - - #### Examples - - * `?fields=temperature,humidity,location(longitude,latitude)` - - * `?fields=configuration,status(powerConsumption/watts)` - required: false - schema: - type: string - PropertyPathPathParam: - name: propertyPath - in: path - description: The path to the property - required: true - schema: - type: string - PutMetadataParam: - name: put-metadata - in: header - description: 'The `put-metadata` header, which sets Metadata information in the Thing.' - required: false - schema: - type: array - description: An array of objects containing metadata to apply. - items: - type: object - description: Object containing a `key` where to apply the metadata and a `value` with the metadata value to apply. - additionalProperties: - properties: - key: - type: string - description: The JsonPointer to set the metadata `value` to. May start with `*/` in order to apply the metadata to all affected JSON leaves. - value: - description: The arbitrary JSON value to set as metadata. - GetMetadataParam: - name: get-metadata - in: header - description: 'The `get-metadata` header, which retrieves Metadata of the Thing.' - required: false - schema: - type: string - description: A string of comma separated JsonPointers to retrieve from the Metadata of the Thing. - DeleteMetadataParam: - name: delete-metadata - in: header - description: 'The `delete-metadata` header, which deletes Metadata of the Thing.' - required: false - schema: - type: string - description: A string of comma separated JsonPointers to delete from the Metadata of the Thing. - RequestedAcksParam: - name: requested-acks - in: query - description: |- - Contains the "requested acknowledgements" for this modifying request as comma separated list. The HTTP call will - block until all requested acknowledgements were aggregated or will time out based on the specified `timeout` - parameter. - - The default (if omitted) requested acks is `requested-acks=twin-persisted` which will block the - HTTP call until the change was persited to the twin. - required: false - schema: - type: string - ResourcePathPathParam: - name: resourcePath - in: path - description: The path of an (Authorization) Resource - required: true - schema: - type: string - ResponseRequiredParam: - name: response-required - in: query - description: |- - Defines whether a response is required to the API call or not - if set to `false` the response will directly - sent back with a status code of `202` (Accepted). - - The default (if ommited) response is `true`. - required: false - schema: - type: boolean - SearchFilter: - name: filter - in: query - description: |- - - #### Filter predicates: - - * ```eq({property},{value})``` (i.e. equal to the given value) - - * ```ne({property},{value})``` (i.e. not equal to the given value) - - * ```gt({property},{value})``` (i.e. greater than the given value) - - * ```ge({property},{value})``` (i.e. equal to the given value or greater than it) - - * ```lt({property},{value})``` (i.e. lower than the given value or equal to it) - - * ```le({property},{value})``` (i.e. lower than the given value) - - * ```in({property},{value},{value},...)``` (i.e. contains at least one of the values listed) - - * ```like({property},{value})``` (i.e. contains values similar to the expressions listed) - - * ```ilike({property},{value})``` (i.e. contains values similar and case insensitive to the expressions listed) - - * ```exists({property})``` (i.e. all things in which the given path exists) - - * ```empty({property})``` (i.e. all things in which the given path is absent, null, an empty array, an empty object or an empty string) - - - Note: When using filter operations, only things with the specified properties are returned. - For example, the filter `ne(attributes/owner, "SID123")` will only return things that do have - the `owner` attribute. - - - #### Logical operations: - - - * ```and({query},{query},...)``` - - * ```or({query},{query},...)``` - - * ```not({query})``` - - - #### Examples: - - * ```eq(attributes/location,"kitchen")``` - - * ```ge(thingId,"myThing1")``` - - * ```gt(_created,"2020-08-05T12:17")``` - - * ```exists(features/featureId)``` - - * ```empty(attributes/tags)``` - - * ```and(eq(attributes/location,"kitchen"),eq(attributes/color,"red"))``` - - * ```or(eq(attributes/location,"kitchen"),eq(attributes/location,"living-room"))``` - - * ```like(attributes/key1,"known-chars-at-start*")``` - - * ```like(attributes/key1,"*known-chars-at-end")``` - - * ```like(attributes/key1,"*known-chars-in-between*")``` - - * ```like(attributes/key1,"just-som?-char?-unkn?wn")``` - - The `like` filters with the wildcard `*` at the beginning can slow down your search request. - required: false - schema: - type: string - SubjectIdPathParam: - name: subjectId - in: path - description: The ID of an (Authorization) Subject - required: true - schema: - type: string - ThingFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields to be included in the returned - JSON. attributes can be selected in the same manner. - - #### Selectable fields - - * `thingId` - * `policyId` - * `definition` - * `attributes` - - Supports selecting arbitrary sub-fields by using a comma-separated list: - * several attribute paths can be passed as a comma-separated list of JSON pointers (RFC-6901) - - For example: - * `?fields=attributes/model` would select only `model` attribute value (if present) - * `?fields=attributes/model,attributes/location` would select only `model` and - `location` attribute values (if present) - - Supports selecting arbitrary sub-fields of objects by wrapping sub-fields inside parentheses `( )`: - * a comma-separated list of sub-fields (a sub-field is a JSON pointer (RFC-6901) - separated with `/`) to select - - * sub-selectors can be used to request only specific sub-fields by placing expressions - in parentheses `( )` after a selected subfield - - For example: - * `?fields=attributes(model,location)` would select only `model` - and `location` attribute values (if present) - * `?fields=attributes(coffeemaker/serialno)` would select the `serialno` value - inside the `coffeemaker` object - * `?fields=attributes/address/postal(city,street)` would select the `city` and - `street` values inside the `postal` object inside the `address` object - - * `features` - - Supports selecting arbitrary fields in features similar to `attributes` (see also features documentation for more details) - - * `_namespace` - - Specifically selects the namespace also contained in the `thingId` - - * `_revision` - - Specifically selects the revision of the thing. The revision is a counter, which is incremented on each modification of a thing. - - * `_created` - - Specifically selects the created timestamp of the thing in ISO-8601 UTC format. The timestamp is set on creation of a thing. - - * `_modified` - - Specifically selects the modified timestamp of the thing in ISO-8601 UTC format. The timestamp is set on each modification of a thing. - - * `_metadata` - - Specifically selects the Metadata of the thing. The content is a JSON object having the Thing's JSON structure with the difference that the JSON leaves of the Thing are JSON objects containing the metadata. - - * `_policy` - - Specifically selects the content of the policy associated to the thing. (By default, only the policyId is returned.) - - #### Examples - - * `?fields=thingId,attributes,features` - * `?fields=attributes(model,manufacturer),features` - required: false - schema: - type: string - ThingIdPathParam: - name: thingId - in: path - description: 'The ID of a thing needs to follow the namespaced entity ID notation (see [Ditto documentation on namespaced entity IDs](https://www.eclipse.dev/ditto/basic-namespaces-and-names.html#namespaced-id)).' - required: true - schema: - type: string - TimeoutParam: - name: timeout - in: query - description: |- - Defines how long the backend should wait for completion of the request, e.g. applied when waiting for requested - acknowledgements via the `requested-acks` param. Can be specified without unit (then seconds are assumed) or - together with `s`, `ms` or `m` unit. Example: `42s`, `1m`. - - The default (if omitted) and maximum timeout is `60s`. A value of `0` applies fire and forget semantics for - the command resulting in setting `response-required=false`. - required: false - schema: - type: string - ConnectionIdPathParam: - name: connectionId - in: path - description: The ID of the connection - required: true - schema: - type: string - ConnectionFieldsQueryParam: - name: fields - in: query - description: |- - Contains a comma-separated list of fields to be included in the returned - JSON. - - #### Selectable fields - - * `id` - * `name` - * `_revision` - - Specifically selects the revision of the connection. The revision is a counter, which is incremented on each modification of a connection. - - * `_created` - - Specifically selects the created timestamp of the connection in ISO-8601 UTC format. The timestamp is set on creation of a connection. - - * `_modified` - - Specifically selects the modified timestamp of the connection in ISO-8601 UTC format. The timestamp is set on each modification of a connection. - - * `connectionType` - * `connectionStatus` - * `credentials` - * `uri` - * `sources` - * `targets` - * `sshTunnel` - * `clientCount` - * `failoverEnabled` - * `validateCertificates` - * `processorPoolSize` - * `specificConfig` - * `mappingDefinitions` - * `tags` - * `ca` - - #### Examples - - * `?fields=id,_revision,sources` - required: false - schema: - type: string - LoggingFieldsQueryParam: - name: includeDisabledLoggers - in: query - description: Include disabled loggers - required: false - schema: - type: boolean - ModuleNamePathParam: - name: moduleName - in: path - description: The name of module - required: true - schema: - type: string - example: gateway - PathParam: - name: path - in: query - description: 'The path points to information on service name, service instance index, JVM arguments and environment variables.' - schema: - type: string - example: ditto.info - required: false - NamePodParam: - name: podName - in: path - description: The name of pod - required: true - schema: - type: string - example: ditto-gateway-764fc5f474-qrm2r - ServiceNameParam: - name: serviceName - in: path - description: Specified service target for the command execution - required: true - schema: - type: string - enum: - - things - - policies - - connectivity - InstanceIndex: - name: instanceIndex - in: path - description: The index of the current instance - required: true - schema: - type: string - schemas: - Error: - properties: - status: - type: integer - description: The HTTP status of the error - message: - type: string - description: The message of the error - what went wrong - description: - type: string - description: A description how to fix the error or more details - href: - type: string - description: A link to further information about the error and how to fix it - required: - - status - - message - AdvancedError: - properties: - status: - type: integer - description: The HTTP status of the error - error: - type: string - description: The error code of the occurred exception - message: - type: string - description: The message of the error - what went wrong - description: - type: string - description: A description how to fix the error or more details - href: - type: string - description: A link to further information about the error and how to fix it - required: - - status - - error - - message - Attributes: - type: object - description: An arbitrary JSON object describing the attributes of a thing. - Definition: - type: string - description: 'A single fully qualified identifier of a definition in the form ''::'' or a valid HTTP(s) URL' - pattern: '([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+)' - FeatureDefinition: - type: array - description: The definitions of a feature. - items: - type: string - description: 'A single fully qualified identifier of a feature definition in the form ''::'' or a valid HTTP(s) URL' - pattern: '([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+):([_a-zA-Z0-9\-.]+)' - FeatureProperties: - type: object - description: An arbitrary JSON object describing the properties of a feature. - FeatureDesiredProperties: - type: object - description: An arbitrary JSON object describing the desired properties of a feature. - Feature: - type: object - properties: - definition: - $ref: '#/components/schemas/FeatureDefinition' - properties: - $ref: '#/components/schemas/FeatureProperties' - desiredProperties: - $ref: '#/components/schemas/FeatureDesiredProperties' - SearchResultThings: - properties: - items: - type: array - items: - $ref: '#/components/schemas/Thing' - cursor: - type: string - SearchResultThingsCount: - type: integer - NewThing: - type: object - properties: - _policy: - allOf: - - $ref: '#/components/schemas/Policy' - description: |- - The initial policy to create for this thing. This will create a separate policy entity managed by resource `/policies/{thingId}`. - - - Use the placeholder `{{ request:subjectId }}` in order to let the backend insert the authenticated subjectId of the HTTP request. - _copyPolicyFrom: - type: string - description: |- - This field may contain - * the policy ID of an existing policy. - - The policy is copied and used for this newly created thing. The - caller needs to have READ and WRITE* access to the policy. - * a placeholder reference to a thing in the format {{ ref:things/[thingId]/policyId }} where you need to - replace [thingId] with a valid thing ID. - - The newly created thing will then obtain a copy of the policy of - the referenced thing. The caller needs to have READ access to the thing and READ and WRITE* - access to the policy of the thing. - - - * The check for WRITE permission avoids locking yourself out of the newly created policy. You can - bypass this check by setting the header `allowPolicyLockout` to `true`. Be aware that the authorized - subject cannot modify the policy if you do not assign WRITE permission on the policy resource! - - If you want to specify a policy ID for the copied policy, use the policyId field. - - This field must not be used together with the field _policy. If you specify both _policy and _copyPolicyFrom - this will lead to an error response. - policyId: - type: string - description: |- - The policy ID used for controlling access to this thing. Managed by - resource `/policies/{policyId}`. - definition: - $ref: '#/components/schemas/Definition' - attributes: - $ref: '#/components/schemas/Attributes' - features: - $ref: '#/components/schemas/Features' - required: - - policyId - PatchThing: - type: object - properties: - thingId: - type: string - description: Unique identifier representing the thing - policyId: - type: string - description: 'The ID of the policy which controls the access to this thing. policies are managed by resource `/policies/{policyId}`' - definition: - $ref: '#/components/schemas/Definition' - attributes: - $ref: '#/components/schemas/Attributes' - features: - $ref: '#/components/schemas/Features' - required: - - thingId - - policyId - Thing: - type: object - properties: - thingId: - type: string - description: Unique identifier representing the thing - policyId: - type: string - description: 'The ID of the policy which controls the access to this thing. policies are managed by resource `/policies/{policyId}`' - definition: - $ref: '#/components/schemas/Definition' - attributes: - $ref: '#/components/schemas/Attributes' - features: - $ref: '#/components/schemas/Features' - _revision: - type: string - description: |- - _(read-only)_ The revision is a counter which is incremented on each modification of a Thing. This field - is not returned by default but must be selected explicitly. - _created: - type: string - description: |- - _(read-only)_ The created timestamp of the Thing in ISO-8601 UTC format. The timestamp is set on creation - of a Thing. This field is not returned by default but must be selected explicitly. - _modified: - type: string - description: |- - _(read-only)_ The modified timestamp of the Thing in ISO-8601 UTC format. The timestamp is set on each - modification of a Thing. This field is not returned by default but must be selected explicitly. - _metadata: - type: object - description: _(read-only)_ The Metadata of the Thing. This field is not returned by default but must be selected explicitly. - required: - - thingId - - policyId - MigrateThingDefinitionRequest: - type: object - description: JSON payload to migrate the definition of a Thing. - properties: - thingDefinitionUrl: - type: string - format: uri - description: The URL of the new Thing definition to be applied. - example: 'https://models.example.com/thing-definition-1.0.0.tm.jsonld' - migrationPayload: - type: object - description: | - Optional migration payload with updates to attributes and features. - String values may contain {{ thing-json: }} placeholders to reference - existing Thing data (e.g. attributes/location, features/sensor/properties/temp). - Resolved values keep their JSON type; missing paths cause the request to fail. - properties: - attributes: - type: object - additionalProperties: true - description: Attributes to be updated in the thing. - example: - manufacturer: New Corp - location: 'Berlin, main floor' - features: - type: object - additionalProperties: - type: object - properties: - properties: - type: object - additionalProperties: true - description: Features to be updated in the thing. - example: - thermostat: - properties: - status: - temperature: - value: 23.5 - unit: DEGREE_CELSIUS - patchConditions: - type: object - description: Optional conditions to apply the migration only if the existing thing matches the specified values. - additionalProperties: - type: string - example: - 'thing:/features/thermostat': not(exists(/features/thermostat)) - initializeMissingPropertiesFromDefaults: - type: boolean - description: Flag indicating whether missing properties should be initialized with default values. - example: true - default: false - required: - - thingDefinitionUrl - MigrateThingDefinitionResponse: - type: object - description: Response payload after applying or simulating a migration to a Thing. - properties: - thingId: - type: string - description: Unique identifier representing the migrated Thing. - patch: - type: object - description: The patch containing updates to the Thing. - properties: - definition: - $ref: '#/components/schemas/Definition' - attributes: - $ref: '#/components/schemas/Attributes' - features: - $ref: '#/components/schemas/Features' - mergeStatus: - type: string - description: | - Indicates the result of the migration process. - - `APPLIED`: The migration was successfully applied. - - `DRY_RUN`: The migration result was calculated but not applied. - enum: - - APPLIED - - DRY_RUN - example: APPLIED - required: - - thingId - - patch - - mergeStatus - NewPolicy: - type: object - description: Policy consisting of policy entries - properties: - entries: - $ref: '#/components/schemas/PolicyEntries' - imports: - $ref: '#/components/schemas/PolicyImports' - required: - - entries - Policy: - type: object - description: Policy consisting of policy entries - properties: - policyId: - type: string - description: Unique identifier representing the policy - entries: - $ref: '#/components/schemas/PolicyEntries' - imports: - $ref: '#/components/schemas/PolicyImports' - required: - - policyId - - entries - PolicyImports: - type: object - description: Policy imports containing one policy import for each key. The key is the policy ID of the referenced policy. - properties: - policyImport1: - $ref: '#/components/schemas/PolicyImport' - policyImportN: - $ref: '#/components/schemas/PolicyImport' - example: - 'com.acme:policyId1': - entries: - - label1 - - label2 - 'com.acme:policyId2': - entries: - - import - 'com.acme:policyId3': {} - PolicyImport: - type: object - description: Single policy import defining which policy entries of the referenced policy are imported. - properties: - entries: - type: array - default: [] - description: |- - The policy entries to import from the referenced policy identified by their labels. - In case the field is omitted or an empty array is provided, - all policy entries defined as implicit ("importable": "implicit") are imported. - items: - type: string - description: Label of a policy entry to import from the referenced policy. - transitiveImports: - $ref: '#/components/schemas/TransitiveImports' - example: - entries: - - default - - import - Importable: - type: string - description: |- - Controls the import behavior of this policy entry i.e. whether this policy entry is implicitly, - explicitly or never imported when referenced from another policy. - * `implicit` (default): the policy entry is imported without being listed in the importing policy individually - * `explicit`: the policy entry is only imported if it is listed in the importing policy - * `never`: the policy entry is not imported, regardless of being listed in the importing policy - If the field is not specified, default value is `implicit`. - enum: - - implicit - - explicit - - never - default: implicit - example: explicit - PolicyEntries: - type: object - description: Policy entries containing one policy entry for each arbitrary `label` key - properties: - label1: - $ref: '#/components/schemas/PolicyEntry' - labelN: - $ref: '#/components/schemas/PolicyEntry' - PolicyEntry: - type: object - description: |- - Single policy entry. Both `subjects` and `resources` are optional — they - default to empty when absent. An entry may define `references` to inherit - subjects, resources, and namespaces from other entries (local or imported). - properties: - subjects: - $ref: '#/components/schemas/Subjects' - resources: - $ref: '#/components/schemas/Resources' - namespaces: - type: array - description: |- - Restricts this policy entry to things whose namespace matches at least one pattern. - If omitted or empty, the entry applies to all namespaces. - * `com.acme` matches only that exact namespace - * `com.acme.*` matches namespaces below `com.acme`, but not `com.acme` itself - items: - type: string - example: - - com.acme - - com.acme.* - importable: - $ref: '#/components/schemas/Importable' - allowedAdditions: - $ref: '#/components/schemas/AllowedAdditions' - references: - $ref: '#/components/schemas/References' - Subjects: - type: object - description: A SubjectEntry defines who is addressed. - properties: - 'nginx:subjectId1': - $ref: '#/components/schemas/SubjectEntry' - 'nginx:subjectIdN': - $ref: '#/components/schemas/SubjectEntry' - SubjectEntry: - type: object - description: Single (Authorization) Subject entry holding its type. - required: - - type - properties: - type: - type: string - description: 'The type is offered only for documentation purposes. You are not restricted to any specific types, but we recommend to use it to specify the kind of the subject as shown in our examples.' - expiry: - type: string - description: The optional expiry timestamp (formatted in ISO-8601) indicates how long this subject should be considered active before it is automatically deleted from the Policy. - format: date-time - announcement: - $ref: '#/components/schemas/SubjectAnnouncement' - example: - type: 'This is some description for this subject, adjust as needed.' - expiry: '2020-12-07T11:36:40Z' - announcement: - beforeExpiry: 5m - whenDeleted: true - Resources: - type: object - description: |- - (Authorization) Resources containing one ResourceEntry for each - `type:path` key, `type` being one of the following `thing`, `policy`, `message`. - additionalProperties: - $ref: '#/components/schemas/ResourceEntry' - example: - 'thing:/': - grant: - - READ - - WRITE - revoke: null - 'thing:/attributes/some/path': - grant: null - revoke: - - READ - 'policy:/': - grant: - - READ - - WRITE - revoke: null - 'message:/': - grant: - - READ - - WRITE - revoke: null - ResourceEntry: - type: object - description: |- - Single (Authorization) Resource entry defining permissions per effect. - Allowed effects are `grant` and `revoke`. - properties: - grant: - type: array - items: - $ref: '#/components/schemas/Permission' - revoke: - type: array - items: - $ref: '#/components/schemas/Permission' - Permission: - type: string - description: A Permission allows a certain action on an entity - enum: - - READ - - WRITE - AllowedAdditions: - type: array - description: |- - Defines which types of additions are allowed when this entry is referenced by other entries - via `references`. - - Semantics: - * Field absent (omitted) — no restriction; the referencing entry's own subjects, resources, - and namespaces are merged in as usual. This is the upgrade-friendly default. - * Field present and empty (`[]`) — no additions allowed; only the referenced entry's content is - effective. - * Field present with values — only the listed kinds of additions survive on the referencing entry. - - This field is enforced as a runtime filter, not as a write-time policy contract: a referencing - entry that declares own subjects/resources/namespaces not permitted here can still be persisted, - but the disallowed own additions are silently stripped at enforcement time. The same filter - applies whether the reference is local (within the same policy) or an import reference. - * `subjects` — allows referencing entries to add additional subjects on top of this entry - * `resources` — allows referencing entries to add additional resources on top of this entry - * `namespaces` — allows referencing entries to add additional namespace patterns on top of this entry - items: - type: string - enum: - - subjects - - resources - - namespaces - example: - - subjects - TransitiveImports: - type: array - description: |- - List of policy IDs from the imported policy's own imports that should be resolved transitively - before extracting entries. This enables multi-level import chains where a template policy defines - resources and an intermediate policy defines entries with "references" that add local subjects. - - Each entry is the policy ID of a policy that the directly imported policy itself imports from. - Only the listed policy IDs are resolved — this is an explicit whitelist, not a recursive flag. - items: - type: string - description: Policy ID of a policy that the imported policy itself imports from. - example: - - 'org.eclipse.ditto:policy-template' - References: - type: array - description: |- - An optional list of references to other policy entries. Each reference points to an entry - either in the same policy (local reference) or in an imported policy (import reference). - When set, subjects, resources, and namespaces from the referenced entries are additively - merged into this entry. - - * Import reference: contains both `entry` (the label) and `import` (the policy ID of the imported policy) - * Local reference: contains only `entry` (the label of another entry in the same policy) - items: - type: object - description: A single reference to a policy entry. - properties: - entry: - type: string - description: The label of the referenced entry. - import: - type: string - description: |- - The ID of the imported policy this reference points to. - If absent, the reference points to a local entry within the same policy. - required: - - entry - example: - - entry: operator - import: 'energy-corp:power-plant-roles' - - entry: local-admin - SubjectAnnouncement: - type: object - description: Settings for announcements to be made about the subject. - properties: - beforeExpiry: - type: string - description: |- - The duration before expiry when an announcement should be made. - Must be a positive integer followed by one of `h` (hour), `m` (minute) or `s` (second). - whenDeleted: - type: boolean - description: Whether an announcement should be made when this subject is deleted. - requestedAcks: - type: object - description: Settings to enable at-least-once delivery for policy announcements. - properties: - labels: - type: array - description: Acknowledgement labels to request when an announcement is published. - items: - type: string - timeout: - type: string - description: How long to wait for requested announcements before retrying publication of an announcement. - example: - labels: - - 'my-connection-id:my-issued-acknowledgement' - timeout: 5s - randomizationInterval: - type: string - default: 5m - description: 'Interval in which the announcement can be sent earlier than the configured `beforeExpiry`. The actual point in time when the announcement will be sent is `beforeExpire` plus a randomly chosen time within the `randomizationInterval`. E.g assuming `beforeExpiry` is set to 5m and `randomizationInterval` is set to 1m, the announcements will be sent between 5 and 6 minutes before the subject expires. If omitted, the default value will be applied. If set to minimum, no randomization will be applied.' - example: - beforeExpiry: 5m - whenDeleted: true - randomizationInterval: 5m - Features: - type: object - description: |- - List of features where the key represents the `featureId` of each feature. - The `featureId` key must be unique in the list. - additionalProperties: - $ref: '#/components/schemas/Feature' - Connection: - allOf: - - type: object - properties: - id: - type: string - description: The generated unique identifier of the connection - - $ref: '#/components/schemas/NewConnection' - NewConnection: - type: object - required: - - connectionType - - connectionStatus - - uri - - sources - - targets - properties: - name: - type: string - description: The name of the connection - connectionType: - $ref: '#/components/schemas/ConnectionType' - connectionStatus: - $ref: '#/components/schemas/ConnectivityStatus' - uri: - type: string - description: The URI of the connection - sources: - $ref: '#/components/schemas/Sources' - targets: - $ref: '#/components/schemas/Targets' - specificConfig: - type: object - description: Configuration which is only applicable for a specific connection type - clientCount: - type: number - description: How many clients on different cluster nodes should establish the connection - failoverEnabled: - type: boolean - description: Whether or not failover is enabled for this connection - validateCertificates: - type: boolean - description: Whether or not to validate server certificates on connection establishment - mappingDefinitions: - $ref: '#/components/schemas/PayloadMappingDefinitions' - mappingContext: - $ref: '#/components/schemas/MappingContext' - sshTunnel: - type: object - description: The configuration of a local SSH port forwarding used to tunnel the connection to the actual endpoint. - required: - - enabled - - uri - - credentials - properties: - enabled: - type: boolean - description: Whether the tunnel is enabled - example: true - uri: - type: string - description: 'The URI of the SSH host in the format `ssh://[host]:[port]`.' - example: 'ssh://some.host:2222' - credentials: - type: object - description: The credentials used to authenticate at the SSH host. Password and public key authentication are supported. - required: - - type - - username - properties: - type: - type: string - description: The type of credentials used to authenticate. Either `password` or `public-key`. - enum: - - password - - public-key - example: password - username: - type: string - description: The username used for the authentication. - example: user42 - password: - type: string - description: The password used for authentication when credentials type `password` is used. - example: secret! - publicKey: - type: string - description: |- - Public key in PEM base64-encoded format using X.509 syntax. This field is required for credentials type - `public-key`. - example: | - -----BEGIN PUBLIC KEY----- - ... - -----END PUBLIC KEY----- - privateKey: - type: string - description: |- - Private key in PEM base64-encoded format using PKCS #8 syntax. This field is required for credentials type - `public-key`. - example: | - -----BEGIN PRIVATE KEY----- - ... - -----END PRIVATE KEY----- - validateHost: - type: boolean - description: Whether the SSH host is validated using the provided fingerprints. - example: true - knownHosts: - type: array - description: |- - A list of accepted public key fingerprints. One of these fingerprints must match the fingerprint - of the public key the SSH host provides. - example: - - 'MD5:e0:3a:34:1c:68:ed:c6:bc:7c:ca:a8:67:c7:45:2b:19' - items: - type: string - description: |- - The fingerprint is in the format which the command line tool `ssh-keygen` produces, - e.g. `MD5:e0:3a:34:1c:68:ed:c6:bc:7c:ca:a8:67:c7:45:2b:19`. The fingerprint is prefixed with the hash algorithm - used to calculate the fingerprint. Supported algorithms are `MD5`, `SHA1`, `SHA224`, `SHA256`, `SHA384` and `SHA512`. - tags: - type: array - items: - type: string - description: The tags of the connection - Sources: - type: array - title: The subscription sources of this connection - description: The subscription sources of this connection - uniqueItems: true - items: - $ref: '#/components/schemas/Source' - Source: - type: object - title: Source - description: A subscription source subscribed by this connection - properties: - addresses: - type: array - uniqueItems: true - title: Array of source addresses - description: | - The source addresses this connection consumes messages from. The "telemetry", "events", - "command_response" aliases should be used for connections of type "hono". - items: - type: string - title: Source address - description: A source address to consume messages from - consumerCount: - type: integer - title: Consumer count - description: The number of consumers that should be attached to each source address - default: 1 - qos: - type: integer - title: Quality of service level - description: Maximum Quality-of-Service level to request when subscribing for messages - authorizationContext: - type: array - title: The authorization context - description: The authorization context defines all authorization subjects associated for this source - uniqueItems: true - items: - type: string - title: Authorization Subject - description: |- - An authorization subject associated with this source. - You can either use a fixed subject or use a placeholder that resolves header values from incoming messages. - For example to use the `device_id` header in the subject, you can specify the placeholder - `{{ header:device_id }}` which is then replaced by Ditto when a message from this source is processed. - By using a placeholder you can access any header value: `{{ header: }}` - example: - - 'ditto:myAuthorizationSubject' - - 'device:{{ header:device_id }}' - enforcement: - type: object - title: Enforcement configuration - description: Defines an enforcement for this source to make sure that a device can only access its associated Thing. - required: - - input - - filters - properties: - input: - type: string - title: Input value of enforcement - description: |- - The input value of the enforcement that should identify the origin of the message (e.g. a - device id). You can use placeholders within this field depending on the connection type. E.g. for AMQP - 1.0 connections you can use `{{ header:[any-header-name] }}` to resolve the value from a message header. - example: '{{ header:device_id }}' - filters: - type: array - title: The enforcement filters - description: An array of filters. One of the defined filters must match the input value from the message otherwise the message is rejected. - uniqueItems: true - items: - type: string - title: Enforcement filter - description: |- - A filter that must match the input value for a message to be accepted. You can use the placeholders - `{{ thing:id }}`, `{{ thing:name }}` or `{{ thing:namespace }}` in a filter. - example: - - '{{ thing:id }}' - - '{{ thing:namespace }}/{{ thing:name }}' - acknowledgementRequests: - type: object - title: Acknowledgement requests configuration - description: Contains requests to acknowledgements which must be fulfilled before a message consumed from this source is technically settled/ACKed at the e.g. message broker. - additionalProperties: false - properties: - includes: - type: array - title: Included acknowledgement requests - description: Acknowledgement requests to be included for each message consumed by this source. - items: - title: String representation of a single acknowledgement request - type: string - filter: - type: string - title: Filter expression whether to include acknowledgements at all - description: 'Optional filter to be applied to the requested acknowledgements - takes an `fn:filter()` function expression' - example: - - 'fn:filter(header:qos,''ne'',0)' - required: - - includes - payloadMapping: - type: array - title: The payload mappings - description: A list of payload mappings that are applied to messages received via this source. If no payload mapping is specified the standard Ditto mapping is used as default. - items: - type: string - title: Payload Mapping - description: References a payload mapping definition by its ID (the key of the PayloadMappingDefinition) - example: - - Ditto - - status - headerMapping: - type: object - title: Header mapping configuration - description: Ditto protocol headers computed from external headers and certain properties of the Ditto protocol messages created by payload mapping. - replyTarget: - type: object - title: Reply target configuration - description: Configuration for sending responses of incoming commands. - additionalProperties: false - properties: - enabled: - type: boolean - title: Whether reply target is enabled - description: Whether reply target is enabled. - address: - type: string - title: Reply target address - description: |- - The target address where responses of incoming commands from the parent source are published to. - The following placeholders are allowed within the target address: - - * Thing ID: `{{ thing:id }}` - - * Thing Namespace: `{{ thing:namespace }}` - - * Thing Name: `{{ thing:name }}` (the part of the ID without the namespace) - - * Ditto protocol topic attribute: `{{ topic:[topic-placeholder-attr] }}` - - * Ditto protocol header value: `{{ header:[any-header-name] }}` - - If placeholder resolution fails for a response, then the response is dropped. - NOTE Use "command" alias for connections of type "hono". - example: - - '{{ header:device_id }}' - - '{{ source:address }}' - headerMapping: - type: object - title: Header mapping configuration - description: External headers computed from headers and other properties of Ditto protocol messages. - expectedResponseTypes: - type: array - title: Expected response types - description: Contains a list of response types that should be published to the reply target. - uniqueItems: true - items: - type: string - title: Response types - enum: - - response - - error - - nack - required: - - address - Targets: - type: array - title: The publish targets of this connection - description: The publish targets of this connection - uniqueItems: true - items: - $ref: '#/components/schemas/Target' - Target: - type: object - title: Target - description: A publish target served by this connection - properties: - address: - type: string - title: Target address - description: |- - The target address where events, commands and messages are published to. - The following placeholders are allowed within the target address: - - * Thing ID: `{{ thing:id }}` - - * Thing Namespace: `{{ thing:namespace }}` - - * Thing Name: `{{ thing:name }}` (the part of the ID without the namespace) - NOTE Use "command" alias for connections of type "hono". - topics: - type: array - title: Topics - description: The topics to which this target is registered for - uniqueItems: true - items: - type: string - enum: - - _/_/things/twin/events - - _/_/things/live/commands - - _/_/things/live/events - - _/_/things/live/messages - - _/_/policies/announcements - - _/_/connections/announcements - title: Subscribed topics - description: |- - Contains the type of messages that are delivered to this target. You can receive - - * Thing events: `_/_/things/twin/events` (notification about twin change) - - * Live events: `_/_/things/live/events` - - * Live commands: `_/_/things/live/commands` - - * Live messages: `_/_/things/live/messages` - - * Policy announcements: `_/_/policies/announcements` - - * Connection announcements: `_/_/connections/announcements` - qos: - type: integer - title: Quality of service level - description: Maximum Quality-of-Service level to request when subscribing for messages - authorizationContext: - type: array - title: The authorisation context - description: The authorization context defines all authorization subjects associated for this target - uniqueItems: true - items: - type: string - title: Authorization Subject - description: An authorization subject associated with this target - example: - - 'ditto:myAuthorizationSubject' - issuedAcknowledgementLabel: - type: string - title: Issued acknowledgement label for this target - description: The optional label of an acknowledgement which should automatically be issued by this target based on the technical settlement/ACK the connection channel provides. - payloadMapping: - type: array - title: The payload mappings - description: A list of payload mappings that are applied to messages sent via this target. If no payload mapping is specified the standard Ditto mapping is used as default. - items: - type: string - title: Payload Mapping - description: References a payload mapping definition by its ID (the key of the PayloadMappingDefinition) - example: - - javascript - headerMapping: - type: object - title: Header mapping configuration - description: External headers computed from headers and other properties of Ditto protocol messages. - ConnectionType: - type: string - description: The type of a connection - enum: - - amqp-091 - - amqp-10 - - http-push - - mqtt - - mqtt-5 - - 'kafka,' - - hono - ConnectivityStatus: - type: string - description: The status of a connection or resource - enum: - - open - - closed - - failed - - misconfigured - - unknown - PayloadMappingDefinitions: - type: object - additionalProperties: - $ref: '#/components/schemas/PayloadMappingDefinition' - description: |- - List of mapping definitions where the key represents the ID of each mapping that can be used in sources and - targets to reference a mapping. - PayloadMappingDefinition: - type: object - description: A mapping definition consisting of the used mappingEngine and the options required by this engine. - required: - - mappingEngine - - options - properties: - mappingEngine: - type: string - description: |- - The mapping engine used to process incoming and outgoing messages. Available mapping engines are - `JavaScript`, `Normalized`, `ConnectionStatus`, `RawMessage`, `Ditto`, `ImplicitThingCreation`, and `UpdateTwinWithLiveResponse`. - options: - type: object - description: |- - Configuration options specific to the used mapping engine: - - #### JavaScript - * `incomingScript` (`string`, required): The mapping script for incoming messages - * `outgoingScript` (`string`, required): The mapping script for outgoing messages - * `loadBytebufferJS` (`boolean`, optional): Whether or not ByteBufferJS library should be included - (default: `false`) - * `loadLongJS` (`boolean`, optional): Whether or not LongJS library should be included (default: `false`) - - #### Normalized - * `fields` (`string`, optional): Comma separated list of fields included in the normalized message - (default: all fields included) - - #### ConnectionStatus - * `thingId` (`string`, required): The ID of the thing - * `featureId` (`string`, optional): The ID of the modified feature (default: `ConnectionStatus`) - - #### RawMessage - * `outgoingContentType` (`string`, optional): The fallback content type for outgoing messages. - * `incomingMessageHeaders` (`object`, optional): The fallback headers for incoming messages - containing the necessary information to map them to message commands and responses. - The relevant header keys are: `content-type`, `ditto-message-subject`, `ditto-message-direction`, - `ditto-message-thing-id`, `ditto-message-feature-id` and `status`. The header values may contain - placeholder expressions. - - #### Ditto - * no options required - - #### ImplicitThingCreation - * `thing` (`object`, required): The template of the thing to be implicitly created - - #### UpdateTwinWithLiveResponse - * `dittoHeadersForMerge` (`object`, optional): The Ditto headers to use for constructing the "merge thing" - command for updating the twin, may for example add a condition to apply in order to update the twin - (default ditto headers: `response-required: false`, `if-match: "*"`). - incomingConditions: - type: object - description: |- - Optional conditions to be checked before applying the mapping engine to inbound messages. - Can use placeholders and functional expressions. - outgoingConditions: - type: object - description: |- - Optional conditions to be checked before applying the mapping engine to outbound messages. - Can use placeholders and functional expressions. - MappingContext: - type: object - deprecated: true - description: |- - MappingContext to apply in this connection containing JavaScript scripts mapping from external messages to - internal Ditto Protocol messages. Usage of MappingContext is deprecated, use PayloadMappingDefinitions instead. - required: - - incomingScript - - outgoingScript - - loadBytebufferJS - - loadLongJS - properties: - incomingScript: - type: string - description: The mapping script for incoming messages - outgoingScript: - type: string - description: The mapping script for outgoing messages - loadBytebufferJS: - type: boolean - description: Whether or not ByteBufferJS library should be included - loadLongJS: - type: boolean - description: Whether or not LongJS library should be included - ConnectionStatus: - type: object - description: Status of a connection and its resources - required: - - connectionId - - connectionStatus - - liveStatus - - connectedSince - properties: - connectionId: - type: string - description: The connection ID - connectionStatus: - allOf: - - $ref: '#/components/schemas/ConnectivityStatus' - description: The desired/target status of the connection - liveStatus: - allOf: - - $ref: '#/components/schemas/ConnectivityStatus' - description: The current/actual status of the connection - connectedSince: - type: string - description: The timestamp since when the connection is connected - example: '2019-01-21T08:57:24.710Z' - clientStatus: - type: array - items: - $ref: '#/components/schemas/ResourceStatus' - description: The client states of the of the connection - sourceStatus: - type: array - items: - $ref: '#/components/schemas/ResourceStatus' - description: The states of the sources the of the connection - targetStatus: - type: array - items: - $ref: '#/components/schemas/ResourceStatus' - description: The states of the targets the of the connection - sshTunnelStatus: - type: array - items: - $ref: '#/components/schemas/ResourceStatus' - description: The states of the ssh tunnel the of the connection - ResourceStatus: - type: object - description: The status of a single resource (e.g. a client or a source/target resource) - required: - - type - - client - - status - properties: - type: - type: string - description: The type of the resource - enum: - - client - - source - - target - client: - type: string - description: A client identifier where the resource is held (e.g. a cluster instance ID) - address: - type: string - description: The address information of the resource (optional) - status: - $ref: '#/components/schemas/ConnectivityStatus' - statusDetails: - type: string - description: Details to the status of the resource - inStateSince: - type: string - description: Date since when the resource is in the present state - ConnectionMetrics: - type: object - description: Metrics of a connection - required: - - connectionId - - containsFailures - - connectionMetrics - - sourceMetrics - - targetMetrics - properties: - connectionId: - type: string - description: The connection ID - containsFailures: - type: boolean - description: Whether the connection metrics contains any failures - example: false - connectionMetrics: - $ref: '#/components/schemas/OverallConnectionMetrics' - sourceMetrics: - $ref: '#/components/schemas/SourceMetrics' - targetMetrics: - $ref: '#/components/schemas/TargetMetrics' - OverallConnectionMetrics: - type: object - description: Overall metrics of the connection - required: - - inbound - - outbound - properties: - inbound: - $ref: '#/components/schemas/InboundMetrics' - outbound: - $ref: '#/components/schemas/OutboundMetrics' - SourceMetrics: - type: object - description: Source metrics of the connection - required: - - addressMetrics - properties: - addressMetrics: - type: object - additionalProperties: - $ref: '#/components/schemas/InboundMetrics' - description: Contains "inbound" from external sources consumed metric counts - TargetMetrics: - type: object - description: Target metrics of the connection - required: - - addressMetrics - properties: - addressMetrics: - type: object - additionalProperties: - $ref: '#/components/schemas/OutboundMetrics' - description: Contains "outbound" towards external targets messages metric counts - InboundMetrics: - type: object - description: Metrics of an inbound (e.g. a Source) resource - required: - - consumed - - mapped - - dropped - - enforced - properties: - consumed: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains from external sources consumed metric counts - mapped: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains mapped (payload mapping) messages metric counts - dropped: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains dropped (in the payload mapping) messages metric counts - enforced: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains enforced (e.g. source address enforcement) messages metric counts - OutboundMetrics: - type: object - description: Metrics of an outbound (e.g. a Target) resource - required: - - dispatched - - filtered - - mapped - - dropped - - published - properties: - dispatched: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains internally dispatched (e.g. a Ditto event) metric counts - filtered: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains the metric counts for messages which passed the filter (e.g. namespace or RQL filter for events) - mapped: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains mapped (payload mapping) messages metric counts - dropped: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains dropped (in the payload mapping) messages metric counts - published: - allOf: - - $ref: '#/components/schemas/TypedMetric' - description: Contains published messages metric counts meaning those messages were published to the external source - TypedMetric: - type: object - description: Metrics of a single metric `type` containing "success" and "failure" metrics - required: - - success - - failure - properties: - success: - allOf: - - $ref: '#/components/schemas/SingleMetric' - description: Contains the successfully processed message counts - failure: - allOf: - - $ref: '#/components/schemas/SingleMetric' - description: Contains the failed processed message counts - SingleMetric: - type: object - description: Contains a single metric consisting of several time intervals and counter values for those intervals including the last message date. - required: - - PT1M - - PT1H - - PT24H - - lastMessageAt - properties: - PT1M: - type: integer - description: The counter containing how many messages were processed in the last minute - example: 0 - PT1H: - type: integer - description: The counter containing how many messages were processed in the last hour - example: 42 - PT24H: - type: integer - description: The counter containing how many messages were processed in the last 24 hours / last day - example: 46346 - lastMessageAt: - type: string - description: The timestamp when the last message was processed - example: '2019-01-21T08:57:24.710Z' - ConnectionLogs: - type: object - description: Log entries of a connection. - required: - - connectionId - - connectionLogs - properties: - connectionId: - type: string - description: ID of the connection for which the log entries were logged. - example: 759304b8-8056-11e9-bc42-526af7764f64 - connectionLogs: - type: array - description: Log entries for the connection. - items: - $ref: '#/components/schemas/LogEntry' - enabledSince: - type: string - description: Since when logging is enabled. Might be missing / null if logging is not enabled. - example: '2019-01-21T08:57:24.710Z' - enabledUntil: - type: string - description: Until when logging is enabled. Might be missing / null if logging is not enabled. - example: '2019-01-22T08:57:24.710Z' - LogEntry: - type: object - description: Represents a log entry for a connection. - required: - - timestamp - - correlationId - - message - - category - - type - - level - properties: - timestamp: - type: string - description: Timestamp of the log entry. - example: '2019-01-21T08:57:24.710Z' - correlationId: - type: string - description: Correlation ID that is associated with the log entry. - example: 759304b8-8056-11e9-bc42-526af7764f64 - message: - type: string - description: The log message. - example: Successfully connected to ... at ... - category: - $ref: '#/components/schemas/LogCategory' - type: - $ref: '#/components/schemas/LogType' - level: - $ref: '#/components/schemas/LogLevel' - address: - type: string - description: Connection address on which the log occurred. - example: telemetry/address - thingId: - type: string - description: The thing for which the log entry was created. - example: 'org.ditto:theThing' - LogCategory: - type: string - description: A category to which the log entry can be referred to. - enum: - - source - - target - - response - - connection - LogType: - type: string - description: The type of a log entry describing during what kind of activity the entry was created. - enum: - - consumed - - dispatched - - filtered - - mapped - - dropped - - enforced - - published - - other - LogLevel: - type: string - description: Escalation level of a log entry. - enum: - - success - - failure - WhoAmI: - type: object - description: Contains information about the current user and the auth subjects available for the used authentication. - properties: - defaultSubject: - $ref: '#/components/schemas/WhoAmISubject' - subjects: - type: array - items: - $ref: '#/components/schemas/WhoAmISubject' - WhoAmISubject: - type: string - description: An auth subject that can be used to provide access for a caller (e.g. in subject entries of policies). - WotThingDescription: - type: object - description: A WoT Thing Description version 1.1 - properties: - '@context': - oneOf: - - type: array - items: - type: string - enum: - - 'https://www.w3.org/2019/wot/td/v1' - - 'http://www.w3.org/ns/td' - - 'https://www.w3.org/2022/wot/td/v1.1' - - type: string - enum: - - 'https://www.w3.org/2019/wot/td/v1' - - 'http://www.w3.org/ns/td' - - 'https://www.w3.org/2022/wot/td/v1.1' - example: - - 'https://www.w3.org/2022/wot/td/v1.1' - title: - type: string - example: My fancy Thing - titles: - type: object - additionalProperties: - type: string - description: - type: string - example: Does fancy stuff with IoT - descriptions: - type: object - additionalProperties: - type: string - '@type': - oneOf: - - type: string - - type: array - items: - type: string - example: Thing - id: - type: string - example: 'urn:org.eclipse.ditto:my-fancy-thing' - base: - type: string - format: iri-reference - example: 'https://ditto.eclipseprojects.io/api/2/org.eclipse.ditto:my-fancy-thing' - version: - type: object - properties: - model: - type: string - instance: - type: string - required: - - instance - example: - model: 1.0.0 - instance: 1.0.0 - links: - type: array - items: - type: object - properties: - href: - type: string - format: iri-reference - rel: - type: string - type: - type: string - anchor: - type: string - required: - - href - additionalProperties: true - security: - oneOf: - - type: string - - type: array - items: - type: string - example: basic_sc - securityDefinitions: - type: object - additionalProperties: - type: object - example: - basic_sc: - in: header - scheme: basic - support: - type: string - format: iri-reference - example: 'https://www.eclipse.dev/ditto/' - created: - type: string - format: date-time - modified: - type: string - format: date-time - forms: - type: array - items: - type: object - properties: - op: - type: string - href: - type: string - 'htv:methodName': - type: string - contentType: - type: string - additionalResponses: - type: array - items: - type: object - properties: - success: - type: boolean - schema: - type: string - properties: - type: object - additionalProperties: - type: object - actions: - type: object - additionalProperties: - type: object - events: - type: object - additionalProperties: - type: object - uriVariables: - type: object - additionalProperties: - type: object - schemaDefinitions: - type: object - additionalProperties: - type: object - profile: - oneOf: - - type: array - items: - type: string - format: iri-reference - - type: string - format: iri-reference - required: - - '@context' - - title - - security - - securityDefinitions - additionalProperties: true - TextUnauthorizeError: - type: string - example: The supplied authentication is invalid - RetrieveConfig: - type: object - properties: - gateway: - type: object - description: Module - properties: - pod: - type: object - description: Return the configuration at the path ditto.info - properties: - type: - type: string - description: 'devops.responses:ResultConfig' - status: - type: integer - description: The HTTP status - config: - type: object - description: name of service - properties: - env: - items: - type: string - properties: - PATH: - type: string - service: - items: - type: string - properties: - instance-index: - type: integer - service-name: - type: string - vm-args: - items: - type: string - RetrieveLoggingConfig: - properties: - gateway: - $ref: '#/components/schemas/Module' - Module: - type: object - description: Module - properties: - pod: - type: object - description: Details of logging configuration - properties: - type: - type: string - description: 'devops.responses:retrieveLoggerConfig' - status: - type: integer - description: The HTTP status - serviceName: - type: string - description: name of service - instance: - type: string - description: instance of module - loggerConfigs: - type: array - items: - type: object - properties: - level: - type: string - logger: - type: string - LoggingUpdateFields: - properties: - level: - type: string - logger: - type: string - description: class where apply logger level - UpdatedLogLevel: - type: object - description: Details of logging configuration - properties: - type: - type: string - description: 'devops.responses:changeLogLevel' - status: - type: integer - description: http code 200 for success operation - serviceName: - type: string - description: name of service that has been updated - instance: - type: string - description: identifier of pod instance - successfull: - type: boolean - description: outcome of the change - ModuleUpdatedLogLevel: - type: object - description: Module that has been updated - properties: - pod: - $ref: '#/components/schemas/UpdatedLogLevel' - ResultUpdateRequest: - type: object - properties: - gateway: - $ref: '#/components/schemas/ModuleUpdatedLogLevel' - things-search: - $ref: '#/components/schemas/ModuleUpdatedLogLevel' - policies: - $ref: '#/components/schemas/ModuleUpdatedLogLevel' - things: - $ref: '#/components/schemas/ModuleUpdatedLogLevel' - connectivity: - $ref: '#/components/schemas/ModuleUpdatedLogLevel' - ModuleConfigService: - type: object - description: Module - properties: - pod: - $ref: '#/components/schemas/ResultConfigService' - ResultConfigService: - type: object - description: Details of specific service instance. - properties: - type: - type: string - description: 'devops.responses:ResultConfigService' - status: - type: integer - description: The HTTP status - config: - type: object - description: name of service - properties: - cluster: - items: - type: string - properties: - number-of-shards: - type: integer - gateway: - items: - type: object - properties: - authentication: - type: object - properties: - devops: - type: object - properties: - password: - type: string - secured: - type: boolean - RetrieveConfigService: - type: object - properties: - gateway: - $ref: '#/components/schemas/ModuleConfigService' - BasePiggybackCommandRequestSchema: - properties: - targetActorSelection: - type: string - headers: - type: object - properties: - aggregate: - type: boolean - default: false - is-group-topic: - type: boolean - default: true - piggybackCommand: - type: object - properties: - type: - type: string - PiggybackManagingBackgroundCleanup: - properties: - targetActorSelection: - type: string - headers: - type: object - properties: - aggregate: - type: boolean - default: false - is-group-topic: - type: boolean - default: true - piggybackCommand: - type: object - properties: - type: - type: string - SearchFilterProperty: - description: |- - - #### Filter predicates: - - * ```eq({property},{value})``` (i.e. equal to the given value) - - * ```ne({property},{value})``` (i.e. not equal to the given value) - - * ```gt({property},{value})``` (i.e. greater than the given value) - - * ```ge({property},{value})``` (i.e. equal to the given value or greater than it) - - * ```lt({property},{value})``` (i.e. lower than the given value or equal to it) - - * ```le({property},{value})``` (i.e. lower than the given value) - - * ```in({property},{value},{value},...)``` (i.e. contains at least one of the values listed) - - * ```like({property},{value})``` (i.e. contains values similar to the expressions listed) - - * ```ilike({property},{value})``` (i.e. contains values similar and case insensitive to the expressions listed) - - * ```exists({property})``` (i.e. all things in which the given path exists) - - * ```empty({property})``` (i.e. all things in which the given path is absent, null, an empty array, an empty object or an empty string) - - - Note: When using filter operations, only things with the specified properties are returned. - For example, the filter `ne(attributes/owner, "SID123")` will only return things that do have - the `owner` attribute. - - - #### Logical operations: - - - * ```and({query},{query},...)``` - - * ```or({query},{query},...)``` - - * ```not({query})``` - - - #### Examples: - - * ```eq(attributes/location,"kitchen")``` - - * ```ge(thingId,"myThing1")``` - - * ```gt(_created,"2020-08-05T12:17")``` - - * ```exists(features/featureId)``` - - * ```empty(attributes/tags)``` - - * ```and(eq(attributes/location,"kitchen"),eq(attributes/color,"red"))``` - - * ```or(eq(attributes/location,"kitchen"),eq(attributes/location,"living-room"))``` - - * ```like(attributes/key1,"known-chars-at-start*")``` - - * ```like(attributes/key1,"*known-chars-at-end")``` - - * ```like(attributes/key1,"*known-chars-in-between*")``` - - * ```like(attributes/key1,"just-som?-char?-unkn?wn")``` - - The `like` filters with the wildcard `*` at the beginning can slow down your search request. - type: string - NamespaceProperty: - description: |- - A comma-separated list of namespaces. This list is used to limit the query to things in the given namespaces - only. - - - #### Examples: - - * `?namespaces=com.example.namespace` - - * `?namespaces=com.example.namespace1,com.example.namespace2` - type: string - ConfigOverrides: - type: object - description: Config overrides for a dynamic config section. - properties: - enabled: - type: boolean - log-warning-instead-of-failing-api-calls: - type: boolean - thing: - $ref: '#/components/schemas/ThingValidationConfig' - feature: - $ref: '#/components/schemas/FeatureValidationConfig' - required: - - enabled - - thing - - feature - ValidationContext: - type: object - description: Validation context for dynamic config section. - properties: - ditto-headers-patterns: - type: array - items: - type: object - additionalProperties: - type: string - thing-definition-patterns: - type: array - items: - type: string - feature-definition-patterns: - type: array - items: - type: string - scope-id: - type: string - required: - - scope-id - ThingValidationConfig: - type: object - description: Thing validation config. - properties: - enforce: - type: object - properties: - enforce-thing-description-modification: - type: boolean - attributes: - type: boolean - inbox-messages-input: - type: boolean - inbox-messages-output: - type: boolean - outbox-messages: - type: boolean - forbid: - type: object - properties: - thing-description-deletion: - type: boolean - non-modeled-attributes: - type: boolean - non-modeled-inbox-messages: - type: boolean - non-modeled-outbox-messages: - type: boolean - FeatureValidationConfig: - type: object - description: Feature validation config. - properties: - enforce: - type: object - properties: - featureDescriptionModification: - type: boolean - presenceOfModeledFeatures: - type: boolean - properties: - type: boolean - desiredProperties: - type: boolean - inbox-messages-input: - type: boolean - inbox-messages-output: - type: boolean - outbox-messages: - type: boolean - forbid: - type: object - properties: - featureDescriptionDeletion: - type: boolean - nonModeledFeatures: - type: boolean - nonModeledProperties: - type: boolean - nonModeledDesiredProperties: - type: boolean - non-modeled-inbox-messages: - type: boolean - non-modeled-outbox-messages: - type: boolean - DynamicValidationConfig: - type: object - description: Dynamic config section for request/response. - properties: - scope-id: - type: string - validation-context: - $ref: '#/components/schemas/ValidationContext' - config-overrides: - $ref: '#/components/schemas/ConfigOverrides' - required: - - scope-id - - validation-context - - config-overrides - WotValidationConfig: - type: object - description: WoT validation configuration object. - properties: - configId: - type: string - description: The unique ID of the config. - enabled: - type: boolean - description: Whether WoT validation is enabled globally. Defaults to true if not specified. - log-warning-instead-of-failing-api-calls: - type: boolean - thing: - $ref: '#/components/schemas/ThingValidationConfig' - feature: - $ref: '#/components/schemas/FeatureValidationConfig' - dynamic-config: - type: array - items: - $ref: '#/components/schemas/DynamicValidationConfig' - revision: - type: integer - format: int64 - created: - type: string - format: date-time - modified: - type: string - format: date-time - deleted: - type: boolean - metadata: - type: object - required: - - configId - - thing - - feature - securitySchemes: - NginxBasic: - type: http - description: Eclipse Ditto sandbox demo user (demo1 ... demo9) + password (demo) - scheme: basic - Bearer: - type: http - scheme: bearer - bearerFormat: JWT - description: A JSON Web Token issued by a supported OAuth 2.0 Identity Provider. - OpenIDConnect: - type: openIdConnect - description: OpenID Connect Discovery URL. The placeholder is replaced by Swagger UI when configured. - openIdConnectUrl: __OIDC_DISCOVERY_URL__ - DevOpsBasic: - type: http - description: Eclipse Ditto devops user (devops) + password (foobar) - scheme: basic - DevOpsBearer: - type: http - scheme: bearer - bearerFormat: JWT - description: A JSON Web Token issued by a supported OAuth 2.0 Identity Provider for the Eclipse Ditto devops user. diff --git a/mcp/examples/ditto-oidc-write.json b/mcp/examples/ditto-oidc-write.json index 6733ed8d91..ea2f32061b 100644 --- a/mcp/examples/ditto-oidc-write.json +++ b/mcp/examples/ditto-oidc-write.json @@ -5,14 +5,19 @@ "credential": { "kind": "oidc", "tokenUrl": "https://idp.example/token", - "clientId": "REPLACE_ME", - "clientSecret": "REPLACE_ME", - "devops": true + "clientId": "REPLACE_ME_APP_CLIENT", + "clientSecret": "REPLACE_ME" + }, + "devopsCredential": { + "kind": "oidc", + "tokenUrl": "https://idp.example/token", + "clientId": "REPLACE_ME_DEVOPS_CLIENT", + "clientSecret": "REPLACE_ME" }, "policy": { "allowMethods": ["GET"], "writeAllowlist": ["putThing"], - "sudoAllowlist": ["sudoRetrieveThing"] + "sudoAllowlist": ["sudoRetrieveThing", "getConnections"] } } } diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts index a19fd7c16c..842be8235e 100644 --- a/mcp/src/config/load.test.ts +++ b/mcp/src/config/load.test.ts @@ -56,15 +56,47 @@ describe("loadConfig", () => { expect(() => loadConfig(file)).toThrow(); }); - it("accepts an oidc credential config", () => { + it("accepts an oidc credential and an optional devopsCredential", () => { const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); const file = join(dir, "c.json"); writeFileSync(file, JSON.stringify({ - ditto: { enabled: true, credential: { kind: "oidc", tokenUrl: "https://idp/token", clientId: "c", clientSecret: "s", scope: "ditto", devops: true } }, + ditto: { + enabled: true, + credential: { kind: "oidc", tokenUrl: "https://idp/token", clientId: "c", clientSecret: "s", scope: "ditto" }, + devopsCredential: { kind: "oidc", tokenUrl: "https://idp/token", clientId: "dev", clientSecret: "s2" }, + }, })); const cfg = loadConfig(file); expect(cfg.ditto.credential.kind).toBe("oidc"); expect(cfg.ditto.credential.tokenUrl).toBe("https://idp/token"); - expect(cfg.ditto.credential.devops).toBe(true); + expect(cfg.ditto.devopsCredential?.clientId).toBe("dev"); + }); + + it("rejects the removed devops credential kind", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ ditto: { enabled: true, credential: { kind: "devops", username: "d", password: "s" } } })); + expect(() => loadConfig(file)).toThrow(); + }); +}); + +describe("openApi version config", () => { + it("defaults versionUrlTemplate to the eclipse-ditto raw URL", () => { + const cfg = loadConfig(); + expect(cfg.ditto.openApi.version).toBeUndefined(); + expect(cfg.ditto.openApi.versionUrlTemplate).toBe( + "https://raw.githubusercontent.com/eclipse-ditto/ditto/${version}/documentation/src/main/resources/openapi/ditto-api-2.yml", + ); + }); + + it("accepts an explicit version and custom template", () => { + const dir = mkdtempSync(join(tmpdir(), "ditto-mcp-")); + const file = join(dir, "c.json"); + writeFileSync(file, JSON.stringify({ + ditto: { openApi: { version: "3.6.0", versionUrlTemplate: "https://mirror.example/${version}/spec.yml" } }, + })); + const cfg = loadConfig(file); + expect(cfg.ditto.openApi.version).toBe("3.6.0"); + expect(cfg.ditto.openApi.versionUrlTemplate).toBe("https://mirror.example/${version}/spec.yml"); }); }); diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index cb2f36dd5b..67bb22e9dd 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -1,5 +1,17 @@ import { z } from "zod"; +const CredentialSchema = z.object({ + kind: z.enum(["basic", "oidc"]).default("basic"), + username: z.string().optional(), + password: z.string().optional(), + tokenUrl: z.string().optional(), + clientId: z.string().optional(), + clientSecret: z.string().optional(), + scope: z.string().optional(), +}); + +export type CredentialConfig = z.infer; + export const AppConfigSchema = z .object({ server: z @@ -84,20 +96,19 @@ export const AppConfigSchema = z enabled: z.boolean().default(false), baseUrl: z.string().optional(), openApi: z - .object({ path: z.string().optional(), url: z.string().optional() }) - .default({}), - credential: z .object({ - kind: z.enum(["basic", "devops", "oidc"]).default("basic"), - username: z.string().optional(), - password: z.string().optional(), - tokenUrl: z.string().optional(), - clientId: z.string().optional(), - clientSecret: z.string().optional(), - scope: z.string().optional(), - devops: z.boolean().optional(), + path: z.string().optional(), + url: z.string().optional(), + version: z.string().optional(), + versionUrlTemplate: z + .string() + .default( + "https://raw.githubusercontent.com/eclipse-ditto/ditto/${version}/documentation/src/main/resources/openapi/ditto-api-2.yml", + ), }) - .default({ kind: "basic" }), + .default({}), + credential: CredentialSchema.default({ kind: "basic" }), + devopsCredential: CredentialSchema.optional(), policy: z .object({ allowMethods: z.array(z.string()).default(["GET"]), diff --git a/mcp/src/ditto/action-tool.test.ts b/mcp/src/ditto/action-tool.test.ts index 3fb5c9c2cf..b291e398a1 100644 --- a/mcp/src/ditto/action-tool.test.ts +++ b/mcp/src/ditto/action-tool.test.ts @@ -21,8 +21,8 @@ describe("operationToTool", () => { fake = await startFakeDitto(() => ({ status: 200, body: JSON.stringify({ thingId: "ns:1" }) })); const client = new HttpDittoClient(fake.baseUrl); const config = cfg({ baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" } }); - const configCredential = createConfigCredential(config); - const tool = operationToTool(op({}), client, configCredential); + const configCredential = createConfigCredential(config.ditto.credential); + const tool = operationToTool(op({}), client, { standard: configCredential }); const res = await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); expect(res.content[0].text).toContain("ns:1"); expect(fake.requests[0].auth).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); @@ -32,8 +32,8 @@ describe("operationToTool", () => { fake = await startFakeDitto(() => ({ status: 200, body: "should not be called" })); const client = new HttpDittoClient(fake.baseUrl); const config = cfg({ baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" } }); - const configCredential = createConfigCredential(config); - const tool = operationToTool(op({ operationId: "sudoRetrieveThing", path: "/sudo/things/{thingId}" }), client, configCredential); + const configCredential = createConfigCredential(config.ditto.credential); + const tool = operationToTool(op({ operationId: "sudoRetrieveThing", path: "/sudo/things/{thingId}" }), client, { standard: configCredential }); const res = await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); expect(res.content[0].text.toLowerCase()).toContain("devops"); expect(fake.requests).toHaveLength(0); @@ -42,7 +42,7 @@ describe("operationToTool", () => { it("produces a typed body schema when bodySchema has props", () => { const client = new HttpDittoClient("http://fake"); const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); - const configCredential = createConfigCredential(config); + const configCredential = createConfigCredential(config.ditto.credential); const withBodySchema = op({ operationId: "postThing", method: "POST", hasBody: true, bodySchema: { props: [ @@ -50,7 +50,7 @@ describe("operationToTool", () => { { name: "counter", type: "number", required: false }, ]}, }); - const tool = operationToTool(withBodySchema, client, configCredential); + const tool = operationToTool(withBodySchema, client, { standard: configCredential }); const schema = tool.inputSchema; expect(Object.keys(schema)).toContain("body"); expect(Object.keys(schema)).toContain("thingId"); @@ -62,9 +62,9 @@ describe("operationToTool", () => { it("exposes a body arg for hasBody even without bodySchema", () => { const client = new HttpDittoClient("http://fake"); const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); - const configCredential = createConfigCredential(config); + const configCredential = createConfigCredential(config.ditto.credential); const noBodySchema = op({ operationId: "postAnything", method: "POST", hasBody: true }); - const tool = operationToTool(noBodySchema, client, configCredential); + const tool = operationToTool(noBodySchema, client, { standard: configCredential }); const schema = tool.inputSchema; expect(Object.keys(schema)).toContain("body"); }); @@ -72,7 +72,7 @@ describe("operationToTool", () => { it("accepts an object value for a $ref/unknown body prop (z.any, not z.string)", () => { const client = new HttpDittoClient("http://fake"); const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); - const configCredential = createConfigCredential(config); + const configCredential = createConfigCredential(config.ditto.credential); const withUnknownProp = op({ operationId: "putThing", method: "PUT", hasBody: true, bodySchema: { props: [ @@ -80,7 +80,7 @@ describe("operationToTool", () => { { name: "attributes", type: "unknown", required: false }, ]}, }); - const tool = operationToTool(withUnknownProp, client, configCredential); + const tool = operationToTool(withUnknownProp, client, { standard: configCredential }); const schema = tool.inputSchema; // Validate that an object value is accepted for 'attributes' (proves it's z.any, not z.string) const bodySchema = (schema.body as any); @@ -91,18 +91,52 @@ describe("operationToTool", () => { it("makes all body props optional (even required props)", () => { const client = new HttpDittoClient("http://fake"); const config = cfg({ baseUrl: "http://fake", credential: { kind: "basic", username: "u", password: "p" } }); - const configCredential = createConfigCredential(config); + const configCredential = createConfigCredential(config.ditto.credential); const withRequiredProp = op({ operationId: "postThing", method: "POST", hasBody: true, bodySchema: { props: [ { name: "thingId", type: "string", required: true }, ]}, }); - const tool = operationToTool(withRequiredProp, client, configCredential); + const tool = operationToTool(withRequiredProp, client, { standard: configCredential }); const schema = tool.inputSchema; const bodySchema = (schema.body as any); // The tool accepts a body without the "required" prop expect(() => bodySchema.parse({ body: {} })).not.toThrow(); expect(() => bodySchema.parse({ body: { otherField: "x" } })).not.toThrow(); }); + + it("routes a sudo op to the devops credential", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ + baseUrl: fake.baseUrl, + credential: { kind: "basic", username: "app", password: "p" }, + devopsCredential: { kind: "basic", username: "dev", password: "s" }, + }); + const creds = { + standard: createConfigCredential(config.ditto.credential), + devops: createConfigCredential(config.ditto.devopsCredential!), + }; + const tool = operationToTool(op({ operationId: "sudoRetrieveThing", path: "/sudo/things/{thingId}" }), client, creds); + await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(fake.requests[0].auth).toBe(`Basic ${Buffer.from("dev:s").toString("base64")}`); + }); + + it("routes a non-sudo op to the standard credential", async () => { + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const client = new HttpDittoClient(fake.baseUrl); + const config = cfg({ + baseUrl: fake.baseUrl, + credential: { kind: "basic", username: "app", password: "p" }, + devopsCredential: { kind: "basic", username: "dev", password: "s" }, + }); + const creds = { + standard: createConfigCredential(config.ditto.credential), + devops: createConfigCredential(config.ditto.devopsCredential!), + }; + const tool = operationToTool(op({}), client, creds); // getThingById, GET /things/{thingId} + await tool.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(fake.requests[0].auth).toBe(`Basic ${Buffer.from("app:p").toString("base64")}`); + }); }); diff --git a/mcp/src/ditto/action-tool.ts b/mcp/src/ditto/action-tool.ts index d1aad01937..47c2ba3e87 100644 --- a/mcp/src/ditto/action-tool.ts +++ b/mcp/src/ditto/action-tool.ts @@ -39,7 +39,12 @@ function text(t: string): ToolResult { return { content: [{ type: "text", text: t }] }; } -export function operationToTool(op: DittoOperation, client: DittoClient, configCredential: DittoCredential): ToolDef { +export interface ToolCredentials { + standard: DittoCredential; + devops?: DittoCredential; +} + +export function operationToTool(op: DittoOperation, client: DittoClient, creds: ToolCredentials): ToolDef { const sudo = isSudo(op); return { name: sanitizeName(op.operationId), @@ -48,10 +53,12 @@ export function operationToTool(op: DittoOperation, client: DittoClient, configC (sudo ? " [sudo — requires a devops credential]" : ""), inputSchema: inputSchema(op), handler: async (args: unknown, ctx: RequestCtx): Promise => { - const credential = resolveCredential(configCredential, { headers: ctx.headers }); - if (sudo && !credential.isDevops) { - return text(`Refused: "${op.operationId}" is a sudo operation and requires a devops credential.`); + // Non-sudo ops always have `standard`; sudo ops require the `devops` slot. + const base = sudo ? creds.devops : creds.standard; + if (!base) { + return text(`Refused: "${op.operationId}" is a sudo operation and requires ditto.devopsCredential.`); } + const credential = resolveCredential(base, { headers: ctx.headers }); const res = await client.execute(op, (args ?? {}) as Record, credential, ctx.signal); return text(`HTTP ${res.status}\n${res.body}`); }, diff --git a/mcp/src/ditto/action-tools.test.ts b/mcp/src/ditto/action-tools.test.ts index d00b1fbb97..18828d6cae 100644 --- a/mcp/src/ditto/action-tools.test.ts +++ b/mcp/src/ditto/action-tools.test.ts @@ -1,9 +1,23 @@ import { describe, it, expect, afterEach } from "vitest"; -import { makeActionTools } from "./action-tools.js"; +import { makeActionTools, buildVersionUrl } from "./action-tools.js"; import { HttpDittoClient } from "./client.js"; import { startFakeDitto } from "./fake-ditto.js"; import { AppConfigSchema } from "../config/schema.js"; +describe("buildVersionUrl", () => { + it("substitutes the ${version} placeholder", () => { + const template = + "https://raw.githubusercontent.com/eclipse-ditto/ditto/${version}/documentation/src/main/resources/openapi/ditto-api-2.yml"; + expect(buildVersionUrl(template, "3.6.0")).toBe( + "https://raw.githubusercontent.com/eclipse-ditto/ditto/3.6.0/documentation/src/main/resources/openapi/ditto-api-2.yml", + ); + }); + + it("substitutes every occurrence", () => { + expect(buildVersionUrl("a/${version}/b/${version}", "1.2.3")).toBe("a/1.2.3/b/1.2.3"); + }); +}); + const SPEC = { paths: { "/things/{thingId}": { @@ -67,4 +81,10 @@ describe("makeActionTools", () => { expect(names).toContain("foo_bar_3"); expect(names.length).toBe(3); }); + + it("a sudo tool refuses at call time when no devopsCredential is configured", async () => { + const { byName, config } = await tools({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: ["sudoRetrieveThing"] }); + const res = await byName.sudoRetrieveThing.handler({ thingId: "ns:1" }, { config, headers: {} } as never); + expect(res.content[0].text.toLowerCase()).toContain("devopscredential"); + }); }); diff --git a/mcp/src/ditto/action-tools.ts b/mcp/src/ditto/action-tools.ts index 8e110aa666..89e26e34c6 100644 --- a/mcp/src/ditto/action-tools.ts +++ b/mcp/src/ditto/action-tools.ts @@ -8,7 +8,7 @@ import type { DittoClient } from "./client.js"; import { HttpDittoClient } from "./client.js"; import { parseOperations } from "./openapi.js"; import { isAllowed } from "./tool-policy.js"; -import { operationToTool } from "./action-tool.js"; +import { operationToTool, type ToolCredentials } from "./action-tool.js"; import { createConfigCredential } from "./credential.js"; export interface ActionToolDeps { @@ -16,21 +16,33 @@ export interface ActionToolDeps { client?: DittoClient; } -// Pinned Ditto OpenAPI bundled with the server. Resolves to mcp/assets/ from -// both src (tsx) and dist (tsc): dirname is src/ditto or dist/ditto → ../../assets. -const BUNDLED_SPEC = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "ditto-openapi.yml"); +// Canonical Ditto OpenAPI committed in the monorepo. Resolves from both src (tsx) +// and dist (tsc): dirname is src/ditto or dist/ditto -> three levels up = repo root. +const CANONICAL_SPEC = join( + dirname(fileURLToPath(import.meta.url)), + "..", "..", "..", + "documentation", "src", "main", "resources", "openapi", "ditto-api-2.yml", +); + +// Substitute the literal ${version} placeholder in a version URL template. +export function buildVersionUrl(template: string, version: string): string { + return template.replaceAll("${version}", version); +} + +async function fetchSpec(url: string): Promise { + const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); + if (!res.ok) throw new Error(`openapi fetch ${url} -> ${res.status}`); + return YAML.parse(await res.text()); +} // YAML.parse also parses JSON, so this handles .yml, .yaml, and .json specs. async function defaultLoadSpec(config: AppConfig): Promise { - const { path, url } = config.ditto.openApi; + const { path, url, version, versionUrlTemplate } = config.ditto.openApi; if (path) return YAML.parse(await readFile(path, "utf8")); - if (url) { - const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); - if (!res.ok) throw new Error(`openapi fetch ${url} -> ${res.status}`); - return YAML.parse(await res.text()); - } - // Fallback: the pinned Ditto spec shipped with the server. - return YAML.parse(await readFile(BUNDLED_SPEC, "utf8")); + if (url) return fetchSpec(url); + if (version) return fetchSpec(buildVersionUrl(versionUrlTemplate, version)); + // Fallback: the canonical Ditto spec committed in this monorepo. + return YAML.parse(await readFile(CANONICAL_SPEC, "utf8")); } export async function makeActionTools(config: AppConfig, deps: ActionToolDeps = {}): Promise { @@ -50,10 +62,19 @@ export async function makeActionTools(config: AppConfig, deps: ActionToolDeps = process.stderr.write(`[ditto-mcp] action tools disabled: ${String(err)}\n`); return []; } - const configCredential = createConfigCredential(config); + const dc = config.ditto.devopsCredential; + if (dc?.kind === "basic" && dc.username === undefined) { + process.stderr.write( + "[ditto-mcp] devopsCredential is 'basic' but has no username; sudo requests will be sent unauthenticated\n", + ); + } + const creds: ToolCredentials = { + standard: createConfigCredential(config.ditto.credential), + devops: dc ? createConfigCredential(dc) : undefined, + }; const tools = parseOperations(spec) .filter((op) => isAllowed(op, config.ditto.policy)) - .map((op) => operationToTool(op, client, configCredential)); + .map((op) => operationToTool(op, client, creds)); // De-duplicate tool names: on collision, append _2, _3, ... const seen = new Map(); for (const tool of tools) { diff --git a/mcp/src/ditto/bundled-spec.test.ts b/mcp/src/ditto/bundled-spec.test.ts index fe422278d1..26428587b4 100644 --- a/mcp/src/ditto/bundled-spec.test.ts +++ b/mcp/src/ditto/bundled-spec.test.ts @@ -6,7 +6,11 @@ import YAML from "yaml"; import { parseOperations } from "./openapi.js"; import { isSudo } from "./tool-policy.js"; -const specPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "ditto-openapi.yml"); +const specPath = join( + dirname(fileURLToPath(import.meta.url)), + "..", "..", "..", + "documentation", "src", "main", "resources", "openapi", "ditto-api-2.yml", +); describe("bundled Ditto spec", () => { it("parses and yields operations incl. a things GET with a resolved path param", () => { diff --git a/mcp/src/ditto/client.test.ts b/mcp/src/ditto/client.test.ts index 751d194c6e..2d1221d658 100644 --- a/mcp/src/ditto/client.test.ts +++ b/mcp/src/ditto/client.test.ts @@ -6,7 +6,7 @@ import type { DittoOperation } from "./openapi.js"; const op = (over: Partial): DittoOperation => ({ operationId: "op", method: "GET", path: "/x", summary: "", description: "", params: [], hasBody: false, ...over, }); -const cred = (h?: string) => ({ isDevops: false, authHeader: async () => h }); +const cred = (h?: string) => ({ authHeader: async () => h }); let fake: Awaited>; afterEach(async () => { await fake?.stop(); }); diff --git a/mcp/src/ditto/credential.test.ts b/mcp/src/ditto/credential.test.ts index 55c696414a..fb0dc92f8f 100644 --- a/mcp/src/ditto/credential.test.ts +++ b/mcp/src/ditto/credential.test.ts @@ -1,45 +1,49 @@ -import { describe, it, expect, afterEach } from "vitest"; +import { describe, it, expect } from "vitest"; import { AppConfigSchema } from "../config/schema.js"; import { createConfigCredential, resolveCredential } from "./credential.js"; import { startFakeOidc } from "./fake-oidc.js"; -const cfg = (ditto: object) => AppConfigSchema.parse({ ditto: { enabled: true, ...ditto } }); +const cred = (c: object) => + AppConfigSchema.parse({ ditto: { enabled: true, credential: c } }).ditto.credential; describe("credentials", () => { - it("config basic → async Basic header", async () => { - const c = createConfigCredential(cfg({ credential: { kind: "basic", username: "u", password: "p" } })); + it("basic → Basic header", async () => { + const c = createConfigCredential(cred({ kind: "basic", username: "u", password: "p" })); expect(await c.authHeader()).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); - expect(c.isDevops).toBe(false); }); - it("devops flag marks isDevops regardless of kind", async () => { - expect(createConfigCredential(cfg({ credential: { kind: "devops", username: "d", password: "s" } })).isDevops).toBe(true); - expect(createConfigCredential(cfg({ credential: { kind: "oidc", tokenUrl: "x", clientId: "c", clientSecret: "s", devops: true } })).isDevops).toBe(true); + it("basic with no username → no header", async () => { + const c = createConfigCredential(cred({ kind: "basic" })); + expect(await c.authHeader()).toBeUndefined(); }); - it("session Authorization overrides config, inherits config isDevops", async () => { - const cc = createConfigCredential(cfg({ credential: { kind: "devops", username: "d", password: "s" } })); - const c = resolveCredential(cc, { headers: { Authorization: "Bearer sess" } }); + it("session Authorization overrides the base credential", async () => { + const base = createConfigCredential(cred({ kind: "basic", username: "u", password: "p" })); + const c = resolveCredential(base, { headers: { Authorization: "Bearer sess" } }); expect(await c.authHeader()).toBe("Bearer sess"); - expect(c.isDevops).toBe(true); + }); + + it("resolveCredential returns the base when no session header", async () => { + const base = createConfigCredential(cred({ kind: "basic", username: "u", password: "p" })); + const c = resolveCredential(base, { headers: {} }); + expect(await c.authHeader()).toBe(`Basic ${Buffer.from("u:p").toString("base64")}`); }); it("oidc fetches a bearer token and caches it (one token call for two uses)", async () => { const oidc = await startFakeOidc({ access_token: "tok123", expires_in: 3600 }); try { - const c = createConfigCredential(cfg({ credential: { kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "c", clientSecret: "s" } })); + const c = createConfigCredential(cred({ kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "c", clientSecret: "s" })); expect(await c.authHeader()).toBe("Bearer tok123"); expect(await c.authHeader()).toBe("Bearer tok123"); - expect(oidc.calls).toBe(1); // cached + expect(oidc.calls).toBe(1); } finally { await oidc.stop(); } }); it("oidc sends grant_type=client_credentials, Basic auth, and scope (when set)", async () => { const oidc = await startFakeOidc({ access_token: "tok", expires_in: 3600 }); try { - const c = createConfigCredential(cfg({ credential: { kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "myClient", clientSecret: "mySecret", scope: "scope1 scope2" } })); + const c = createConfigCredential(cred({ kind: "oidc", tokenUrl: oidc.tokenUrl, clientId: "myClient", clientSecret: "mySecret", scope: "scope1 scope2" })); await c.authHeader(); - expect(oidc.requests).toHaveLength(1); const req = oidc.requests[0]; expect(req.method).toBe("POST"); expect(req.headers.authorization).toBe(`Basic ${Buffer.from("myClient:mySecret").toString("base64")}`); diff --git a/mcp/src/ditto/credential.ts b/mcp/src/ditto/credential.ts index df1ad277fc..7ca8558c12 100644 --- a/mcp/src/ditto/credential.ts +++ b/mcp/src/ditto/credential.ts @@ -1,7 +1,6 @@ -import type { AppConfig } from "../config/schema.js"; +import type { CredentialConfig } from "../config/schema.js"; export interface DittoCredential { - readonly isDevops: boolean; authHeader(signal?: AbortSignal): Promise; } @@ -10,7 +9,7 @@ function first(h: string | string[] | undefined): string | undefined { } class StaticCredential implements DittoCredential { - constructor(readonly isDevops: boolean, private readonly header: string | undefined) {} + constructor(private readonly header: string | undefined) {} async authHeader(): Promise { return this.header; } @@ -21,18 +20,14 @@ interface OidcOptions { clientId: string; clientSecret: string; scope?: string; - isDevops: boolean; } export class OidcClientCredential implements DittoCredential { - readonly isDevops: boolean; private token?: string; private expiresAt = 0; private inflight?: Promise; - constructor(private readonly opts: OidcOptions, private readonly fetchFn: typeof fetch = fetch) { - this.isDevops = opts.isDevops; - } + constructor(private readonly opts: OidcOptions, private readonly fetchFn: typeof fetch = fetch) {} async authHeader(signal?: AbortSignal): Promise { const now = Date.now(); @@ -63,33 +58,32 @@ export class OidcClientCredential implements DittoCredential { } } -export function createConfigCredential(config: AppConfig, fetchFn: typeof fetch = fetch): DittoCredential { - const c = config.ditto.credential; - const isDevops = c.devops ?? c.kind === "devops"; +/** Build a credential from a single credential-config object (standard or devops slot). */ +export function createConfigCredential(c: CredentialConfig, fetchFn: typeof fetch = fetch): DittoCredential { if (c.kind === "oidc") { if (!c.tokenUrl || !c.clientId || !c.clientSecret) { process.stderr.write("[ditto-mcp] oidc credential missing tokenUrl/clientId/clientSecret; using no credential\n"); - return new StaticCredential(isDevops, undefined); + return new StaticCredential(undefined); } return new OidcClientCredential( - { tokenUrl: c.tokenUrl, clientId: c.clientId, clientSecret: c.clientSecret, scope: c.scope, isDevops }, + { tokenUrl: c.tokenUrl, clientId: c.clientId, clientSecret: c.clientSecret, scope: c.scope }, fetchFn, ); } if (c.username !== undefined) { - const header = `Basic ${Buffer.from(`${c.username}:${c.password ?? ""}`).toString("base64")}`; - return new StaticCredential(isDevops, header); + return new StaticCredential(`Basic ${Buffer.from(`${c.username}:${c.password ?? ""}`).toString("base64")}`); } - return new StaticCredential(false, undefined); + return new StaticCredential(undefined); } +/** A per-session `Authorization` header, when present, replaces the base credential. */ export function resolveCredential( - configCredential: DittoCredential, + base: DittoCredential, ctx: { headers?: Record }, ): DittoCredential { const headers = ctx.headers ?? {}; const key = Object.keys(headers).find((k) => k.toLowerCase() === "authorization"); const sessionAuth = first(key ? headers[key] : undefined); - if (sessionAuth) return new StaticCredential(configCredential.isDevops, sessionAuth); - return configCredential; + if (sessionAuth) return new StaticCredential(sessionAuth); + return base; } diff --git a/mcp/src/ditto/tool-policy.test.ts b/mcp/src/ditto/tool-policy.test.ts index 49700ed683..0780892758 100644 --- a/mcp/src/ditto/tool-policy.test.ts +++ b/mcp/src/ditto/tool-policy.test.ts @@ -45,4 +45,11 @@ describe("tool-policy", () => { const op2 = op({ securitySchemes: ["DevOpsBearer"], path: "/api/2/connections/foo" }); expect(isSudo(op2)).toBe(true); }); + + it("treats /api/2/connections as sudo even without declared devops security (path rule)", () => { + const conn = op({ operationId: "getConnections", method: "GET", path: "/api/2/connections", securitySchemes: [] }); + expect(isSudo(conn)).toBe(true); + expect(isAllowed(conn, policy({}))).toBe(false); // GET but connections -> not auto-allowed + expect(isAllowed(conn, policy({ sudoAllowlist: ["getConnections"] }))).toBe(true); + }); }); diff --git a/mcp/src/ditto/tool-policy.ts b/mcp/src/ditto/tool-policy.ts index 59bdcb64f5..9da983440e 100644 --- a/mcp/src/ditto/tool-policy.ts +++ b/mcp/src/ditto/tool-policy.ts @@ -8,7 +8,8 @@ export function isSudo(op: DittoOperation): boolean { hasDevopsSecurity || op.operationId.toLowerCase().startsWith("sudo") || p.includes("/sudo") || - p.startsWith("/devops") + p.startsWith("/devops") || + p.includes("/connections") // connectivity is secret-bearing → always devops ); } From be04178aca665020db7fbcebfb8b1b3ab85ec8b9 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Wed, 12 Aug 2026 09:57:20 +0300 Subject: [PATCH 08/11] feat(mcp): Docker deployment + configurable chunking - Dockerfile (multistage node:22-slim, glibc), .dockerignore, and docker/config.docker.json: HTTP transport, non-root, native deps baked in. Secure-by-default DNS rebinding protection + loopback allowedHosts. - bin shebangs so stdio/http/ingest run as installed CLIs. - Lazy-load sqlite/pg store backends and local embeddings; friendly errors naming the missing native dep instead of raw MODULE_NOT_FOUND. - knowledge.chunk.{maxChars,overlap} config wired through makeSources to PublicSource + LocalDirSource (was hardcoded to chunker defaults). - README: Docker Deployment and Chunking sections (incl. bge-small 512-token ceiling for vector/hybrid). Co-Authored-By: Claude Opus 4.8 --- mcp/.dockerignore | 19 +++++++ mcp/Dockerfile | 53 +++++++++++++++++++ mcp/README.md | 79 +++++++++++++++++++++++++++++ mcp/docker/config.docker.json | 16 ++++++ mcp/src/bin/http.ts | 1 + mcp/src/bin/ingest.ts | 1 + mcp/src/bin/stdio.ts | 1 + mcp/src/config/schema.ts | 10 ++++ mcp/src/knowledge/factories.test.ts | 48 ++++++++++++++++++ mcp/src/knowledge/factories.ts | 21 +++++++- mcp/src/knowledge/store-factory.ts | 25 +++++++-- 11 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 mcp/.dockerignore create mode 100644 mcp/Dockerfile create mode 100644 mcp/docker/config.docker.json create mode 100644 mcp/src/knowledge/factories.test.ts diff --git a/mcp/.dockerignore b/mcp/.dockerignore new file mode 100644 index 0000000000..d6da748a59 --- /dev/null +++ b/mcp/.dockerignore @@ -0,0 +1,19 @@ +# Rebuilt inside the image +node_modules/ +dist/ + +# Local-only / secrets / caches — never bake into image +.cache/ +models/ +config.local.json +*.db +*.db-wal +*.db-shm +*.db.tmp +*.log + +# Not needed at runtime +.git/ +examples/ +*.test.ts +vitest*.ts diff --git a/mcp/Dockerfile b/mcp/Dockerfile new file mode 100644 index 0000000000..58d0bd9e74 --- /dev/null +++ b/mcp/Dockerfile @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 + +# ---- builder: compile native deps + tsc, then drop devDeps ---- +# node:22-slim = Debian (glibc). Do NOT use alpine: onnxruntime-node +# (pulled by @huggingface/transformers) ships no musl prebuild. +FROM node:22-slim AS builder +WORKDIR /app + +# Toolchain so better-sqlite3 can build from source if no prebuilt +# binary matches this platform/ABI. Builder is throwaway — not shipped. +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 make g++ ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install with full lockfile first (better layer caching). +COPY package.json package-lock.json ./ +RUN npm ci + +# Build TypeScript -> dist/ +COPY tsconfig.json ./ +COPY src ./src +RUN npm run build + +# Strip devDeps but keep the compiled native .node binaries. +RUN npm prune --omit=dev + +# ---- runtime: slim image, prod deps + dist only, non-root ---- +FROM node:22-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production + +# Same base/arch as builder -> copying node_modules keeps native bindings valid. +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./package.json + +# Baked default config (http, 0.0.0.0). Override by bind-mounting a file +# and pointing DITTO_MCP_CONFIG at it. +COPY docker/config.docker.json ./config.docker.json +ENV DITTO_MCP_CONFIG=/app/config.docker.json + +# Writable dir for the sqlite knowledge index (see config.docker.json). +# Mount a volume here to persist across restarts. +RUN mkdir -p /app/data + +# Run unprivileged. +RUN useradd --system --uid 10001 --home-dir /app ditto \ + && chown -R ditto:ditto /app +USER ditto +VOLUME ["/app/data"] + +EXPOSE 3000 +CMD ["node", "dist/bin/http.js"] diff --git a/mcp/README.md b/mcp/README.md index 2caf8f73c4..98229e9ba2 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -71,6 +71,74 @@ node dist/bin/http.js By default, the HTTP server binds to `127.0.0.1:3000` and serves at `/mcp`. Configure via `server.http` in the config (see below). +## Docker Deployment + +The HTTP transport is the intended production deployment: one long-lived container that MCP clients reach over a URL. The image bundles all native dependencies (`better-sqlite3`, `sqlite-vec`, `onnxruntime-node`) so consumers need no build toolchain. (stdio transport is for local development only.) + +The repo ships a multi-stage `Dockerfile` and a baked default config at `docker/config.docker.json` (binds `0.0.0.0:3000`, sqlite index under `/app/data`). + +### Build & run + +```bash +docker build -t ditto-mcp-server:0.1.0 . + +docker run -d -p 3000:3000 \ + -v ditto-data:/app/data \ + ditto-mcp-server:0.1.0 +``` + +Point your MCP client at the HTTP endpoint: + +```json +{ "mcpServers": { "ditto": { "url": "http://your-host:3000/mcp" } } } +``` + +### ⚠️ Required: set `allowedHosts` for your hostname + +The baked config keeps DNS rebinding protection **on**, with `allowedHosts` limited to `localhost:3000` / `127.0.0.1:3000`. A request whose `Host` header is not on that list is rejected with **HTTP 403 `Invalid Host header`** — so a container reached via a service name, public hostname, or reverse proxy is unreachable until you add that host. + +Override the config with your real hostname (see the [Server HTTP options](#server-http-transport-options) table): + +```json +{ + "server": { + "http": { + "host": "0.0.0.0", + "port": 3000, + "enableDnsRebindingProtection": true, + "allowedHosts": ["ditto-mcp.internal:3000", "mcp.example.com"] + } + } +} +``` + +Mount it and point the server at it: + +```bash +docker run -d -p 3000:3000 \ + -v ditto-data:/app/data \ + -v /path/to/config.json:/app/config.docker.json:ro \ + ditto-mcp-server:0.1.0 +``` + +Disabling `enableDnsRebindingProtection` is only appropriate when the container sits behind a proxy or network boundary that validates the `Host` header for you — do not ship it disabled on a directly exposed service. + +### Publishing to a private registry (e.g. Artifactory) + +Artifactory (and most registries) host Docker images directly — no npm publish needed. + +```bash +docker login your.artifactory.com # user + API token +docker build -t your.artifactory.com/docker-local/ditto-mcp-server:0.1.0 . +docker push your.artifactory.com/docker-local/ditto-mcp-server:0.1.0 +``` + +Consumers then `docker run your.artifactory.com/docker-local/ditto-mcp-server:0.1.0`. + +### Vector / hybrid retrieval in containers + +The default `retriever: fts` needs no embedding model. If you switch to `vector` or `hybrid`, the bge model is downloaded at runtime from the Hugging Face Hub — which fails in air-gapped networks. For offline use, either bake the model into the image and set `knowledge.embedding.modelPath`, or mount a pre-populated cache and set `knowledge.embedding.cacheDir`. Persist the built index with the ingest process (see below) so the container doesn't rebuild in memory on every start. + ## Two Processes: Ingest vs. Server The MCP server supports **persistent knowledge indexes** (SQLite or Postgres). The index must be built **before** the server starts (or the server falls back to building it in memory at startup). @@ -157,6 +225,17 @@ The index can be stored in **SQLite** (file-based, default) or **Postgres** (pgv | `knowledge.localDir.path` | `string?` | `undefined` | Path to local markdown directory | | `knowledge.localDir.id` | `string` | `"local"` | Source ID for local chunks | +#### Chunking + +Controls how source markdown is split into indexed chunks. Applies to all sources (public + local). Re-run `ditto-mcp-ingest` after changing these to rebuild the index. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.chunk.maxChars` | `number` | `1000` | Max characters per chunk. Larger = more contiguous context per hit, fewer split-across-boundary gaps; coarser ranking precision and more tokens returned per result. | +| `knowledge.chunk.overlap` | `number` | `150` | Characters carried between adjacent pieces when a paragraph is hard-split. Must be `< maxChars`. Raise to reduce boundary loss without growing chunks much. | + +**⚠️ Ceiling for `vector`/`hybrid`:** the embedding model bounds useful chunk size. `bge-small-en-v1.5` has a **512-token (~1500–2000 char) window** — text beyond it is silently truncated before embedding, so a chunk larger than the window loses semantic recall on its tail. Keep `maxChars` at/under the model's window for vector search, or switch to a longer-context embedding model. For `fts` (no embeddings) there is no such limit; larger chunks are safe. + #### Embedding (for vector/hybrid) | Field | Type | Default | Description | diff --git a/mcp/docker/config.docker.json b/mcp/docker/config.docker.json new file mode 100644 index 0000000000..62c740a776 --- /dev/null +++ b/mcp/docker/config.docker.json @@ -0,0 +1,16 @@ +{ + "server": { + "name": "ditto-mcp", + "http": { + "host": "0.0.0.0", + "port": 3000, + "enableDnsRebindingProtection": true, + "allowedHosts": ["localhost:3000", "127.0.0.1:3000"] + } + }, + "knowledge": { + "enabled": true, + "retriever": "fts", + "store": { "kind": "sqlite", "sqlite": { "path": "/app/data/ditto_kn.db" } } + } +} diff --git a/mcp/src/bin/http.ts b/mcp/src/bin/http.ts index 5e6c535703..a0da02aa5b 100644 --- a/mcp/src/bin/http.ts +++ b/mcp/src/bin/http.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import { loadConfig } from "../config/load.js"; import { createHttpApp } from "../server/http-app.js"; import { buildKnowledgeService } from "../knowledge/build.js"; diff --git a/mcp/src/bin/ingest.ts b/mcp/src/bin/ingest.ts index 9157d4df4d..3ca17e2c95 100644 --- a/mcp/src/bin/ingest.ts +++ b/mcp/src/bin/ingest.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import { loadConfig } from "../config/load.js"; import { buildIndex, metaFor } from "../knowledge/build-index.js"; import { makeSources, makeEmbedder } from "../knowledge/factories.js"; diff --git a/mcp/src/bin/stdio.ts b/mcp/src/bin/stdio.ts index 3c66253f72..964189b7df 100644 --- a/mcp/src/bin/stdio.ts +++ b/mcp/src/bin/stdio.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { loadConfig } from "../config/load.js"; import { registerTools } from "../tools/index.js"; diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index 67bb22e9dd..0a315766d1 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -52,6 +52,15 @@ export const AppConfigSchema = z batchSize: z.number().int().positive().default(32), }) .default({ model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }), + chunk: z + .object({ + maxChars: z.number().int().positive().default(1000), + overlap: z.number().int().min(0).default(150), + }) + .refine((c) => c.overlap < c.maxChars, { + message: "knowledge.chunk.overlap must be less than maxChars", + }) + .default({ maxChars: 1000, overlap: 150 }), localDir: z .object({ enabled: z.boolean().default(false), @@ -87,6 +96,7 @@ export const AppConfigSchema = z enabled: true, retriever: "fts", embedding: { model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }, + chunk: { maxChars: 1000, overlap: 150 }, localDir: { enabled: false, id: "local" }, publicSource: { enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }, store: { kind: "sqlite", sqlite: {}, pgvector: { table: "ditto_kn" } }, diff --git a/mcp/src/knowledge/factories.test.ts b/mcp/src/knowledge/factories.test.ts new file mode 100644 index 0000000000..b2848c1c01 --- /dev/null +++ b/mcp/src/knowledge/factories.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from "vitest"; +import { makeSources } from "./factories.js"; +import { AppConfigSchema } from "../config/schema.js"; + +const LONG_DOC = [ + "Alpha paragraph about token integration here.", + "Beta paragraph about policy subjects here now.", + "Gamma paragraph about activation actions here.", + "Delta paragraph about JWT permission grants.", +].join("\n\n"); + +const INDEX = `# Ditto docs\n- [Doc](https://eclipse.dev/ditto/doc.md): a doc\n`; + +const DOCS: Record = { + "https://eclipse.dev/ditto/llms.txt": INDEX, + "https://eclipse.dev/ditto/doc.md": LONG_DOC, +}; + +const fakeFetch = async (url: string): Promise => { + if (!(url in DOCS)) throw new Error(`404 ${url}`); + return DOCS[url]; +}; + +describe("knowledge.chunk config", () => { + it("defaults to maxChars 1000 / overlap 150", () => { + const cfg = AppConfigSchema.parse({}); + expect(cfg.knowledge.chunk).toEqual({ maxChars: 1000, overlap: 150 }); + }); + + it("rejects overlap >= maxChars", () => { + expect(() => + AppConfigSchema.parse({ knowledge: { chunk: { maxChars: 100, overlap: 100 } } }), + ).toThrow(); + }); + + it("flows configured chunk size through makeSources to PublicSource", async () => { + const cfg = AppConfigSchema.parse({ + knowledge: { chunk: { maxChars: 60, overlap: 10 } }, + }); + const sources = makeSources(cfg, { fetchFn: fakeFetch }); + const pub = sources.find((s) => s.id === "public"); + expect(pub).toBeDefined(); + const chunks = await pub!.loadChunks(); + // With maxChars=60 the 4 paragraphs cannot pack into one chunk. + expect(chunks.length).toBeGreaterThan(1); + expect(Math.max(...chunks.map((c) => c.text.length))).toBeLessThanOrEqual(60); + }); +}); diff --git a/mcp/src/knowledge/factories.ts b/mcp/src/knowledge/factories.ts index 3bbef8b0a7..f2837f5a95 100644 --- a/mcp/src/knowledge/factories.ts +++ b/mcp/src/knowledge/factories.ts @@ -11,18 +11,24 @@ export interface FactoryDeps { export function makeSources(config: AppConfig, deps?: FactoryDeps): KnowledgeSource[] { const sources: KnowledgeSource[] = []; + const chunkOptions = config.knowledge.chunk; if (config.knowledge.publicSource.enabled) { sources.push( new PublicSource({ url: config.knowledge.publicSource.url, maxDocs: config.knowledge.publicSource.maxDocs, fetchFn: deps?.fetchFn, + chunkOptions, }), ); } if (config.knowledge.localDir.enabled && config.knowledge.localDir.path) { sources.push( - new LocalDirSource({ dir: config.knowledge.localDir.path, id: config.knowledge.localDir.id }), + new LocalDirSource({ + dir: config.knowledge.localDir.path, + id: config.knowledge.localDir.id, + chunkOptions, + }), ); } return sources; @@ -30,7 +36,18 @@ export function makeSources(config: AppConfig, deps?: FactoryDeps): KnowledgeSou export async function makeEmbedder(config: AppConfig, deps?: FactoryDeps): Promise { if (deps?.embeddingProvider) return deps.embeddingProvider; - const { LocalEmbeddings } = await import("./embedding.js"); + // Lazy: local embeddings pull @huggingface/transformers (onnxruntime-node). + // Only needed for retriever=vector|hybrid without an injected provider. + let LocalEmbeddings: typeof import("./embedding.js").LocalEmbeddings; + try { + ({ LocalEmbeddings } = await import("./embedding.js")); + } catch (err) { + const reason = err instanceof Error ? err.message : String(err); + throw new Error( + `local embeddings require @huggingface/transformers, which failed to load: ${reason}. ` + + `Install it, inject deps.embeddingProvider, or use retriever=fts (no embeddings).`, + ); + } const e = config.knowledge.embedding; return new LocalEmbeddings({ model: e.model, dim: e.dim, modelPath: e.modelPath, diff --git a/mcp/src/knowledge/store-factory.ts b/mcp/src/knowledge/store-factory.ts index e3936d5bc0..c6975651fd 100644 --- a/mcp/src/knowledge/store-factory.ts +++ b/mcp/src/knowledge/store-factory.ts @@ -1,9 +1,14 @@ import type { AppConfig } from "../config/schema.js"; import type { KnowledgeStore } from "./knowledge-store.js"; -import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; + +const reason = (e: unknown) => (e instanceof Error ? e.message : String(e)); /** Open a KnowledgeStore for the configured backend. `opts.path` overrides the - * sqlite path (e.g. ":memory:" for the in-memory fallback). */ + * sqlite path (e.g. ":memory:" for the in-memory fallback). + * + * Backends are imported lazily so their native deps load only when selected: + * sqlite pulls better-sqlite3 + sqlite-vec, pgvector pulls pg. A missing dep + * surfaces as a clear error naming the fix, not a raw MODULE_NOT_FOUND. */ export async function openStore( config: AppConfig, opts: { path?: string } = {}, @@ -11,12 +16,26 @@ export async function openStore( const kind = config.knowledge.store.kind; if (kind === "sqlite") { const path = opts.path ?? config.knowledge.store.sqlite.path ?? ":memory:"; + let SqliteKnowledgeStore: typeof import("./sqlite-knowledge-store.js").SqliteKnowledgeStore; + try { + ({ SqliteKnowledgeStore } = await import("./sqlite-knowledge-store.js")); + } catch (e) { + throw new Error( + `sqlite store requires better-sqlite3 + sqlite-vec, which failed to load: ${reason(e)}. ` + + `Install them, or set knowledge.store.kind=pgvector.`, + ); + } return new SqliteKnowledgeStore(path); } if (kind === "pgvector") { const { connectionString, table } = config.knowledge.store.pgvector; if (!connectionString) throw new Error("knowledge.store.pgvector.connectionString is required"); - const { PgKnowledgeStore } = await import("./pg-knowledge-store.js"); + let PgKnowledgeStore: typeof import("./pg-knowledge-store.js").PgKnowledgeStore; + try { + ({ PgKnowledgeStore } = await import("./pg-knowledge-store.js")); + } catch (e) { + throw new Error(`pgvector store requires the 'pg' package, which failed to load: ${reason(e)}.`); + } return PgKnowledgeStore.connect(connectionString, table); } throw new Error(`unsupported knowledge.store.kind: ${kind}`); From a4a2361438915ef194fccddfcaf5670d0610d2ab Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Wed, 12 Aug 2026 11:57:19 +0300 Subject: [PATCH 09/11] feat(mcp): neighbor expansion for knowledge search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retriever-agnostic context expansion in KnowledgeService.search: around each anchor, pull ±context same-document chunks (same cite, adjacent ordinal) by id — never re-scored or re-embedded. Results are deduped and emitted as contiguous spans ordered by best anchor rank; neighbors are tagged role="context" so their relevance isn't over-weighted vs. matches. - Stops at document boundaries (different cite) even when ordinals are globally contiguous, so expansion never leaks across docs. - knowledge.search.{limit,context} config (defaults 5 / 1); search tool gains a per-call `context` arg that overrides. `limit` counts anchors; neighbors extra. - Works for fts/vector/hybrid: an anchor's neighbors are defined by document layout, not by how it was matched — so expansion is embedding-neutral and sidesteps the 512-token ceiling. - README: Search (query-time) section incl. overlap-vs-expansion interaction. Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/README.md | 25 ++++ mcp/src/config/schema.ts | 7 ++ mcp/src/knowledge/knowledge-service.test.ts | 61 +++++++++- mcp/src/knowledge/knowledge-service.ts | 120 +++++++++++++++++++- mcp/src/knowledge/types.ts | 3 + mcp/src/tools/index.ts | 2 +- mcp/src/tools/knowledge.test.ts | 24 ++++ mcp/src/tools/knowledge.ts | 38 +++++-- 8 files changed, 269 insertions(+), 11 deletions(-) diff --git a/mcp/README.md b/mcp/README.md index 98229e9ba2..6d946cca12 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -236,6 +236,31 @@ Controls how source markdown is split into indexed chunks. Applies to all source **⚠️ Ceiling for `vector`/`hybrid`:** the embedding model bounds useful chunk size. `bge-small-en-v1.5` has a **512-token (~1500–2000 char) window** — text beyond it is silently truncated before embedding, so a chunk larger than the window loses semantic recall on its tail. Keep `maxChars` at/under the model's window for vector search, or switch to a longer-context embedding model. For `fts` (no embeddings) there is no such limit; larger chunks are safe. +#### Search (query-time) + +Applies at search time — no re-ingest needed. The `search` tool accepts per-call `limit` and `context` args that override these defaults. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `knowledge.search.limit` | `number` | `5` | Default number of **matches (anchors)** returned. Max 20. Neighbors from `context` do not count against it. | +| `knowledge.search.context` | `number` | `1` | **Neighbor expansion**: adjacent same-document chunks pulled in on each side of every match, for surrounding context. Max 5. `0` = matches only. | + +**Neighbor expansion** decouples *retrieval granularity* from *context delivery*. The retriever finds the relevant chunk (anchor); expansion then fetches its positional neighbors (same document, adjacent ordinal) **by id — never re-scored or re-embedded**. This is retriever-agnostic: an `fts`, `vector`, or `hybrid` match all get the same neighbors, since neighbors are defined by document layout, not by how the anchor was found. Results are emitted as contiguous spans in reading order; neighbors are tagged `context` (vs. `matched: …`) so their relevance isn't over-weighted. + +Why it matters for `vector`/`hybrid`: keep chunks **small** (sharp embeddings, under the 512-token ceiling) and recover surrounding context via `context` instead of fat chunks. Because neighbors are fetched by id, expansion never touches the embedding ceiling. + +**Interaction with `chunk.overlap`:** overlap and neighbor expansion both fight boundary loss, so they partly overlap in purpose. With `context ≥ 1`, adjacent chunks are already returned — so a positive `overlap` duplicates the seam text. When using expansion, set `chunk.overlap` low or `0`. + +Recommended for `hybrid`: small chunks + expansion, no overlap: + +```json +{ "knowledge": { + "retriever": "hybrid", + "chunk": { "maxChars": 1000, "overlap": 0 }, + "search": { "limit": 5, "context": 1 } +} } +``` + #### Embedding (for vector/hybrid) | Field | Type | Default | Description | diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index 0a315766d1..59da82b98a 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -61,6 +61,12 @@ export const AppConfigSchema = z message: "knowledge.chunk.overlap must be less than maxChars", }) .default({ maxChars: 1000, overlap: 150 }), + search: z + .object({ + limit: z.number().int().positive().max(20).default(5), + context: z.number().int().min(0).max(5).default(1), + }) + .default({ limit: 5, context: 1 }), localDir: z .object({ enabled: z.boolean().default(false), @@ -97,6 +103,7 @@ export const AppConfigSchema = z retriever: "fts", embedding: { model: "Xenova/bge-small-en-v1.5", dim: 384, allowRemoteModels: true, batchSize: 32 }, chunk: { maxChars: 1000, overlap: 150 }, + search: { limit: 5, context: 1 }, localDir: { enabled: false, id: "local" }, publicSource: { enabled: true, url: "https://eclipse.dev/ditto/llms.txt" }, store: { kind: "sqlite", sqlite: {}, pgvector: { table: "ditto_kn" } }, diff --git a/mcp/src/knowledge/knowledge-service.test.ts b/mcp/src/knowledge/knowledge-service.test.ts index d1e2462dc8..4a764ff09b 100644 --- a/mcp/src/knowledge/knowledge-service.test.ts +++ b/mcp/src/knowledge/knowledge-service.test.ts @@ -3,11 +3,25 @@ import { KnowledgeService } from "./knowledge-service.js"; import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; import { FtsRetriever } from "./fts-retriever.js"; import { buildIndex } from "./build-index.js"; -import type { KnowledgeSource, Chunk } from "./types.js"; +import type { KnowledgeSource, Chunk, RetrievedChunk, Retriever } from "./types.js"; const src = (id: string, chunks: Chunk[]): KnowledgeSource => ({ id, loadChunks: async () => chunks }); const chunk = (id: string, text: string): Chunk => ({ id, source: "s", title: "T", text, cite: `https://x/${id}` }); +// A chunk positioned in a document: id `pub#`, grouped by `cite`. +const c = (n: number, cite: string, text = `text ${n}`): Chunk => ({ + id: `pub#${n}`, + source: "pub", + title: `T${n}`, + text, + cite, +}); + +const stubRetriever = (anchors: RetrievedChunk[]): Retriever => ({ + kind: "stub", + search: async () => anchors, +}); + let store: SqliteKnowledgeStore; afterEach(async () => await store?.close()); @@ -21,3 +35,48 @@ describe("KnowledgeService", () => { expect((await svc.getChunk("a"))?.text).toBe("reconnect memory crash"); }); }); + +describe("KnowledgeService neighbor expansion", () => { + it("context=0 returns only anchors", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A")]); + const svc = new KnowledgeService(store, stubRetriever([{ chunk: c(1, "A"), matchedBy: ["fts"] }])); + const hits = await svc.search("q", 5, { context: 0 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#1"]); + }); + + it("expands to same-cite neighbors in ordinal order, tagged by role", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A")]); + const svc = new KnowledgeService(store, stubRetriever([{ chunk: c(1, "A"), matchedBy: ["vector"] }])); + const hits = await svc.search("q", 5, { context: 1 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#0", "pub#1", "pub#2"]); + expect(hits.map((h) => h.role)).toEqual(["context", "anchor", "context"]); + expect(hits.find((h) => h.chunk.id === "pub#1")!.matchedBy).toEqual(["vector"]); + }); + + it("does not cross document boundaries (different cite)", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A"), c(3, "B"), c(4, "B")]); + // anchor pub#3 = first chunk of doc B; pub#2 is numerically adjacent but belongs to doc A. + const svc = new KnowledgeService(store, stubRetriever([{ chunk: c(3, "B"), matchedBy: ["fts"] }])); + const hits = await svc.search("q", 5, { context: 1 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#3", "pub#4"]); + }); + + it("dedups and merges overlapping anchor windows into one contiguous span", async () => { + store = new SqliteKnowledgeStore(); + await store.addChunks([c(0, "A"), c(1, "A"), c(2, "A"), c(3, "A")]); + const svc = new KnowledgeService( + store, + stubRetriever([ + { chunk: c(1, "A"), matchedBy: ["fts"] }, + { chunk: c(2, "A"), matchedBy: ["vector"] }, + ]), + ); + const hits = await svc.search("q", 5, { context: 1 }); + expect(hits.map((h) => h.chunk.id)).toEqual(["pub#0", "pub#1", "pub#2", "pub#3"]); + expect(hits.map((h) => h.role)).toEqual(["context", "anchor", "anchor", "context"]); + expect(new Set(hits.map((h) => h.chunk.id)).size).toBe(hits.length); // no dupes + }); +}); diff --git a/mcp/src/knowledge/knowledge-service.ts b/mcp/src/knowledge/knowledge-service.ts index 780247cfb1..02f8abf85f 100644 --- a/mcp/src/knowledge/knowledge-service.ts +++ b/mcp/src/knowledge/knowledge-service.ts @@ -1,17 +1,133 @@ import type { Chunk, RetrievedChunk, Retriever } from "./types.js"; import type { KnowledgeStore } from "./knowledge-store.js"; +export interface SearchOptions { + /** Positional neighbors to pull in around each anchor (same document). 0 = anchors only. */ + context?: number; +} + +/** Parse a chunk id of the form `${source}#${ordinal}`. Returns null if the id + * has no numeric ordinal suffix (such chunks can't be expanded positionally). */ +function parseId(id: string): { source: string; ord: number } | null { + const h = id.lastIndexOf("#"); + if (h < 0) return null; + const ord = Number(id.slice(h + 1)); + if (!Number.isInteger(ord)) return null; + return { source: id.slice(0, h), ord }; +} + +interface Rec { + chunk: Chunk; + matchedBy: string[]; + isAnchor: boolean; + rank: number; // best (lowest) anchor rank this chunk belongs to; drives ordering + ord: number | null; +} + export class KnowledgeService { constructor( private readonly store: KnowledgeStore, private readonly retriever: Retriever, ) {} - search(query: string, k: number): Promise { - return this.retriever.search(query, k); + async search(query: string, k: number, opts: SearchOptions = {}): Promise { + const anchors = await this.retriever.search(query, k); + const context = opts.context ?? 0; + if (context <= 0 || anchors.length === 0) { + return anchors.map((a) => ({ ...a, role: "anchor" as const })); + } + return this.expand(anchors, context); } getChunk(id: string): Promise { return this.store.getChunk(id); } + + /** Retriever-agnostic neighbor expansion: fetch ±context same-document chunks + * around each anchor, dedup, and emit contiguous spans ordered by best anchor + * rank (ordinal order within a span). Neighbors are fetched by id, never + * re-scored; "same document" = identical `cite`, which also stops expansion at + * document boundaries even when ordinals are globally contiguous. */ + private async expand(anchors: RetrievedChunk[], context: number): Promise { + const recs = new Map(); + + anchors.forEach((a, rank) => { + recs.set(a.chunk.id, { + chunk: a.chunk, + matchedBy: a.matchedBy, + isAnchor: true, + rank, + ord: parseId(a.chunk.id)?.ord ?? null, + }); + }); + + for (let rank = 0; rank < anchors.length; rank++) { + const anchor = anchors[rank]; + const p = parseId(anchor.chunk.id); + if (!p) continue; + for (const dir of [-1, 1]) { + for (let j = 1; j <= context; j++) { + const nid = `${p.source}#${p.ord + dir * j}`; + const existing = recs.get(nid); + if (existing) { + if (existing.chunk.cite !== anchor.chunk.cite) break; // boundary + if (rank < existing.rank) existing.rank = rank; + continue; + } + const c = await this.store.getChunk(nid); + if (!c || c.cite !== anchor.chunk.cite) break; // boundary or missing + recs.set(nid, { chunk: c, matchedBy: [], isAnchor: false, rank, ord: p.ord + dir * j }); + } + } + } + + return this.orderSegments([...recs.values()]); + } + + /** Group recs by document (cite), split into contiguous ordinal runs, order + * runs by their best anchor rank, and flatten (ordinal order within a run). */ + private orderSegments(recs: Rec[]): RetrievedChunk[] { + const byCite = new Map(); + const loose: Rec[] = []; // chunks without an ordinal — emitted as singletons + for (const r of recs) { + if (r.ord === null) { + loose.push(r); + continue; + } + const arr = byCite.get(r.chunk.cite) ?? []; + arr.push(r); + byCite.set(r.chunk.cite, arr); + } + + const segments: { rank: number; recs: Rec[] }[] = []; + for (const arr of byCite.values()) { + arr.sort((x, y) => (x.ord as number) - (y.ord as number)); + let seg: Rec[] = []; + let prev: number | undefined; + for (const r of arr) { + if (prev !== undefined && (r.ord as number) !== prev + 1) { + segments.push({ rank: Math.min(...seg.map((s) => s.rank)), recs: seg }); + seg = []; + } + seg.push(r); + prev = r.ord as number; + } + if (seg.length) segments.push({ rank: Math.min(...seg.map((s) => s.rank)), recs: seg }); + } + for (const r of loose) segments.push({ rank: r.rank, recs: [r] }); + + segments.sort((a, b) => a.rank - b.rank); + + const out: RetrievedChunk[] = []; + for (const s of segments) { + for (const r of s.recs) { + out.push({ + chunk: r.chunk, + matchedBy: r.matchedBy, + role: r.isAnchor ? "anchor" : "context", + }); + } + } + return out; + } } diff --git a/mcp/src/knowledge/types.ts b/mcp/src/knowledge/types.ts index f8aa8d3201..39f65a4107 100644 --- a/mcp/src/knowledge/types.ts +++ b/mcp/src/knowledge/types.ts @@ -9,6 +9,9 @@ export interface Chunk { export interface RetrievedChunk { chunk: Chunk; matchedBy: string[]; // leaf retriever kinds, e.g. ["fts"], ["vector"], ["fts","vector"] + // "anchor" = matched by the retriever; "context" = a positional neighbor pulled + // in by expansion. Absent means anchor (pre-expansion results). + role?: "anchor" | "context"; } /** A corpus provider: yields chunks. Does not search. */ diff --git a/mcp/src/tools/index.ts b/mcp/src/tools/index.ts index 77bf18e3a8..f572025fd4 100644 --- a/mcp/src/tools/index.ts +++ b/mcp/src/tools/index.ts @@ -11,7 +11,7 @@ export async function registerTools( const registry = new ToolRegistry(); if (config.tools.ping) registry.register(pingTool); if (config.knowledge.enabled && knowledgeService) { - for (const tool of makeKnowledgeTools(knowledgeService)) + for (const tool of makeKnowledgeTools(knowledgeService, config.knowledge.search)) registry.register(tool); } if (config.ditto.enabled) { diff --git a/mcp/src/tools/knowledge.test.ts b/mcp/src/tools/knowledge.test.ts index 567f9032bf..36f8feb59f 100644 --- a/mcp/src/tools/knowledge.test.ts +++ b/mcp/src/tools/knowledge.test.ts @@ -42,4 +42,28 @@ describe("knowledge tools", () => { const miss = await t.get_chunk.handler({ id: "nope" }, ctx); expect(miss.content[0].text.toLowerCase()).toContain("not found"); }); + + it("search pulls in same-document neighbors as labeled context", async () => { + store = new SqliteKnowledgeStore(":memory:"); + await store.addChunks([ + { id: "pub#0", source: "pub", title: "T", text: "alpha prelude", cite: "https://x/doc" }, + { id: "pub#1", source: "pub", title: "T", text: "bravo uniquematchword baz", cite: "https://x/doc" }, + { id: "pub#2", source: "pub", title: "T", text: "charlie epilogue", cite: "https://x/doc" }, + ]); + const svc = new KnowledgeService(store, new FtsRetriever(store)); + const t = Object.fromEntries( + makeKnowledgeTools(svc, { limit: 5, context: 1 }).map((tool) => [tool.name, tool]), + ); + const res = await t.search.handler({ query: "uniquematchword" }, ctx); + const text = res.content.map((p) => p.text).join("\n"); + // anchor labeled matched, neighbors labeled context + expect(text).toContain("matched: fts"); + expect(text).toContain("context"); + expect(text).toContain("alpha prelude"); // neighbor pub#0 + expect(text).toContain("charlie epilogue"); // neighbor pub#2 + // context=0 override suppresses neighbors + const only = await t.search.handler({ query: "uniquematchword", context: 0 }, ctx); + const onlyText = only.content.map((p) => p.text).join("\n"); + expect(onlyText).not.toContain("alpha prelude"); + }); }); diff --git a/mcp/src/tools/knowledge.ts b/mcp/src/tools/knowledge.ts index 3e3cd7a4b4..de1ab5498c 100644 --- a/mcp/src/tools/knowledge.ts +++ b/mcp/src/tools/knowledge.ts @@ -9,21 +9,33 @@ function formatChunk(c: Chunk, extraFooter = ""): string { } function formatRetrievedChunk(rc: RetrievedChunk): string { - return formatChunk(rc.chunk, ` · matched: ${rc.matchedBy.join("+")}`); + // Neighbors pulled in by context expansion aren't retriever matches — label + // them so their relevance isn't over-weighted vs. the actual anchors. + const tag = rc.role === "context" ? "context" : `matched: ${rc.matchedBy.join("+")}`; + return formatChunk(rc.chunk, ` · ${tag}`); } function textResult(text: string): ToolResult { return { content: [{ type: "text", text }] }; } -export function makeKnowledgeTools(service: KnowledgeService): ToolDef[] { +export interface SearchDefaults { + limit: number; + context: number; +} + +export function makeKnowledgeTools( + service: KnowledgeService, + defaults: SearchDefaults = { limit: 5, context: 0 }, +): ToolDef[] { const search: ToolDef = { name: "search", description: "Search the Ditto knowledge base (official docs plus any configured corpora) and " + "return the most relevant documentation excerpts, each with a source URL and a chunk id. " + "Use natural-language questions about Ditto concepts, configuration, HTTP/Ditto protocol, " + - "connectivity, policies, or operations.", + "connectivity, policies, or operations. Each match ('matched: ...') may be followed by " + + "adjacent 'context' excerpts from the same document to preserve surrounding meaning.", inputSchema: { query: z .string() @@ -38,13 +50,25 @@ export function makeKnowledgeTools(service: KnowledgeService): ToolDef[] { .max(20) .optional() .describe( - "Maximum number of documentation excerpts to return. Default 5, maximum 20. " + - "Increase for broader context, decrease for only the top matches.", + `Maximum number of matching excerpts (anchors) to return. Default ${defaults.limit}, maximum 20. ` + + "Neighbors added by 'context' do not count against this.", + ), + context: z + .number() + .int() + .min(0) + .max(5) + .optional() + .describe( + `Adjacent same-document excerpts to include on each side of every match, for ` + + `surrounding context. Default ${defaults.context}, maximum 5. Set 0 for matches only.`, ), }, handler: async (args: unknown): Promise => { - const { query, limit } = args as { query: string; limit?: number }; - const hits = await service.search(query, limit ?? 5); + const { query, limit, context } = args as { query: string; limit?: number; context?: number }; + const hits = await service.search(query, limit ?? defaults.limit, { + context: context ?? defaults.context, + }); if (hits.length === 0) return textResult(`No results for "${query}".`); return textResult(hits.map(formatRetrievedChunk).join("\n\n---\n\n")); }, From 82b67156dc345542fd01084ebd2e5f068cac5400 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Mon, 17 Aug 2026 21:22:09 +0300 Subject: [PATCH 10/11] add eclipse license where appropriate --- mcp/.gitignore | 1 + mcp/Dockerfile | 11 ++++++++++- mcp/src/bin/http.ts | 13 +++++++++++++ mcp/src/bin/ingest.test.ts | 13 +++++++++++++ mcp/src/bin/ingest.ts | 13 +++++++++++++ mcp/src/bin/stdio.test.ts | 13 +++++++++++++ mcp/src/bin/stdio.ts | 13 +++++++++++++ mcp/src/config/examples.test.ts | 13 +++++++++++++ mcp/src/config/load.test.ts | 13 +++++++++++++ mcp/src/config/load.ts | 13 +++++++++++++ mcp/src/config/schema.ts | 13 +++++++++++++ mcp/src/core/types.ts | 13 +++++++++++++ mcp/src/ditto/action-tool.test.ts | 13 +++++++++++++ mcp/src/ditto/action-tool.ts | 13 +++++++++++++ mcp/src/ditto/action-tools.test.ts | 13 +++++++++++++ mcp/src/ditto/action-tools.ts | 13 +++++++++++++ mcp/src/ditto/bundled-spec.test.ts | 13 +++++++++++++ mcp/src/ditto/client.test.ts | 13 +++++++++++++ mcp/src/ditto/client.ts | 13 +++++++++++++ mcp/src/ditto/credential.test.ts | 13 +++++++++++++ mcp/src/ditto/credential.ts | 13 +++++++++++++ mcp/src/ditto/fake-ditto.ts | 13 +++++++++++++ mcp/src/ditto/fake-oidc.ts | 13 +++++++++++++ mcp/src/ditto/openapi.test.ts | 13 +++++++++++++ mcp/src/ditto/openapi.ts | 13 +++++++++++++ mcp/src/ditto/tool-policy.test.ts | 13 +++++++++++++ mcp/src/ditto/tool-policy.ts | 13 +++++++++++++ mcp/src/knowledge/build-index.test.ts | 13 +++++++++++++ mcp/src/knowledge/build-index.ts | 13 +++++++++++++ mcp/src/knowledge/build.test.ts | 13 +++++++++++++ mcp/src/knowledge/build.ts | 13 +++++++++++++ mcp/src/knowledge/chunker.test.ts | 13 +++++++++++++ mcp/src/knowledge/chunker.ts | 13 +++++++++++++ mcp/src/knowledge/embedding.itest.ts | 13 +++++++++++++ mcp/src/knowledge/embedding.test.ts | 13 +++++++++++++ mcp/src/knowledge/embedding.ts | 13 +++++++++++++ mcp/src/knowledge/factories.test.ts | 13 +++++++++++++ mcp/src/knowledge/factories.ts | 13 +++++++++++++ mcp/src/knowledge/fts-retriever.test.ts | 13 +++++++++++++ mcp/src/knowledge/fts-retriever.ts | 13 +++++++++++++ mcp/src/knowledge/hybrid-retriever.test.ts | 13 +++++++++++++ mcp/src/knowledge/hybrid-retriever.ts | 13 +++++++++++++ mcp/src/knowledge/ingest-store.test.ts | 13 +++++++++++++ mcp/src/knowledge/ingest-store.ts | 13 +++++++++++++ mcp/src/knowledge/knowledge-service.test.ts | 13 +++++++++++++ mcp/src/knowledge/knowledge-service.ts | 13 +++++++++++++ mcp/src/knowledge/knowledge-store.ts | 13 +++++++++++++ mcp/src/knowledge/local-dir-source.test.ts | 13 +++++++++++++ mcp/src/knowledge/local-dir-source.ts | 13 +++++++++++++ mcp/src/knowledge/pg-e2e.pgtest.ts | 13 +++++++++++++ mcp/src/knowledge/pg-knowledge-store.pgtest.ts | 13 +++++++++++++ mcp/src/knowledge/pg-knowledge-store.ts | 13 +++++++++++++ mcp/src/knowledge/pg-smoke.pgtest.ts | 13 +++++++++++++ mcp/src/knowledge/pg-testcontainer.ts | 13 +++++++++++++ mcp/src/knowledge/public-source.test.ts | 13 +++++++++++++ mcp/src/knowledge/public-source.ts | 13 +++++++++++++ mcp/src/knowledge/sqlite-knowledge-store.test.ts | 13 +++++++++++++ mcp/src/knowledge/sqlite-knowledge-store.ts | 13 +++++++++++++ mcp/src/knowledge/store-factory.test.ts | 13 +++++++++++++ mcp/src/knowledge/store-factory.ts | 13 +++++++++++++ mcp/src/knowledge/types.ts | 13 +++++++++++++ mcp/src/knowledge/vector-retriever.test.ts | 13 +++++++++++++ mcp/src/knowledge/vector-retriever.ts | 13 +++++++++++++ mcp/src/registry/tool-registry.test.ts | 13 +++++++++++++ mcp/src/registry/tool-registry.ts | 13 +++++++++++++ mcp/src/sanity.test.ts | 13 +++++++++++++ mcp/src/server/build-server.test.ts | 13 +++++++++++++ mcp/src/server/build-server.ts | 13 +++++++++++++ mcp/src/server/http-app.test.ts | 13 +++++++++++++ mcp/src/server/http-app.ts | 13 +++++++++++++ mcp/src/server/request-ctx.test.ts | 13 +++++++++++++ mcp/src/server/request-ctx.ts | 13 +++++++++++++ mcp/src/tools/index.test.ts | 13 +++++++++++++ mcp/src/tools/index.ts | 13 +++++++++++++ mcp/src/tools/knowledge.test.ts | 13 +++++++++++++ mcp/src/tools/knowledge.ts | 13 +++++++++++++ mcp/src/tools/ping.ts | 13 +++++++++++++ mcp/vitest.config.ts | 13 +++++++++++++ mcp/vitest.pg.config.ts | 13 +++++++++++++ 79 files changed, 1012 insertions(+), 1 deletion(-) diff --git a/mcp/.gitignore b/mcp/.gitignore index 763b596b9f..b908ed1bfb 100644 --- a/mcp/.gitignore +++ b/mcp/.gitignore @@ -14,3 +14,4 @@ dist/ # Local config with secrets config.local.json +config.*.local.json diff --git a/mcp/Dockerfile b/mcp/Dockerfile index 58d0bd9e74..e2a595c923 100644 --- a/mcp/Dockerfile +++ b/mcp/Dockerfile @@ -1,4 +1,13 @@ -# syntax=docker/dockerfile:1 +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Eclipse Public License 2.0 which is available at +# http://www.eclipse.org/legal/epl-2.0 +# +# SPDX-License-Identifier: EPL-2.0 # ---- builder: compile native deps + tsc, then drop devDeps ---- # node:22-slim = Debian (glibc). Do NOT use alpine: onnxruntime-node diff --git a/mcp/src/bin/http.ts b/mcp/src/bin/http.ts index a0da02aa5b..1548ead437 100644 --- a/mcp/src/bin/http.ts +++ b/mcp/src/bin/http.ts @@ -1,4 +1,17 @@ #!/usr/bin/env node +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { loadConfig } from "../config/load.js"; import { createHttpApp } from "../server/http-app.js"; import { buildKnowledgeService } from "../knowledge/build.js"; diff --git a/mcp/src/bin/ingest.test.ts b/mcp/src/bin/ingest.test.ts index ef719a6b4f..cba525a992 100644 --- a/mcp/src/bin/ingest.test.ts +++ b/mcp/src/bin/ingest.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; diff --git a/mcp/src/bin/ingest.ts b/mcp/src/bin/ingest.ts index 3ca17e2c95..364314bf86 100644 --- a/mcp/src/bin/ingest.ts +++ b/mcp/src/bin/ingest.ts @@ -1,4 +1,17 @@ #!/usr/bin/env node +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { loadConfig } from "../config/load.js"; import { buildIndex, metaFor } from "../knowledge/build-index.js"; import { makeSources, makeEmbedder } from "../knowledge/factories.js"; diff --git a/mcp/src/bin/stdio.test.ts b/mcp/src/bin/stdio.test.ts index 4b68873bbd..de149e7561 100644 --- a/mcp/src/bin/stdio.test.ts +++ b/mcp/src/bin/stdio.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; diff --git a/mcp/src/bin/stdio.ts b/mcp/src/bin/stdio.ts index 964189b7df..5349c9608e 100644 --- a/mcp/src/bin/stdio.ts +++ b/mcp/src/bin/stdio.ts @@ -1,4 +1,17 @@ #!/usr/bin/env node +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { loadConfig } from "../config/load.js"; import { registerTools } from "../tools/index.js"; diff --git a/mcp/src/config/examples.test.ts b/mcp/src/config/examples.test.ts index 5aaba73a57..cdb8a178b9 100644 --- a/mcp/src/config/examples.test.ts +++ b/mcp/src/config/examples.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { readdirSync } from "node:fs"; import { join, dirname } from "node:path"; diff --git a/mcp/src/config/load.test.ts b/mcp/src/config/load.test.ts index 842be8235e..ba120316ab 100644 --- a/mcp/src/config/load.test.ts +++ b/mcp/src/config/load.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { writeFileSync, mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; diff --git a/mcp/src/config/load.ts b/mcp/src/config/load.ts index c375e0a1d1..9bbe8227ea 100644 --- a/mcp/src/config/load.ts +++ b/mcp/src/config/load.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { readFileSync } from "node:fs"; import { AppConfigSchema, type AppConfig } from "./schema.js"; diff --git a/mcp/src/config/schema.ts b/mcp/src/config/schema.ts index 59da82b98a..ca0559cbf3 100644 --- a/mcp/src/config/schema.ts +++ b/mcp/src/config/schema.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { z } from "zod"; const CredentialSchema = z.object({ diff --git a/mcp/src/core/types.ts b/mcp/src/core/types.ts index ac6c78c6e5..42c0aac2ee 100644 --- a/mcp/src/core/types.ts +++ b/mcp/src/core/types.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { ZodRawShape } from "zod"; import type { AppConfig } from "../config/schema.js"; diff --git a/mcp/src/ditto/action-tool.test.ts b/mcp/src/ditto/action-tool.test.ts index b291e398a1..1c2c19224c 100644 --- a/mcp/src/ditto/action-tool.test.ts +++ b/mcp/src/ditto/action-tool.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { operationToTool } from "./action-tool.js"; import { HttpDittoClient } from "./client.js"; diff --git a/mcp/src/ditto/action-tool.ts b/mcp/src/ditto/action-tool.ts index 47c2ba3e87..9abd1209ce 100644 --- a/mcp/src/ditto/action-tool.ts +++ b/mcp/src/ditto/action-tool.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { z, type ZodRawShape } from "zod"; import type { ToolDef, ToolResult, RequestCtx } from "../core/types.js"; import type { DittoOperation } from "./openapi.js"; diff --git a/mcp/src/ditto/action-tools.test.ts b/mcp/src/ditto/action-tools.test.ts index 18828d6cae..2a21c92088 100644 --- a/mcp/src/ditto/action-tools.test.ts +++ b/mcp/src/ditto/action-tools.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { makeActionTools, buildVersionUrl } from "./action-tools.js"; import { HttpDittoClient } from "./client.js"; diff --git a/mcp/src/ditto/action-tools.ts b/mcp/src/ditto/action-tools.ts index 89e26e34c6..9158c23132 100644 --- a/mcp/src/ditto/action-tools.ts +++ b/mcp/src/ditto/action-tools.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; diff --git a/mcp/src/ditto/bundled-spec.test.ts b/mcp/src/ditto/bundled-spec.test.ts index 26428587b4..9e38335e0c 100644 --- a/mcp/src/ditto/bundled-spec.test.ts +++ b/mcp/src/ditto/bundled-spec.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; diff --git a/mcp/src/ditto/client.test.ts b/mcp/src/ditto/client.test.ts index 2d1221d658..7a7ad16ebd 100644 --- a/mcp/src/ditto/client.test.ts +++ b/mcp/src/ditto/client.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { HttpDittoClient } from "./client.js"; import { startFakeDitto } from "./fake-ditto.js"; diff --git a/mcp/src/ditto/client.ts b/mcp/src/ditto/client.ts index 243890489a..521946eb1a 100644 --- a/mcp/src/ditto/client.ts +++ b/mcp/src/ditto/client.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { DittoOperation } from "./openapi.js"; import type { DittoCredential } from "./credential.js"; diff --git a/mcp/src/ditto/credential.test.ts b/mcp/src/ditto/credential.test.ts index fb0dc92f8f..5eea2fbac8 100644 --- a/mcp/src/ditto/credential.test.ts +++ b/mcp/src/ditto/credential.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { AppConfigSchema } from "../config/schema.js"; import { createConfigCredential, resolveCredential } from "./credential.js"; diff --git a/mcp/src/ditto/credential.ts b/mcp/src/ditto/credential.ts index 7ca8558c12..bb676f6949 100644 --- a/mcp/src/ditto/credential.ts +++ b/mcp/src/ditto/credential.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { CredentialConfig } from "../config/schema.js"; export interface DittoCredential { diff --git a/mcp/src/ditto/fake-ditto.ts b/mcp/src/ditto/fake-ditto.ts index 5057ed1221..3ebac59dea 100644 --- a/mcp/src/ditto/fake-ditto.ts +++ b/mcp/src/ditto/fake-ditto.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { createServer, type Server } from "node:http"; export interface RecordedRequest { method: string; url: string; auth?: string; body?: string } diff --git a/mcp/src/ditto/fake-oidc.ts b/mcp/src/ditto/fake-oidc.ts index 1bcb6a48ea..4ccdbd15dd 100644 --- a/mcp/src/ditto/fake-oidc.ts +++ b/mcp/src/ditto/fake-oidc.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { createServer, type Server } from "node:http"; export interface FakeOidcRequest { diff --git a/mcp/src/ditto/openapi.test.ts b/mcp/src/ditto/openapi.test.ts index ff1fb62380..f03aa6fc74 100644 --- a/mcp/src/ditto/openapi.test.ts +++ b/mcp/src/ditto/openapi.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { parseOperations } from "./openapi.js"; diff --git a/mcp/src/ditto/openapi.ts b/mcp/src/ditto/openapi.ts index 6f9d021e4b..7e2f4d8949 100644 --- a/mcp/src/ditto/openapi.ts +++ b/mcp/src/ditto/openapi.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + export interface OpParam { name: string; in: "path" | "query"; diff --git a/mcp/src/ditto/tool-policy.test.ts b/mcp/src/ditto/tool-policy.test.ts index 0780892758..04875353de 100644 --- a/mcp/src/ditto/tool-policy.test.ts +++ b/mcp/src/ditto/tool-policy.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { isSudo, isAllowed } from "./tool-policy.js"; import type { DittoOperation } from "./openapi.js"; diff --git a/mcp/src/ditto/tool-policy.ts b/mcp/src/ditto/tool-policy.ts index 9da983440e..95e00d21ea 100644 --- a/mcp/src/ditto/tool-policy.ts +++ b/mcp/src/ditto/tool-policy.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { AppConfig } from "../config/schema.js"; import type { DittoOperation } from "./openapi.js"; diff --git a/mcp/src/knowledge/build-index.test.ts b/mcp/src/knowledge/build-index.test.ts index d5d9d4e1bb..2cf6574d00 100644 --- a/mcp/src/knowledge/build-index.test.ts +++ b/mcp/src/knowledge/build-index.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { buildIndex } from "./build-index.js"; import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; diff --git a/mcp/src/knowledge/build-index.ts b/mcp/src/knowledge/build-index.ts index d43ff7cc15..01b534ca08 100644 --- a/mcp/src/knowledge/build-index.ts +++ b/mcp/src/knowledge/build-index.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { KnowledgeSource } from "./types.js"; import type { KnowledgeStore } from "./knowledge-store.js"; import type { EmbeddingProvider } from "./embedding.js"; diff --git a/mcp/src/knowledge/build.test.ts b/mcp/src/knowledge/build.test.ts index f0cd92e979..789c9a416e 100644 --- a/mcp/src/knowledge/build.test.ts +++ b/mcp/src/knowledge/build.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; diff --git a/mcp/src/knowledge/build.ts b/mcp/src/knowledge/build.ts index e814aad25e..96fbcdaa45 100644 --- a/mcp/src/knowledge/build.ts +++ b/mcp/src/knowledge/build.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { existsSync } from "node:fs"; import type { AppConfig } from "../config/schema.js"; import type { Retriever } from "./types.js"; diff --git a/mcp/src/knowledge/chunker.test.ts b/mcp/src/knowledge/chunker.test.ts index 5eacf8e6ea..c82170590a 100644 --- a/mcp/src/knowledge/chunker.test.ts +++ b/mcp/src/knowledge/chunker.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { chunkMarkdown } from "./chunker.js"; diff --git a/mcp/src/knowledge/chunker.ts b/mcp/src/knowledge/chunker.ts index 3ba065934f..039c4889ae 100644 --- a/mcp/src/knowledge/chunker.ts +++ b/mcp/src/knowledge/chunker.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { Chunk } from "./types.js"; export interface ChunkOptions { diff --git a/mcp/src/knowledge/embedding.itest.ts b/mcp/src/knowledge/embedding.itest.ts index 3abeb364c6..1cb08905d9 100644 --- a/mcp/src/knowledge/embedding.itest.ts +++ b/mcp/src/knowledge/embedding.itest.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { LocalEmbeddings } from "./embedding.js"; diff --git a/mcp/src/knowledge/embedding.test.ts b/mcp/src/knowledge/embedding.test.ts index 0b686bfcac..bcd5d0da70 100644 --- a/mcp/src/knowledge/embedding.test.ts +++ b/mcp/src/knowledge/embedding.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { toBatches } from "./embedding.js"; diff --git a/mcp/src/knowledge/embedding.ts b/mcp/src/knowledge/embedding.ts index 2d39447aed..f103c23101 100644 --- a/mcp/src/knowledge/embedding.ts +++ b/mcp/src/knowledge/embedding.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { pipeline, env, type FeatureExtractionPipeline } from "@huggingface/transformers"; export interface EmbeddingProvider { diff --git a/mcp/src/knowledge/factories.test.ts b/mcp/src/knowledge/factories.test.ts index b2848c1c01..e9487a9426 100644 --- a/mcp/src/knowledge/factories.test.ts +++ b/mcp/src/knowledge/factories.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { makeSources } from "./factories.js"; import { AppConfigSchema } from "../config/schema.js"; diff --git a/mcp/src/knowledge/factories.ts b/mcp/src/knowledge/factories.ts index f2837f5a95..65574669de 100644 --- a/mcp/src/knowledge/factories.ts +++ b/mcp/src/knowledge/factories.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { AppConfig } from "../config/schema.js"; import type { KnowledgeSource } from "./types.js"; import type { EmbeddingProvider } from "./embedding.js"; diff --git a/mcp/src/knowledge/fts-retriever.test.ts b/mcp/src/knowledge/fts-retriever.test.ts index a138e85902..6d11664f8a 100644 --- a/mcp/src/knowledge/fts-retriever.test.ts +++ b/mcp/src/knowledge/fts-retriever.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { FtsRetriever } from "./fts-retriever.js"; import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; diff --git a/mcp/src/knowledge/fts-retriever.ts b/mcp/src/knowledge/fts-retriever.ts index 25b2769a48..72fc030e27 100644 --- a/mcp/src/knowledge/fts-retriever.ts +++ b/mcp/src/knowledge/fts-retriever.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { RetrievedChunk, Retriever } from "./types.js"; import type { KnowledgeStore } from "./knowledge-store.js"; diff --git a/mcp/src/knowledge/hybrid-retriever.test.ts b/mcp/src/knowledge/hybrid-retriever.test.ts index c645e970b4..10a5c1f02b 100644 --- a/mcp/src/knowledge/hybrid-retriever.test.ts +++ b/mcp/src/knowledge/hybrid-retriever.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { HybridRetriever } from "./hybrid-retriever.js"; import type { Retriever, Chunk, RetrievedChunk } from "./types.js"; diff --git a/mcp/src/knowledge/hybrid-retriever.ts b/mcp/src/knowledge/hybrid-retriever.ts index 3e9a2d706a..ecdad1b9bf 100644 --- a/mcp/src/knowledge/hybrid-retriever.ts +++ b/mcp/src/knowledge/hybrid-retriever.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { Chunk, Retriever, RetrievedChunk } from "./types.js"; const RRF_K = 60; diff --git a/mcp/src/knowledge/ingest-store.test.ts b/mcp/src/knowledge/ingest-store.test.ts index 313add52e6..22a2af307b 100644 --- a/mcp/src/knowledge/ingest-store.test.ts +++ b/mcp/src/knowledge/ingest-store.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { mkdtempSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; diff --git a/mcp/src/knowledge/ingest-store.ts b/mcp/src/knowledge/ingest-store.ts index ae4b7fe673..a096797060 100644 --- a/mcp/src/knowledge/ingest-store.ts +++ b/mcp/src/knowledge/ingest-store.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { existsSync, renameSync, rmSync } from "node:fs"; import type { AppConfig } from "../config/schema.js"; import type { KnowledgeStore } from "./knowledge-store.js"; diff --git a/mcp/src/knowledge/knowledge-service.test.ts b/mcp/src/knowledge/knowledge-service.test.ts index 4a764ff09b..403ab820cf 100644 --- a/mcp/src/knowledge/knowledge-service.test.ts +++ b/mcp/src/knowledge/knowledge-service.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { KnowledgeService } from "./knowledge-service.js"; import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; diff --git a/mcp/src/knowledge/knowledge-service.ts b/mcp/src/knowledge/knowledge-service.ts index 02f8abf85f..b2f085aa13 100644 --- a/mcp/src/knowledge/knowledge-service.ts +++ b/mcp/src/knowledge/knowledge-service.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { Chunk, RetrievedChunk, Retriever } from "./types.js"; import type { KnowledgeStore } from "./knowledge-store.js"; diff --git a/mcp/src/knowledge/knowledge-store.ts b/mcp/src/knowledge/knowledge-store.ts index b7fcd70729..48cfa9dcce 100644 --- a/mcp/src/knowledge/knowledge-store.ts +++ b/mcp/src/knowledge/knowledge-store.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { Chunk } from "./types.js"; export const SCHEMA_VERSION = 1; diff --git a/mcp/src/knowledge/local-dir-source.test.ts b/mcp/src/knowledge/local-dir-source.test.ts index 05dc7a2d44..ec00f1fb95 100644 --- a/mcp/src/knowledge/local-dir-source.test.ts +++ b/mcp/src/knowledge/local-dir-source.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, beforeEach } from "vitest"; import { mkdtempSync, writeFileSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; diff --git a/mcp/src/knowledge/local-dir-source.ts b/mcp/src/knowledge/local-dir-source.ts index 856d4e8d93..af6b727685 100644 --- a/mcp/src/knowledge/local-dir-source.ts +++ b/mcp/src/knowledge/local-dir-source.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { readdirSync, readFileSync } from "node:fs"; import { join, basename, extname } from "node:path"; import type { Chunk, KnowledgeSource } from "./types.js"; diff --git a/mcp/src/knowledge/pg-e2e.pgtest.ts b/mcp/src/knowledge/pg-e2e.pgtest.ts index b37539c32a..d7868640e4 100644 --- a/mcp/src/knowledge/pg-e2e.pgtest.ts +++ b/mcp/src/knowledge/pg-e2e.pgtest.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { startPgVector } from "./pg-testcontainer.js"; import { AppConfigSchema } from "../config/schema.js"; diff --git a/mcp/src/knowledge/pg-knowledge-store.pgtest.ts b/mcp/src/knowledge/pg-knowledge-store.pgtest.ts index f4f2ded84c..15f2324df0 100644 --- a/mcp/src/knowledge/pg-knowledge-store.pgtest.ts +++ b/mcp/src/knowledge/pg-knowledge-store.pgtest.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { startPgVector } from "./pg-testcontainer.js"; import { PgKnowledgeStore } from "./pg-knowledge-store.js"; diff --git a/mcp/src/knowledge/pg-knowledge-store.ts b/mcp/src/knowledge/pg-knowledge-store.ts index 41eb277cdd..7cfe8b8d4e 100644 --- a/mcp/src/knowledge/pg-knowledge-store.ts +++ b/mcp/src/knowledge/pg-knowledge-store.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import pg from "pg"; import type { Chunk } from "./types.js"; import type { IndexMeta, KnowledgeStore } from "./knowledge-store.js"; diff --git a/mcp/src/knowledge/pg-smoke.pgtest.ts b/mcp/src/knowledge/pg-smoke.pgtest.ts index 403cf62c1c..72ff233687 100644 --- a/mcp/src/knowledge/pg-smoke.pgtest.ts +++ b/mcp/src/knowledge/pg-smoke.pgtest.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { Client } from "pg"; import { startPgVector } from "./pg-testcontainer.js"; diff --git a/mcp/src/knowledge/pg-testcontainer.ts b/mcp/src/knowledge/pg-testcontainer.ts index 983af0b93c..5373f6403c 100644 --- a/mcp/src/knowledge/pg-testcontainer.ts +++ b/mcp/src/knowledge/pg-testcontainer.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { PostgreSqlContainer } from "@testcontainers/postgresql"; export async function startPgVector(): Promise<{ connectionString: string; stop: () => Promise }> { diff --git a/mcp/src/knowledge/public-source.test.ts b/mcp/src/knowledge/public-source.test.ts index fe4c1327e0..5889f1dcdb 100644 --- a/mcp/src/knowledge/public-source.test.ts +++ b/mcp/src/knowledge/public-source.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { PublicSource } from "./public-source.js"; diff --git a/mcp/src/knowledge/public-source.ts b/mcp/src/knowledge/public-source.ts index 38f6c250b1..280faaf7b5 100644 --- a/mcp/src/knowledge/public-source.ts +++ b/mcp/src/knowledge/public-source.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { Chunk, KnowledgeSource } from "./types.js"; import { chunkMarkdown } from "./chunker.js"; diff --git a/mcp/src/knowledge/sqlite-knowledge-store.test.ts b/mcp/src/knowledge/sqlite-knowledge-store.test.ts index 741195673d..8d8184a481 100644 --- a/mcp/src/knowledge/sqlite-knowledge-store.test.ts +++ b/mcp/src/knowledge/sqlite-knowledge-store.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; diff --git a/mcp/src/knowledge/sqlite-knowledge-store.ts b/mcp/src/knowledge/sqlite-knowledge-store.ts index 2bd859dedb..69e5a1a59c 100644 --- a/mcp/src/knowledge/sqlite-knowledge-store.ts +++ b/mcp/src/knowledge/sqlite-knowledge-store.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import Database from "better-sqlite3"; import * as sqliteVec from "sqlite-vec"; import type { Chunk } from "./types.js"; diff --git a/mcp/src/knowledge/store-factory.test.ts b/mcp/src/knowledge/store-factory.test.ts index 74570c88a8..8488bf6361 100644 --- a/mcp/src/knowledge/store-factory.test.ts +++ b/mcp/src/knowledge/store-factory.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { AppConfigSchema } from "../config/schema.js"; import { openStore } from "./store-factory.js"; diff --git a/mcp/src/knowledge/store-factory.ts b/mcp/src/knowledge/store-factory.ts index c6975651fd..04513097c9 100644 --- a/mcp/src/knowledge/store-factory.ts +++ b/mcp/src/knowledge/store-factory.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { AppConfig } from "../config/schema.js"; import type { KnowledgeStore } from "./knowledge-store.js"; diff --git a/mcp/src/knowledge/types.ts b/mcp/src/knowledge/types.ts index 39f65a4107..88894588b9 100644 --- a/mcp/src/knowledge/types.ts +++ b/mcp/src/knowledge/types.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + export interface Chunk { id: string; source: string; diff --git a/mcp/src/knowledge/vector-retriever.test.ts b/mcp/src/knowledge/vector-retriever.test.ts index c82aa98354..fabda53c7d 100644 --- a/mcp/src/knowledge/vector-retriever.test.ts +++ b/mcp/src/knowledge/vector-retriever.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { VectorRetriever } from "./vector-retriever.js"; import { SqliteKnowledgeStore } from "./sqlite-knowledge-store.js"; diff --git a/mcp/src/knowledge/vector-retriever.ts b/mcp/src/knowledge/vector-retriever.ts index a8a56e0c5c..4a1164832d 100644 --- a/mcp/src/knowledge/vector-retriever.ts +++ b/mcp/src/knowledge/vector-retriever.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { RetrievedChunk, Retriever } from "./types.js"; import type { EmbeddingProvider } from "./embedding.js"; import type { KnowledgeStore } from "./knowledge-store.js"; diff --git a/mcp/src/registry/tool-registry.test.ts b/mcp/src/registry/tool-registry.test.ts index ddae13e23d..12d510efa8 100644 --- a/mcp/src/registry/tool-registry.test.ts +++ b/mcp/src/registry/tool-registry.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { ToolRegistry } from "./tool-registry.js"; import type { ToolDef } from "../core/types.js"; diff --git a/mcp/src/registry/tool-registry.ts b/mcp/src/registry/tool-registry.ts index 368615ca8e..9e491a5131 100644 --- a/mcp/src/registry/tool-registry.ts +++ b/mcp/src/registry/tool-registry.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { ToolDef } from "../core/types.js"; export class ToolRegistry { diff --git a/mcp/src/sanity.test.ts b/mcp/src/sanity.test.ts index ea99891018..4b46ba0ae3 100644 --- a/mcp/src/sanity.test.ts +++ b/mcp/src/sanity.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; describe("toolchain sanity", () => { diff --git a/mcp/src/server/build-server.test.ts b/mcp/src/server/build-server.test.ts index facef634e5..ae2d74b8ee 100644 --- a/mcp/src/server/build-server.test.ts +++ b/mcp/src/server/build-server.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; diff --git a/mcp/src/server/build-server.ts b/mcp/src/server/build-server.ts index 2037f236d1..44d5ef5c2c 100644 --- a/mcp/src/server/build-server.ts +++ b/mcp/src/server/build-server.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { ToolRegistry } from "../registry/tool-registry.js"; import type { AppConfig } from "../config/schema.js"; diff --git a/mcp/src/server/http-app.test.ts b/mcp/src/server/http-app.test.ts index 79031b9833..598a1d0254 100644 --- a/mcp/src/server/http-app.test.ts +++ b/mcp/src/server/http-app.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { request } from "node:http"; import type { Server } from "node:http"; diff --git a/mcp/src/server/http-app.ts b/mcp/src/server/http-app.ts index 7552f2cf12..b5e30eb879 100644 --- a/mcp/src/server/http-app.ts +++ b/mcp/src/server/http-app.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import express, { type Express, type Request, type Response } from "express"; import { randomUUID } from "node:crypto"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; diff --git a/mcp/src/server/request-ctx.test.ts b/mcp/src/server/request-ctx.test.ts index b1160d3de6..d42f53db5a 100644 --- a/mcp/src/server/request-ctx.test.ts +++ b/mcp/src/server/request-ctx.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect } from "vitest"; import { AppConfigSchema } from "../config/schema.js"; import { buildCtx } from "./request-ctx.js"; diff --git a/mcp/src/server/request-ctx.ts b/mcp/src/server/request-ctx.ts index 89b8453349..643820a64d 100644 --- a/mcp/src/server/request-ctx.ts +++ b/mcp/src/server/request-ctx.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { AppConfig } from "../config/schema.js"; import type { RequestCtx } from "../core/types.js"; diff --git a/mcp/src/tools/index.test.ts b/mcp/src/tools/index.test.ts index 1dc73671e5..9e25a9b322 100644 --- a/mcp/src/tools/index.test.ts +++ b/mcp/src/tools/index.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { registerTools } from "./index.js"; import { AppConfigSchema } from "../config/schema.js"; diff --git a/mcp/src/tools/index.ts b/mcp/src/tools/index.ts index f572025fd4..9b3aea59a9 100644 --- a/mcp/src/tools/index.ts +++ b/mcp/src/tools/index.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { ToolRegistry } from "../registry/tool-registry.js"; import type { AppConfig } from "../config/schema.js"; import { pingTool } from "./ping.js"; diff --git a/mcp/src/tools/knowledge.test.ts b/mcp/src/tools/knowledge.test.ts index 36f8feb59f..469e87badb 100644 --- a/mcp/src/tools/knowledge.test.ts +++ b/mcp/src/tools/knowledge.test.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { describe, it, expect, afterEach } from "vitest"; import { KnowledgeService } from "../knowledge/knowledge-service.js"; import { SqliteKnowledgeStore } from "../knowledge/sqlite-knowledge-store.js"; diff --git a/mcp/src/tools/knowledge.ts b/mcp/src/tools/knowledge.ts index de1ab5498c..6a81520b16 100644 --- a/mcp/src/tools/knowledge.ts +++ b/mcp/src/tools/knowledge.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { z } from "zod"; import type { ToolDef, ToolResult } from "../core/types.js"; import type { Chunk, RetrievedChunk } from "../knowledge/types.js"; diff --git a/mcp/src/tools/ping.ts b/mcp/src/tools/ping.ts index 47d0fabedf..181adfb272 100644 --- a/mcp/src/tools/ping.ts +++ b/mcp/src/tools/ping.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import type { ToolDef } from "../core/types.js"; export const pingTool: ToolDef = { diff --git a/mcp/vitest.config.ts b/mcp/vitest.config.ts index 54bc60652a..2f3dc01dd8 100644 --- a/mcp/vitest.config.ts +++ b/mcp/vitest.config.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { defineConfig } from "vitest/config"; export default defineConfig({ diff --git a/mcp/vitest.pg.config.ts b/mcp/vitest.pg.config.ts index 17a0216006..a82c52d88d 100644 --- a/mcp/vitest.pg.config.ts +++ b/mcp/vitest.pg.config.ts @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: false, environment: "node", include: ["src/**/*.pgtest.ts"], testTimeout: 120000, hookTimeout: 120000 }, From b4d6bcac999b323904c300f7ba202165ffb21586 Mon Sep 17 00:00:00 2001 From: Kalin Kostashki Date: Wed, 19 Aug 2026 15:03:39 +0300 Subject: [PATCH 11/11] fix: create friendlier configs for writeAllowlist - removed writeAllowlist underscores from configs - stripped unnecessary underscores from tools generation - updated documentation to reflect the changes --- mcp/README.md | 20 +++++++++++--------- mcp/examples/ditto-oidc-write.json | 4 ++-- mcp/src/ditto/action-tool.ts | 11 ++++++++++- mcp/src/ditto/action-tools.test.ts | 16 ++++++++++++++++ mcp/src/ditto/tool-policy.test.ts | 18 ++++++++++++++++++ mcp/src/ditto/tool-policy.ts | 12 ++++++++++-- 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/mcp/README.md b/mcp/README.md index 6d946cca12..6572686eb6 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -364,7 +364,7 @@ Example (separate OIDC identities): "baseUrl": "http://localhost:8080", "credential": { "kind": "oidc", "tokenUrl": "https://idp/token", "clientId": "app", "clientSecret": "..." }, "devopsCredential": { "kind": "oidc", "tokenUrl": "https://idp/token", "clientId": "devops", "clientSecret": "..." }, - "policy": { "sudoAllowlist": ["getConnections"] } + "policy": { "sudoAllowlist": ["GET /api/2/connections"] } } } ``` @@ -380,28 +380,30 @@ By default, action tools only expose **read** (`GET`) operations. Write and priv | Field | Type | Default | Description | |-------|------|---------|-------------| | `ditto.policy.allowMethods` | `string[]` | `["GET"]` | Wholesale HTTP method allowlist (applies to all non-sudo operations) | -| `ditto.policy.writeAllowlist` | `string[]` | `[]` | Per-operation granular allowlist for enabling specific write operations (operationIds) | -| `ditto.policy.sudoAllowlist` | `string[]` | `[]` | Per-operation allowlist for sudo/devops-privileged operations (operationIds) | +| `ditto.policy.writeAllowlist` | `string[]` | `[]` | Per-operation granular allowlist for enabling specific write operations (see key format below) | +| `ditto.policy.sudoAllowlist` | `string[]` | `[]` | Per-operation allowlist for sudo/devops-privileged operations (see key format below) | + +**Allowlist key format:** each entry is either the OpenAPI `operationId` (e.g. `putThing`) **or** a `METHOD path` key (e.g. `PUT /api/2/things/{thingId}`). Ditto's OpenAPI spec does not declare `operationId`s, so use the `METHOD path` form — the `path` is the raw spec path, keeping the `{param}` braces. `allowMethods` (wholesale, non-sudo) is unaffected. **Sudo operations & devops credential:** Ditto secures `/api/2/connections*` (secret-bearing) with `DevOpsBasic`/`DevOpsBearer` security, and `/devops/*` paths are devops-privileged. These operations are classified as "sudo" and: -- Must be explicitly listed in `sudoAllowlist` (by operationId) +- Must be explicitly listed in `sudoAllowlist` (by `METHOD path` key, or operationId if the spec has one) - Require `ditto.devopsCredential` to be configured. - Are NOT auto-allowed even if the method is `GET` and in `allowMethods`. -Connectivity is always devops-gated (classified sudo regardless of the OpenAPI spec's declared security), but policy granularity is unchanged — `sudoAllowlist` is a per-`operationId` opt-in (default `[]` = all sudo blocked). Allow connections while blocking direct-actor/devops commands by listing only the connection operationIds: +Connectivity is always devops-gated (classified sudo regardless of the OpenAPI spec's declared security), but policy granularity is unchanged — `sudoAllowlist` is a per-operation opt-in (default `[]` = all sudo blocked). Allow connections while blocking direct-actor/devops commands by listing only the connection operations: -- Connections read only: `"sudoAllowlist": ["getConnections", "getConnection"]` -- Full connections CRUD, still blocking piggyback/devops: `"sudoAllowlist": ["getConnections","getConnection","createConnection","modifyConnection","deleteConnection"]` +- Connections read only: `"sudoAllowlist": ["GET /api/2/connections", "GET /api/2/connections/{connectionId}"]` +- Full connections CRUD, still blocking piggyback/devops: `"sudoAllowlist": ["GET /api/2/connections","GET /api/2/connections/{connectionId}","POST /api/2/connections","PUT /api/2/connections/{connectionId}","DELETE /api/2/connections/{connectionId}"]` All sudo operations still require `ditto.devopsCredential` to be set. **Examples:** - **Read-only (default):** `{ "allowMethods": ["GET"] }` — only non-sudo GET operations are allowed. -- **Enable specific writes:** `{ "allowMethods": ["GET", "POST", "PATCH"], "writeAllowlist": ["putThing", "modifyThing"] }` — enables specific write operations. -- **Enable sudo:** `{ "allowMethods": ["GET"], "sudoAllowlist": ["getConnections", "getLogging"] }` — enables specific devops-privileged operations (requires devops credential). +- **Enable specific writes:** `{ "allowMethods": ["GET"], "writeAllowlist": ["PUT /api/2/things/{thingId}", "PATCH /api/2/things/{thingId}"] }` — enables specific write operations. +- **Enable sudo:** `{ "allowMethods": ["GET"], "sudoAllowlist": ["GET /api/2/connections", "GET /devops/logging"] }` — enables specific devops-privileged operations (requires devops credential). ### Tools Exposed diff --git a/mcp/examples/ditto-oidc-write.json b/mcp/examples/ditto-oidc-write.json index ea2f32061b..30938f16e1 100644 --- a/mcp/examples/ditto-oidc-write.json +++ b/mcp/examples/ditto-oidc-write.json @@ -16,8 +16,8 @@ }, "policy": { "allowMethods": ["GET"], - "writeAllowlist": ["putThing"], - "sudoAllowlist": ["sudoRetrieveThing", "getConnections"] + "writeAllowlist": ["PUT /api/2/things/{thingId}"], + "sudoAllowlist": ["GET /api/2/connections"] } } } diff --git a/mcp/src/ditto/action-tool.ts b/mcp/src/ditto/action-tool.ts index 9abd1209ce..6a34748d3d 100644 --- a/mcp/src/ditto/action-tool.ts +++ b/mcp/src/ditto/action-tool.ts @@ -19,8 +19,17 @@ import type { DittoCredential } from "./credential.js"; import { resolveCredential } from "./credential.js"; import { isSudo } from "./tool-policy.js"; +// Map an operationId to a legal MCP tool name (MCP names must match [A-Za-z0-9_-]). +// Replace illegal chars, collapse runs of "_", and trim the edges. For specs that +// declare no operationId, the id is a synthesized "_" fallback, so +// collapsing keeps names clean: the /api/2/things GET tool becomes "GET_api_2_things" +// rather than "GET__api_2_things". function sanitizeName(id: string): string { - return id.replace(/[^A-Za-z0-9_]/g, "_").slice(0, 64); + return id + .replace(/[^A-Za-z0-9_]/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + .slice(0, 64); } function inputSchema(op: DittoOperation): ZodRawShape { diff --git a/mcp/src/ditto/action-tools.test.ts b/mcp/src/ditto/action-tools.test.ts index 2a21c92088..eee6294024 100644 --- a/mcp/src/ditto/action-tools.test.ts +++ b/mcp/src/ditto/action-tools.test.ts @@ -95,6 +95,22 @@ describe("makeActionTools", () => { expect(names.length).toBe(3); }); + it("builds a clean tool name from a synthesized operationId (no operationId in spec)", async () => { + const specNoOpId = { + paths: { + "/api/2/things/{thingId}": { + get: { summary: "get", parameters: [{ name: "thingId", in: "path", required: true, schema: { type: "string" } }] }, + }, + }, + }; + fake = await startFakeDitto(() => ({ status: 200, body: "{}" })); + const config = AppConfigSchema.parse({ + ditto: { enabled: true, baseUrl: fake.baseUrl, credential: { kind: "basic", username: "u", password: "p" }, policy: { allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: [] } }, + }); + const list = await makeActionTools(config, { loadSpec: async () => specNoOpId, client: new HttpDittoClient(fake.baseUrl) }); + expect(list.map((t) => t.name)).toEqual(["GET_api_2_things_thingId"]); + }); + it("a sudo tool refuses at call time when no devopsCredential is configured", async () => { const { byName, config } = await tools({ allowMethods: ["GET"], writeAllowlist: [], sudoAllowlist: ["sudoRetrieveThing"] }); const res = await byName.sudoRetrieveThing.handler({ thingId: "ns:1" }, { config, headers: {} } as never); diff --git a/mcp/src/ditto/tool-policy.test.ts b/mcp/src/ditto/tool-policy.test.ts index 04875353de..171e4236cf 100644 --- a/mcp/src/ditto/tool-policy.test.ts +++ b/mcp/src/ditto/tool-policy.test.ts @@ -28,6 +28,24 @@ describe("tool-policy", () => { it("allows a write only when allowlisted", () => { expect(isAllowed(op({ operationId: "putThing", method: "PUT" }), policy({ writeAllowlist: ["putThing"] }))).toBe(true); }); + it("allows a write via a 'METHOD path' allowlist entry (spec has no operationId)", () => { + const putThing = op({ + operationId: "PUT_/api/2/things/{thingId}", // synthesized fallback + method: "PUT", + path: "/api/2/things/{thingId}", + }); + expect(isAllowed(putThing, policy({ writeAllowlist: ["PUT /api/2/things/{thingId}"] }))).toBe(true); + expect(isAllowed(putThing, policy({ writeAllowlist: ["PUT /api/2/other"] }))).toBe(false); + }); + it("allows a sudo op via a 'METHOD path' allowlist entry", () => { + const conn = op({ + operationId: "PUT_/api/2/connections/{connectionId}", + method: "PUT", + path: "/api/2/connections/{connectionId}", + securitySchemes: ["DevOpsBasic"], + }); + expect(isAllowed(conn, policy({ sudoAllowlist: ["PUT /api/2/connections/{connectionId}"] }))).toBe(true); + }); it("treats sudo ops specially: only via sudoAllowlist", () => { const s = op({ operationId: "sudoRetrieveThing", method: "GET" }); expect(isSudo(s)).toBe(true); diff --git a/mcp/src/ditto/tool-policy.ts b/mcp/src/ditto/tool-policy.ts index 95e00d21ea..61e1f4b908 100644 --- a/mcp/src/ditto/tool-policy.ts +++ b/mcp/src/ditto/tool-policy.ts @@ -26,7 +26,15 @@ export function isSudo(op: DittoOperation): boolean { ); } +// Allowlist entries may be either the OpenAPI operationId (e.g. "putThing") or a +// "METHOD path" key (e.g. "PUT /api/2/things/{thingId}"). The latter is the stable, +// user-friendly form when the spec omits operationIds (Ditto's does), in which case +// operationId is a synthesized "METHOD_path" fallback. +function matchesAllowlist(op: DittoOperation, list: string[]): boolean { + return list.includes(op.operationId) || list.includes(`${op.method} ${op.path}`); +} + export function isAllowed(op: DittoOperation, policy: AppConfig["ditto"]["policy"]): boolean { - if (isSudo(op)) return policy.sudoAllowlist.includes(op.operationId); - return policy.allowMethods.includes(op.method) || policy.writeAllowlist.includes(op.operationId); + if (isSudo(op)) return matchesAllowlist(op, policy.sudoAllowlist); + return policy.allowMethods.includes(op.method) || matchesAllowlist(op, policy.writeAllowlist); }