dineway 0.1.3 → 0.1.4

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 (191) hide show
  1. package/package.json +6 -3
  2. package/src/astro/routes/PluginRegistry.tsx +21 -0
  3. package/src/astro/routes/admin.astro +83 -0
  4. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  5. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  6. package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
  7. package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
  8. package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
  9. package/src/astro/routes/api/admin/bylines/index.ts +72 -0
  10. package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
  11. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  12. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  13. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  14. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  15. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
  16. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  17. package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
  18. package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
  19. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
  20. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
  21. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  22. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
  23. package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
  24. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  25. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
  26. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  27. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +64 -0
  28. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  29. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  30. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  31. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
  32. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  33. package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
  34. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  35. package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
  36. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  37. package/src/astro/routes/api/admin/users/index.ts +66 -0
  38. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  39. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  40. package/src/astro/routes/api/auth/invite/complete.ts +86 -0
  41. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  42. package/src/astro/routes/api/auth/logout.ts +40 -0
  43. package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
  44. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  45. package/src/astro/routes/api/auth/me.ts +60 -0
  46. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
  47. package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
  48. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  49. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  50. package/src/astro/routes/api/auth/passkey/options.ts +84 -0
  51. package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
  52. package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
  53. package/src/astro/routes/api/auth/passkey/verify.ts +68 -0
  54. package/src/astro/routes/api/auth/signup/complete.ts +87 -0
  55. package/src/astro/routes/api/auth/signup/request.ts +77 -0
  56. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  57. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +311 -0
  58. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  59. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
  60. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
  61. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
  62. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  63. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
  64. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
  65. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  66. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +105 -0
  67. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
  68. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
  69. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
  70. package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
  71. package/src/astro/routes/api/content/[collection]/index.ts +59 -0
  72. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  73. package/src/astro/routes/api/dashboard.ts +32 -0
  74. package/src/astro/routes/api/dev/emails.ts +36 -0
  75. package/src/astro/routes/api/import/probe.ts +47 -0
  76. package/src/astro/routes/api/import/wordpress/analyze.ts +531 -0
  77. package/src/astro/routes/api/import/wordpress/execute.ts +296 -0
  78. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  79. package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
  80. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
  81. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  82. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  83. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +357 -0
  84. package/src/astro/routes/api/manifest.ts +63 -0
  85. package/src/astro/routes/api/mcp.ts +124 -0
  86. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  87. package/src/astro/routes/api/media/[id].ts +145 -0
  88. package/src/astro/routes/api/media/file/[...key].ts +79 -0
  89. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
  90. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  91. package/src/astro/routes/api/media/providers/index.ts +30 -0
  92. package/src/astro/routes/api/media/upload-url.ts +137 -0
  93. package/src/astro/routes/api/media.ts +202 -0
  94. package/src/astro/routes/api/menus/[name]/items.ts +87 -0
  95. package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
  96. package/src/astro/routes/api/menus/[name].ts +65 -0
  97. package/src/astro/routes/api/menus/index.ts +47 -0
  98. package/src/astro/routes/api/oauth/authorize.ts +417 -0
  99. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  100. package/src/astro/routes/api/oauth/device/code.ts +55 -0
  101. package/src/astro/routes/api/oauth/device/token.ts +69 -0
  102. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  103. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  104. package/src/astro/routes/api/oauth/token.ts +184 -0
  105. package/src/astro/routes/api/openapi.json.ts +32 -0
  106. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
  107. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  108. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  109. package/src/astro/routes/api/redirects/[id].ts +84 -0
  110. package/src/astro/routes/api/redirects/index.ts +52 -0
  111. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  112. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
  113. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
  114. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
  115. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
  116. package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
  117. package/src/astro/routes/api/schema/collections/index.ts +47 -0
  118. package/src/astro/routes/api/schema/index.ts +109 -0
  119. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  120. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  121. package/src/astro/routes/api/search/enable.ts +64 -0
  122. package/src/astro/routes/api/search/index.ts +51 -0
  123. package/src/astro/routes/api/search/rebuild.ts +72 -0
  124. package/src/astro/routes/api/search/stats.ts +35 -0
  125. package/src/astro/routes/api/search/suggest.ts +49 -0
  126. package/src/astro/routes/api/sections/[slug].ts +84 -0
  127. package/src/astro/routes/api/sections/index.ts +52 -0
  128. package/src/astro/routes/api/settings/email.ts +150 -0
  129. package/src/astro/routes/api/settings.ts +67 -0
  130. package/src/astro/routes/api/setup/admin-verify.ts +102 -0
  131. package/src/astro/routes/api/setup/admin.ts +96 -0
  132. package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
  133. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  134. package/src/astro/routes/api/setup/index.ts +127 -0
  135. package/src/astro/routes/api/setup/status.ts +122 -0
  136. package/src/astro/routes/api/snapshot.ts +76 -0
  137. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
  138. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
  139. package/src/astro/routes/api/taxonomies/index.ts +59 -0
  140. package/src/astro/routes/api/themes/preview.ts +78 -0
  141. package/src/astro/routes/api/typegen.ts +114 -0
  142. package/src/astro/routes/api/well-known/auth.ts +69 -0
  143. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +45 -0
  144. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +38 -0
  145. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +72 -0
  146. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
  147. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
  148. package/src/astro/routes/api/widget-areas/[name].ts +87 -0
  149. package/src/astro/routes/api/widget-areas/index.ts +99 -0
  150. package/src/astro/routes/api/widget-components.ts +22 -0
  151. package/src/astro/routes/robots.txt.ts +81 -0
  152. package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
  153. package/src/astro/routes/sitemap.xml.ts +92 -0
  154. package/src/components/Break.astro +45 -0
  155. package/src/components/Button.astro +71 -0
  156. package/src/components/Buttons.astro +49 -0
  157. package/src/components/Code.astro +59 -0
  158. package/src/components/Columns.astro +59 -0
  159. package/src/components/CommentForm.astro +315 -0
  160. package/src/components/Comments.astro +232 -0
  161. package/src/components/Cover.astro +128 -0
  162. package/src/components/DinewayBodyEnd.astro +32 -0
  163. package/src/components/DinewayBodyStart.astro +32 -0
  164. package/src/components/DinewayHead.astro +53 -0
  165. package/src/components/DinewayImage.astro +178 -0
  166. package/src/components/DinewayMedia.astro +167 -0
  167. package/src/components/Embed.astro +128 -0
  168. package/src/components/File.astro +122 -0
  169. package/src/components/Gallery.astro +93 -0
  170. package/src/components/HtmlBlock.astro +33 -0
  171. package/src/components/Image.astro +178 -0
  172. package/src/components/InlineEditor.astro +27 -0
  173. package/src/components/InlinePortableTextEditor.tsx +1937 -0
  174. package/src/components/LiveSearch.astro +614 -0
  175. package/src/components/PortableText.astro +51 -0
  176. package/src/components/Pullquote.astro +51 -0
  177. package/src/components/Table.astro +108 -0
  178. package/src/components/WidgetArea.astro +22 -0
  179. package/src/components/WidgetRenderer.astro +72 -0
  180. package/src/components/index.ts +116 -0
  181. package/src/components/marks/Link.astro +31 -0
  182. package/src/components/marks/StrikeThrough.astro +7 -0
  183. package/src/components/marks/Subscript.astro +7 -0
  184. package/src/components/marks/Superscript.astro +7 -0
  185. package/src/components/marks/Underline.astro +7 -0
  186. package/src/components/widgets/Archives.astro +65 -0
  187. package/src/components/widgets/Categories.astro +35 -0
  188. package/src/components/widgets/RecentPosts.astro +51 -0
  189. package/src/components/widgets/Search.astro +18 -0
  190. package/src/components/widgets/Tags.astro +38 -0
  191. package/src/ui.ts +75 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * POST /_dineway/api/oauth/token
3
+ *
4
+ * Unified token endpoint per OAuth 2.1. Routes by `grant_type`:
5
+ * - authorization_code: Authorization Code + PKCE exchange
6
+ * - urn:ietf:params:oauth:grant-type:device_code: Device Flow
7
+ * - refresh_token: Token refresh
8
+ *
9
+ * Accepts both application/x-www-form-urlencoded (spec-standard) and
10
+ * application/json (for backwards compatibility with existing clients).
11
+ *
12
+ * This is an unauthenticated endpoint — callers present tokens/codes
13
+ * instead of session cookies.
14
+ */
15
+
16
+ import type { APIRoute } from "astro";
17
+ import { z } from "zod";
18
+
19
+ import { apiError, handleError } from "#api/error.js";
20
+ import { handleDeviceTokenExchange, handleTokenRefresh } from "#api/handlers/device-flow.js";
21
+ import { handleAuthorizationCodeExchange } from "#api/handlers/oauth-authorization.js";
22
+
23
+ export const prerender = false;
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Parse helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Parse the request body from either form-encoded or JSON.
31
+ * OAuth 2.1 mandates form-encoded, but we accept both.
32
+ */
33
+ async function parseTokenBody(request: Request): Promise<Record<string, string>> {
34
+ const contentType = request.headers.get("content-type") ?? "";
35
+
36
+ if (contentType.includes("application/x-www-form-urlencoded")) {
37
+ const text = await request.text();
38
+ const params = new URLSearchParams(text);
39
+ const result: Record<string, string> = {};
40
+ for (const [key, value] of params) {
41
+ result[key] = value;
42
+ }
43
+ return result;
44
+ }
45
+
46
+ // Fallback: try JSON
47
+ try {
48
+ const json = Object(await request.json()) as Record<string, unknown>;
49
+ const result: Record<string, string> = {};
50
+ for (const [key, value] of Object.entries(json)) {
51
+ if (typeof value === "string") {
52
+ result[key] = value;
53
+ } else if (typeof value === "number") {
54
+ result[key] = String(value);
55
+ }
56
+ }
57
+ return result;
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Schemas
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const authCodeSchema = z.object({
68
+ grant_type: z.literal("authorization_code"),
69
+ code: z.string().min(1),
70
+ redirect_uri: z.string().min(1),
71
+ client_id: z.string().min(1),
72
+ code_verifier: z.string().min(43).max(128),
73
+ resource: z.string().optional(),
74
+ });
75
+
76
+ const deviceCodeSchema = z.object({
77
+ grant_type: z.literal("urn:ietf:params:oauth:grant-type:device_code"),
78
+ device_code: z.string().min(1),
79
+ });
80
+
81
+ const refreshSchema = z.object({
82
+ grant_type: z.literal("refresh_token"),
83
+ refresh_token: z.string().min(1),
84
+ });
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Handler
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export const POST: APIRoute = async ({ request, locals }) => {
91
+ const { dineway } = locals;
92
+
93
+ if (!dineway?.db) {
94
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
95
+ }
96
+
97
+ try {
98
+ const body = await parseTokenBody(request);
99
+ const grantType = body.grant_type;
100
+
101
+ if (!grantType) {
102
+ return oauthError("invalid_request", "grant_type is required", 400);
103
+ }
104
+
105
+ switch (grantType) {
106
+ case "authorization_code": {
107
+ const parsed = authCodeSchema.safeParse(body);
108
+ if (!parsed.success) {
109
+ return oauthError("invalid_request", formatZodError(parsed.error), 400);
110
+ }
111
+
112
+ const result = await handleAuthorizationCodeExchange(dineway.db, parsed.data);
113
+ if (!result.success) {
114
+ const err = result.error ?? { code: "unknown", message: "Unknown error" };
115
+ return oauthError(err.code, err.message, 400);
116
+ }
117
+ return oauthSuccess(result.data);
118
+ }
119
+
120
+ case "urn:ietf:params:oauth:grant-type:device_code": {
121
+ const parsed = deviceCodeSchema.safeParse(body);
122
+ if (!parsed.success) {
123
+ return oauthError("invalid_request", formatZodError(parsed.error), 400);
124
+ }
125
+
126
+ const result = await handleDeviceTokenExchange(dineway.db, parsed.data);
127
+ if (!result.success) {
128
+ const err = result.error ?? { code: "unknown", message: "Unknown error" };
129
+ // RFC 8628 requires specific error format
130
+ if (result.deviceFlowError) {
131
+ return oauthError(result.deviceFlowError, err.message, 400);
132
+ }
133
+ return oauthError(err.code, err.message, 400);
134
+ }
135
+ return oauthSuccess(result.data);
136
+ }
137
+
138
+ case "refresh_token": {
139
+ const parsed = refreshSchema.safeParse(body);
140
+ if (!parsed.success) {
141
+ return oauthError("invalid_request", formatZodError(parsed.error), 400);
142
+ }
143
+
144
+ const result = await handleTokenRefresh(dineway.db, parsed.data);
145
+ if (!result.success) {
146
+ const err = result.error ?? { code: "unknown", message: "Unknown error" };
147
+ return oauthError(err.code, err.message, 400);
148
+ }
149
+ return oauthSuccess(result.data);
150
+ }
151
+
152
+ default:
153
+ return oauthError("unsupported_grant_type", `Unsupported grant_type: ${grantType}`, 400);
154
+ }
155
+ } catch (error) {
156
+ return handleError(error, "Failed to process token request", "TOKEN_ERROR");
157
+ }
158
+ };
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // OAuth response helpers (RFC 6749 §5.1 / §5.2)
162
+ // ---------------------------------------------------------------------------
163
+
164
+ /** RFC 6749 §5.1 requires Cache-Control: no-store and Pragma: no-cache on token responses */
165
+ const OAUTH_TOKEN_HEADERS: HeadersInit = {
166
+ "Content-Type": "application/json",
167
+ "Cache-Control": "no-store",
168
+ Pragma: "no-cache",
169
+ };
170
+
171
+ function oauthSuccess(data: unknown): Response {
172
+ return Response.json(data, { headers: OAUTH_TOKEN_HEADERS });
173
+ }
174
+
175
+ function oauthError(error: string, description: string, status: number): Response {
176
+ return Response.json(
177
+ { error, error_description: description },
178
+ { status, headers: OAUTH_TOKEN_HEADERS },
179
+ );
180
+ }
181
+
182
+ function formatZodError(error: z.ZodError): string {
183
+ return error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
184
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * OpenAPI spec endpoint
3
+ *
4
+ * GET /_dineway/api/openapi.json
5
+ *
6
+ * Returns the generated OpenAPI 3.1 document. The spec is generated once
7
+ * and cached for the lifetime of the process.
8
+ */
9
+
10
+ import type { APIRoute } from "astro";
11
+
12
+ import { generateOpenApiDocument } from "../../../api/openapi/index.js";
13
+
14
+ export const prerender = false;
15
+
16
+ let cachedSpec: string | null = null;
17
+
18
+ export const GET: APIRoute = async () => {
19
+ if (!cachedSpec) {
20
+ const doc = generateOpenApiDocument();
21
+ cachedSpec = JSON.stringify(doc);
22
+ }
23
+
24
+ return new Response(cachedSpec, {
25
+ status: 200,
26
+ headers: {
27
+ "Content-Type": "application/json",
28
+ "Cache-Control": "public, max-age=3600",
29
+ "Access-Control-Allow-Origin": "*",
30
+ },
31
+ });
32
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Plugin API routes - dynamic handler for plugin-defined endpoints
3
+ *
4
+ * Routes are mounted at /_dineway/api/plugins/{pluginId}/*
5
+ * Plugins register routes like "POST /do-something" which becomes
6
+ * POST /_dineway/api/plugins/{pluginId}/do-something
7
+ *
8
+ * Routes marked as `public: true` skip authentication and CSRF checks.
9
+ * Private routes (the default) require authentication and appropriate permissions.
10
+ */
11
+
12
+ import type { APIRoute } from "astro";
13
+
14
+ import { requirePerm } from "#api/authorize.js";
15
+ import { apiError, apiSuccess } from "#api/error.js";
16
+ import { requireScope } from "#auth/scopes.js";
17
+
18
+ export const prerender = false;
19
+
20
+ /**
21
+ * Handle all methods by matching against plugin-defined routes
22
+ */
23
+ const handleRequest: APIRoute = async ({ params, request, locals }) => {
24
+ const { dineway, user } = locals;
25
+ const pluginId = params.pluginId!;
26
+ const path = params.path || "";
27
+ const method = request.method.toUpperCase();
28
+
29
+ if (!dineway?.handlePluginApiRoute) {
30
+ return apiError("NOT_CONFIGURED", "Dineway is not configured", 500);
31
+ }
32
+
33
+ // Resolve route metadata to decide auth before dispatch
34
+ const routeMeta = dineway.getPluginRouteMeta(pluginId, `/${path}`);
35
+
36
+ if (!routeMeta) {
37
+ return apiError("NOT_FOUND", "Plugin route not found", 404);
38
+ }
39
+
40
+ // Public routes skip auth, CSRF, and scope checks entirely
41
+ if (!routeMeta.public) {
42
+ // Private routes require authentication and permission checks
43
+ const permission = ["GET", "HEAD", "OPTIONS"].includes(method)
44
+ ? "plugins:read"
45
+ : "plugins:manage";
46
+ const denied = requirePerm(user, permission);
47
+ if (denied) return denied;
48
+
49
+ // Token scope enforcement — plugin routes require "admin" scope.
50
+ // Session auth is implicitly full-access (requireScope returns null).
51
+ const scopeError = requireScope(locals, "admin");
52
+ if (scopeError) return scopeError;
53
+
54
+ // CSRF protection for state-changing requests on private routes.
55
+ // Plugin routes use soft auth in the middleware (user resolved but not required),
56
+ // so the middleware's CSRF check doesn't run. We enforce it here for private routes.
57
+ // Token-authed requests (which set tokenScopes) are exempt — tokens aren't
58
+ // ambient credentials like cookies.
59
+ if (
60
+ !["GET", "HEAD", "OPTIONS"].includes(method) &&
61
+ !locals.tokenScopes &&
62
+ request.headers.get("X-Dineway-Request") !== "1"
63
+ ) {
64
+ return apiError("CSRF_REJECTED", "Missing required header", 403);
65
+ }
66
+ }
67
+
68
+ const result = await dineway.handlePluginApiRoute(pluginId, method, `/${path}`, request);
69
+
70
+ if (!result.success) {
71
+ const code = result.error?.code ?? "PLUGIN_ERROR";
72
+ // Pass through messages from known plugin errors (PluginRouteError),
73
+ // but mask internal errors (unhandled exceptions) to avoid leaking
74
+ // database errors, file paths, etc. from sandboxed plugins.
75
+ const message =
76
+ code === "INTERNAL_ERROR"
77
+ ? "Plugin route error"
78
+ : (result.error?.message ?? "Plugin route error");
79
+ // PluginRouteError status is returned at the top level of the result
80
+ const status = (result as { status?: number }).status ?? (code === "NOT_FOUND" ? 404 : 400);
81
+ return apiError(code, message, status);
82
+ }
83
+
84
+ return apiSuccess(result.data);
85
+ };
86
+
87
+ // Export handlers for all HTTP methods
88
+ export const GET = handleRequest;
89
+ export const POST = handleRequest;
90
+ export const PUT = handleRequest;
91
+ export const PATCH = handleRequest;
92
+ export const DELETE = handleRequest;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 404 log list and management endpoints
3
+ *
4
+ * GET /_dineway/api/redirects/404s - List 404 log entries
5
+ * DELETE /_dineway/api/redirects/404s - Clear all 404 log entries
6
+ * POST /_dineway/api/redirects/404s - Prune 404 log entries older than date
7
+ */
8
+
9
+ import type { APIRoute } from "astro";
10
+
11
+ import { requirePerm } from "#api/authorize.js";
12
+ import { handleError, unwrapResult } from "#api/error.js";
13
+ import {
14
+ handleNotFoundClear,
15
+ handleNotFoundList,
16
+ handleNotFoundPrune,
17
+ } from "#api/handlers/redirects.js";
18
+ import { isParseError, parseBody, parseQuery } from "#api/parse.js";
19
+ import { notFoundListQuery, notFoundPruneBody } from "#api/schemas.js";
20
+
21
+ export const prerender = false;
22
+
23
+ export const GET: APIRoute = async ({ url, locals }) => {
24
+ const { dineway, user } = locals;
25
+ const db = dineway.db;
26
+
27
+ const denied = requirePerm(user, "redirects:read");
28
+ if (denied) return denied;
29
+
30
+ try {
31
+ const query = parseQuery(url, notFoundListQuery);
32
+ if (isParseError(query)) return query;
33
+
34
+ const result = await handleNotFoundList(db, query);
35
+ return unwrapResult(result);
36
+ } catch (error) {
37
+ return handleError(error, "Failed to fetch 404 log", "NOT_FOUND_LIST_ERROR");
38
+ }
39
+ };
40
+
41
+ export const DELETE: APIRoute = async ({ locals }) => {
42
+ const { dineway, user } = locals;
43
+ const db = dineway.db;
44
+
45
+ const denied = requirePerm(user, "redirects:manage");
46
+ if (denied) return denied;
47
+
48
+ try {
49
+ const result = await handleNotFoundClear(db);
50
+ return unwrapResult(result);
51
+ } catch (error) {
52
+ return handleError(error, "Failed to clear 404 log", "NOT_FOUND_CLEAR_ERROR");
53
+ }
54
+ };
55
+
56
+ export const POST: APIRoute = async ({ request, locals }) => {
57
+ const { dineway, user } = locals;
58
+ const db = dineway.db;
59
+
60
+ const denied = requirePerm(user, "redirects:manage");
61
+ if (denied) return denied;
62
+
63
+ try {
64
+ const body = await parseBody(request, notFoundPruneBody);
65
+ if (isParseError(body)) return body;
66
+
67
+ const result = await handleNotFoundPrune(db, body.olderThan);
68
+ return unwrapResult(result);
69
+ } catch (error) {
70
+ return handleError(error, "Failed to prune 404 log", "NOT_FOUND_PRUNE_ERROR");
71
+ }
72
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 404 summary endpoint
3
+ *
4
+ * GET /_dineway/api/redirects/404s/summary - Get 404 summary grouped by path
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ import { requirePerm } from "#api/authorize.js";
10
+ import { handleError, unwrapResult } from "#api/error.js";
11
+ import { handleNotFoundSummary } from "#api/handlers/redirects.js";
12
+ import { isParseError, parseQuery } from "#api/parse.js";
13
+ import { notFoundSummaryQuery } from "#api/schemas.js";
14
+
15
+ export const prerender = false;
16
+
17
+ export const GET: APIRoute = async ({ url, locals }) => {
18
+ const { dineway, user } = locals;
19
+ const db = dineway.db;
20
+
21
+ const denied = requirePerm(user, "redirects:read");
22
+ if (denied) return denied;
23
+
24
+ try {
25
+ const query = parseQuery(url, notFoundSummaryQuery);
26
+ if (isParseError(query)) return query;
27
+
28
+ const result = await handleNotFoundSummary(db, query.limit);
29
+ return unwrapResult(result);
30
+ } catch (error) {
31
+ return handleError(error, "Failed to fetch 404 summary", "NOT_FOUND_SUMMARY_ERROR");
32
+ }
33
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Redirect by ID endpoints
3
+ *
4
+ * GET /_dineway/api/redirects/:id - Get redirect
5
+ * PUT /_dineway/api/redirects/:id - Update redirect
6
+ * DELETE /_dineway/api/redirects/:id - Delete redirect
7
+ */
8
+
9
+ import type { APIRoute } from "astro";
10
+
11
+ import { requirePerm } from "#api/authorize.js";
12
+ import { apiError, handleError, unwrapResult } from "#api/error.js";
13
+ import {
14
+ handleRedirectDelete,
15
+ handleRedirectGet,
16
+ handleRedirectUpdate,
17
+ } from "#api/handlers/redirects.js";
18
+ import { isParseError, parseBody } from "#api/parse.js";
19
+ import { updateRedirectBody } from "#api/schemas.js";
20
+
21
+ export const prerender = false;
22
+
23
+ export const GET: APIRoute = async ({ params, locals }) => {
24
+ const { dineway, user } = locals;
25
+ const db = dineway.db;
26
+ const { id } = params;
27
+
28
+ const denied = requirePerm(user, "redirects:read");
29
+ if (denied) return denied;
30
+
31
+ if (!id) {
32
+ return apiError("VALIDATION_ERROR", "id is required", 400);
33
+ }
34
+
35
+ try {
36
+ const result = await handleRedirectGet(db, id);
37
+ return unwrapResult(result);
38
+ } catch (error) {
39
+ return handleError(error, "Failed to fetch redirect", "REDIRECT_GET_ERROR");
40
+ }
41
+ };
42
+
43
+ export const PUT: APIRoute = async ({ params, request, locals }) => {
44
+ const { dineway, user } = locals;
45
+ const db = dineway.db;
46
+ const { id } = params;
47
+
48
+ const denied = requirePerm(user, "redirects:manage");
49
+ if (denied) return denied;
50
+
51
+ if (!id) {
52
+ return apiError("VALIDATION_ERROR", "id is required", 400);
53
+ }
54
+
55
+ try {
56
+ const body = await parseBody(request, updateRedirectBody);
57
+ if (isParseError(body)) return body;
58
+
59
+ const result = await handleRedirectUpdate(db, id, body);
60
+ return unwrapResult(result);
61
+ } catch (error) {
62
+ return handleError(error, "Failed to update redirect", "REDIRECT_UPDATE_ERROR");
63
+ }
64
+ };
65
+
66
+ export const DELETE: APIRoute = async ({ params, locals }) => {
67
+ const { dineway, user } = locals;
68
+ const db = dineway.db;
69
+ const { id } = params;
70
+
71
+ const denied = requirePerm(user, "redirects:manage");
72
+ if (denied) return denied;
73
+
74
+ if (!id) {
75
+ return apiError("VALIDATION_ERROR", "id is required", 400);
76
+ }
77
+
78
+ try {
79
+ const result = await handleRedirectDelete(db, id);
80
+ return unwrapResult(result);
81
+ } catch (error) {
82
+ return handleError(error, "Failed to delete redirect", "REDIRECT_DELETE_ERROR");
83
+ }
84
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Redirects list and create endpoints
3
+ *
4
+ * GET /_dineway/api/redirects - List redirects (with filters)
5
+ * POST /_dineway/api/redirects - Create redirect
6
+ */
7
+
8
+ import type { APIRoute } from "astro";
9
+
10
+ import { requirePerm } from "#api/authorize.js";
11
+ import { handleError, unwrapResult } from "#api/error.js";
12
+ import { handleRedirectCreate, handleRedirectList } from "#api/handlers/redirects.js";
13
+ import { isParseError, parseBody, parseQuery } from "#api/parse.js";
14
+ import { createRedirectBody, redirectsListQuery } from "#api/schemas.js";
15
+
16
+ export const prerender = false;
17
+
18
+ export const GET: APIRoute = async ({ url, locals }) => {
19
+ const { dineway, user } = locals;
20
+ const db = dineway.db;
21
+
22
+ const denied = requirePerm(user, "redirects:read");
23
+ if (denied) return denied;
24
+
25
+ try {
26
+ const query = parseQuery(url, redirectsListQuery);
27
+ if (isParseError(query)) return query;
28
+
29
+ const result = await handleRedirectList(db, query);
30
+ return unwrapResult(result);
31
+ } catch (error) {
32
+ return handleError(error, "Failed to fetch redirects", "REDIRECT_LIST_ERROR");
33
+ }
34
+ };
35
+
36
+ export const POST: APIRoute = async ({ request, locals }) => {
37
+ const { dineway, user } = locals;
38
+ const db = dineway.db;
39
+
40
+ const denied = requirePerm(user, "redirects:manage");
41
+ if (denied) return denied;
42
+
43
+ try {
44
+ const body = await parseBody(request, createRedirectBody);
45
+ if (isParseError(body)) return body;
46
+
47
+ const result = await handleRedirectCreate(db, body);
48
+ return unwrapResult(result, 201);
49
+ } catch (error) {
50
+ return handleError(error, "Failed to create redirect", "REDIRECT_CREATE_ERROR");
51
+ }
52
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Single revision endpoint - injected by Dineway integration
3
+ *
4
+ * GET /_dineway/api/revisions/{revisionId} - Get revision details
5
+ * POST /_dineway/api/revisions/{revisionId}/restore - Restore revision
6
+ */
7
+
8
+ import type { APIRoute } from "astro";
9
+
10
+ import { requirePerm } from "#api/authorize.js";
11
+ import { apiError, unwrapResult } from "#api/error.js";
12
+
13
+ export const prerender = false;
14
+
15
+ export const GET: APIRoute = async ({ params, locals }) => {
16
+ const { dineway, user } = locals;
17
+ const revisionId = params.revisionId!;
18
+
19
+ const denied = requirePerm(user, "content:read");
20
+ if (denied) return denied;
21
+
22
+ if (!dineway?.handleRevisionGet) {
23
+ return apiError("NOT_CONFIGURED", "Dineway is not configured", 500);
24
+ }
25
+
26
+ const result = await dineway.handleRevisionGet(revisionId);
27
+
28
+ return unwrapResult(result);
29
+ };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Restore revision endpoint - injected by Dineway integration
3
+ *
4
+ * POST /_dineway/api/revisions/{revisionId}/restore - Restore revision
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ import { requireOwnerPerm } from "#api/authorize.js";
10
+ import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
11
+
12
+ export const prerender = false;
13
+
14
+ export const POST: APIRoute = async ({ params, locals }) => {
15
+ const { dineway, user } = locals;
16
+ const revisionId = params.revisionId!;
17
+
18
+ if (
19
+ !dineway?.handleRevisionRestore ||
20
+ !dineway?.handleRevisionGet ||
21
+ !dineway?.handleContentGet
22
+ ) {
23
+ return apiError("NOT_CONFIGURED", "Dineway is not configured", 500);
24
+ }
25
+
26
+ // Fetch the revision to discover which content entry it belongs to
27
+ const revision = await dineway.handleRevisionGet(revisionId);
28
+ if (!revision.success) {
29
+ return apiError(
30
+ revision.error?.code ?? "UNKNOWN_ERROR",
31
+ revision.error?.message ?? "Revision not found",
32
+ mapErrorStatus(revision.error?.code),
33
+ );
34
+ }
35
+
36
+ const collection = revision.data?.item?.collection;
37
+ const entryId = revision.data?.item?.entryId;
38
+
39
+ if (!collection || !entryId) {
40
+ return apiError("INVALID_REVISION", "Revision is missing collection or entry reference", 400);
41
+ }
42
+
43
+ // Fetch the content entry to check ownership
44
+ const existing = await dineway.handleContentGet(collection, entryId);
45
+ if (!existing.success) {
46
+ return apiError(
47
+ existing.error?.code ?? "UNKNOWN_ERROR",
48
+ existing.error?.message ?? "Content not found",
49
+ mapErrorStatus(existing.error?.code),
50
+ );
51
+ }
52
+
53
+ const authorId = existing.data?.item?.authorId ?? "";
54
+
55
+ // Check ownership: authors can only restore their own content, editors+ can restore any
56
+ const denied = requireOwnerPerm(user, authorId, "content:edit_own", "content:edit_any");
57
+ if (denied) return denied;
58
+
59
+ const result = await dineway.handleRevisionRestore(revisionId, user!.id);
60
+
61
+ return unwrapResult(result);
62
+ };