dineway 0.1.3 → 0.1.5
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/README.md +6 -3
- package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
- package/dist/astro/index.d.mts +18 -9
- package/dist/astro/index.mjs +238 -16
- package/dist/astro/middleware/auth.d.mts +16 -5
- package/dist/astro/middleware/auth.mjs +74 -37
- package/dist/astro/middleware/redirect.mjs +24 -8
- package/dist/astro/middleware/request-context.mjs +18 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +411 -169
- package/dist/astro/types.d.mts +25 -8
- package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
- package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
- package/dist/cache-BdSY-gQN.mjs +42 -0
- package/dist/chunks--4F8ddV4.mjs +18 -0
- package/dist/cli/index.mjs +935 -15
- package/dist/client/external-auth-headers.d.mts +1 -1
- package/dist/client/index.d.mts +11 -3
- package/dist/client/index.mjs +4 -3
- package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
- package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
- package/dist/database/instrumentation.d.mts +34 -0
- package/dist/database/instrumentation.mjs +53 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.mjs +11 -5
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +7 -1
- package/dist/db-errors-CEqD7qH9.mjs +23 -0
- package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
- package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
- package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +24 -22
- package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/page/index.d.mts +10 -2
- package/dist/page/index.mjs +22 -1
- package/dist/patterns-CrCYkMBb.mjs +92 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
- package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
- package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
- package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
- package/dist/request-cache-Dk5qPSOx.mjs +66 -0
- package/dist/request-context.d.mts +4 -16
- package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
- package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BNruJHDL.mjs → search-ByRGV2pq.mjs} +570 -424
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +11 -10
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +11 -3
- package/dist/storage/s3.mjs +78 -15
- package/dist/taxonomies-1s5PaS_8.mjs +266 -0
- package/dist/transaction-Cn2rjY78.mjs +27 -0
- package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
- package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
- package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
- package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
- package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
- package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
- package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
- package/dist/version-BKXPsfmJ.mjs +6 -0
- package/package.json +53 -39
- package/src/astro/routes/PluginRegistry.tsx +21 -0
- package/src/astro/routes/admin.astro +99 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +44 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +90 -0
- package/src/astro/routes/api/admin/briefing.ts +76 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +90 -0
- package/src/astro/routes/api/admin/bylines/index.ts +74 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
- package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
- package/src/astro/routes/api/admin/context/diff.ts +35 -0
- package/src/astro/routes/api/admin/context/index.ts +69 -0
- package/src/astro/routes/api/admin/context/stale.ts +35 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
- package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +132 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +137 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +95 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +98 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +154 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +135 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
- package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +72 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +166 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +86 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +90 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +85 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +72 -0
- package/src/astro/routes/api/auth/signup/complete.ts +87 -0
- package/src/astro/routes/api/auth/signup/request.ts +89 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +310 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +68 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +77 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +42 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +100 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +64 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +129 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +143 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +50 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +69 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +173 -0
- package/src/astro/routes/api/content/[collection]/index.ts +103 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/health.ts +54 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +523 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +330 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +212 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +425 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +399 -0
- package/src/astro/routes/api/manifest.ts +75 -0
- package/src/astro/routes/api/mcp.ts +125 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[...key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +91 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +146 -0
- package/src/astro/routes/api/media.ts +204 -0
- package/src/astro/routes/api/menus/[name]/items.ts +206 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +79 -0
- package/src/astro/routes/api/menus/[name].ts +145 -0
- package/src/astro/routes/api/menus/index.ts +91 -0
- package/src/astro/routes/api/oauth/authorize.ts +430 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +56 -0
- package/src/astro/routes/api/oauth/device/token.ts +70 -0
- package/src/astro/routes/api/oauth/register.ts +182 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +195 -0
- package/src/astro/routes/api/openapi.json.ts +33 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +109 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +183 -0
- package/src/astro/routes/api/redirects/index.ts +100 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +104 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +67 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +45 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +107 -0
- package/src/astro/routes/api/schema/collections/index.ts +61 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +52 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +50 -0
- package/src/astro/routes/api/sections/[slug].ts +203 -0
- package/src/astro/routes/api/sections/index.ts +107 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +116 -0
- package/src/astro/routes/api/setup/admin-verify.ts +122 -0
- package/src/astro/routes/api/setup/admin.ts +104 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +128 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +76 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +232 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +131 -0
- package/src/astro/routes/api/taxonomies/index.ts +114 -0
- package/src/astro/routes/api/themes/preview.ts +78 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +71 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +48 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +39 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +114 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +213 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +126 -0
- package/src/astro/routes/api/widget-areas/[name].ts +135 -0
- package/src/astro/routes/api/widget-areas/index.ts +149 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +81 -0
- package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
- package/src/astro/routes/sitemap.xml.ts +92 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/DinewayBodyEnd.astro +32 -0
- package/src/components/DinewayBodyStart.astro +32 -0
- package/src/components/DinewayHead.astro +61 -0
- package/src/components/DinewayImage.astro +178 -0
- package/src/components/DinewayMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1937 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +135 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +106 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/marks.ts +19 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/ui.ts +75 -0
- package/LICENSE +0 -9
- /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
- /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
- /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
- /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
- /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
- /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
- /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
- /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
- /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
- /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
|
@@ -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,72 @@
|
|
|
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, PasskeyAuthenticationError } 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
|
+
if (error instanceof PasskeyAuthenticationError) {
|
|
67
|
+
return apiError("UNAUTHORIZED", "Authentication failed", 401);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return handleError(error, "Authentication failed", "PASSKEY_VERIFY_ERROR");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /_dineway/api/auth/signup/request
|
|
3
|
+
*
|
|
4
|
+
* Request self-signup. Sends verification email if domain is allowed.
|
|
5
|
+
* Always returns 200 to prevent email enumeration.
|
|
6
|
+
*
|
|
7
|
+
* Rate limited: 3 requests per 5 minutes per IP. Mirrors magic-link/send.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
14
|
+
import { requestSignup } from "@dineway-ai/auth";
|
|
15
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
16
|
+
|
|
17
|
+
import { apiError, apiSuccess } from "#api/error.js";
|
|
18
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
|
+
import { signupRequestBody } from "#api/schemas.js";
|
|
20
|
+
import { getSiteBaseUrl } from "#api/site-url.js";
|
|
21
|
+
import { checkRateLimit, getClientIp } from "#auth/rate-limit.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "#auth/trusted-proxy.js";
|
|
23
|
+
import { OptionsRepository } from "#db/repositories/options.js";
|
|
24
|
+
|
|
25
|
+
const GENERIC_SUCCESS = {
|
|
26
|
+
success: true,
|
|
27
|
+
message: "If your email domain is allowed, you'll receive a verification email.",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
31
|
+
const { dineway } = locals;
|
|
32
|
+
|
|
33
|
+
if (!dineway?.db) {
|
|
34
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if email pipeline is available
|
|
38
|
+
if (!dineway.email?.isAvailable()) {
|
|
39
|
+
return apiError(
|
|
40
|
+
"EMAIL_NOT_CONFIGURED",
|
|
41
|
+
"Email not configured. Self-signup is unavailable.",
|
|
42
|
+
503,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const body = await parseBody(request, signupRequestBody);
|
|
48
|
+
if (isParseError(body)) return body;
|
|
49
|
+
|
|
50
|
+
// Rate limit: 3 requests per 300 seconds per IP. Return the same
|
|
51
|
+
// response as the normal path so callers cannot observe the limit.
|
|
52
|
+
const trustedHeaders = getTrustedProxyHeaders(dineway.config);
|
|
53
|
+
const ip = getClientIp(request, trustedHeaders);
|
|
54
|
+
const rateLimit = await checkRateLimit(dineway.db, ip, "signup/request", 3, 300);
|
|
55
|
+
if (!rateLimit.allowed) {
|
|
56
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
60
|
+
|
|
61
|
+
// Get site config for signup email
|
|
62
|
+
const options = new OptionsRepository(dineway.db);
|
|
63
|
+
const siteName = (await options.get<string>("dineway:site_title")) || "Dineway";
|
|
64
|
+
|
|
65
|
+
// Use stored site URL to prevent Host header spoofing in signup emails
|
|
66
|
+
const baseUrl = await getSiteBaseUrl(dineway.db, request);
|
|
67
|
+
|
|
68
|
+
// Request signup - this handles all checks internally and fails silently
|
|
69
|
+
// if domain not allowed or user exists (to prevent enumeration)
|
|
70
|
+
await requestSignup(
|
|
71
|
+
{
|
|
72
|
+
baseUrl,
|
|
73
|
+
siteName,
|
|
74
|
+
email: (message) => dineway.email!.send(message, "system"),
|
|
75
|
+
},
|
|
76
|
+
adapter,
|
|
77
|
+
body.email.toLowerCase().trim(),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Always return success to prevent email enumeration
|
|
81
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error("Signup request error:", error);
|
|
84
|
+
|
|
85
|
+
// Don't reveal internal errors - just return generic success
|
|
86
|
+
// to prevent information leakage
|
|
87
|
+
return apiSuccess(GENERIC_SUCCESS);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /_dineway/api/auth/signup/verify
|
|
3
|
+
*
|
|
4
|
+
* Validate a signup verification token (called when user clicks email link).
|
|
5
|
+
* Returns the email and role for the UI to display.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIRoute } from "astro";
|
|
9
|
+
|
|
10
|
+
export const prerender = false;
|
|
11
|
+
|
|
12
|
+
import { validateSignupToken, SignupError, roleFromLevel } from "@dineway-ai/auth";
|
|
13
|
+
import { createKyselyAdapter } from "@dineway-ai/auth/adapters/kysely";
|
|
14
|
+
|
|
15
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
16
|
+
|
|
17
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
18
|
+
const { dineway } = locals;
|
|
19
|
+
|
|
20
|
+
if (!dineway?.db) {
|
|
21
|
+
return apiError("NOT_CONFIGURED", "Dineway is not initialized", 500);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const token = url.searchParams.get("token");
|
|
25
|
+
|
|
26
|
+
if (!token) {
|
|
27
|
+
return apiError("MISSING_PARAM", "Token is required", 400);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const adapter = createKyselyAdapter(dineway.db);
|
|
32
|
+
const result = await validateSignupToken(adapter, token);
|
|
33
|
+
|
|
34
|
+
return apiSuccess({
|
|
35
|
+
success: true,
|
|
36
|
+
email: result.email,
|
|
37
|
+
role: result.role,
|
|
38
|
+
roleName: roleFromLevel(result.role),
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof SignupError) {
|
|
42
|
+
const statusMap: Record<string, number> = {
|
|
43
|
+
invalid_token: 404,
|
|
44
|
+
token_expired: 410,
|
|
45
|
+
user_exists: 409,
|
|
46
|
+
domain_not_allowed: 403,
|
|
47
|
+
};
|
|
48
|
+
return apiError(error.code.toUpperCase(), error.message, statusMap[error.code] ?? 400);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return handleError(error, "Failed to validate signup token", "SIGNUP_VERIFY_ERROR");
|
|
52
|
+
}
|
|
53
|
+
};
|