@bojanrajkovic/mcp-paprika 1.6.0 → 2.0.0-beta.1

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.
Files changed (253) hide show
  1. package/README.md +29 -220
  2. package/dist/aisle/disk.d.ts +3 -0
  3. package/dist/aisle/disk.js +6 -0
  4. package/dist/{cache/aisle-store.d.ts → aisle/store.d.ts} +2 -1
  5. package/dist/aisle/types.d.ts +44 -0
  6. package/dist/aisle/types.js +21 -0
  7. package/dist/auth/allowlist.d.ts +1 -1
  8. package/dist/auth/allowlist.js +1 -1
  9. package/dist/auth/auth-code-store.d.ts +2 -0
  10. package/dist/auth/auth-code-store.js +3 -1
  11. package/dist/auth/auth-request-store.d.ts +2 -0
  12. package/dist/auth/auth-request-store.js +3 -1
  13. package/dist/auth/build.d.ts +1 -1
  14. package/dist/auth/build.js +20 -8
  15. package/dist/auth/cleanup.d.ts +8 -4
  16. package/dist/auth/cleanup.js +7 -2
  17. package/dist/auth/client-registration.d.ts +1 -1
  18. package/dist/auth/client-registration.js +1 -1
  19. package/dist/auth/consent-page.d.ts +55 -0
  20. package/dist/auth/consent-page.js +197 -0
  21. package/dist/auth/dcr-validator.js +7 -16
  22. package/dist/auth/errors.js +1 -1
  23. package/dist/auth/metadata.d.ts +1 -0
  24. package/dist/auth/metadata.js +9 -0
  25. package/dist/auth/oidc-client.d.ts +2 -2
  26. package/dist/auth/oidc-client.js +3 -13
  27. package/dist/auth/pending-authorization-store.d.ts +28 -0
  28. package/dist/auth/pending-authorization-store.js +30 -0
  29. package/dist/auth/presets.d.ts +1 -1
  30. package/dist/auth/presets.js +1 -1
  31. package/dist/auth/provider.d.ts +16 -8
  32. package/dist/auth/provider.js +63 -25
  33. package/dist/auth/redirect-allowlist.d.ts +50 -0
  34. package/dist/auth/redirect-allowlist.js +81 -0
  35. package/dist/auth/routes.d.ts +7 -5
  36. package/dist/auth/routes.js +53 -3
  37. package/dist/auth/token-store.d.ts +4 -4
  38. package/dist/auth/token-store.js +2 -2
  39. package/dist/auth/tokens.d.ts +18 -1
  40. package/dist/auth/tokens.js +27 -2
  41. package/dist/auth/ttl-store.d.ts +16 -2
  42. package/dist/auth/ttl-store.js +21 -1
  43. package/dist/auth/types.d.ts +83 -38
  44. package/dist/auth/types.js +21 -0
  45. package/dist/auth/upstream-redirect.d.ts +49 -0
  46. package/dist/auth/upstream-redirect.js +60 -0
  47. package/dist/cache/{disk/root.d.ts → disk-cache-root.d.ts} +15 -5
  48. package/dist/cache/{disk/root.js → disk-cache-root.js} +45 -78
  49. package/dist/cache/{disk/base.d.ts → disk-cache.d.ts} +15 -0
  50. package/dist/cache/{disk/base.js → disk-cache.js} +2 -2
  51. package/dist/cache/{disk/oauth-clients.d.ts → oauth-client-disk-cache.d.ts} +2 -2
  52. package/dist/cache/{disk/oauth-clients.js → oauth-client-disk-cache.js} +2 -2
  53. package/dist/category/disk.d.ts +3 -0
  54. package/dist/category/disk.js +6 -0
  55. package/dist/{cache/category-store.d.ts → category/store.d.ts} +2 -1
  56. package/dist/category/types.d.ts +44 -0
  57. package/dist/category/types.js +22 -0
  58. package/dist/features/discover-feature.d.ts +22 -5
  59. package/dist/features/discover-feature.js +79 -9
  60. package/dist/features/embeddings.d.ts +1 -1
  61. package/dist/features/embeddings.js +3 -3
  62. package/dist/features/generated-image-store.d.ts +59 -0
  63. package/dist/features/generated-image-store.js +77 -0
  64. package/dist/features/json-vector-index.d.ts +153 -0
  65. package/dist/features/json-vector-index.js +358 -0
  66. package/dist/features/photography.d.ts +1 -1
  67. package/dist/features/photography.js +2 -2
  68. package/dist/features/vector-store.d.ts +10 -7
  69. package/dist/features/vector-store.js +77 -30
  70. package/dist/grocery-ingredient/disk.d.ts +3 -0
  71. package/dist/grocery-ingredient/disk.js +6 -0
  72. package/dist/{cache/grocery-ingredient-store.d.ts → grocery-ingredient/store.d.ts} +1 -1
  73. package/dist/grocery-ingredient/types.d.ts +44 -0
  74. package/dist/grocery-ingredient/types.js +21 -0
  75. package/dist/grocery-item/disk.d.ts +3 -0
  76. package/dist/grocery-item/disk.js +6 -0
  77. package/dist/{cache/grocery-item-store.d.ts → grocery-item/store.d.ts} +2 -1
  78. package/dist/grocery-item/types.d.ts +116 -0
  79. package/dist/grocery-item/types.js +41 -0
  80. package/dist/grocery-list/disk.d.ts +3 -0
  81. package/dist/grocery-list/disk.js +6 -0
  82. package/dist/{cache/grocery-list-store.d.ts → grocery-list/store.d.ts} +2 -1
  83. package/dist/grocery-list/types.d.ts +60 -0
  84. package/dist/grocery-list/types.js +27 -0
  85. package/dist/ids.d.ts +63 -0
  86. package/dist/ids.js +51 -0
  87. package/dist/meal/disk.d.ts +3 -0
  88. package/dist/meal/disk.js +6 -0
  89. package/dist/{cache/meal-store.d.ts → meal/store.d.ts} +10 -2
  90. package/dist/{cache/meal-store.js → meal/store.js} +3 -1
  91. package/dist/meal/types.d.ts +93 -0
  92. package/dist/meal/types.js +54 -0
  93. package/dist/meal-type/disk.d.ts +3 -0
  94. package/dist/meal-type/disk.js +6 -0
  95. package/dist/{cache/meal-type-store.d.ts → meal-type/store.d.ts} +2 -1
  96. package/dist/meal-type/types.d.ts +76 -0
  97. package/dist/meal-type/types.js +42 -0
  98. package/dist/menu/disk.d.ts +3 -0
  99. package/dist/menu/disk.js +6 -0
  100. package/dist/{cache/menu-store.d.ts → menu/store.d.ts} +2 -1
  101. package/dist/menu/types.d.ts +61 -0
  102. package/dist/menu/types.js +34 -0
  103. package/dist/menu-item/disk.d.ts +3 -0
  104. package/dist/menu-item/disk.js +6 -0
  105. package/dist/{cache/menu-item-store.d.ts → menu-item/store.d.ts} +2 -1
  106. package/dist/menu-item/types.d.ts +77 -0
  107. package/dist/menu-item/types.js +49 -0
  108. package/dist/pantry/disk.d.ts +3 -0
  109. package/dist/pantry/disk.js +6 -0
  110. package/dist/{cache/pantry-store.d.ts → pantry/store.d.ts} +2 -1
  111. package/dist/pantry/types.d.ts +100 -0
  112. package/dist/pantry/types.js +41 -0
  113. package/dist/paprika/auth-response.d.ts +19 -0
  114. package/dist/paprika/auth-response.js +9 -0
  115. package/dist/paprika/client.d.ts +35 -3
  116. package/dist/paprika/client.js +67 -13
  117. package/dist/paprika/recipe-hash.d.ts +35 -0
  118. package/dist/paprika/recipe-hash.js +69 -0
  119. package/dist/paprika/sync-types.d.ts +29 -0
  120. package/dist/paprika/sync-types.js +1 -0
  121. package/dist/paprika/sync.d.ts +17 -4
  122. package/dist/paprika/sync.js +88 -80
  123. package/dist/photo/disk.d.ts +3 -0
  124. package/dist/photo/disk.js +6 -0
  125. package/dist/{cache/photo-store.d.ts → photo/store.d.ts} +3 -2
  126. package/dist/{cache/photo-store.js → photo/store.js} +1 -1
  127. package/dist/photo/types.d.ts +69 -0
  128. package/dist/photo/types.js +52 -0
  129. package/dist/{cache/disk/recipes.d.ts → recipe/disk.d.ts} +3 -2
  130. package/dist/{cache/disk/recipes.js → recipe/disk.js} +7 -3
  131. package/dist/{cache/recipe-store.d.ts → recipe/store.d.ts} +2 -1
  132. package/dist/recipe/types.d.ts +257 -0
  133. package/dist/recipe/types.js +112 -0
  134. package/dist/resources/recipes.js +4 -1
  135. package/dist/server/app-context.d.ts +18 -15
  136. package/dist/server/build.d.ts +7 -25
  137. package/dist/server/build.js +180 -118
  138. package/dist/server/notifier.d.ts +15 -0
  139. package/dist/server/notifier.js +9 -0
  140. package/dist/tools/aisle-helpers.d.ts +3 -2
  141. package/dist/tools/aisle-helpers.js +3 -3
  142. package/dist/tools/categories.js +1 -1
  143. package/dist/tools/category-helpers.d.ts +10 -2
  144. package/dist/tools/category-helpers.js +28 -1
  145. package/dist/tools/category-writes.js +9 -7
  146. package/dist/tools/create.js +15 -9
  147. package/dist/tools/delete.js +4 -4
  148. package/dist/tools/discover.d.ts +1 -1
  149. package/dist/tools/discover.js +13 -3
  150. package/dist/tools/empty-trash.js +17 -12
  151. package/dist/tools/grocery-clear.js +10 -10
  152. package/dist/tools/grocery-helpers.d.ts +2 -1
  153. package/dist/tools/grocery-item-purchase.d.ts +11 -0
  154. package/dist/tools/grocery-item-purchase.js +37 -0
  155. package/dist/tools/grocery-item.d.ts +17 -0
  156. package/dist/tools/grocery-item.js +19 -16
  157. package/dist/tools/grocery-list.js +3 -3
  158. package/dist/tools/grocery-move.js +8 -8
  159. package/dist/tools/helpers.d.ts +22 -2
  160. package/dist/tools/helpers.js +127 -1
  161. package/dist/tools/meal-add-menu.js +12 -12
  162. package/dist/tools/meal-helpers.d.ts +23 -4
  163. package/dist/tools/meal-helpers.js +80 -4
  164. package/dist/tools/meal-history-search.d.ts +59 -0
  165. package/dist/tools/meal-history-search.js +151 -0
  166. package/dist/tools/meal-log-cooked.d.ts +47 -0
  167. package/dist/tools/meal-log-cooked.js +85 -0
  168. package/dist/tools/meal-plan-read.d.ts +11 -0
  169. package/dist/tools/meal-plan-read.js +54 -0
  170. package/dist/tools/meal-reschedule.d.ts +47 -0
  171. package/dist/tools/meal-reschedule.js +96 -0
  172. package/dist/tools/meal-types.js +2 -2
  173. package/dist/tools/meal-writes.d.ts +4 -19
  174. package/dist/tools/meal-writes.js +21 -56
  175. package/dist/tools/menu-helpers.d.ts +3 -1
  176. package/dist/tools/menu-item-move.d.ts +14 -0
  177. package/dist/tools/menu-item-move.js +94 -0
  178. package/dist/tools/menu-item-write.d.ts +9 -12
  179. package/dist/tools/menu-item-write.js +22 -58
  180. package/dist/tools/menu-read.js +1 -1
  181. package/dist/tools/menu-write.js +3 -3
  182. package/dist/tools/pantry-batch-add.js +8 -8
  183. package/dist/tools/pantry-delete.js +1 -1
  184. package/dist/tools/pantry-get.js +5 -5
  185. package/dist/tools/pantry-helpers.d.ts +1 -1
  186. package/dist/tools/pantry-list.js +5 -5
  187. package/dist/tools/pantry-stock.d.ts +19 -0
  188. package/dist/tools/pantry-stock.js +69 -0
  189. package/dist/tools/pantry-update.d.ts +23 -0
  190. package/dist/tools/pantry-update.js +30 -26
  191. package/dist/tools/photo-fetch.d.ts +1 -1
  192. package/dist/tools/photo-fetch.js +2 -2
  193. package/dist/tools/photo-generate.js +23 -15
  194. package/dist/tools/photo-helpers.d.ts +14 -6
  195. package/dist/tools/photo-helpers.js +16 -6
  196. package/dist/tools/photo-writes.d.ts +58 -6
  197. package/dist/tools/photo-writes.js +93 -39
  198. package/dist/tools/read.js +1 -1
  199. package/dist/tools/recipe-categorize.d.ts +17 -0
  200. package/dist/tools/recipe-categorize.js +76 -0
  201. package/dist/tools/recipe-favorite.d.ts +19 -0
  202. package/dist/tools/recipe-favorite.js +70 -0
  203. package/dist/tools/recipe-rating.d.ts +14 -0
  204. package/dist/tools/recipe-rating.js +38 -0
  205. package/dist/tools/recipe-restore.d.ts +11 -0
  206. package/dist/tools/recipe-restore.js +66 -0
  207. package/dist/tools/search.d.ts +26 -0
  208. package/dist/tools/search.js +161 -26
  209. package/dist/tools/update.d.ts +47 -0
  210. package/dist/tools/update.js +39 -48
  211. package/dist/transport/favicon.d.ts +11 -0
  212. package/dist/transport/favicon.js +25 -0
  213. package/dist/transport/http.d.ts +1 -1
  214. package/dist/transport/http.js +9 -3
  215. package/dist/transport/stdio.js +9 -7
  216. package/dist/utils/branding.d.ts +44 -0
  217. package/dist/utils/branding.js +75 -0
  218. package/dist/utils/config.d.ts +52 -34
  219. package/dist/utils/config.js +26 -8
  220. package/dist/utils/dates.d.ts +64 -36
  221. package/dist/utils/dates.js +152 -55
  222. package/dist/utils/duration.js +1 -1
  223. package/dist/utils/log.d.ts +2 -2
  224. package/dist/utils/log.js +2 -2
  225. package/dist/utils/resilience.js +2 -2
  226. package/dist/utils/xdg.js +1 -1
  227. package/docs/configuration.md +222 -0
  228. package/docs/deployment.md +133 -0
  229. package/docs/embedding-providers.md +103 -0
  230. package/docs/http-transport.md +120 -0
  231. package/docs/oauth-configuration.md +117 -0
  232. package/docs/quick-start-http.md +90 -0
  233. package/package.json +18 -12
  234. package/dist/cache/disk/index.d.ts +0 -5
  235. package/dist/cache/disk/index.js +0 -4
  236. package/dist/paprika/dates.d.ts +0 -25
  237. package/dist/paprika/dates.js +0 -57
  238. package/dist/paprika/types.d.ts +0 -1094
  239. package/dist/paprika/types.js +0 -517
  240. package/dist/tools/filter.d.ts +0 -3
  241. package/dist/tools/filter.js +0 -141
  242. package/dist/tools/meal-history.d.ts +0 -3
  243. package/dist/tools/meal-history.js +0 -188
  244. /package/dist/{cache/aisle-store.js → aisle/store.js} +0 -0
  245. /package/dist/{cache/category-store.js → category/store.js} +0 -0
  246. /package/dist/{cache/grocery-ingredient-store.js → grocery-ingredient/store.js} +0 -0
  247. /package/dist/{cache/grocery-item-store.js → grocery-item/store.js} +0 -0
  248. /package/dist/{cache/grocery-list-store.js → grocery-list/store.js} +0 -0
  249. /package/dist/{cache/meal-type-store.js → meal-type/store.js} +0 -0
  250. /package/dist/{cache/menu-store.js → menu/store.js} +0 -0
  251. /package/dist/{cache/menu-item-store.js → menu-item/store.js} +0 -0
  252. /package/dist/{cache/pantry-store.js → pantry/store.js} +0 -0
  253. /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
- - **40 tools** for recipe, pantry, grocery, meal-planner, and menu management — search, filter, CRUD, categories, pagination, pantry inventory, aisles, grocery lists and items, meal planning (history, meal types, dated planner entries), and menus (recipe collections, their items, and one-shot add-to-planner)
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 `generate_photo` — generate a styled food photo for a recipe (or restyle its existing one) using OpenRouter image models, and attach it automatically
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** — expose recipes as `paprika://recipe/{uid}`, grocery lists as `paprika://grocery-list/{uid}`, and menus as `paprika://menu/{uid}` 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** — `Dockerfile` ships a distroless runtime ready for self-hosting
13
+ - **Container image** — a distroless runtime ready for self-hosting
14
14
 
15
15
  ## Transports
16
16
 
17
- `mcp-paprika` can speak the MCP protocol over two transports, selected via `MCP_TRANSPORT`:
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 dynamic client registration). See the [HTTP transport quick start](#quick-start--http-transport) below.
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
- ## Quick start HTTP transport
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
- ```bash
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
- **Container deployment with Docker Compose:**
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
- ```yaml
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
- volumes:
253
- mcp-paprika-data:
254
- ```
60
+ **Guides**
255
61
 
256
- OAuth provides authentication-level controls; the reverse proxy provides TLS, rate limiting, and any additional network-level restrictions the deployment requires.
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
- ## Documentation
65
+ **Reference**
259
66
 
260
- - **[Configuration](docs/configuration.md)** — env vars, config files, transport options, platform paths
261
- - **[Tools reference](docs/tools/)** — every tool with parameters and examples
262
- - **[Embedding providers](docs/embedding-providers.md)** — set up semantic search with Ollama, OpenAI, OpenRouter, etc.
263
- - **[Architecture](docs/architecture.md)** — how it works under the hood
264
- - **[Releasing](docs/releasing.md)** — maintainer-facing release model, prerelease validation, attestation verification
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
 
@@ -0,0 +1,3 @@
1
+ import type { DiskCacheDescriptor } from "../cache/disk-cache.js";
2
+ import type { Aisle } from "./types.js";
3
+ export declare const aisleDiskDescriptor: DiskCacheDescriptor<Aisle>;
@@ -0,0 +1,6 @@
1
+ import { AisleStoredSchema } from "./types.js";
2
+ export const aisleDiskDescriptor = {
3
+ subdir: "aisles",
4
+ parse: (raw) => AisleStoredSchema.parse(raw),
5
+ getKey: (a) => a.uid,
6
+ };
@@ -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
+ }));
@@ -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>;
@@ -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 { ok, err } from "neverthrow";
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
  }
@@ -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/index.js";
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>;
@@ -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 { MintingOAuthServerProvider } from "./provider.js";
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 provider = new MintingOAuthServerProvider(clientStore, tokenStore, requestStore, codeStore, discovery, resolved, resolved.publicUrl, authLog);
74
- const cleanup = new AuthCleanup(clientStore, tokenStore, cache, requestStore, codeStore, authLog);
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,
@@ -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 and AuthCodeStore entries.
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/index.js";
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
  }
@@ -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 and AuthCodeStore entries.
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/index.js";
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.