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,124 @@
1
+ /**
2
+ * PATCH/DELETE /_dineway/api/auth/passkey/[id]
3
+ *
4
+ * Rename or delete a passkey
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ export const prerender = false;
10
+
11
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
12
+
13
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
14
+ import { isParseError, parseBody } from "#api/parse.js";
15
+ import { passkeyRenameBody } from "#api/schemas.js";
16
+
17
+ interface PasskeyResponse {
18
+ id: string;
19
+ name: string | null;
20
+ deviceType: "singleDevice" | "multiDevice";
21
+ backedUp: boolean;
22
+ createdAt: string;
23
+ lastUsedAt: string;
24
+ }
25
+
26
+ /**
27
+ * PATCH - Rename a passkey
28
+ */
29
+ export const PATCH: APIRoute = async ({ params, request, locals }) => {
30
+ const { dineway, user } = locals;
31
+ const { id } = params;
32
+
33
+ if (!dineway?.db) {
34
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
35
+ }
36
+
37
+ // Require authentication
38
+ if (!user) {
39
+ return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
40
+ }
41
+
42
+ if (!id) {
43
+ return apiError("MISSING_PARAM", "Passkey ID is required", 400);
44
+ }
45
+
46
+ try {
47
+ const adapter = createKyselyAdapter(dineway.db);
48
+
49
+ // Get the credential and verify ownership
50
+ const credential = await adapter.getCredentialById(id);
51
+
52
+ if (!credential || credential.userId !== user.id) {
53
+ return apiError("NOT_FOUND", "Passkey not found", 404);
54
+ }
55
+
56
+ // Parse request body
57
+ const body = await parseBody(request, passkeyRenameBody);
58
+ if (isParseError(body)) return body;
59
+
60
+ // Update the name
61
+ const trimmedName = body.name.trim() || null;
62
+ await adapter.updateCredentialName(id, trimmedName);
63
+
64
+ // Return updated passkey info
65
+ const passkey: PasskeyResponse = {
66
+ id: credential.id,
67
+ name: trimmedName,
68
+ deviceType: credential.deviceType,
69
+ backedUp: credential.backedUp,
70
+ createdAt: credential.createdAt.toISOString(),
71
+ lastUsedAt: credential.lastUsedAt.toISOString(),
72
+ };
73
+
74
+ return apiSuccess({ passkey });
75
+ } catch (error) {
76
+ return handleError(error, "Failed to rename passkey", "PASSKEY_RENAME_ERROR");
77
+ }
78
+ };
79
+
80
+ /**
81
+ * DELETE - Remove a passkey
82
+ */
83
+ export const DELETE: APIRoute = async ({ params, locals }) => {
84
+ const { dineway, user } = locals;
85
+ const { id } = params;
86
+
87
+ if (!dineway?.db) {
88
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
89
+ }
90
+
91
+ // Require authentication
92
+ if (!user) {
93
+ return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
94
+ }
95
+
96
+ if (!id) {
97
+ return apiError("MISSING_PARAM", "Passkey ID is required", 400);
98
+ }
99
+
100
+ try {
101
+ const adapter = createKyselyAdapter(dineway.db);
102
+
103
+ // Get the credential and verify ownership
104
+ const credential = await adapter.getCredentialById(id);
105
+
106
+ if (!credential || credential.userId !== user.id) {
107
+ return apiError("NOT_FOUND", "Passkey not found", 404);
108
+ }
109
+
110
+ // Check that this isn't the last passkey
111
+ const count = await adapter.countCredentialsByUserId(user.id);
112
+
113
+ if (count <= 1) {
114
+ return apiError("LAST_PASSKEY", "Cannot remove your last passkey", 400);
115
+ }
116
+
117
+ // Delete the passkey
118
+ await adapter.deleteCredential(id);
119
+
120
+ return apiSuccess({ success: true });
121
+ } catch (error) {
122
+ return handleError(error, "Failed to delete passkey", "PASSKEY_DELETE_ERROR");
123
+ }
124
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * GET /_dineway/api/auth/passkey
3
+ *
4
+ * List all passkeys for the authenticated user
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ export const prerender = false;
10
+
11
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
12
+
13
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
14
+
15
+ interface PasskeyResponse {
16
+ id: string;
17
+ name: string | null;
18
+ deviceType: "singleDevice" | "multiDevice";
19
+ backedUp: boolean;
20
+ createdAt: string;
21
+ lastUsedAt: string;
22
+ }
23
+
24
+ export const GET: APIRoute = async ({ locals }) => {
25
+ const { dineway, user } = locals;
26
+
27
+ if (!dineway?.db) {
28
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
29
+ }
30
+
31
+ // Require authentication
32
+ if (!user) {
33
+ return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
34
+ }
35
+
36
+ try {
37
+ const adapter = createKyselyAdapter(dineway.db);
38
+ const credentials = await adapter.getCredentialsByUserId(user.id);
39
+
40
+ // Map to public response format (exclude sensitive fields)
41
+ const passkeys: PasskeyResponse[] = credentials.map((cred) => ({
42
+ id: cred.id,
43
+ name: cred.name,
44
+ deviceType: cred.deviceType,
45
+ backedUp: cred.backedUp,
46
+ createdAt: cred.createdAt.toISOString(),
47
+ lastUsedAt: cred.lastUsedAt.toISOString(),
48
+ }));
49
+
50
+ return apiSuccess({ items: passkeys });
51
+ } catch (error) {
52
+ return handleError(error, "Failed to list passkeys", "PASSKEY_LIST_ERROR");
53
+ }
54
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * POST /_dineway/api/auth/passkey/options
3
+ *
4
+ * Get authentication options for passkey login.
5
+ *
6
+ * Rate limited: 10 requests per minute per IP.
7
+ */
8
+
9
+ import type { APIRoute } from "astro";
10
+
11
+ export const prerender = false;
12
+
13
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
14
+ import { generateAuthenticationOptions } from "@dineway-ai/auth/passkey";
15
+
16
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
17
+ import { isParseError, parseOptionalBody } from "#api/parse.js";
18
+ import { getPublicOrigin } from "#api/public-url.js";
19
+ import { passkeyOptionsBody } from "#api/schemas.js";
20
+ import { createChallengeStore, cleanupExpiredChallenges } from "#auth/challenge-store.js";
21
+ import { getPasskeyConfig } from "#auth/passkey-config.js";
22
+ import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
23
+ import { OptionsRepository } from "#db/repositories/options.js";
24
+
25
+ export const POST: APIRoute = async ({ request, locals }) => {
26
+ const { dineway } = locals;
27
+
28
+ if (!dineway?.db) {
29
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
30
+ }
31
+
32
+ try {
33
+ // Fire-and-forget cleanup of expired challenges -- prevents accumulation
34
+ void cleanupExpiredChallenges(dineway.db).catch(() => {});
35
+
36
+ // Parse body before rate limiting so malformed requests don't consume slots
37
+ const body = await parseOptionalBody(request, passkeyOptionsBody, {});
38
+ if (isParseError(body)) return body;
39
+
40
+ // Rate limit: 10 requests per 60 seconds per IP
41
+ const ip = getClientIp(request);
42
+ const rateLimit = await checkRateLimit(dineway.db, ip, "passkey/options", 10, 60);
43
+ if (!rateLimit.allowed) {
44
+ return rateLimitResponse(60);
45
+ }
46
+
47
+ const adapter = createKyselyAdapter(dineway.db);
48
+
49
+ // Get credentials to allow
50
+ let credentials: Awaited<ReturnType<typeof adapter.getCredentialsByUserId>> = [];
51
+
52
+ if (body.email) {
53
+ // Get credentials for specific user
54
+ const user = await adapter.getUserByEmail(body.email);
55
+ if (user) {
56
+ credentials = await adapter.getCredentialsByUserId(user.id);
57
+ }
58
+ // Don't reveal if user exists - just return empty allowCredentials
59
+ }
60
+ // If no email provided, allowCredentials will be undefined (allow any discoverable credential)
61
+
62
+ // Get passkey config
63
+ const url = new URL(request.url);
64
+ const options = new OptionsRepository(dineway.db);
65
+ const siteName = (await options.get<string>("dineway:site_title")) ?? undefined;
66
+ const siteUrl = getPublicOrigin(url, dineway?.config);
67
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
68
+
69
+ // Generate authentication options
70
+ const challengeStore = createChallengeStore(dineway.db);
71
+ const authOptions = await generateAuthenticationOptions(
72
+ passkeyConfig,
73
+ credentials,
74
+ challengeStore,
75
+ );
76
+
77
+ return apiSuccess({
78
+ success: true,
79
+ options: authOptions,
80
+ });
81
+ } catch (error) {
82
+ return handleError(error, "Failed to generate passkey options", "PASSKEY_OPTIONS_ERROR");
83
+ }
84
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * POST /_dineway/api/auth/passkey/register/options
3
+ *
4
+ * Get WebAuthn registration options for adding a new passkey (authenticated user)
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ export const prerender = false;
10
+
11
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
12
+ import { generateRegistrationOptions } from "@dineway-ai/auth/passkey";
13
+
14
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
15
+ import { isParseError, parseOptionalBody } from "#api/parse.js";
16
+ import { getPublicOrigin } from "#api/public-url.js";
17
+ import { passkeyRegisterOptionsBody } from "#api/schemas.js";
18
+ import { createChallengeStore } from "#auth/challenge-store.js";
19
+ import { getPasskeyConfig } from "#auth/passkey-config.js";
20
+ import { OptionsRepository } from "#db/repositories/options.js";
21
+
22
+ const MAX_PASSKEYS = 10;
23
+
24
+ export const POST: APIRoute = async ({ request, locals }) => {
25
+ const { dineway, user } = locals;
26
+
27
+ if (!dineway?.db) {
28
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
29
+ }
30
+
31
+ // Require authentication
32
+ if (!user) {
33
+ return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
34
+ }
35
+
36
+ try {
37
+ const adapter = createKyselyAdapter(dineway.db);
38
+
39
+ // Check passkey limit
40
+ const count = await adapter.countCredentialsByUserId(user.id);
41
+ if (count >= MAX_PASSKEYS) {
42
+ return apiError("PASSKEY_LIMIT", `Maximum of ${MAX_PASSKEYS} passkeys allowed`, 400);
43
+ }
44
+
45
+ // Parse optional name from request
46
+ const body = await parseOptionalBody(request, passkeyRegisterOptionsBody, {});
47
+ if (isParseError(body)) return body;
48
+
49
+ // Get existing credentials for excludeCredentials
50
+ const existingCredentials = await adapter.getCredentialsByUserId(user.id);
51
+
52
+ // Get passkey config
53
+ const url = new URL(request.url);
54
+ const optionsRepo = new OptionsRepository(dineway.db);
55
+ const siteName = (await optionsRepo.get<string>("dineway:site_title")) ?? undefined;
56
+ const siteUrl = getPublicOrigin(url, dineway?.config);
57
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
58
+
59
+ // Generate registration options
60
+ const challengeStore = createChallengeStore(dineway.db);
61
+ const registrationOptions = await generateRegistrationOptions(
62
+ passkeyConfig,
63
+ { id: user.id, email: user.email, name: user.name },
64
+ existingCredentials,
65
+ challengeStore,
66
+ );
67
+
68
+ // Store the passkey name in the challenge metadata if provided
69
+ // We'll retrieve it during verification
70
+ if (body.name) {
71
+ // Store name with challenge for later retrieval
72
+ // The challenge store will need this when verifying
73
+ await optionsRepo.set(`dineway:passkey_pending:${user.id}`, {
74
+ name: body.name,
75
+ });
76
+ }
77
+
78
+ return apiSuccess({
79
+ options: registrationOptions,
80
+ });
81
+ } catch (error) {
82
+ return handleError(
83
+ error,
84
+ "Failed to generate registration options",
85
+ "PASSKEY_REGISTER_OPTIONS_ERROR",
86
+ );
87
+ }
88
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * POST /_dineway/api/auth/passkey/register/verify
3
+ *
4
+ * Verify and store a new passkey credential (authenticated user)
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ export const prerender = false;
10
+
11
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
12
+ import { verifyRegistrationResponse, registerPasskey } from "@dineway-ai/auth/passkey";
13
+
14
+ import { apiError, apiSuccess } from "#api/error.js";
15
+ import { isParseError, parseBody } from "#api/parse.js";
16
+ import { getPublicOrigin } from "#api/public-url.js";
17
+ import { passkeyRegisterVerifyBody } from "#api/schemas.js";
18
+ import { createChallengeStore } from "#auth/challenge-store.js";
19
+ import { getPasskeyConfig } from "#auth/passkey-config.js";
20
+ import { OptionsRepository } from "#db/repositories/options.js";
21
+
22
+ const MAX_PASSKEYS = 10;
23
+
24
+ interface PasskeyResponse {
25
+ id: string;
26
+ name: string | null;
27
+ deviceType: "singleDevice" | "multiDevice";
28
+ backedUp: boolean;
29
+ createdAt: string;
30
+ lastUsedAt: string;
31
+ }
32
+
33
+ export const POST: APIRoute = async ({ request, locals }) => {
34
+ const { dineway, user } = locals;
35
+
36
+ if (!dineway?.db) {
37
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
38
+ }
39
+
40
+ // Require authentication
41
+ if (!user) {
42
+ return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
43
+ }
44
+
45
+ try {
46
+ const adapter = createKyselyAdapter(dineway.db);
47
+
48
+ // Check passkey limit again (in case of concurrent requests)
49
+ const count = await adapter.countCredentialsByUserId(user.id);
50
+ if (count >= MAX_PASSKEYS) {
51
+ return apiError("PASSKEY_LIMIT", `Maximum of ${MAX_PASSKEYS} passkeys allowed`, 400);
52
+ }
53
+
54
+ // Parse request body
55
+ const body = await parseBody(request, passkeyRegisterVerifyBody);
56
+ if (isParseError(body)) return body;
57
+
58
+ // Get passkey config
59
+ const url = new URL(request.url);
60
+ const optionsRepo = new OptionsRepository(dineway.db);
61
+ const siteName = (await optionsRepo.get<string>("dineway:site_title")) ?? undefined;
62
+ const siteUrl = getPublicOrigin(url, dineway?.config);
63
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
64
+
65
+ // Verify the registration response
66
+ const challengeStore = createChallengeStore(dineway.db);
67
+ const verified = await verifyRegistrationResponse(
68
+ passkeyConfig,
69
+ body.credential,
70
+ challengeStore,
71
+ );
72
+
73
+ // Get passkey name - prefer body.name, then check stored pending name
74
+ let passKeyName: string | undefined = body.name ?? undefined;
75
+ if (!passKeyName) {
76
+ const pending = await optionsRepo.get<{ name?: string }>(
77
+ `dineway:passkey_pending:${user.id}`,
78
+ );
79
+ if (pending?.name) {
80
+ passKeyName = pending.name;
81
+ }
82
+ }
83
+
84
+ // Clean up pending state
85
+ await optionsRepo.delete(`dineway:passkey_pending:${user.id}`);
86
+
87
+ // Register the passkey
88
+ const credential = await registerPasskey(adapter, user.id, verified, passKeyName);
89
+
90
+ // Return the new passkey info
91
+ const passkey: PasskeyResponse = {
92
+ id: credential.id,
93
+ name: credential.name,
94
+ deviceType: credential.deviceType,
95
+ backedUp: credential.backedUp,
96
+ createdAt: credential.createdAt.toISOString(),
97
+ lastUsedAt: credential.lastUsedAt.toISOString(),
98
+ };
99
+
100
+ return apiSuccess({ passkey });
101
+ } catch (error) {
102
+ console.error("Passkey registration verify error:", error);
103
+
104
+ // Handle specific errors
105
+ const message = error instanceof Error ? error.message : "";
106
+
107
+ // Check for duplicate credential error
108
+ if (message.includes("credential_exists") || message.includes("already")) {
109
+ return apiError("CREDENTIAL_EXISTS", "This passkey is already registered", 400);
110
+ }
111
+
112
+ // Check for challenge errors
113
+ if (message.includes("challenge") || message.includes("expired")) {
114
+ return apiError("CHALLENGE_EXPIRED", "Registration expired. Please try again.", 400);
115
+ }
116
+
117
+ return apiError("PASSKEY_REGISTER_ERROR", "Registration failed", 500);
118
+ }
119
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * POST /_dineway/api/auth/passkey/verify
3
+ *
4
+ * Verify a passkey authentication and create a session
5
+ */
6
+
7
+ import type { APIRoute } from "astro";
8
+
9
+ export const prerender = false;
10
+
11
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
12
+ import { authenticateWithPasskey } from "@dineway-ai/auth/passkey";
13
+
14
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
15
+ import { isParseError, parseBody } from "#api/parse.js";
16
+ import { getPublicOrigin } from "#api/public-url.js";
17
+ import { passkeyVerifyBody } from "#api/schemas.js";
18
+ import { createChallengeStore } from "#auth/challenge-store.js";
19
+ import { getPasskeyConfig } from "#auth/passkey-config.js";
20
+ import { OptionsRepository } from "#db/repositories/options.js";
21
+
22
+ export const POST: APIRoute = async ({ request, locals, session }) => {
23
+ const { dineway } = locals;
24
+
25
+ if (!dineway?.db) {
26
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
27
+ }
28
+
29
+ try {
30
+ const body = await parseBody(request, passkeyVerifyBody);
31
+ if (isParseError(body)) return body;
32
+
33
+ // Get passkey config
34
+ const url = new URL(request.url);
35
+ const options = new OptionsRepository(dineway.db);
36
+ const siteName = (await options.get<string>("dineway:site_title")) ?? undefined;
37
+ const siteUrl = getPublicOrigin(url, dineway?.config);
38
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
39
+
40
+ // Authenticate with passkey
41
+ const adapter = createKyselyAdapter(dineway.db);
42
+ const challengeStore = createChallengeStore(dineway.db);
43
+
44
+ const user = await authenticateWithPasskey(
45
+ passkeyConfig,
46
+ adapter,
47
+ body.credential,
48
+ challengeStore,
49
+ );
50
+
51
+ // Create session
52
+ if (session) {
53
+ session.set("user", { id: user.id });
54
+ }
55
+
56
+ return apiSuccess({
57
+ success: true,
58
+ user: {
59
+ id: user.id,
60
+ email: user.email,
61
+ name: user.name,
62
+ role: user.role,
63
+ },
64
+ });
65
+ } catch (error) {
66
+ return handleError(error, "Authentication failed", "PASSKEY_VERIFY_ERROR");
67
+ }
68
+ };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * POST /_dineway/api/auth/signup/complete
3
+ *
4
+ * Complete self-signup by registering a passkey for the new user.
5
+ * This creates the user account and establishes a session.
6
+ */
7
+
8
+ import type { APIRoute } from "astro";
9
+
10
+ export const prerender = false;
11
+
12
+ import { completeSignup, SignupError } from "@dineway-ai/auth";
13
+ import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
14
+ import { verifyRegistrationResponse, registerPasskey } from "@dineway-ai/auth/passkey";
15
+
16
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
17
+ import { isParseError, parseBody } from "#api/parse.js";
18
+ import { getPublicOrigin } from "#api/public-url.js";
19
+ import { signupCompleteBody } from "#api/schemas.js";
20
+ import { createChallengeStore } from "#auth/challenge-store.js";
21
+ import { getPasskeyConfig } from "#auth/passkey-config.js";
22
+ import { OptionsRepository } from "#db/repositories/options.js";
23
+
24
+ export const POST: APIRoute = async ({ request, locals, session }) => {
25
+ const { dineway } = locals;
26
+
27
+ if (!dineway?.db) {
28
+ return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
29
+ }
30
+
31
+ try {
32
+ const body = await parseBody(request, signupCompleteBody);
33
+ if (isParseError(body)) return body;
34
+
35
+ const adapter = createKyselyAdapter(dineway.db);
36
+
37
+ // Get passkey config
38
+ const url = new URL(request.url);
39
+ const options = new OptionsRepository(dineway.db);
40
+ const siteName = (await options.get<string>("dineway:site_title")) ?? undefined;
41
+ const siteUrl = getPublicOrigin(url, dineway?.config);
42
+ const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
43
+
44
+ // Verify the passkey registration response
45
+ const challengeStore = createChallengeStore(dineway.db);
46
+ const verified = await verifyRegistrationResponse(
47
+ passkeyConfig,
48
+ body.credential,
49
+ challengeStore,
50
+ );
51
+
52
+ // Complete the signup - creates the user
53
+ const user = await completeSignup(adapter, body.token, {
54
+ name: body.name,
55
+ });
56
+
57
+ // Register the passkey for the new user
58
+ await registerPasskey(adapter, user.id, verified, "Initial passkey");
59
+
60
+ // Create session
61
+ if (session) {
62
+ session.set("user", { id: user.id });
63
+ }
64
+
65
+ return apiSuccess({
66
+ success: true,
67
+ user: {
68
+ id: user.id,
69
+ email: user.email,
70
+ name: user.name,
71
+ role: user.role,
72
+ },
73
+ });
74
+ } catch (error) {
75
+ if (error instanceof SignupError) {
76
+ const statusMap: Record<string, number> = {
77
+ invalid_token: 404,
78
+ token_expired: 410,
79
+ user_exists: 409,
80
+ domain_not_allowed: 403,
81
+ };
82
+ return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
83
+ }
84
+
85
+ return handleError(error, "Failed to complete signup", "SIGNUP_COMPLETE_ERROR");
86
+ }
87
+ };