@beechcms/api 0.4.0-preview.1 → 0.4.0-preview.10

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 (76) hide show
  1. package/assets/dashboard/BeechLogo.svg +18 -0
  2. package/assets/dashboard/BeechLogoLIght.svg +48 -0
  3. package/assets/dashboard/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  4. package/assets/dashboard/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  5. package/assets/dashboard/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  6. package/assets/dashboard/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  7. package/assets/dashboard/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  8. package/assets/dashboard/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  9. package/assets/dashboard/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  10. package/assets/dashboard/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  11. package/assets/dashboard/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  12. package/assets/dashboard/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  13. package/assets/dashboard/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  14. package/assets/dashboard/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  15. package/assets/dashboard/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  16. package/assets/dashboard/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  17. package/assets/dashboard/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  18. package/assets/dashboard/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  19. package/assets/dashboard/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  20. package/assets/dashboard/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  21. package/assets/dashboard/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  22. package/assets/dashboard/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  23. package/assets/dashboard/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  24. package/assets/dashboard/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  25. package/assets/dashboard/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  26. package/assets/dashboard/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  27. package/assets/dashboard/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  28. package/assets/dashboard/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  29. package/assets/dashboard/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  30. package/assets/dashboard/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  31. package/assets/dashboard/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  32. package/assets/dashboard/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  33. package/assets/dashboard/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  34. package/assets/dashboard/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  35. package/assets/dashboard/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  36. package/assets/dashboard/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  37. package/assets/dashboard/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  38. package/assets/dashboard/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  39. package/assets/dashboard/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  40. package/assets/dashboard/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  41. package/assets/dashboard/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  42. package/assets/dashboard/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  43. package/assets/dashboard/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  44. package/assets/dashboard/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  45. package/assets/dashboard/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  46. package/assets/dashboard/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  47. package/assets/dashboard/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  48. package/assets/dashboard/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  49. package/assets/dashboard/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  50. package/assets/dashboard/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  51. package/assets/dashboard/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  52. package/assets/dashboard/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. package/assets/dashboard/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  54. package/assets/dashboard/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  55. package/assets/dashboard/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  56. package/assets/dashboard/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  57. package/assets/dashboard/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  58. package/assets/dashboard/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  59. package/assets/dashboard/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  60. package/assets/dashboard/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  61. package/assets/dashboard/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  62. package/assets/dashboard/assets/index-CFTJe1vb.js +554 -0
  63. package/assets/dashboard/assets/index-CQODXprH.css +1 -0
  64. package/assets/dashboard/beechLogoDark.svg +48 -0
  65. package/assets/dashboard/index.html +18 -0
  66. package/assets/dashboard/sol.svg +3 -0
  67. package/assets/dashboard/undraw_clouds_bmtk.svg +1 -0
  68. package/assets/dashboard/undraw_enter_nwx3.svg +36 -0
  69. package/assets/dashboard/vite.svg +1 -0
  70. package/package.json +4 -3
  71. package/src/factory.ts +266 -8
  72. package/src/features/password-reset/request.ts +1 -1
  73. package/src/features/schema/schema.handler.ts +16 -0
  74. package/src/index.ts +6 -339
  75. package/src/search-utils.ts +5 -1
  76. package/src/types.ts +1 -0
package/src/index.ts CHANGED
@@ -1,344 +1,11 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
- import { Hono } from 'hono'
3
- import type { Context } from 'hono'
4
- import { getCookie, setCookie, deleteCookie } from 'hono/cookie'
5
1
  import { createBeechApp } from './factory'
6
- import { SEED_REGISTRY } from '@beechcms/core'
7
- import { AUTH_ERRORS } from './auth/constants'
8
- import {
9
- parseLoginBody,
10
- validateLoginInput,
11
- findUserByEmail,
12
- verifyPassword,
13
- DUMMY_PASSWORD_HASH,
14
- } from './auth/login'
15
- import {
16
- generateRefreshToken,
17
- saveRefreshToken,
18
- generateAccessToken,
19
- validateRefreshToken,
20
- revokeRefreshToken,
21
- } from './auth/refresh'
22
- import { authMiddleware } from './middleware'
23
- import { contentRoutes } from './content'
24
- import { widgetApp } from './widget'
25
- // TODO: refactor — rotate-field è una VSA slice. Il resto di index.ts (auth inline, content monolith)
26
- // va migrato a slices dedicati sotto src/features/ seguendo lo stesso pattern.
27
- import { rotateFieldApp } from './features/rotate-field'
28
- import { passwordResetApp } from './features/password-reset'
29
- import { setupApp } from './features/setup'
30
- import { draftApp } from './features/draft'
31
- import { settingsApp } from './features/settings/settings.handler'
32
- import { notificationsApp } from './features/notifications'
33
- import { statsApp } from './features/stats'
34
- import { uploadRoutes, serveMediaHandler } from './upload'
35
- import { publicRoutes, apiKeyMiddleware, publicRateLimitMiddleware } from './public'
36
- import { searchRouter } from "./search"
37
- import type { Env, Variables } from './types'
38
2
 
39
- // --- Costanti e helper ---
3
+ /**
4
+ * Entry point per lo sviluppo locale del monorepo.
5
+ * In produzione (progetto utente), viene usato worker.ts che importa createBeechApp.
6
+ */
7
+ const app = createBeechApp({ seeds: [] })
40
8
 
41
- /** Giorni di validità del refresh token */
42
- const REFRESH_TOKEN_EXPIRY_DAYS = 7
43
-
44
- /** Secondi in un giorno (per maxAge cookie) */
45
- const SECONDS_PER_DAY = 24 * 60 * 60
46
-
47
- /** Restituisce true se la richiesta è su HTTPS */
48
- function isRequestSecure(url: string): boolean {
49
- return new URL(url).protocol === 'https:'
50
- }
51
-
52
- /** Estrae l'IP del client (header Cloudflare) o 'unknown' se non disponibile */
53
- function getClientIp(headers: Headers): string {
54
- return headers.get('cf-connecting-ip') ?? 'unknown'
55
- }
56
-
57
- /** Opzioni comuni per il cookie refresh_token */
58
- function getRefreshTokenCookieOptions(secure: boolean) {
59
- return {
60
- httpOnly: true,
61
- secure,
62
- sameSite: 'Strict' as const,
63
- maxAge: REFRESH_TOKEN_EXPIRY_DAYS * SECONDS_PER_DAY,
64
- path: '/auth',
65
- }
66
- }
67
-
68
- function getRefreshTokenDeleteCookieOptions(secure: boolean) {
69
- return {
70
- httpOnly: true,
71
- secure,
72
- sameSite: 'Strict' as const,
73
- path: '/auth',
74
- }
75
- }
76
-
77
- /** Logga l'errore solo in sviluppo e restituisce risposta 500 generica */
78
- function handleAuthError(
79
- c: Context<{ Bindings: Env; Variables: Variables }>,
80
- err: unknown,
81
- operationName: string
82
- ): Response {
83
- if (c.env.ENV !== 'production') {
84
- console.error(`${operationName} error:`, err)
85
- }
86
- return c.json({ error: AUTH_ERRORS.GENERIC_ERROR }, 500)
87
- }
88
-
89
- // --- App ---
90
-
91
- const app = createBeechApp({ seeds: Object.values(SEED_REGISTRY) })
92
-
93
- // Rota root di test
94
- app.get('/', (c) => c.text('Beech API is running!'))
95
-
96
- /** Estrae il seed slug dalle route pubbliche (/api/v1/public/:seed).
97
- * Restituisce '' per tutte le altre route (metrica globale). */
98
- function extractPublicSeed(path: string): string {
99
- const match = path.match(/^\/api\/v1\/public\/([^/?]+)/)
100
- return match ? match[1] : ''
101
- }
102
-
103
- // Middleware Analytics: traccia le richieste per la dashboard (Cloudflare-style metrics)
104
- app.use('/api/*', async (c, next) => {
105
- await next()
106
-
107
- // Tracciamo solo richieste andate a buon fine (2xx) e non OPTIONS
108
- if (c.req.method !== 'OPTIONS' && c.res.status >= 200 && c.res.status < 300) {
109
- const db = c.env.DB
110
- // Protezione per ambienti (es. test) dove executionCtx non è definito
111
- let executionCtx: { waitUntil: (p: Promise<any>) => void } | undefined
112
- try {
113
- executionCtx = c.executionCtx
114
- } catch {
115
- // In ambiente di test Hono lancia se non presente
116
- }
117
-
118
- if (db && executionCtx) {
119
- // Usa waitUntil per non bloccare la risposta al client
120
- const seed = extractPublicSeed(c.req.path)
121
- executionCtx.waitUntil((async () => {
122
- try {
123
- const today = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000)
124
-
125
- // Incrementa contatore richieste: seed='' per route interne, seed='articoli' per public API
126
- await db.prepare(
127
- `INSERT INTO analytics (day_ts, metric, seed, value)
128
- VALUES (?, 'requests', ?, 1)
129
- ON CONFLICT(day_ts, metric, seed) DO UPDATE SET value = value + 1`
130
- ).bind(today, seed).run()
131
- } catch (err) {
132
- console.error('Analytics middleware error:', err)
133
- }
134
- })())
135
- }
136
- }
137
- })
138
-
139
- // POST /auth/login: autenticazione con email e password + refresh token
140
- app.post('/auth/login', async (c) => {
141
- try {
142
- let body: unknown
143
- try {
144
- body = await c.req.json()
145
- } catch {
146
- return c.json({ error: AUTH_ERRORS.INVALID_REQUEST }, 400)
147
- }
148
-
149
- const credentials = parseLoginBody(body)
150
- if (!credentials) {
151
- return c.json({ error: AUTH_ERRORS.INVALID_REQUEST }, 400)
152
- }
153
-
154
- const { email, password } = credentials
155
- if (!validateLoginInput(email, password)) {
156
- return c.json({ error: AUTH_ERRORS.INVALID_REQUEST }, 400)
157
- }
158
-
159
- // Rate limiting: 5 tentativi per IP+email ogni 60 secondi
160
- const loginLimiter = c.env.LOGIN_RATE_LIMITER
161
- if (loginLimiter) {
162
- const clientIp = getClientIp(c.req.raw.headers)
163
- const { success } = await loginLimiter.limit({ key: `${clientIp}:${email}` })
164
- if (!success) {
165
- return c.json({ error: AUTH_ERRORS.RATE_LIMIT_EXCEEDED }, 429)
166
- }
167
- }
168
-
169
- const { DB, JWT_SECRET } = c.env
170
- const user = await findUserByEmail(DB, email)
171
- // Sempre verifyPassword per evitare timing attack (utente non trovato vs password errata)
172
- const hashToCompare = user?.password_hash ?? DUMMY_PASSWORD_HASH
173
- const isValid = await verifyPassword(password, hashToCompare)
174
-
175
- if (!user || !isValid) {
176
- return c.json({ error: AUTH_ERRORS.INVALID_CREDENTIALS }, 401)
177
- }
178
-
179
- // Recupera name per includerlo nel JWT
180
- const userProfile = await DB.prepare('SELECT name FROM users WHERE id = ? LIMIT 1').bind(user.id).first<{ name: string | null }>()
181
-
182
- // Genera access token (15min) e refresh token (7 giorni)
183
- const accessToken = await generateAccessToken(user.id, user.email, JWT_SECRET, {
184
- issuer: c.env.JWT_ISSUER,
185
- audience: c.env.JWT_AUDIENCE,
186
- }, userProfile?.name ?? undefined)
187
- const refreshToken = generateRefreshToken()
188
-
189
- // Salva refresh token in DB (hashed)
190
- await saveRefreshToken(DB, user.id, refreshToken, REFRESH_TOKEN_EXPIRY_DAYS)
191
-
192
- setCookie(c, 'refresh_token', refreshToken, {
193
- ...getRefreshTokenCookieOptions(isRequestSecure(c.req.url)),
194
- })
195
-
196
- // Restituisci solo access token nel body
197
- return c.json({ token: accessToken, expiresIn: '15m' }, 200)
198
- } catch (err) {
199
- return handleAuthError(c, err, 'Login')
200
- }
201
- })
202
-
203
- // POST /auth/refresh: ottieni nuovo access token usando refresh token
204
- app.post('/auth/refresh', async (c) => {
205
- try {
206
- // Rate limiting: 20 richieste per IP ogni 60 secondi
207
- const refreshLimiter = c.env.REFRESH_RATE_LIMITER
208
- if (refreshLimiter) {
209
- const clientIp = getClientIp(c.req.raw.headers)
210
- const { success } = await refreshLimiter.limit({ key: clientIp })
211
- if (!success) {
212
- return c.json({ error: AUTH_ERRORS.RATE_LIMIT_EXCEEDED }, 429)
213
- }
214
- }
215
-
216
- // Leggi refresh token dal cookie usando helper Hono
217
- const refreshToken = getCookie(c, 'refresh_token')
218
-
219
- if (!refreshToken) {
220
- return c.json({ error: 'Refresh token missing' }, 401)
221
- }
222
-
223
- const { DB, JWT_SECRET } = c.env
224
-
225
- // Valida refresh token
226
- const validation = await validateRefreshToken(DB, refreshToken)
227
- if (!validation.valid || !validation.userId) {
228
- return c.json({ error: 'Invalid refresh token' }, 401)
229
- }
230
-
231
- // Ottieni info utente per generare nuovo access token
232
- const user = await DB.prepare(
233
- 'SELECT id, email, name FROM users WHERE id = ? LIMIT 1'
234
- ).bind(validation.userId).first<{ id: string; email: string; name: string | null }>()
235
-
236
- if (!user) {
237
- return c.json({ error: 'User not found' }, 401)
238
- }
239
-
240
- // ROTAZIONE: Invalida vecchio refresh token
241
- const revoked = await revokeRefreshToken(DB, refreshToken)
242
- if (!revoked) {
243
- // Token già consumato/revocato (race) o appena scaduto: tratta come invalido.
244
- return c.json({ error: 'Invalid refresh token' }, 401)
245
- }
246
-
247
- // Genera NUOVO access token e NUOVO refresh token
248
- const newAccessToken = await generateAccessToken(user.id, user.email, JWT_SECRET, {
249
- issuer: c.env.JWT_ISSUER,
250
- audience: c.env.JWT_AUDIENCE,
251
- }, user.name ?? undefined)
252
- const newRefreshToken = generateRefreshToken()
253
-
254
- // Salva nuovo refresh token in DB
255
- await saveRefreshToken(DB, user.id, newRefreshToken, REFRESH_TOKEN_EXPIRY_DAYS)
256
-
257
- setCookie(c, 'refresh_token', newRefreshToken, {
258
- ...getRefreshTokenCookieOptions(isRequestSecure(c.req.url)),
259
- })
260
-
261
- // Restituisci nuovo access token
262
- return c.json({ token: newAccessToken, expiresIn: '15m' }, 200)
263
- } catch (err) {
264
- return handleAuthError(c, err, 'Refresh')
265
- }
266
- })
267
-
268
- // POST /auth/logout: invalida refresh token e cancella cookie
269
- app.post('/auth/logout', async (c) => {
270
- try {
271
- // Leggi refresh token dal cookie usando helper Hono
272
- const refreshToken = getCookie(c, 'refresh_token')
273
-
274
- if (refreshToken) {
275
- await revokeRefreshToken(c.env.DB, refreshToken)
276
- }
277
-
278
- deleteCookie(c, 'refresh_token', {
279
- ...getRefreshTokenDeleteCookieOptions(isRequestSecure(c.req.url)),
280
- })
281
-
282
- return c.json({ message: 'Logged out' }, 200)
283
- } catch (err) {
284
- return handleAuthError(c, err, 'Logout')
285
- }
286
- })
287
-
288
- // First-run setup: GET /auth/setup, POST /auth/setup (pubblici, bloccati dopo il primo utente)
289
- app.route('/', setupApp)
290
-
291
- // Password reset: GET /auth/features, POST /auth/forgot-password, POST /auth/reset-password (pubblici)
292
- app.route('/', passwordResetApp)
293
-
294
- // API Settings: gestione profilo e preferenze utente, protetto da JWT
295
- const apiSettings = new Hono<{ Bindings: Env; Variables: Variables }>()
296
- apiSettings.use('*', async (c, next) => {
297
- await authMiddleware(c.env.JWT_SECRET, {
298
- issuer: c.env.JWT_ISSUER,
299
- audience: c.env.JWT_AUDIENCE,
300
- })(c, next)
301
- })
302
- apiSettings.route('/', settingsApp)
303
- app.route('/api/settings', apiSettings)
304
-
305
- // API Content: CRUD universale protetto da JWT
306
- const apiContent = new Hono<{ Bindings: Env; Variables: Variables }>()
307
- apiContent.use('*', async (c, next) => {
308
- await authMiddleware(c.env.JWT_SECRET, {
309
- issuer: c.env.JWT_ISSUER,
310
- audience: c.env.JWT_AUDIENCE,
311
- })(c, next)
312
- })
313
- // Specific routes before wildcard content routes to avoid pattern conflicts
314
- apiContent.route('/', notificationsApp)
315
- apiContent.route('/', statsApp)
316
- apiContent.route('/', rotateFieldApp)
317
- apiContent.route('/', draftApp)
318
- apiContent.route('/', contentRoutes)
319
- app.route('/api/content', apiContent)
320
- app.route('/api/search', searchRouter)
321
-
322
- // API Widget: endpoint aggregati per i widget della dashboard, protetti da JWT
323
- const apiWidget = new Hono<{ Bindings: Env; Variables: Variables }>()
324
- apiWidget.use('*', async (c, next) => {
325
- await authMiddleware(c.env.JWT_SECRET, {
326
- issuer: c.env.JWT_ISSUER,
327
- audience: c.env.JWT_AUDIENCE,
328
- })(c, next)
329
- })
330
- apiWidget.route('/', widgetApp)
331
- app.route('/api/widget', apiWidget)
332
-
333
- // API Upload: POST /api/upload (JWT) + GET /api/media/:key (pubblico)
334
- app.route('/api', uploadRoutes)
335
- app.get('/api/media/:key', (c) => serveMediaHandler(c))
336
-
337
- // API Pubblica: endpoint per consumatori esterni, protetti da API Key
338
- const apiPublic = new Hono<{ Bindings: Env; Variables: Variables }>()
339
- apiPublic.use('*', publicRateLimitMiddleware())
340
- apiPublic.use('*', apiKeyMiddleware())
341
- apiPublic.route('/', publicRoutes)
342
- app.route('/api/v1/public', apiPublic)
9
+ app.get('/', (c) => c.text('Beech API is running (Production Ready Mode)'))
343
10
 
344
11
  export default app
@@ -175,6 +175,10 @@ export function buildFtsQuery(
175
175
 
176
176
  // ─── Mapper ──────────────────────────────────────────────────────────────────
177
177
 
178
+ function stripHtmlPreserveMark(html: string): string {
179
+ return html.replace(/<(?!\/?mark\b)[^>]*>/gi, ' ').replace(/\s+/g, ' ').trim()
180
+ }
181
+
178
182
  export function mapFtsRow(row: FtsRow): SearchResultItem {
179
183
  return {
180
184
  id: row.entry_id,
@@ -182,7 +186,7 @@ export function mapFtsRow(row: FtsRow): SearchResultItem {
182
186
  slug: row.slug,
183
187
  status: row.status,
184
188
  title: row.title ?? "",
185
- excerpt: row.excerpt ?? "",
189
+ excerpt: stripHtmlPreserveMark(row.excerpt ?? ""),
186
190
  data: {},
187
191
  }
188
192
  }
package/src/types.ts CHANGED
@@ -26,6 +26,7 @@ export interface Env {
26
26
  FORGOT_PASSWORD_RATE_LIMITER?: RateLimit
27
27
  RESET_PASSWORD_RATE_LIMITER?: RateLimit
28
28
  ENV?: string
29
+ ASSETS?: Fetcher
29
30
  }
30
31
 
31
32
  export interface Variables {