@beechcms/api 0.4.2 → 0.4.3
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.
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<!-- Poppins: pesi normali + italic. Inter: fallback con piena copertura degli stili. -->
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet" />
|
|
11
11
|
<title>dashboard</title>
|
|
12
|
-
<script type="module" crossorigin src="/admin/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/admin/assets/index-BMkd1Irh.js"></script>
|
|
13
13
|
<link rel="stylesheet" crossorigin href="/admin/assets/index-BKWnlvnV.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beechcms/api",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/factory.ts"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@aws-sdk/client-s3": "^3.995.0",
|
|
26
|
-
"@beechcms/core": "^0.4.
|
|
26
|
+
"@beechcms/core": "^0.4.3",
|
|
27
27
|
"bcryptjs": "^2.4.3",
|
|
28
28
|
"hono": "^4.11.9",
|
|
29
29
|
"jose": "^6.1.3"
|
package/src/public/public-add.ts
CHANGED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
import { isValidContentStatus, SlugConflictError } from '@beechcms/core'
|
|
2
|
-
import type { Context } from 'hono'
|
|
3
|
-
import { cleanStr } from '../shared/query-utils'
|
|
4
|
-
import { checkPublicOperation } from './access-policy'
|
|
5
|
-
import { publicProblem, internalErrorDetail } from './problem-details'
|
|
6
|
-
import { generateEntrySlug, slugify } from './slug-utils'
|
|
7
|
-
import { sanitizePublicPayload } from './sanitize'
|
|
8
|
-
import { parseIdempotencyKey, buildRequestFingerprint } from './idempotency'
|
|
9
|
-
import { AppEnv } from '../types'
|
|
10
|
-
|
|
11
|
-
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
12
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
-
? (value as Record<string, unknown>)
|
|
14
|
-
: null
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function pickSlug(body: Record<string, unknown>, sanitizedData: Record<string, unknown>): string {
|
|
18
|
-
const explicit = cleanStr(body.slug)
|
|
19
|
-
if (explicit) return slugify(explicit)
|
|
20
|
-
return generateEntrySlug({ title: sanitizedData.title, name: sanitizedData.name })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function publicAddHandler(context: Context<AppEnv>) {
|
|
24
|
-
const seedSlug = context.req.param('seed') ?? ''
|
|
25
|
-
const seed = context.get('getSeed')(seedSlug)
|
|
26
|
-
if (!seed) {
|
|
27
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed Not Found', status: 404, detail: `The content type '${seedSlug}' does not exist.` })
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const access = checkPublicOperation(seed, 'add')
|
|
31
|
-
if (!access.ok) {
|
|
32
|
-
return publicProblem(context, { type: 'operation-not-allowed', title: access.error.error, status: 403, detail: access.error.message })
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let body: Record<string, unknown>
|
|
36
|
-
try {
|
|
37
|
-
const parsed = await context.req.json<unknown>()
|
|
38
|
-
body = asRecord(parsed) ?? {}
|
|
39
|
-
} catch {
|
|
40
|
-
return publicProblem(context, { type: 'invalid-json-body', title: 'Bad Request', status: 400, detail: 'Invalid JSON body' })
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const rawData = asRecord(body.data)
|
|
44
|
-
if (!rawData || Object.keys(rawData).length === 0) {
|
|
45
|
-
return publicProblem(context, { type: 'invalid-data-object', title: 'Bad Request', status: 400, detail: "Field 'data' is required and must be a non-empty object" })
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const statusValue = body.status ?? 'draft'
|
|
49
|
-
if (!isValidContentStatus(statusValue)) {
|
|
50
|
-
return publicProblem(context, { type: 'invalid-status', title: 'Bad Request', status: 400, detail: 'Invalid status. Allowed values are: draft, review, published' })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const sanitized = sanitizePublicPayload(seed, rawData, { operation: 'create', allowNull: false, requireAtLeastOneValidField: true, enforceRequiredFields: true })
|
|
54
|
-
if (!sanitized.ok) {
|
|
55
|
-
if (sanitized.status === 422) {
|
|
56
|
-
return publicProblem(context, { type: sanitized.code, title: 'Unprocessable Entity', status: 422, detail: sanitized.message })
|
|
57
|
-
}
|
|
58
|
-
return publicProblem(context, { type: sanitized.code, title: 'Bad Request', status: 400, detail: sanitized.message, errors: sanitized.details })
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const idempotencyKey = parseIdempotencyKey(context.req.header('Idempotency-Key'))
|
|
62
|
-
const finalSlug = pickSlug(body, sanitized.data) || context.get('idGenerator').uuid().slice(0, 8)
|
|
63
|
-
const repository = context.get('repository')
|
|
64
|
-
const idempotencyRepository = context.get('idempotencyRepository')
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const now = Math.floor(Date.now() / 1000)
|
|
68
|
-
const fingerprint = await buildRequestFingerprint({ seedSlug, statusValue, slug: cleanStr(body.slug) ?? null, data: sanitized.data })
|
|
69
|
-
const idempotencyTtl = Math.max(60, Number.parseInt(context.env.PUBLIC_IDEMPOTENCY_TTL_SECONDS ?? '86400', 10) || 86400)
|
|
70
|
-
|
|
71
|
-
if (idempotencyKey) {
|
|
72
|
-
const existing = await idempotencyRepository.lookup(idempotencyKey)
|
|
73
|
-
if (existing && existing.expiresAt >= now) {
|
|
74
|
-
if (existing.fingerprint !== fingerprint) {
|
|
75
|
-
return publicProblem(context, { type: 'idempotency-key-conflict', title: 'Conflict', status: 409, detail: 'Idempotency-Key was already used with a different request payload.' })
|
|
76
|
-
}
|
|
77
|
-
let parsedBody: unknown = null
|
|
78
|
-
try { parsedBody = JSON.parse(existing.responseBody) } catch { parsedBody = { success: true } }
|
|
79
|
-
return context.json(parsedBody, existing.responseStatus as 201)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const id = context.get('idGenerator').uuid()
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await repository.create(seed, id, finalSlug, statusValue as any, sanitized.data)
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (error instanceof SlugConflictError) {
|
|
89
|
-
return publicProblem(context, { type: 'slug-conflict', title: 'Conflict', status: 409, detail: `An entry with slug '${finalSlug}' already exists for content type '${seedSlug}'.` })
|
|
90
|
-
}
|
|
91
|
-
throw error
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const responseBody = { success: true, id, slug: finalSlug }
|
|
95
|
-
if (idempotencyKey) {
|
|
96
|
-
await idempotencyRepository.store({ key: idempotencyKey, fingerprint, responseStatus: 201, responseBody: JSON.stringify(responseBody), expiresAt: now + idempotencyTtl })
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
context.get('notificationService').notify({
|
|
100
|
-
title: `${seed.label}: New entry`,
|
|
101
|
-
message: `A new entry ("${sanitized.data.title || sanitized.data.name || finalSlug}") has been added via the public API.`,
|
|
102
|
-
type: 'success',
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
return context.json(responseBody, 201)
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.error('Public add error:', error)
|
|
108
|
-
return publicProblem(context, { type: 'internal-server-error', title: 'Internal Server Error', status: 500, detail: internalErrorDetail(context.env, error) })
|
|
109
|
-
}
|
|
110
|
-
}
|
|
1
|
+
import { isValidContentStatus, SlugConflictError } from '@beechcms/core'
|
|
2
|
+
import type { Context } from 'hono'
|
|
3
|
+
import { cleanStr } from '../shared/query-utils'
|
|
4
|
+
import { checkPublicOperation } from './access-policy'
|
|
5
|
+
import { publicProblem, internalErrorDetail } from './problem-details'
|
|
6
|
+
import { generateEntrySlug, slugify } from './slug-utils'
|
|
7
|
+
import { sanitizePublicPayload } from './sanitize'
|
|
8
|
+
import { parseIdempotencyKey, buildRequestFingerprint } from './idempotency'
|
|
9
|
+
import { AppEnv } from '../types'
|
|
10
|
+
|
|
11
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
? (value as Record<string, unknown>)
|
|
14
|
+
: null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function pickSlug(body: Record<string, unknown>, sanitizedData: Record<string, unknown>): string {
|
|
18
|
+
const explicit = cleanStr(body.slug)
|
|
19
|
+
if (explicit) return slugify(explicit)
|
|
20
|
+
return generateEntrySlug({ title: sanitizedData.title, name: sanitizedData.name })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function publicAddHandler(context: Context<AppEnv>) {
|
|
24
|
+
const seedSlug = context.req.param('seed') ?? ''
|
|
25
|
+
const seed = context.get('getSeed')(seedSlug)
|
|
26
|
+
if (!seed) {
|
|
27
|
+
return publicProblem(context, { type: 'seed-not-found', title: 'Seed Not Found', status: 404, detail: `The content type '${seedSlug}' does not exist.` })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const access = checkPublicOperation(seed, 'add')
|
|
31
|
+
if (!access.ok) {
|
|
32
|
+
return publicProblem(context, { type: 'operation-not-allowed', title: access.error.error, status: 403, detail: access.error.message })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let body: Record<string, unknown>
|
|
36
|
+
try {
|
|
37
|
+
const parsed = await context.req.json<unknown>()
|
|
38
|
+
body = asRecord(parsed) ?? {}
|
|
39
|
+
} catch {
|
|
40
|
+
return publicProblem(context, { type: 'invalid-json-body', title: 'Bad Request', status: 400, detail: 'Invalid JSON body' })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rawData = asRecord(body.data)
|
|
44
|
+
if (!rawData || Object.keys(rawData).length === 0) {
|
|
45
|
+
return publicProblem(context, { type: 'invalid-data-object', title: 'Bad Request', status: 400, detail: "Field 'data' is required and must be a non-empty object" })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const statusValue = body.status ?? 'draft'
|
|
49
|
+
if (!isValidContentStatus(statusValue)) {
|
|
50
|
+
return publicProblem(context, { type: 'invalid-status', title: 'Bad Request', status: 400, detail: 'Invalid status. Allowed values are: draft, review, published' })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sanitized = sanitizePublicPayload(seed, rawData, { operation: 'create', allowNull: false, requireAtLeastOneValidField: true, enforceRequiredFields: true })
|
|
54
|
+
if (!sanitized.ok) {
|
|
55
|
+
if (sanitized.status === 422) {
|
|
56
|
+
return publicProblem(context, { type: sanitized.code, title: 'Unprocessable Entity', status: 422, detail: sanitized.message })
|
|
57
|
+
}
|
|
58
|
+
return publicProblem(context, { type: sanitized.code, title: 'Bad Request', status: 400, detail: sanitized.message, errors: sanitized.details })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const idempotencyKey = parseIdempotencyKey(context.req.header('Idempotency-Key'))
|
|
62
|
+
const finalSlug = pickSlug(body, sanitized.data) || context.get('idGenerator').uuid().slice(0, 8)
|
|
63
|
+
const repository = context.get('repository')
|
|
64
|
+
const idempotencyRepository = context.get('idempotencyRepository')
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const now = Math.floor(Date.now() / 1000)
|
|
68
|
+
const fingerprint = await buildRequestFingerprint({ seedSlug, statusValue, slug: cleanStr(body.slug) ?? null, data: sanitized.data })
|
|
69
|
+
const idempotencyTtl = Math.max(60, Number.parseInt(context.env.PUBLIC_IDEMPOTENCY_TTL_SECONDS ?? '86400', 10) || 86400)
|
|
70
|
+
|
|
71
|
+
if (idempotencyKey) {
|
|
72
|
+
const existing = await idempotencyRepository.lookup(idempotencyKey)
|
|
73
|
+
if (existing && existing.expiresAt >= now) {
|
|
74
|
+
if (existing.fingerprint !== fingerprint) {
|
|
75
|
+
return publicProblem(context, { type: 'idempotency-key-conflict', title: 'Conflict', status: 409, detail: 'Idempotency-Key was already used with a different request payload.' })
|
|
76
|
+
}
|
|
77
|
+
let parsedBody: unknown = null
|
|
78
|
+
try { parsedBody = JSON.parse(existing.responseBody) } catch { parsedBody = { success: true } }
|
|
79
|
+
return context.json(parsedBody, existing.responseStatus as 201)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const id = context.get('idGenerator').uuid()
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await repository.create(seed, id, finalSlug, statusValue as any, sanitized.data)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error instanceof SlugConflictError) {
|
|
89
|
+
return publicProblem(context, { type: 'slug-conflict', title: 'Conflict', status: 409, detail: `An entry with slug '${finalSlug}' already exists for content type '${seedSlug}'.` })
|
|
90
|
+
}
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const responseBody = { success: true, id, slug: finalSlug }
|
|
95
|
+
if (idempotencyKey) {
|
|
96
|
+
await idempotencyRepository.store({ key: idempotencyKey, fingerprint, responseStatus: 201, responseBody: JSON.stringify(responseBody), expiresAt: now + idempotencyTtl })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
context.get('notificationService').notify({
|
|
100
|
+
title: `${seed.label}: New entry`,
|
|
101
|
+
message: `A new entry ("${sanitized.data.title || sanitized.data.name || finalSlug}") has been added via the public API.`,
|
|
102
|
+
type: 'success',
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return context.json(responseBody, 201)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Public add error:', error)
|
|
108
|
+
return publicProblem(context, { type: 'internal-server-error', title: 'Internal Server Error', status: 500, detail: internalErrorDetail(context.env, error) })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import type { Context } from 'hono'
|
|
2
|
-
import { cleanStr } from '../shared/query-utils'
|
|
3
|
-
import { checkPublicOperation } from './access-policy'
|
|
4
|
-
import { publicProblem, internalErrorDetail } from './problem-details'
|
|
5
|
-
import { resolveEdgeCache, withCachedResponse } from './cache-utils'
|
|
6
|
-
import { readSingleEntry } from './read-single'
|
|
7
|
-
import { readListEntries } from './read-list'
|
|
8
|
-
import { AppEnv } from '../types'
|
|
9
|
-
|
|
10
|
-
export async function publicReadHandler(context: Context<AppEnv>) {
|
|
11
|
-
const seedSlug = context.req.param('seed') ?? ''
|
|
12
|
-
const seed = context.get('getSeed')(seedSlug)
|
|
13
|
-
if (!seed) {
|
|
14
|
-
const available = context.get('seedRegistry').all().map(s => s.slug).join(', ')
|
|
15
|
-
return publicProblem(context, {
|
|
16
|
-
type: 'seed-not-found',
|
|
17
|
-
title: 'Seed Not Found',
|
|
18
|
-
status: 404,
|
|
19
|
-
detail: `The content type '${seedSlug}' does not exist. Available types: ${available}.`,
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const access = checkPublicOperation(seed, 'read')
|
|
24
|
-
if (!access.ok) {
|
|
25
|
-
return publicProblem(context, { type: 'operation-not-allowed', title: access.error.error, status: 403, detail: access.error.message })
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const edgeCache = resolveEdgeCache(context)
|
|
29
|
-
const cacheKey = context.req.raw
|
|
30
|
-
if (edgeCache) {
|
|
31
|
-
const hit = await edgeCache.cache.match(cacheKey)
|
|
32
|
-
if (hit) return hit
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const query = context.req.query()
|
|
36
|
-
const id = cleanStr(query.id)
|
|
37
|
-
const slug = cleanStr(query.slug)
|
|
38
|
-
const publishedOnly = context.env.PUBLIC_PUBLISHED_ONLY !== 'false'
|
|
39
|
-
const repository = context.get('repository')
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
if (id || slug) {
|
|
43
|
-
const result = await readSingleEntry({ seed, seedSlug, repository, id, slug, publishedOnly, fieldsParam: query.fields })
|
|
44
|
-
if (!result.ok) {
|
|
45
|
-
return publicProblem(context, { type: 'entry-not-found', title: 'Not Found', status: 404, detail: result.detail })
|
|
46
|
-
}
|
|
47
|
-
return withCachedResponse(edgeCache, cacheKey, context.json({ data: result.data, meta: result.meta }, 200))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const result = await readListEntries({ seed, seedSlug, repository, query, publishedOnly })
|
|
51
|
-
return withCachedResponse(edgeCache, cacheKey, context.json(result, 200))
|
|
52
|
-
} catch (error) {
|
|
53
|
-
if (error instanceof Error && error.message.startsWith('Invalid filter:')) {
|
|
54
|
-
return publicProblem(context, { type: 'invalid-filter', title: 'Bad Request', status: 400, detail: error.message })
|
|
55
|
-
}
|
|
56
|
-
console.error('Public read error:', error)
|
|
57
|
-
return publicProblem(context, { type: 'internal-server-error', title: 'Internal Server Error', status: 500, detail: internalErrorDetail(context.env, error) })
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
import type { Context } from 'hono'
|
|
2
|
+
import { cleanStr } from '../shared/query-utils'
|
|
3
|
+
import { checkPublicOperation } from './access-policy'
|
|
4
|
+
import { publicProblem, internalErrorDetail } from './problem-details'
|
|
5
|
+
import { resolveEdgeCache, withCachedResponse } from './cache-utils'
|
|
6
|
+
import { readSingleEntry } from './read-single'
|
|
7
|
+
import { readListEntries } from './read-list'
|
|
8
|
+
import { AppEnv } from '../types'
|
|
9
|
+
|
|
10
|
+
export async function publicReadHandler(context: Context<AppEnv>) {
|
|
11
|
+
const seedSlug = context.req.param('seed') ?? ''
|
|
12
|
+
const seed = context.get('getSeed')(seedSlug)
|
|
13
|
+
if (!seed) {
|
|
14
|
+
const available = context.get('seedRegistry').all().map(s => s.slug).join(', ')
|
|
15
|
+
return publicProblem(context, {
|
|
16
|
+
type: 'seed-not-found',
|
|
17
|
+
title: 'Seed Not Found',
|
|
18
|
+
status: 404,
|
|
19
|
+
detail: `The content type '${seedSlug}' does not exist. Available types: ${available}.`,
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const access = checkPublicOperation(seed, 'read')
|
|
24
|
+
if (!access.ok) {
|
|
25
|
+
return publicProblem(context, { type: 'operation-not-allowed', title: access.error.error, status: 403, detail: access.error.message })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const edgeCache = resolveEdgeCache(context)
|
|
29
|
+
const cacheKey = context.req.raw
|
|
30
|
+
if (edgeCache) {
|
|
31
|
+
const hit = await edgeCache.cache.match(cacheKey)
|
|
32
|
+
if (hit) return hit
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const query = context.req.query()
|
|
36
|
+
const id = cleanStr(query.id)
|
|
37
|
+
const slug = cleanStr(query.slug)
|
|
38
|
+
const publishedOnly = context.env.PUBLIC_PUBLISHED_ONLY !== 'false'
|
|
39
|
+
const repository = context.get('repository')
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (id || slug) {
|
|
43
|
+
const result = await readSingleEntry({ seed, seedSlug, repository, id, slug, publishedOnly, fieldsParam: query.fields })
|
|
44
|
+
if (!result.ok) {
|
|
45
|
+
return publicProblem(context, { type: 'entry-not-found', title: 'Not Found', status: 404, detail: result.detail })
|
|
46
|
+
}
|
|
47
|
+
return withCachedResponse(edgeCache, cacheKey, context.json({ data: result.data, meta: result.meta }, 200))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await readListEntries({ seed, seedSlug, repository, query, publishedOnly })
|
|
51
|
+
return withCachedResponse(edgeCache, cacheKey, context.json(result, 200))
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof Error && error.message.startsWith('Invalid filter:')) {
|
|
54
|
+
return publicProblem(context, { type: 'invalid-filter', title: 'Bad Request', status: 400, detail: error.message })
|
|
55
|
+
}
|
|
56
|
+
console.error('Public read error:', error)
|
|
57
|
+
return publicProblem(context, { type: 'internal-server-error', title: 'Internal Server Error', status: 500, detail: internalErrorDetail(context.env, error) })
|
|
58
|
+
}
|
|
59
|
+
}
|