@xexr/blog-platform 0.2.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/CHANGELOG.md +61 -0
- package/LICENSE +21 -0
- package/README.md +1175 -0
- package/dist/app/api/blog/publish/route.ts +360 -0
- package/dist/app/api/blog/slugs/[slug]/route.ts +166 -0
- package/dist/app/api/blog/unpublish/route.ts +301 -0
- package/dist/app/api/blog/version/route.ts +259 -0
- package/dist/app/blog/[slug]/page.tsx +304 -0
- package/dist/app/blog/page.tsx +316 -0
- package/dist/index.d.mts +1799 -0
- package/dist/index.d.ts +1799 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +31 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/index.d.mts +142 -0
- package/dist/types/index.d.ts +142 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +2 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getStorage } from "@/lib/database";
|
|
3
|
+
import { getBlogPlatformVersion, parseVersionHeader } from "@/lib/version";
|
|
4
|
+
import { requireAuth, authenticateRequest } from "@/lib/auth";
|
|
5
|
+
import { enforceRateLimit, getRateLimitHeaders } from "@/lib/ratelimit";
|
|
6
|
+
import { createCorsResponse, handleCorsOptions } from "@/lib/cors";
|
|
7
|
+
import type { UnpublishPayload, PublishResult } from "@/types/contracts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/blog/unpublish
|
|
11
|
+
*
|
|
12
|
+
* Unpublishes an article from the blog platform with full idempotency support.
|
|
13
|
+
* This endpoint receives unpublish requests from Bloggy core and removes the content.
|
|
14
|
+
*
|
|
15
|
+
* Request body should contain an UnpublishPayload with:
|
|
16
|
+
* - slug: The article slug to unpublish
|
|
17
|
+
* - idempotencyKey: SHA-256 hash for duplicate prevention
|
|
18
|
+
* - Optional dry run flag for validation
|
|
19
|
+
*/
|
|
20
|
+
export async function POST(
|
|
21
|
+
request: NextRequest,
|
|
22
|
+
): Promise<NextResponse<PublishResult>> {
|
|
23
|
+
try {
|
|
24
|
+
// Authenticate the request
|
|
25
|
+
const auth = await authenticateRequest(request);
|
|
26
|
+
if (!auth.authenticated) {
|
|
27
|
+
const authCheck = await requireAuth(request);
|
|
28
|
+
return NextResponse.json(authCheck.error!, { status: 401 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Apply rate limiting using the authentication token
|
|
32
|
+
const token = auth.token ?? "anonymous";
|
|
33
|
+
const rateLimitCheck = await enforceRateLimit(token);
|
|
34
|
+
if (!rateLimitCheck.allowed) {
|
|
35
|
+
// Extract rate limit info from error response, with safe fallback
|
|
36
|
+
const rateLimitData =
|
|
37
|
+
rateLimitCheck.errorResponse.error?.details?.rateLimit;
|
|
38
|
+
const rateLimitResult =
|
|
39
|
+
rateLimitData &&
|
|
40
|
+
typeof rateLimitData === "object" &&
|
|
41
|
+
"limit" in rateLimitData &&
|
|
42
|
+
"remaining" in rateLimitData &&
|
|
43
|
+
"reset" in rateLimitData &&
|
|
44
|
+
"count" in rateLimitData &&
|
|
45
|
+
"success" in rateLimitData
|
|
46
|
+
? (rateLimitData as {
|
|
47
|
+
limit: number;
|
|
48
|
+
remaining: number;
|
|
49
|
+
reset: number;
|
|
50
|
+
count: number;
|
|
51
|
+
success: boolean;
|
|
52
|
+
})
|
|
53
|
+
: {
|
|
54
|
+
limit: 10,
|
|
55
|
+
remaining: 0,
|
|
56
|
+
reset: Date.now() + 60000,
|
|
57
|
+
count: 10,
|
|
58
|
+
success: false,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const rateLimitHeaders = getRateLimitHeaders(rateLimitResult);
|
|
62
|
+
|
|
63
|
+
return createCorsResponse(request, rateLimitCheck.errorResponse, {
|
|
64
|
+
status: 429,
|
|
65
|
+
headers: rateLimitHeaders,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate request content type
|
|
70
|
+
const contentType = request.headers.get("content-type");
|
|
71
|
+
if (!contentType?.includes("application/json")) {
|
|
72
|
+
return createCorsResponse(
|
|
73
|
+
request,
|
|
74
|
+
{
|
|
75
|
+
success: false,
|
|
76
|
+
error: {
|
|
77
|
+
code: "PUB001",
|
|
78
|
+
message: "Content-Type must be application/json",
|
|
79
|
+
},
|
|
80
|
+
idempotencyKey: "",
|
|
81
|
+
},
|
|
82
|
+
{ status: 400 },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Parse request body
|
|
87
|
+
let payload: UnpublishPayload;
|
|
88
|
+
try {
|
|
89
|
+
payload = (await request.json()) as UnpublishPayload;
|
|
90
|
+
} catch {
|
|
91
|
+
return createCorsResponse(
|
|
92
|
+
request,
|
|
93
|
+
{
|
|
94
|
+
success: false,
|
|
95
|
+
error: {
|
|
96
|
+
code: "PUB001",
|
|
97
|
+
message: "Invalid JSON in request body",
|
|
98
|
+
},
|
|
99
|
+
idempotencyKey: "",
|
|
100
|
+
},
|
|
101
|
+
{ status: 400 },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate required fields
|
|
106
|
+
const validationErrors = validateUnpublishPayload(payload);
|
|
107
|
+
if (validationErrors.length > 0) {
|
|
108
|
+
return createCorsResponse(
|
|
109
|
+
request,
|
|
110
|
+
{
|
|
111
|
+
success: false,
|
|
112
|
+
error: {
|
|
113
|
+
code: "PUB001",
|
|
114
|
+
message: "Missing required fields",
|
|
115
|
+
details: { errors: validationErrors },
|
|
116
|
+
},
|
|
117
|
+
idempotencyKey: payload.idempotencyKey ?? "",
|
|
118
|
+
},
|
|
119
|
+
{ status: 400 },
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check version compatibility if header provided
|
|
124
|
+
const clientVersionHeader = request.headers.get("X-Bloggy-Version");
|
|
125
|
+
if (clientVersionHeader) {
|
|
126
|
+
const parsedVersions = parseVersionHeader(clientVersionHeader);
|
|
127
|
+
if (parsedVersions?.platformVersion) {
|
|
128
|
+
const platformVersion = getBlogPlatformVersion();
|
|
129
|
+
|
|
130
|
+
// Check for major version incompatibility
|
|
131
|
+
if (parsedVersions.platformVersion.major !== platformVersion.major) {
|
|
132
|
+
return createCorsResponse(
|
|
133
|
+
request,
|
|
134
|
+
{
|
|
135
|
+
success: false,
|
|
136
|
+
error: {
|
|
137
|
+
code: "PUB009",
|
|
138
|
+
message:
|
|
139
|
+
"Major version incompatibility. Please upgrade your blog platform package.",
|
|
140
|
+
details: {
|
|
141
|
+
clientVersion: parsedVersions.platformVersion.version,
|
|
142
|
+
serverVersion: platformVersion.version,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
idempotencyKey: payload.idempotencyKey,
|
|
146
|
+
},
|
|
147
|
+
{ status: 409 },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate slug format
|
|
154
|
+
if (!isValidSlug(payload.slug)) {
|
|
155
|
+
return createCorsResponse(
|
|
156
|
+
request,
|
|
157
|
+
{
|
|
158
|
+
success: false,
|
|
159
|
+
error: {
|
|
160
|
+
code: "PUB002",
|
|
161
|
+
message:
|
|
162
|
+
"Slug can only contain lowercase letters, numbers, and hyphens",
|
|
163
|
+
},
|
|
164
|
+
idempotencyKey: payload.idempotencyKey,
|
|
165
|
+
},
|
|
166
|
+
{ status: 400 },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Handle dry run validation
|
|
171
|
+
if (payload.dryRun) {
|
|
172
|
+
const storage = getStorage();
|
|
173
|
+
const existingArticle = await storage.getArticle(payload.slug);
|
|
174
|
+
|
|
175
|
+
const warnings: string[] = [];
|
|
176
|
+
if (!existingArticle) {
|
|
177
|
+
warnings.push("Article with this slug does not exist");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return createCorsResponse(
|
|
181
|
+
request,
|
|
182
|
+
{
|
|
183
|
+
success: true,
|
|
184
|
+
warnings,
|
|
185
|
+
idempotencyKey: payload.idempotencyKey,
|
|
186
|
+
},
|
|
187
|
+
{ status: 200 },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Get storage and attempt to remove article
|
|
192
|
+
const storage = getStorage();
|
|
193
|
+
const result = await storage.removeArticle(
|
|
194
|
+
payload.slug,
|
|
195
|
+
payload.idempotencyKey,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Prepare response headers
|
|
199
|
+
const platformVersion = getBlogPlatformVersion();
|
|
200
|
+
const rateLimitHeaders = getRateLimitHeaders(
|
|
201
|
+
rateLimitCheck.rateLimitResult,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const responseHeaders = {
|
|
205
|
+
"X-Bloggy-Version": `platform/${platformVersion.version}`,
|
|
206
|
+
"X-Idempotency-Key": payload.idempotencyKey,
|
|
207
|
+
...rateLimitHeaders,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Return appropriate status code
|
|
211
|
+
const statusCode = result.success
|
|
212
|
+
? 200
|
|
213
|
+
: result.error?.code === "PUB007"
|
|
214
|
+
? 404
|
|
215
|
+
: 400;
|
|
216
|
+
|
|
217
|
+
return createCorsResponse(request, result, {
|
|
218
|
+
status: statusCode,
|
|
219
|
+
headers: responseHeaders,
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error("Unpublish endpoint error:", error);
|
|
223
|
+
|
|
224
|
+
const errorResult: PublishResult = {
|
|
225
|
+
success: false,
|
|
226
|
+
error: {
|
|
227
|
+
code: "PUB007",
|
|
228
|
+
message: "Internal server error during unpublishing",
|
|
229
|
+
},
|
|
230
|
+
idempotencyKey: "",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return createCorsResponse(request, errorResult, { status: 500 });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* DELETE /api/blog/unpublish
|
|
239
|
+
*
|
|
240
|
+
* Alternative HTTP method for unpublishing (some clients prefer DELETE for removal operations)
|
|
241
|
+
* Accepts the same payload structure as POST
|
|
242
|
+
*/
|
|
243
|
+
export async function DELETE(
|
|
244
|
+
request: NextRequest,
|
|
245
|
+
): Promise<NextResponse<PublishResult>> {
|
|
246
|
+
return POST(request);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* OPTIONS /api/blog/unpublish
|
|
251
|
+
*
|
|
252
|
+
* Handle CORS preflight requests
|
|
253
|
+
*/
|
|
254
|
+
export async function OPTIONS(request: NextRequest): Promise<NextResponse> {
|
|
255
|
+
return handleCorsOptions(request, ["POST", "DELETE"]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Validate the unpublish payload structure
|
|
260
|
+
*/
|
|
261
|
+
function validateUnpublishPayload(payload: unknown): string[] {
|
|
262
|
+
const errors: string[] = [];
|
|
263
|
+
|
|
264
|
+
if (!payload || typeof payload !== "object") {
|
|
265
|
+
errors.push("Request body must be a valid object");
|
|
266
|
+
return errors;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const p = payload as Record<string, unknown>;
|
|
270
|
+
|
|
271
|
+
// Check required fields
|
|
272
|
+
if (!p.slug || typeof p.slug !== "string") {
|
|
273
|
+
errors.push("Missing required field: slug");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!p.idempotencyKey || typeof p.idempotencyKey !== "string") {
|
|
277
|
+
errors.push("Missing required field: idempotencyKey");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate slug is not empty
|
|
281
|
+
if (p.slug && typeof p.slug === "string" && !p.slug.trim()) {
|
|
282
|
+
errors.push("Slug cannot be empty");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return errors;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Validate slug format
|
|
290
|
+
* Must be lowercase, alphanumeric, and hyphens only
|
|
291
|
+
*/
|
|
292
|
+
function isValidSlug(slug: string): boolean {
|
|
293
|
+
if (!slug || typeof slug !== "string") {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Allow lowercase letters, numbers, and hyphens
|
|
298
|
+
// Must start and end with alphanumeric character
|
|
299
|
+
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
300
|
+
return slugRegex.test(slug) && slug.length >= 1 && slug.length <= 100;
|
|
301
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import {
|
|
4
|
+
getBlogPlatformVersion,
|
|
5
|
+
parseVersionHeader,
|
|
6
|
+
checkVersionCompatibility,
|
|
7
|
+
type VersionCompatibilityResult,
|
|
8
|
+
type VersionInfo,
|
|
9
|
+
} from "@/lib/version";
|
|
10
|
+
import { requireAuth, authenticateRequest } from "@/lib/auth";
|
|
11
|
+
import { enforceRateLimit, getRateLimitHeaders } from "@/lib/ratelimit";
|
|
12
|
+
import { createCorsResponse, handleCorsOptions } from "@/lib/cors";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Version compatibility response
|
|
16
|
+
*/
|
|
17
|
+
interface VersionResponse {
|
|
18
|
+
success: boolean;
|
|
19
|
+
versions: {
|
|
20
|
+
bloggy: VersionInfo;
|
|
21
|
+
platform: VersionInfo;
|
|
22
|
+
};
|
|
23
|
+
compatibility: VersionCompatibilityResult;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* GET /api/blog/version
|
|
29
|
+
*
|
|
30
|
+
* Returns version information and compatibility status for Bloggy core and blog platform.
|
|
31
|
+
* Used by customers to verify compatibility before publishing.
|
|
32
|
+
*
|
|
33
|
+
* Optional X-Bloggy-Version header can be provided to check compatibility
|
|
34
|
+
* with specific versions.
|
|
35
|
+
*/
|
|
36
|
+
export async function GET(
|
|
37
|
+
request: NextRequest,
|
|
38
|
+
): Promise<NextResponse<VersionResponse>> {
|
|
39
|
+
try {
|
|
40
|
+
// Authenticate the request
|
|
41
|
+
const auth = await authenticateRequest(request);
|
|
42
|
+
if (!auth.authenticated) {
|
|
43
|
+
const authCheck = await requireAuth(request);
|
|
44
|
+
const errorResponse: VersionResponse = {
|
|
45
|
+
success: false,
|
|
46
|
+
versions: {
|
|
47
|
+
bloggy: {
|
|
48
|
+
name: "unknown",
|
|
49
|
+
version: "unknown",
|
|
50
|
+
major: 0,
|
|
51
|
+
minor: 0,
|
|
52
|
+
patch: 0,
|
|
53
|
+
},
|
|
54
|
+
platform: {
|
|
55
|
+
name: "unknown",
|
|
56
|
+
version: "unknown",
|
|
57
|
+
major: 0,
|
|
58
|
+
minor: 0,
|
|
59
|
+
patch: 0,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
compatibility: {
|
|
63
|
+
compatible: false,
|
|
64
|
+
errorMessage: authCheck.error?.error?.message ?? "Authentication required",
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return createCorsResponse(request, errorResponse, { status: 401 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Apply rate limiting using the authentication token
|
|
71
|
+
const token = auth.token ?? "anonymous";
|
|
72
|
+
const rateLimitCheck = await enforceRateLimit(token);
|
|
73
|
+
if (!rateLimitCheck.allowed) {
|
|
74
|
+
// Extract rate limit info from error response, with safe fallback
|
|
75
|
+
const rateLimitData =
|
|
76
|
+
rateLimitCheck.errorResponse.error?.details?.rateLimit;
|
|
77
|
+
const rateLimitResult =
|
|
78
|
+
rateLimitData &&
|
|
79
|
+
typeof rateLimitData === "object" &&
|
|
80
|
+
"limit" in rateLimitData &&
|
|
81
|
+
"remaining" in rateLimitData &&
|
|
82
|
+
"reset" in rateLimitData &&
|
|
83
|
+
"count" in rateLimitData &&
|
|
84
|
+
"success" in rateLimitData
|
|
85
|
+
? (rateLimitData as {
|
|
86
|
+
limit: number;
|
|
87
|
+
remaining: number;
|
|
88
|
+
reset: number;
|
|
89
|
+
count: number;
|
|
90
|
+
success: boolean;
|
|
91
|
+
})
|
|
92
|
+
: {
|
|
93
|
+
limit: 10,
|
|
94
|
+
remaining: 0,
|
|
95
|
+
reset: Date.now() + 60000,
|
|
96
|
+
count: 10,
|
|
97
|
+
success: false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const rateLimitHeaders = getRateLimitHeaders(rateLimitResult);
|
|
101
|
+
|
|
102
|
+
// Create a proper version response with error
|
|
103
|
+
const errorVersionResponse: VersionResponse = {
|
|
104
|
+
success: false,
|
|
105
|
+
versions: {
|
|
106
|
+
bloggy: {
|
|
107
|
+
name: "unknown",
|
|
108
|
+
version: "unknown",
|
|
109
|
+
major: 0,
|
|
110
|
+
minor: 0,
|
|
111
|
+
patch: 0,
|
|
112
|
+
},
|
|
113
|
+
platform: {
|
|
114
|
+
name: "unknown",
|
|
115
|
+
version: "unknown",
|
|
116
|
+
major: 0,
|
|
117
|
+
minor: 0,
|
|
118
|
+
patch: 0,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
compatibility: {
|
|
122
|
+
compatible: false,
|
|
123
|
+
errorMessage:
|
|
124
|
+
rateLimitCheck.errorResponse.error?.message ??
|
|
125
|
+
"Rate limit exceeded",
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return createCorsResponse(request, errorVersionResponse, {
|
|
130
|
+
status: 429,
|
|
131
|
+
headers: rateLimitHeaders,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Get current platform version information
|
|
136
|
+
const platformVersion = getBlogPlatformVersion();
|
|
137
|
+
|
|
138
|
+
// Parse Bloggy version from X-Bloggy-Version header
|
|
139
|
+
const clientVersionHeader = request.headers.get("X-Bloggy-Version");
|
|
140
|
+
let bloggyVersion: VersionInfo = {
|
|
141
|
+
name: "unknown",
|
|
142
|
+
version: "unknown",
|
|
143
|
+
major: 0,
|
|
144
|
+
minor: 0,
|
|
145
|
+
patch: 0,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (clientVersionHeader) {
|
|
149
|
+
try {
|
|
150
|
+
// Try parsing as simple version string first (e.g., "0.1.0")
|
|
151
|
+
if (/^\d+\.\d+\.\d+/.test(clientVersionHeader)) {
|
|
152
|
+
const parts = clientVersionHeader.split(".");
|
|
153
|
+
const major = parseInt(parts[0] ?? "0", 10);
|
|
154
|
+
const minor = parseInt(parts[1] ?? "0", 10);
|
|
155
|
+
const patch = parseInt(parts[2]?.split("-")[0] ?? "0", 10);
|
|
156
|
+
|
|
157
|
+
bloggyVersion = {
|
|
158
|
+
name: "bloggy",
|
|
159
|
+
version: clientVersionHeader,
|
|
160
|
+
major,
|
|
161
|
+
minor,
|
|
162
|
+
patch,
|
|
163
|
+
};
|
|
164
|
+
} else {
|
|
165
|
+
// Try parsing as formatted header (e.g., "bloggy/1.0.0 platform/1.0.0")
|
|
166
|
+
const parsedVersions = parseVersionHeader(clientVersionHeader);
|
|
167
|
+
if (parsedVersions?.bloggyVersion) {
|
|
168
|
+
bloggyVersion = parsedVersions.bloggyVersion;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn("Failed to parse X-Bloggy-Version header:", error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check compatibility between Bloggy core and platform versions
|
|
177
|
+
// Only perform check if we have a valid Bloggy version (not "unknown")
|
|
178
|
+
let compatibility: VersionCompatibilityResult = { compatible: true };
|
|
179
|
+
|
|
180
|
+
if (bloggyVersion.version !== "unknown") {
|
|
181
|
+
// Compare Bloggy core version with platform version
|
|
182
|
+
// Both should follow semantic versioning
|
|
183
|
+
compatibility = checkVersionCompatibility(bloggyVersion, platformVersion);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const response: VersionResponse = {
|
|
187
|
+
success: true,
|
|
188
|
+
versions: {
|
|
189
|
+
bloggy: bloggyVersion,
|
|
190
|
+
platform: platformVersion,
|
|
191
|
+
},
|
|
192
|
+
compatibility,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Prepare response headers
|
|
196
|
+
const rateLimitHeaders = getRateLimitHeaders(
|
|
197
|
+
rateLimitCheck.rateLimitResult,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const responseHeaders = {
|
|
201
|
+
"X-Bloggy-Version": `platform/${platformVersion.version}`,
|
|
202
|
+
"Cache-Control": "public, max-age=300, stale-while-revalidate=60",
|
|
203
|
+
...rateLimitHeaders,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Always return 200 OK with compatibility details in response body
|
|
207
|
+
// The client should check compatibility.compatible field, not HTTP status
|
|
208
|
+
return createCorsResponse(request, response, {
|
|
209
|
+
status: 200,
|
|
210
|
+
headers: responseHeaders,
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error("Version endpoint error:", error);
|
|
214
|
+
|
|
215
|
+
const errorResponse: VersionResponse = {
|
|
216
|
+
success: false,
|
|
217
|
+
versions: {
|
|
218
|
+
bloggy: {
|
|
219
|
+
name: "unknown",
|
|
220
|
+
version: "unknown",
|
|
221
|
+
major: 0,
|
|
222
|
+
minor: 0,
|
|
223
|
+
patch: 0,
|
|
224
|
+
},
|
|
225
|
+
platform: {
|
|
226
|
+
name: "unknown",
|
|
227
|
+
version: "unknown",
|
|
228
|
+
major: 0,
|
|
229
|
+
minor: 0,
|
|
230
|
+
patch: 0,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
compatibility: {
|
|
234
|
+
compatible: false,
|
|
235
|
+
errorMessage:
|
|
236
|
+
"Internal server error while checking version compatibility",
|
|
237
|
+
},
|
|
238
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return createCorsResponse(request, errorResponse, { status: 500 });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* OPTIONS /api/blog/version
|
|
247
|
+
*
|
|
248
|
+
* Handle CORS preflight requests
|
|
249
|
+
*/
|
|
250
|
+
export async function OPTIONS(request: NextRequest): Promise<NextResponse> {
|
|
251
|
+
return handleCorsOptions(request, ["GET"], {
|
|
252
|
+
allowedHeaders: [
|
|
253
|
+
"Content-Type",
|
|
254
|
+
"X-Bloggy-Version",
|
|
255
|
+
"Authorization",
|
|
256
|
+
"X-Requested-With",
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
}
|