@bojanrajkovic/mcp-paprika 1.6.0 → 2.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -220
- package/dist/aisle/disk.d.ts +3 -0
- package/dist/aisle/disk.js +6 -0
- package/dist/{cache/aisle-store.d.ts → aisle/store.d.ts} +2 -1
- package/dist/aisle/types.d.ts +44 -0
- package/dist/aisle/types.js +21 -0
- package/dist/auth/allowlist.d.ts +1 -1
- package/dist/auth/allowlist.js +1 -1
- package/dist/auth/auth-code-store.d.ts +2 -0
- package/dist/auth/auth-code-store.js +3 -1
- package/dist/auth/auth-request-store.d.ts +2 -0
- package/dist/auth/auth-request-store.js +3 -1
- package/dist/auth/build.d.ts +1 -1
- package/dist/auth/build.js +20 -8
- package/dist/auth/cleanup.d.ts +8 -4
- package/dist/auth/cleanup.js +7 -2
- package/dist/auth/client-registration.d.ts +1 -1
- package/dist/auth/client-registration.js +1 -1
- package/dist/auth/consent-page.d.ts +49 -0
- package/dist/auth/consent-page.js +183 -0
- package/dist/auth/dcr-validator.js +7 -16
- package/dist/auth/errors.js +1 -1
- package/dist/auth/metadata.d.ts +1 -0
- package/dist/auth/metadata.js +9 -0
- package/dist/auth/oidc-client.d.ts +2 -2
- package/dist/auth/oidc-client.js +3 -13
- package/dist/auth/pending-authorization-store.d.ts +28 -0
- package/dist/auth/pending-authorization-store.js +30 -0
- package/dist/auth/presets.d.ts +1 -1
- package/dist/auth/presets.js +1 -1
- package/dist/auth/provider.d.ts +16 -8
- package/dist/auth/provider.js +59 -25
- package/dist/auth/redirect-allowlist.d.ts +50 -0
- package/dist/auth/redirect-allowlist.js +81 -0
- package/dist/auth/routes.d.ts +7 -5
- package/dist/auth/routes.js +53 -3
- package/dist/auth/token-store.d.ts +4 -4
- package/dist/auth/token-store.js +2 -2
- package/dist/auth/tokens.d.ts +18 -1
- package/dist/auth/tokens.js +27 -2
- package/dist/auth/ttl-store.d.ts +16 -2
- package/dist/auth/ttl-store.js +21 -1
- package/dist/auth/types.d.ts +83 -38
- package/dist/auth/types.js +21 -0
- package/dist/auth/upstream-redirect.d.ts +49 -0
- package/dist/auth/upstream-redirect.js +60 -0
- package/dist/cache/{disk/root.d.ts → disk-cache-root.d.ts} +15 -5
- package/dist/cache/{disk/root.js → disk-cache-root.js} +45 -78
- package/dist/cache/{disk/base.d.ts → disk-cache.d.ts} +15 -0
- package/dist/cache/{disk/base.js → disk-cache.js} +2 -2
- package/dist/cache/{disk/oauth-clients.d.ts → oauth-client-disk-cache.d.ts} +2 -2
- package/dist/cache/{disk/oauth-clients.js → oauth-client-disk-cache.js} +2 -2
- package/dist/category/disk.d.ts +3 -0
- package/dist/category/disk.js +6 -0
- package/dist/{cache/category-store.d.ts → category/store.d.ts} +2 -1
- package/dist/category/types.d.ts +44 -0
- package/dist/category/types.js +22 -0
- package/dist/features/discover-feature.d.ts +22 -5
- package/dist/features/discover-feature.js +79 -9
- package/dist/features/embeddings.d.ts +1 -1
- package/dist/features/embeddings.js +3 -3
- package/dist/features/generated-image-store.d.ts +59 -0
- package/dist/features/generated-image-store.js +77 -0
- package/dist/features/json-vector-index.d.ts +153 -0
- package/dist/features/json-vector-index.js +358 -0
- package/dist/features/photography.d.ts +1 -1
- package/dist/features/photography.js +2 -2
- package/dist/features/vector-store.d.ts +10 -7
- package/dist/features/vector-store.js +77 -30
- package/dist/grocery-ingredient/disk.d.ts +3 -0
- package/dist/grocery-ingredient/disk.js +6 -0
- package/dist/{cache/grocery-ingredient-store.d.ts → grocery-ingredient/store.d.ts} +1 -1
- package/dist/grocery-ingredient/types.d.ts +44 -0
- package/dist/grocery-ingredient/types.js +21 -0
- package/dist/grocery-item/disk.d.ts +3 -0
- package/dist/grocery-item/disk.js +6 -0
- package/dist/{cache/grocery-item-store.d.ts → grocery-item/store.d.ts} +2 -1
- package/dist/grocery-item/types.d.ts +116 -0
- package/dist/grocery-item/types.js +41 -0
- package/dist/grocery-list/disk.d.ts +3 -0
- package/dist/grocery-list/disk.js +6 -0
- package/dist/{cache/grocery-list-store.d.ts → grocery-list/store.d.ts} +2 -1
- package/dist/grocery-list/types.d.ts +60 -0
- package/dist/grocery-list/types.js +27 -0
- package/dist/ids.d.ts +63 -0
- package/dist/ids.js +51 -0
- package/dist/meal/disk.d.ts +3 -0
- package/dist/meal/disk.js +6 -0
- package/dist/{cache/meal-store.d.ts → meal/store.d.ts} +10 -2
- package/dist/{cache/meal-store.js → meal/store.js} +3 -1
- package/dist/meal/types.d.ts +93 -0
- package/dist/meal/types.js +54 -0
- package/dist/meal-type/disk.d.ts +3 -0
- package/dist/meal-type/disk.js +6 -0
- package/dist/{cache/meal-type-store.d.ts → meal-type/store.d.ts} +2 -1
- package/dist/meal-type/types.d.ts +76 -0
- package/dist/meal-type/types.js +42 -0
- package/dist/menu/disk.d.ts +3 -0
- package/dist/menu/disk.js +6 -0
- package/dist/{cache/menu-store.d.ts → menu/store.d.ts} +2 -1
- package/dist/menu/types.d.ts +61 -0
- package/dist/menu/types.js +34 -0
- package/dist/menu-item/disk.d.ts +3 -0
- package/dist/menu-item/disk.js +6 -0
- package/dist/{cache/menu-item-store.d.ts → menu-item/store.d.ts} +2 -1
- package/dist/menu-item/types.d.ts +77 -0
- package/dist/menu-item/types.js +49 -0
- package/dist/pantry/disk.d.ts +3 -0
- package/dist/pantry/disk.js +6 -0
- package/dist/{cache/pantry-store.d.ts → pantry/store.d.ts} +2 -1
- package/dist/pantry/types.d.ts +100 -0
- package/dist/pantry/types.js +41 -0
- package/dist/paprika/auth-response.d.ts +19 -0
- package/dist/paprika/auth-response.js +9 -0
- package/dist/paprika/client.d.ts +35 -3
- package/dist/paprika/client.js +67 -13
- package/dist/paprika/recipe-hash.d.ts +35 -0
- package/dist/paprika/recipe-hash.js +69 -0
- package/dist/paprika/sync-types.d.ts +29 -0
- package/dist/paprika/sync-types.js +1 -0
- package/dist/paprika/sync.d.ts +17 -4
- package/dist/paprika/sync.js +88 -80
- package/dist/photo/disk.d.ts +3 -0
- package/dist/photo/disk.js +6 -0
- package/dist/{cache/photo-store.d.ts → photo/store.d.ts} +3 -2
- package/dist/{cache/photo-store.js → photo/store.js} +1 -1
- package/dist/photo/types.d.ts +69 -0
- package/dist/photo/types.js +52 -0
- package/dist/{cache/disk/recipes.d.ts → recipe/disk.d.ts} +3 -2
- package/dist/{cache/disk/recipes.js → recipe/disk.js} +7 -3
- package/dist/{cache/recipe-store.d.ts → recipe/store.d.ts} +2 -1
- package/dist/recipe/types.d.ts +257 -0
- package/dist/recipe/types.js +112 -0
- package/dist/resources/recipes.js +4 -1
- package/dist/server/app-context.d.ts +18 -15
- package/dist/server/build.d.ts +7 -25
- package/dist/server/build.js +180 -118
- package/dist/server/notifier.d.ts +15 -0
- package/dist/server/notifier.js +9 -0
- package/dist/tools/aisle-helpers.d.ts +3 -2
- package/dist/tools/aisle-helpers.js +3 -3
- package/dist/tools/categories.js +1 -1
- package/dist/tools/category-helpers.d.ts +10 -2
- package/dist/tools/category-helpers.js +28 -1
- package/dist/tools/category-writes.js +9 -7
- package/dist/tools/create.js +15 -9
- package/dist/tools/delete.js +4 -4
- package/dist/tools/discover.d.ts +1 -1
- package/dist/tools/discover.js +13 -3
- package/dist/tools/empty-trash.js +17 -12
- package/dist/tools/grocery-clear.js +10 -10
- package/dist/tools/grocery-helpers.d.ts +2 -1
- package/dist/tools/grocery-item-purchase.d.ts +11 -0
- package/dist/tools/grocery-item-purchase.js +37 -0
- package/dist/tools/grocery-item.d.ts +17 -0
- package/dist/tools/grocery-item.js +19 -16
- package/dist/tools/grocery-list.js +3 -3
- package/dist/tools/grocery-move.js +8 -8
- package/dist/tools/helpers.d.ts +22 -2
- package/dist/tools/helpers.js +127 -1
- package/dist/tools/meal-add-menu.js +12 -12
- package/dist/tools/meal-helpers.d.ts +23 -4
- package/dist/tools/meal-helpers.js +80 -4
- package/dist/tools/meal-history-search.d.ts +59 -0
- package/dist/tools/meal-history-search.js +151 -0
- package/dist/tools/meal-log-cooked.d.ts +47 -0
- package/dist/tools/meal-log-cooked.js +85 -0
- package/dist/tools/meal-plan-read.d.ts +11 -0
- package/dist/tools/meal-plan-read.js +54 -0
- package/dist/tools/meal-reschedule.d.ts +47 -0
- package/dist/tools/meal-reschedule.js +96 -0
- package/dist/tools/meal-types.js +2 -2
- package/dist/tools/meal-writes.d.ts +4 -19
- package/dist/tools/meal-writes.js +21 -56
- package/dist/tools/menu-helpers.d.ts +3 -1
- package/dist/tools/menu-item-move.d.ts +14 -0
- package/dist/tools/menu-item-move.js +94 -0
- package/dist/tools/menu-item-write.d.ts +9 -12
- package/dist/tools/menu-item-write.js +22 -58
- package/dist/tools/menu-read.js +1 -1
- package/dist/tools/menu-write.js +3 -3
- package/dist/tools/pantry-batch-add.js +8 -8
- package/dist/tools/pantry-delete.js +1 -1
- package/dist/tools/pantry-get.js +5 -5
- package/dist/tools/pantry-helpers.d.ts +1 -1
- package/dist/tools/pantry-list.js +5 -5
- package/dist/tools/pantry-stock.d.ts +19 -0
- package/dist/tools/pantry-stock.js +69 -0
- package/dist/tools/pantry-update.d.ts +23 -0
- package/dist/tools/pantry-update.js +30 -26
- package/dist/tools/photo-fetch.d.ts +1 -1
- package/dist/tools/photo-fetch.js +2 -2
- package/dist/tools/photo-generate.js +23 -15
- package/dist/tools/photo-helpers.d.ts +14 -6
- package/dist/tools/photo-helpers.js +16 -6
- package/dist/tools/photo-writes.d.ts +58 -6
- package/dist/tools/photo-writes.js +93 -39
- package/dist/tools/read.js +1 -1
- package/dist/tools/recipe-categorize.d.ts +17 -0
- package/dist/tools/recipe-categorize.js +76 -0
- package/dist/tools/recipe-favorite.d.ts +19 -0
- package/dist/tools/recipe-favorite.js +70 -0
- package/dist/tools/recipe-rating.d.ts +14 -0
- package/dist/tools/recipe-rating.js +38 -0
- package/dist/tools/recipe-restore.d.ts +11 -0
- package/dist/tools/recipe-restore.js +66 -0
- package/dist/tools/search.d.ts +26 -0
- package/dist/tools/search.js +161 -26
- package/dist/tools/update.d.ts +47 -0
- package/dist/tools/update.js +39 -48
- package/dist/transport/favicon.d.ts +11 -0
- package/dist/transport/favicon.js +25 -0
- package/dist/transport/http.d.ts +1 -1
- package/dist/transport/http.js +9 -3
- package/dist/transport/stdio.js +9 -7
- package/dist/utils/branding.d.ts +44 -0
- package/dist/utils/branding.js +75 -0
- package/dist/utils/config.d.ts +52 -34
- package/dist/utils/config.js +26 -8
- package/dist/utils/dates.d.ts +64 -36
- package/dist/utils/dates.js +152 -55
- package/dist/utils/duration.js +1 -1
- package/dist/utils/log.d.ts +2 -2
- package/dist/utils/log.js +2 -2
- package/dist/utils/resilience.js +2 -2
- package/dist/utils/xdg.js +1 -1
- package/docs/configuration.md +222 -0
- package/docs/deployment.md +133 -0
- package/docs/embedding-providers.md +103 -0
- package/docs/http-transport.md +120 -0
- package/docs/oauth-configuration.md +117 -0
- package/docs/quick-start-http.md +90 -0
- package/package.json +18 -12
- package/dist/cache/disk/index.d.ts +0 -5
- package/dist/cache/disk/index.js +0 -4
- package/dist/paprika/dates.d.ts +0 -25
- package/dist/paprika/dates.js +0 -57
- package/dist/paprika/types.d.ts +0 -1094
- package/dist/paprika/types.js +0 -517
- package/dist/tools/filter.d.ts +0 -3
- package/dist/tools/filter.js +0 -141
- package/dist/tools/meal-history.d.ts +0 -3
- package/dist/tools/meal-history.js +0 -188
- /package/dist/{cache/aisle-store.js → aisle/store.js} +0 -0
- /package/dist/{cache/category-store.js → category/store.js} +0 -0
- /package/dist/{cache/grocery-ingredient-store.js → grocery-ingredient/store.js} +0 -0
- /package/dist/{cache/grocery-item-store.js → grocery-item/store.js} +0 -0
- /package/dist/{cache/grocery-list-store.js → grocery-list/store.js} +0 -0
- /package/dist/{cache/meal-type-store.js → meal-type/store.js} +0 -0
- /package/dist/{cache/menu-store.js → menu/store.js} +0 -0
- /package/dist/{cache/menu-item-store.js → menu-item/store.js} +0 -0
- /package/dist/{cache/pantry-store.js → pantry/store.js} +0 -0
- /package/dist/{cache/recipe-store.js → recipe/store.js} +0 -0
package/README.md
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# @bojanrajkovic/mcp-paprika
|
|
2
2
|
|
|
3
|
-
An [MCP](https://modelcontextprotocol.io/) server for [Paprika](https://www.paprikaapp.com/) recipe manager. Search, browse, create, and manage your recipes from any MCP client.
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io/) server for the [Paprika](https://www.paprikaapp.com/) recipe manager. Search, browse, create, and manage your recipes from any MCP client.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **Full tool coverage** for recipe, pantry, grocery, meal-planner, and menu management — search, filter, CRUD, categories, pagination, pantry inventory, aisles, grocery lists and items, meal planning (the upcoming plan, history recall, meal types, dated planner entries), and menus (recipe collections, their items, and one-shot add-to-planner)
|
|
8
8
|
- **Semantic search** via `discover_recipes` — find recipes by natural language description using any OpenAI-compatible embedding provider
|
|
9
|
-
- **AI recipe photos** via `
|
|
9
|
+
- **AI recipe photos** via `generate_recipe_photo` — generate a styled food photo for a recipe (or restyle its existing one) using OpenRouter image models, and attach it automatically
|
|
10
10
|
- **Background sync** — keeps your local cache in sync with Paprika's cloud
|
|
11
|
-
- **MCP resources** —
|
|
11
|
+
- **MCP resources** — recipes as `paprika://recipe/{uid}`, grocery lists as `paprika://grocery-list/{uid}`, and menus as `paprika://menu/{uid}`
|
|
12
12
|
- **Two transports** — stdio (default, for CLI clients) and Streamable HTTP (for mobile/web clients)
|
|
13
|
-
- **Container image** —
|
|
13
|
+
- **Container image** — a distroless runtime ready for self-hosting
|
|
14
14
|
|
|
15
15
|
## Transports
|
|
16
16
|
|
|
17
|
-
`mcp-paprika`
|
|
17
|
+
`mcp-paprika` speaks the MCP protocol over two transports, selected via `MCP_TRANSPORT`:
|
|
18
18
|
|
|
19
19
|
| Transport | Default? | Use it for |
|
|
20
20
|
| --------- | -------- | ----------------------------------------------------------------------------------- |
|
|
21
21
|
| `stdio` | yes | Local CLI clients: Claude Code, Claude Desktop, Cursor, mcp-cli |
|
|
22
22
|
| `http` | no | Streamable HTTP for Claude Mobile and other HTTP-based MCP clients, or self-hosting |
|
|
23
23
|
|
|
24
|
-
The HTTP transport ships with **OAuth 2.1** (authorization code + PKCE, RFC 7591
|
|
24
|
+
The HTTP transport ships with **OAuth 2.1** (authorization code + PKCE, RFC 7591
|
|
25
|
+
dynamic client registration); the [HTTP transport quick start](docs/quick-start-http.md)
|
|
26
|
+
sets it up end to end.
|
|
25
27
|
|
|
26
28
|
## Quick start — stdio (Claude Desktop / Claude Code / Cursor)
|
|
27
29
|
|
|
@@ -42,226 +44,33 @@ Add to your MCP client config:
|
|
|
42
44
|
}
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
The HTTP transport uses **OAuth 2.1 with OIDC delegation**: `mcp-paprika` acts as the OAuth authorization server toward MCP clients, and delegates authentication to an upstream identity provider (IdP) of your choice.
|
|
48
|
-
|
|
49
|
-
### Step 1 — Choose an upstream IdP
|
|
50
|
-
|
|
51
|
-
Pick a preset or supply a raw discovery URL:
|
|
52
|
-
|
|
53
|
-
| Preset value | IdP | Notes |
|
|
54
|
-
| ------------ | ----------------------------- | ------------------------------------------------------------- |
|
|
55
|
-
| `google` | Google | Discovery URL built-in |
|
|
56
|
-
| `entra` | Microsoft Entra ID (Azure AD) | Tenant-bound — requires `MCP_OIDC_DISCOVERY_URL` |
|
|
57
|
-
| `okta` | Okta | Tenant-bound — requires `MCP_OIDC_DISCOVERY_URL` |
|
|
58
|
-
| `auth0` | Auth0 | Tenant-bound — requires `MCP_OIDC_DISCOVERY_URL` |
|
|
59
|
-
| `keycloak` | Keycloak | Tenant-bound — requires `MCP_OIDC_DISCOVERY_URL` |
|
|
60
|
-
| _(none)_ | Custom | Set `MCP_OIDC_DISCOVERY_URL` directly; omit `MCP_OIDC_PRESET` |
|
|
61
|
-
|
|
62
|
-
### Step 2 — Register one OAuth client in your IdP
|
|
63
|
-
|
|
64
|
-
In your IdP's developer console (e.g., Google Cloud Console → APIs & Services → Credentials → OAuth 2.0 Client IDs), create a **single** OAuth 2.0 client with:
|
|
65
|
-
|
|
66
|
-
- **Application type:** Web application
|
|
67
|
-
- **Redirect URI:** `https://<your-MCP_PUBLIC_URL>/oauth/callback`
|
|
68
|
-
|
|
69
|
-
Copy the resulting client ID and client secret — these become `MCP_OIDC_CLIENT_ID` and `MCP_OIDC_CLIENT_SECRET`.
|
|
70
|
-
|
|
71
|
-
> **Tenant-bound presets (entra, okta, auth0, keycloak):** also copy the tenant-specific discovery URL from your IdP. For Entra this is `https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration`.
|
|
72
|
-
|
|
73
|
-
### Step 3 — Configure and start the server
|
|
74
|
-
|
|
75
|
-
Full env-var reference:
|
|
76
|
-
|
|
77
|
-
| Env var | Required? | Description |
|
|
78
|
-
| -------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
-
| `MCP_TRANSPORT` | yes | Set to `http` |
|
|
80
|
-
| `MCP_PUBLIC_URL` | yes | Canonical `https://` URL of this server; used as OAuth issuer. No trailing slash. |
|
|
81
|
-
| `MCP_OIDC_PRESET` | one of preset or discoveryUrl | `google`, `entra`, `okta`, `auth0`, or `keycloak` |
|
|
82
|
-
| `MCP_OIDC_DISCOVERY_URL` | one of preset or discoveryUrl | Raw OIDC discovery URL; required for tenant-bound presets |
|
|
83
|
-
| `MCP_OIDC_CLIENT_ID` | yes | Client ID from upstream IdP |
|
|
84
|
-
| `MCP_OIDC_CLIENT_SECRET` | yes | Client secret from upstream IdP |
|
|
85
|
-
| `MCP_ALLOWED_EMAILS` | one of emails or subs | Comma-separated list of allowed email addresses |
|
|
86
|
-
| `MCP_ALLOWED_SUBS` | one of emails or subs | Comma-separated list of allowed subject IDs |
|
|
87
|
-
| `MCP_OIDC_SCOPES` | no | Override preset's scope list (comma-separated; default `openid email profile`) |
|
|
88
|
-
| `MCP_OIDC_EMAIL_VERIFIED_POLICY` | no | `strict` (default), `skip`, or `if-present` |
|
|
89
|
-
| `MCP_OIDC_ALLOWED_ALGS` | no | Override preset's allowed id_token signing algorithms (comma-separated) |
|
|
90
|
-
| `MCP_TRUST_PROXY` | no | `true` to trust `X-Forwarded-For` / `CF-Connecting-IP` for the DCR rate-limit key. Default `false` (safe for direct exposure). Set `true` only behind a sanitizing reverse proxy (k8s ingress, Tailscale Funnel, Cloudflare). |
|
|
91
|
-
|
|
92
|
-
Example startup command:
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
MCP_TRANSPORT=http \
|
|
96
|
-
MCP_PUBLIC_URL=https://mcp.example.com \
|
|
97
|
-
MCP_OIDC_PRESET=google \
|
|
98
|
-
MCP_OIDC_CLIENT_ID=123456789-abc.apps.googleusercontent.com \
|
|
99
|
-
MCP_OIDC_CLIENT_SECRET=GOCSPX-... \
|
|
100
|
-
MCP_ALLOWED_EMAILS=you@example.com \
|
|
101
|
-
PAPRIKA_EMAIL=you@example.com \
|
|
102
|
-
PAPRIKA_PASSWORD=your-password \
|
|
103
|
-
npx -y @bojanrajkovic/mcp-paprika
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Step 4 — Configure the allowlist
|
|
107
|
-
|
|
108
|
-
The allowlist uses **OR semantics**: access is granted if the authenticated user's email is in `MCP_ALLOWED_EMAILS` OR their subject ID is in `MCP_ALLOWED_SUBS`. At least one list must be non-empty.
|
|
109
|
-
|
|
110
|
-
- **`MCP_ALLOWED_EMAILS`** — comma-separated email addresses. Subject to `MCP_OIDC_EMAIL_VERIFIED_POLICY`:
|
|
111
|
-
- `strict` (default): email must be present and `email_verified = true`
|
|
112
|
-
- `skip`: email is accepted without checking `email_verified`
|
|
113
|
-
- `if-present`: if `email_verified` is in the id_token, it must be `true`; if absent, the email is accepted
|
|
114
|
-
- **`MCP_ALLOWED_SUBS`** — comma-separated subject IDs (stable per-user opaque identifiers from the IdP). Useful when you want to allow access regardless of email verification status.
|
|
115
|
-
|
|
116
|
-
### Step 5 — Add as a Claude connector
|
|
117
|
-
|
|
118
|
-
1. Open [claude.ai](https://claude.ai) → Settings → Connectors
|
|
119
|
-
2. Click "Add custom connector"
|
|
120
|
-
3. Enter your server URL: `https://<MCP_PUBLIC_URL>/mcp`
|
|
121
|
-
4. Claude will redirect your browser to the upstream IdP for authentication
|
|
122
|
-
5. After sign-in, you are redirected back and the connector is authorized
|
|
123
|
-
|
|
124
|
-
### Verify the OAuth metadata
|
|
125
|
-
|
|
126
|
-
```bash
|
|
127
|
-
curl -sf https://<MCP_PUBLIC_URL>/.well-known/oauth-authorization-server | jq .issuer
|
|
128
|
-
# → "https://<MCP_PUBLIC_URL>"
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
The server also exposes:
|
|
132
|
-
|
|
133
|
-
- `POST /mcp` — MCP JSON-RPC over Streamable HTTP
|
|
134
|
-
- `GET /mcp` — long-lived SSE channel for server→client notifications
|
|
135
|
-
- `DELETE /mcp` — session termination
|
|
136
|
-
- `GET /healthz` — liveness probe returning `{ "ok": true, "sessions": <n> }`
|
|
137
|
-
|
|
138
|
-
## Quick start — container
|
|
139
|
-
|
|
140
|
-
The image defaults to `MCP_TRANSPORT=http`, so a container run needs the same
|
|
141
|
-
OAuth environment that the [HTTP transport quick start](#quick-start--http-transport)
|
|
142
|
-
walks through — `MCP_PUBLIC_URL`, an OIDC preset (or discovery URL), upstream
|
|
143
|
-
client credentials, and a non-empty allowlist. Without those, the server exits
|
|
144
|
-
during config validation.
|
|
145
|
-
|
|
146
|
-
Pull the published image (multi-arch: `linux/amd64`, `linux/arm64`):
|
|
47
|
+
That's the whole local setup. To enable semantic search or AI recipe photos, add the
|
|
48
|
+
optional provider credentials from [configuration.md](docs/configuration.md).
|
|
147
49
|
|
|
148
|
-
|
|
149
|
-
docker pull ghcr.io/bojanrajkovic/mcp-paprika:latest
|
|
150
|
-
|
|
151
|
-
docker run --rm \
|
|
152
|
-
-e PAPRIKA_EMAIL=you@example.com \
|
|
153
|
-
-e PAPRIKA_PASSWORD=your-password \
|
|
154
|
-
-e MCP_PUBLIC_URL=https://mcp.example.com \
|
|
155
|
-
-e MCP_OIDC_PRESET=google \
|
|
156
|
-
-e MCP_OIDC_CLIENT_ID=123456789-abc.apps.googleusercontent.com \
|
|
157
|
-
-e MCP_OIDC_CLIENT_SECRET=GOCSPX-... \
|
|
158
|
-
-e MCP_ALLOWED_EMAILS=you@example.com \
|
|
159
|
-
-v "$(pwd)/data:/data" \
|
|
160
|
-
-p 3000:3000 \
|
|
161
|
-
ghcr.io/bojanrajkovic/mcp-paprika:latest
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
The image is signed with [sigstore/cosign](https://github.com/sigstore/cosign) keyless OIDC and ships SLSA build provenance + an SPDX SBOM as OCI attestations. Verify both before running in untrusted environments — `gh attestation verify` without `--predicate-type` only validates the default (provenance) attestation, so the SBOM needs its own verification:
|
|
165
|
-
|
|
166
|
-
```bash
|
|
167
|
-
# SLSA build provenance
|
|
168
|
-
gh attestation verify oci://ghcr.io/bojanrajkovic/mcp-paprika:latest \
|
|
169
|
-
--owner bojanrajkovic \
|
|
170
|
-
--predicate-type https://slsa.dev/provenance/v1
|
|
171
|
-
|
|
172
|
-
# SPDX SBOM
|
|
173
|
-
gh attestation verify oci://ghcr.io/bojanrajkovic/mcp-paprika:latest \
|
|
174
|
-
--owner bojanrajkovic \
|
|
175
|
-
--predicate-type https://spdx.dev/Document/v2.3
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Contributors building from source can use `docker build -t mcp-paprika:dev .` and substitute `mcp-paprika:dev` for the image reference below.
|
|
179
|
-
|
|
180
|
-
For a one-shot smoke test that just verifies the image launches (no OAuth, no
|
|
181
|
-
remote clients), override the transport to `stdio` — note that this turns the
|
|
182
|
-
container into a CLI process that speaks MCP on stdin/stdout, so the port
|
|
183
|
-
mapping isn't used:
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
docker run --rm -i \
|
|
187
|
-
-e MCP_TRANSPORT=stdio \
|
|
188
|
-
-e PAPRIKA_EMAIL=you@example.com \
|
|
189
|
-
-e PAPRIKA_PASSWORD=your-password \
|
|
190
|
-
-v "$(pwd)/data:/data" \
|
|
191
|
-
ghcr.io/bojanrajkovic/mcp-paprika:latest
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
The HTTP-mode image binds on `0.0.0.0:3000` and persists the disk cache and
|
|
195
|
-
vector index under `/data` (the documented mount point). Both `/data`
|
|
196
|
-
sub-directories (`config/`, `cache/`) are pre-created with `nonroot` (UID 65532)
|
|
197
|
-
ownership in the image so writes work the first time even on a fresh bind-mount.
|
|
198
|
-
|
|
199
|
-
If you bind-mount a host directory you created as root, pre-chown it:
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
mkdir -p ./data && sudo chown -R 65532:65532 ./data
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Or use a named volume (Docker handles ownership automatically):
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
docker run --rm \
|
|
209
|
-
-e PAPRIKA_EMAIL=... -e PAPRIKA_PASSWORD=... \
|
|
210
|
-
-v mcp-paprika-data:/data \
|
|
211
|
-
-p 3000:3000 \
|
|
212
|
-
ghcr.io/bojanrajkovic/mcp-paprika:latest
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
The image also declares a `HEALTHCHECK` that hits `GET /healthz`; verify with:
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
docker inspect --format '{{.State.Health.Status}}' <container>
|
|
219
|
-
# → healthy
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
## Deployment patterns (HTTP transport)
|
|
223
|
-
|
|
224
|
-
The HTTP transport ships with OAuth 2.1 built in. The primary remaining concerns are TLS termination and, optionally, additional network-layer controls.
|
|
225
|
-
|
|
226
|
-
**TLS termination** is required — `MCP_PUBLIC_URL` must be `https://` and OAuth requires encrypted connections end-to-end. Recommended options:
|
|
227
|
-
|
|
228
|
-
- **Reverse proxy with TLS** (nginx / Caddy) — terminates TLS, passes `X-Forwarded-For` headers (required for rate limiting), and forwards to the container on a private port.
|
|
229
|
-
- **Cloudflare Tunnel** — no inbound port exposed; Cloudflare terminates TLS. Works well with Cloudflare Access for an additional authentication layer if desired.
|
|
230
|
-
- **Tailscale HTTPS** — Tailscale's built-in HTTPS cert provisioning; suitable for homelab setups where all clients are on your tailnet.
|
|
50
|
+
## Quick start — HTTP transport
|
|
231
51
|
|
|
232
|
-
|
|
52
|
+
For remote clients (Claude Mobile, claude.ai), the server speaks Streamable HTTP behind
|
|
53
|
+
OAuth 2.1, delegating identity to an upstream OIDC provider you choose. The
|
|
54
|
+
[HTTP transport quick start](docs/quick-start-http.md) walks an IdP from zero to a
|
|
55
|
+
working Claude connector, and [deployment.md](docs/deployment.md) covers running it in a
|
|
56
|
+
container, behind a reverse proxy, or with Docker Compose.
|
|
233
57
|
|
|
234
|
-
|
|
235
|
-
services:
|
|
236
|
-
mcp-paprika:
|
|
237
|
-
image: ghcr.io/bojanrajkovic/mcp-paprika:latest
|
|
238
|
-
environment:
|
|
239
|
-
MCP_TRANSPORT: http
|
|
240
|
-
MCP_PUBLIC_URL: https://mcp.example.com
|
|
241
|
-
MCP_OIDC_PRESET: google
|
|
242
|
-
MCP_OIDC_CLIENT_ID: "<your-client-id>"
|
|
243
|
-
MCP_OIDC_CLIENT_SECRET: "<your-client-secret>"
|
|
244
|
-
MCP_ALLOWED_EMAILS: "you@example.com"
|
|
245
|
-
PAPRIKA_EMAIL: "you@example.com"
|
|
246
|
-
PAPRIKA_PASSWORD: "<your-paprika-password>"
|
|
247
|
-
volumes:
|
|
248
|
-
- mcp-paprika-data:/data
|
|
249
|
-
ports:
|
|
250
|
-
- "127.0.0.1:3000:3000"
|
|
58
|
+
## Documentation
|
|
251
59
|
|
|
252
|
-
|
|
253
|
-
mcp-paprika-data:
|
|
254
|
-
```
|
|
60
|
+
**Guides**
|
|
255
61
|
|
|
256
|
-
|
|
62
|
+
- **[HTTP transport quick start](docs/quick-start-http.md)** — OIDC setup from zero to a Claude connector
|
|
63
|
+
- **[Deployment](docs/deployment.md)** — container, TLS termination, Docker Compose
|
|
257
64
|
|
|
258
|
-
|
|
65
|
+
**Reference**
|
|
259
66
|
|
|
260
|
-
- **[Configuration](docs/configuration.md)** — env vars, config files,
|
|
261
|
-
- **[
|
|
262
|
-
- **[
|
|
263
|
-
- **[
|
|
264
|
-
- **[
|
|
67
|
+
- **[Configuration](docs/configuration.md)** — env vars, config files, platform paths
|
|
68
|
+
- **[HTTP transport](docs/http-transport.md)** — binding, host/origin allowlists, graceful shutdown
|
|
69
|
+
- **[OAuth 2.1 configuration](docs/oauth-configuration.md)** — OIDC providers, allowlist, consent gate
|
|
70
|
+
- **[Tools reference](https://github.com/bojanrajkovic/mcp-paprika/tree/main/docs/tools)** — every tool with parameters and examples
|
|
71
|
+
- **[Embedding providers](docs/embedding-providers.md)** — semantic search with Ollama, OpenAI, OpenRouter
|
|
72
|
+
- **[Architecture](https://github.com/bojanrajkovic/mcp-paprika/blob/main/docs/architecture.md)** — how it works under the hood
|
|
73
|
+
- **[Releasing](https://github.com/bojanrajkovic/mcp-paprika/blob/main/docs/releasing.md)** — release model, prerelease validation, attestation verification
|
|
265
74
|
|
|
266
75
|
## License
|
|
267
76
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { AisleUid } from "../ids.js";
|
|
2
|
+
import type { Aisle } from "./types.js";
|
|
1
3
|
import { EntityStore } from "../entity/index.js";
|
|
2
|
-
import type { Aisle, AisleUid } from "../paprika/types.js";
|
|
3
4
|
export declare class AisleStore extends EntityStore<Aisle, AisleUid> {
|
|
4
5
|
constructor(opts?: {
|
|
5
6
|
readonly pendingWriteTtlMs?: number;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const AisleStoredSchema: z.ZodObject<{
|
|
3
|
+
uid: z.ZodBranded<z.ZodString, "AisleUid">;
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
orderFlag: z.ZodNumber;
|
|
6
|
+
deleted: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
uid: string & z.BRAND<"AisleUid">;
|
|
9
|
+
name: string;
|
|
10
|
+
orderFlag: number;
|
|
11
|
+
deleted: boolean;
|
|
12
|
+
}, {
|
|
13
|
+
uid: string;
|
|
14
|
+
name: string;
|
|
15
|
+
orderFlag: number;
|
|
16
|
+
deleted?: boolean | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export type Aisle = z.infer<typeof AisleStoredSchema>;
|
|
19
|
+
export declare const AisleSchema: z.ZodEffects<z.ZodObject<{
|
|
20
|
+
uid: z.ZodBranded<z.ZodString, "AisleUid">;
|
|
21
|
+
name: z.ZodString;
|
|
22
|
+
order_flag: z.ZodNumber;
|
|
23
|
+
deleted: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
24
|
+
}, "strip", z.ZodTypeAny, {
|
|
25
|
+
uid: string & z.BRAND<"AisleUid">;
|
|
26
|
+
name: string;
|
|
27
|
+
deleted: boolean;
|
|
28
|
+
order_flag: number;
|
|
29
|
+
}, {
|
|
30
|
+
uid: string;
|
|
31
|
+
name: string;
|
|
32
|
+
order_flag: number;
|
|
33
|
+
deleted?: boolean | undefined;
|
|
34
|
+
}>, {
|
|
35
|
+
uid: string & z.BRAND<"AisleUid">;
|
|
36
|
+
name: string;
|
|
37
|
+
orderFlag: number;
|
|
38
|
+
deleted: boolean;
|
|
39
|
+
}, {
|
|
40
|
+
uid: string;
|
|
41
|
+
name: string;
|
|
42
|
+
order_flag: number;
|
|
43
|
+
deleted?: boolean | undefined;
|
|
44
|
+
}>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AisleUidSchema } from "../ids.js";
|
|
3
|
+
// AisleStoredSchema — validates camelCase JSON read back from disk. No transform.
|
|
4
|
+
export const AisleStoredSchema = z.object({
|
|
5
|
+
uid: AisleUidSchema,
|
|
6
|
+
name: z.string(),
|
|
7
|
+
orderFlag: z.number().int(),
|
|
8
|
+
deleted: z.boolean().optional().default(false),
|
|
9
|
+
});
|
|
10
|
+
// AisleSchema — accepts snake_case wire format, transforms to camelCase Aisle.
|
|
11
|
+
export const AisleSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
uid: AisleUidSchema,
|
|
14
|
+
name: z.string(),
|
|
15
|
+
order_flag: z.number().int(),
|
|
16
|
+
deleted: z.boolean().optional().default(false),
|
|
17
|
+
})
|
|
18
|
+
.transform(({ order_flag, ...rest }) => ({
|
|
19
|
+
...rest,
|
|
20
|
+
orderFlag: order_flag,
|
|
21
|
+
}));
|
package/dist/auth/allowlist.d.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* - if-present: deny only if email_verified === false; undefined (missing) is OK
|
|
16
16
|
*/
|
|
17
17
|
import { type Result } from "neverthrow";
|
|
18
|
+
import type { EmailVerifiedPolicy, IdTokenPayload } from "./types.js";
|
|
18
19
|
import { OAuthAllowlistDenialError } from "./errors.js";
|
|
19
|
-
import type { IdTokenPayload, EmailVerifiedPolicy } from "./types.js";
|
|
20
20
|
export interface AllowlistInput {
|
|
21
21
|
readonly emails: ReadonlySet<string>;
|
|
22
22
|
readonly subs: ReadonlySet<string>;
|
package/dist/auth/allowlist.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - skip: ignore email_verified entirely
|
|
15
15
|
* - if-present: deny only if email_verified === false; undefined (missing) is OK
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { err, ok } from "neverthrow";
|
|
18
18
|
import { OAuthAllowlistDenialError } from "./errors.js";
|
|
19
19
|
/**
|
|
20
20
|
* Determines if a policy allows the given email_verified claim.
|
|
@@ -20,10 +20,12 @@ export declare class AuthCodeStore extends TtlStore<AuthCodeState> {
|
|
|
20
20
|
*
|
|
21
21
|
* @param opts.ttlMs - TTL in milliseconds (default: AUTH_CODE_TTL_SECONDS * 1000)
|
|
22
22
|
* @param opts.now - Clock function returning milliseconds (default: Date.now)
|
|
23
|
+
* @param opts.maxEntries - Entry cap (default: MAX_INMEMORY_AUTH_ENTRIES)
|
|
23
24
|
*/
|
|
24
25
|
constructor(opts?: {
|
|
25
26
|
readonly ttlMs?: number;
|
|
26
27
|
readonly now?: () => number;
|
|
28
|
+
readonly maxEntries?: number;
|
|
27
29
|
});
|
|
28
30
|
/**
|
|
29
31
|
* Retrieve WITHOUT consuming an authorization code state.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* No persistence across restart (in-memory only). Auth codes do NOT survive process restart,
|
|
13
13
|
* which aligns with OAuth 2.1 short-lived code semantics and prevents code-reuse across restarts.
|
|
14
14
|
*/
|
|
15
|
-
import { AUTH_CODE_TTL_SECONDS } from "./tokens.js";
|
|
15
|
+
import { AUTH_CODE_TTL_SECONDS, MAX_INMEMORY_AUTH_ENTRIES } from "./tokens.js";
|
|
16
16
|
import { TtlStore } from "./ttl-store.js";
|
|
17
17
|
export class AuthCodeStore extends TtlStore {
|
|
18
18
|
/**
|
|
@@ -20,10 +20,12 @@ export class AuthCodeStore extends TtlStore {
|
|
|
20
20
|
*
|
|
21
21
|
* @param opts.ttlMs - TTL in milliseconds (default: AUTH_CODE_TTL_SECONDS * 1000)
|
|
22
22
|
* @param opts.now - Clock function returning milliseconds (default: Date.now)
|
|
23
|
+
* @param opts.maxEntries - Entry cap (default: MAX_INMEMORY_AUTH_ENTRIES)
|
|
23
24
|
*/
|
|
24
25
|
constructor(opts) {
|
|
25
26
|
super({
|
|
26
27
|
ttlMs: opts?.ttlMs ?? AUTH_CODE_TTL_SECONDS * 1000,
|
|
28
|
+
maxEntries: opts?.maxEntries ?? MAX_INMEMORY_AUTH_ENTRIES,
|
|
27
29
|
...(opts?.now !== undefined ? { now: opts.now } : {}),
|
|
28
30
|
});
|
|
29
31
|
}
|
|
@@ -17,9 +17,11 @@ export declare class AuthRequestStore extends TtlStore<AuthRequestState> {
|
|
|
17
17
|
*
|
|
18
18
|
* @param opts.ttlMs - TTL in milliseconds (default: AUTH_REQUEST_TTL_SECONDS * 1000)
|
|
19
19
|
* @param opts.now - Clock function returning milliseconds (default: Date.now)
|
|
20
|
+
* @param opts.maxEntries - Entry cap (default: MAX_INMEMORY_AUTH_ENTRIES)
|
|
20
21
|
*/
|
|
21
22
|
constructor(opts?: {
|
|
22
23
|
readonly ttlMs?: number;
|
|
23
24
|
readonly now?: () => number;
|
|
25
|
+
readonly maxEntries?: number;
|
|
24
26
|
});
|
|
25
27
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* as the entries typically consume within seconds of creation. `sweepExpired()` is called
|
|
10
10
|
* periodically by `AuthCleanup` for memory hygiene.
|
|
11
11
|
*/
|
|
12
|
-
import { AUTH_REQUEST_TTL_SECONDS } from "./tokens.js";
|
|
12
|
+
import { AUTH_REQUEST_TTL_SECONDS, MAX_INMEMORY_AUTH_ENTRIES } from "./tokens.js";
|
|
13
13
|
import { TtlStore } from "./ttl-store.js";
|
|
14
14
|
export class AuthRequestStore extends TtlStore {
|
|
15
15
|
/**
|
|
@@ -17,10 +17,12 @@ export class AuthRequestStore extends TtlStore {
|
|
|
17
17
|
*
|
|
18
18
|
* @param opts.ttlMs - TTL in milliseconds (default: AUTH_REQUEST_TTL_SECONDS * 1000)
|
|
19
19
|
* @param opts.now - Clock function returning milliseconds (default: Date.now)
|
|
20
|
+
* @param opts.maxEntries - Entry cap (default: MAX_INMEMORY_AUTH_ENTRIES)
|
|
20
21
|
*/
|
|
21
22
|
constructor(opts) {
|
|
22
23
|
super({
|
|
23
24
|
ttlMs: opts?.ttlMs ?? AUTH_REQUEST_TTL_SECONDS * 1000,
|
|
25
|
+
maxEntries: opts?.maxEntries ?? MAX_INMEMORY_AUTH_ENTRIES,
|
|
24
26
|
...(opts?.now !== undefined ? { now: opts.now } : {}),
|
|
25
27
|
});
|
|
26
28
|
}
|
package/dist/auth/build.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* after cache.init() completes.
|
|
11
11
|
*/
|
|
12
12
|
import type { Logger } from "pino";
|
|
13
|
-
import type { DiskCacheRoot } from "../cache/disk
|
|
13
|
+
import type { DiskCacheRoot } from "../cache/disk-cache-root.js";
|
|
14
14
|
import type { PaprikaConfig } from "../utils/config.js";
|
|
15
15
|
import type { AuthContext } from "./types.js";
|
|
16
16
|
export declare function buildAuthContext(config: PaprikaConfig, cache: DiskCacheRoot, parentLog: Logger): Promise<AuthContext | null>;
|
package/dist/auth/build.js
CHANGED
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
* Called once per process from buildAppContext (src/server/build.ts)
|
|
10
10
|
* after cache.init() completes.
|
|
11
11
|
*/
|
|
12
|
-
import { resolvePreset } from "./presets.js";
|
|
13
|
-
import { loadDiscovery, createJwksFor } from "./oidc-client.js";
|
|
14
|
-
import { DiskClientRegistrationStore } from "./client-registration.js";
|
|
15
|
-
import { TokenStore } from "./token-store.js";
|
|
16
|
-
import { AuthRequestStore } from "./auth-request-store.js";
|
|
17
12
|
import { AuthCodeStore } from "./auth-code-store.js";
|
|
18
|
-
import {
|
|
13
|
+
import { AuthRequestStore } from "./auth-request-store.js";
|
|
19
14
|
import { AuthCleanup } from "./cleanup.js";
|
|
15
|
+
import { DiskClientRegistrationStore } from "./client-registration.js";
|
|
16
|
+
import { createJwksFor, loadDiscovery } from "./oidc-client.js";
|
|
17
|
+
import { PendingAuthorizationStore } from "./pending-authorization-store.js";
|
|
18
|
+
import { resolvePreset } from "./presets.js";
|
|
19
|
+
import { MintingOAuthServerProvider } from "./provider.js";
|
|
20
|
+
import { normalizeOrigin } from "./redirect-allowlist.js";
|
|
20
21
|
import { MAX_REGISTERED_CLIENTS } from "./routes.js";
|
|
22
|
+
import { TokenStore } from "./token-store.js";
|
|
21
23
|
export async function buildAuthContext(config, cache, parentLog) {
|
|
22
24
|
if (config.transport !== "http")
|
|
23
25
|
return null;
|
|
@@ -53,6 +55,13 @@ export async function buildAuthContext(config, cache, parentLog) {
|
|
|
53
55
|
if (config.oauth.clientSecret === undefined) {
|
|
54
56
|
throw new Error("invariant: oauth.clientSecret must be set when transport=http");
|
|
55
57
|
}
|
|
58
|
+
// Normalize the raw redirect-allowlist strings to canonical origins, failing
|
|
59
|
+
// fast at startup on any malformed/non-permitted entry (#147). config.ts
|
|
60
|
+
// keeps the strings raw so it has no dependency on src/auth/; the auth layer
|
|
61
|
+
// owns origin validation.
|
|
62
|
+
const redirectAllowlist = config.oauth.redirectAllowlist.map((entry) => normalizeOrigin(entry).match((origin) => origin, (e) => {
|
|
63
|
+
throw e; // fail-fast at startup
|
|
64
|
+
}));
|
|
56
65
|
const resolved = {
|
|
57
66
|
...presetResult,
|
|
58
67
|
publicUrl: config.oauth.publicUrl,
|
|
@@ -60,6 +69,7 @@ export async function buildAuthContext(config, cache, parentLog) {
|
|
|
60
69
|
clientSecret: config.oauth.clientSecret,
|
|
61
70
|
trustProxy: config.oauth.trustProxy,
|
|
62
71
|
allowlist: config.oauth.allowlist,
|
|
72
|
+
redirectAllowlist,
|
|
63
73
|
};
|
|
64
74
|
const authLog = parentLog.child({ component: "auth" });
|
|
65
75
|
const oidcClientLog = parentLog.child({ component: "oidc-client" });
|
|
@@ -70,8 +80,9 @@ export async function buildAuthContext(config, cache, parentLog) {
|
|
|
70
80
|
const tokenStore = new TokenStore(cache);
|
|
71
81
|
const requestStore = new AuthRequestStore();
|
|
72
82
|
const codeStore = new AuthCodeStore();
|
|
73
|
-
const
|
|
74
|
-
const
|
|
83
|
+
const pendingStore = new PendingAuthorizationStore();
|
|
84
|
+
const provider = new MintingOAuthServerProvider(clientStore, tokenStore, requestStore, codeStore, pendingStore, discovery, resolved, resolved.publicUrl, authLog);
|
|
85
|
+
const cleanup = new AuthCleanup(clientStore, tokenStore, cache, requestStore, codeStore, pendingStore, authLog);
|
|
75
86
|
return {
|
|
76
87
|
provider,
|
|
77
88
|
config: resolved,
|
|
@@ -79,6 +90,7 @@ export async function buildAuthContext(config, cache, parentLog) {
|
|
|
79
90
|
jwks,
|
|
80
91
|
authRequests: requestStore,
|
|
81
92
|
authCodes: codeStore,
|
|
93
|
+
pendingAuthorizations: pendingStore,
|
|
82
94
|
tokenStore,
|
|
83
95
|
clientStore,
|
|
84
96
|
cleanup,
|
package/dist/auth/cleanup.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Responsibilities (run every CLEANUP_INTERVAL_MS = 6h):
|
|
8
8
|
* 1. Remove stale DCR clients (lastTokenActivityAt > 90 days old) + cascade their tokens.
|
|
9
|
-
* 2. Sweep expired in-memory AuthRequestStore
|
|
9
|
+
* 2. Sweep expired in-memory AuthRequestStore, AuthCodeStore, and
|
|
10
|
+
* PendingAuthorizationStore (consent-ticket) entries.
|
|
10
11
|
* 3. Sweep expired OAuth tokens (expiresAt < now). `rotateRefresh` deletes the
|
|
11
12
|
* previous refresh token but not the previous access token — every refresh
|
|
12
13
|
* leaves a soon-to-expire access record behind. Without this sweep,
|
|
@@ -16,10 +17,11 @@
|
|
|
16
17
|
* Public `sweepOnce()` is exposed for direct testing and for startup use.
|
|
17
18
|
*/
|
|
18
19
|
import type { Logger } from "pino";
|
|
19
|
-
import type { DiskCacheRoot } from "../cache/disk
|
|
20
|
-
import type { AuthRequestStore } from "./auth-request-store.js";
|
|
20
|
+
import type { DiskCacheRoot } from "../cache/disk-cache-root.js";
|
|
21
21
|
import type { AuthCodeStore } from "./auth-code-store.js";
|
|
22
|
+
import type { AuthRequestStore } from "./auth-request-store.js";
|
|
22
23
|
import type { DiskClientRegistrationStore } from "./client-registration.js";
|
|
24
|
+
import type { PendingAuthorizationStore } from "./pending-authorization-store.js";
|
|
23
25
|
import type { TokenStore } from "./token-store.js";
|
|
24
26
|
export declare class AuthCleanup {
|
|
25
27
|
private readonly _clientStore;
|
|
@@ -27,11 +29,12 @@ export declare class AuthCleanup {
|
|
|
27
29
|
private readonly _cache;
|
|
28
30
|
private readonly _authRequests;
|
|
29
31
|
private readonly _authCodes;
|
|
32
|
+
private readonly _pendingAuthorizations;
|
|
30
33
|
private readonly log;
|
|
31
34
|
private readonly _now;
|
|
32
35
|
private readonly _intervalMs;
|
|
33
36
|
private _ac;
|
|
34
|
-
constructor(_clientStore: DiskClientRegistrationStore, _tokenStore: TokenStore, _cache: DiskCacheRoot, _authRequests: AuthRequestStore, _authCodes: AuthCodeStore, log: Logger, _now?: () => number, _intervalMs?: number);
|
|
37
|
+
constructor(_clientStore: DiskClientRegistrationStore, _tokenStore: TokenStore, _cache: DiskCacheRoot, _authRequests: AuthRequestStore, _authCodes: AuthCodeStore, _pendingAuthorizations: PendingAuthorizationStore, log: Logger, _now?: () => number, _intervalMs?: number);
|
|
35
38
|
/** Start the background cleanup loop. Idempotent — second call is a no-op. */
|
|
36
39
|
start(): void;
|
|
37
40
|
/** Stop the background cleanup loop. Idempotent — second call is a no-op. */
|
|
@@ -53,6 +56,7 @@ export declare class AuthCleanup {
|
|
|
53
56
|
expiredTokensRemoved: number;
|
|
54
57
|
authRequestsRemoved: number;
|
|
55
58
|
authCodesRemoved: number;
|
|
59
|
+
pendingAuthorizationsRemoved: number;
|
|
56
60
|
}>;
|
|
57
61
|
private _loop;
|
|
58
62
|
}
|
package/dist/auth/cleanup.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Responsibilities (run every CLEANUP_INTERVAL_MS = 6h):
|
|
8
8
|
* 1. Remove stale DCR clients (lastTokenActivityAt > 90 days old) + cascade their tokens.
|
|
9
|
-
* 2. Sweep expired in-memory AuthRequestStore
|
|
9
|
+
* 2. Sweep expired in-memory AuthRequestStore, AuthCodeStore, and
|
|
10
|
+
* PendingAuthorizationStore (consent-ticket) entries.
|
|
10
11
|
* 3. Sweep expired OAuth tokens (expiresAt < now). `rotateRefresh` deletes the
|
|
11
12
|
* previous refresh token but not the previous access token — every refresh
|
|
12
13
|
* leaves a soon-to-expire access record behind. Without this sweep,
|
|
@@ -24,16 +25,18 @@ export class AuthCleanup {
|
|
|
24
25
|
_cache;
|
|
25
26
|
_authRequests;
|
|
26
27
|
_authCodes;
|
|
28
|
+
_pendingAuthorizations;
|
|
27
29
|
log;
|
|
28
30
|
_now;
|
|
29
31
|
_intervalMs;
|
|
30
32
|
_ac = null;
|
|
31
|
-
constructor(_clientStore, _tokenStore, _cache, _authRequests, _authCodes, log, _now = () => nowSeconds(), _intervalMs = CLEANUP_INTERVAL_MS) {
|
|
33
|
+
constructor(_clientStore, _tokenStore, _cache, _authRequests, _authCodes, _pendingAuthorizations, log, _now = () => nowSeconds(), _intervalMs = CLEANUP_INTERVAL_MS) {
|
|
32
34
|
this._clientStore = _clientStore;
|
|
33
35
|
this._tokenStore = _tokenStore;
|
|
34
36
|
this._cache = _cache;
|
|
35
37
|
this._authRequests = _authRequests;
|
|
36
38
|
this._authCodes = _authCodes;
|
|
39
|
+
this._pendingAuthorizations = _pendingAuthorizations;
|
|
37
40
|
this.log = log;
|
|
38
41
|
this._now = _now;
|
|
39
42
|
this._intervalMs = _intervalMs;
|
|
@@ -87,6 +90,7 @@ export class AuthCleanup {
|
|
|
87
90
|
// (2) In-memory store sweeps — bound memory under sustained /authorize traffic
|
|
88
91
|
const authRequestsRemoved = this._authRequests.sweepExpired();
|
|
89
92
|
const authCodesRemoved = this._authCodes.sweepExpired();
|
|
93
|
+
const pendingAuthorizationsRemoved = this._pendingAuthorizations.sweepExpired();
|
|
90
94
|
// (3) Expired-token sweep — remove tokens past `expiresAt` whose owning
|
|
91
95
|
// client is still active (stale-client cascade already covers the
|
|
92
96
|
// others). `rotateRefresh` deletes the old refresh but not the old
|
|
@@ -105,6 +109,7 @@ export class AuthCleanup {
|
|
|
105
109
|
expiredTokensRemoved,
|
|
106
110
|
authRequestsRemoved,
|
|
107
111
|
authCodesRemoved,
|
|
112
|
+
pendingAuthorizationsRemoved,
|
|
108
113
|
};
|
|
109
114
|
}
|
|
110
115
|
async _loop() {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - verifyRegistrationAccessToken: used by route handlers to gate PUT/DELETE access
|
|
11
11
|
*/
|
|
12
12
|
import type { Logger } from "pino";
|
|
13
|
-
import type { DiskCacheRoot } from "../cache/disk
|
|
13
|
+
import type { DiskCacheRoot } from "../cache/disk-cache-root.js";
|
|
14
14
|
/**
|
|
15
15
|
* Wire format for client information (RFC 7591 response format).
|
|
16
16
|
* Snake_case to match RFC 7591.
|