Conversation
…rename feat(nuxt)!: rename runtime configuration key
## [13.0.0-beta.1](12.15.1...13.0.0-beta.1) (2025-07-08) ### ⚠ BREAKING CHANGES * **nuxt:** rename runtime configuration key ### Features * **nuxt:** rename runtime configuration key ([325238f](325238f))
Resolves #1720
## [13.0.0-beta.2](13.0.0-beta.1...13.0.0-beta.2) (2025-12-02) ### Features * **account:** rework creation success page ([7d51369](7d51369)) * **account:** show profile picture in menu ([2d454fd](2d454fd)) * **account:** unify detail buttons ([09ec5e6](09ec5e6)) * **app:** disable text selection ([df7b53d](df7b53d)) * **components:** double menu bottom padding for ios ([54a81f3](54a81f3)) * **components:** replace toast with sonner ([500e1bd](500e1bd)) * **components:** update error design ([07e75d7](07e75d7)) * **composables:** add alert error ([3119c37](3119c37)) * **composables:** centralize urql result handling ([cec435e](cec435e)) * **content:** remove dispute resolution section from imprint ([ce18763](ce18763)) * **docs:** add redirection to app stores ([4369d28](4369d28)) * **docs:** do not redirect when app is installed ([c82b422](c82b422)) * **event:** show link option no matter the attendance type ([1c28141](1c28141)) * **head:** test disabled scaling ([e8730bd](e8730bd)) * **i18n:** uppercase global site name ([bd96c08](bd96c08)) * **notification:** replace `moment-timezone` with `Intl` ([8c88d57](8c88d57)) * **pages:** drop marketing ([c67f333](c67f333)) * **recommendation:** add ([19b3f5b](19b3f5b)) * **recommendation:** show up to 10 ([ec9678d](ec9678d)) * **session:** allow to clear api client data ([278b484](278b484)) * **session:** redirect to root on sign out ([295bc31](295bc31)) * **tiptap:** support justified alignment ([f17ca9c](f17ca9c)) ### Bug Fixes * **account:** unref date for registration ([e35cb03](e35cb03)) * **attendance:** add missing hint to camera error ([d3b7233](d3b7233)) * **attendance:** don't log error twice ([d2e12d1](d2e12d1)) * **components:** add more padding for bottom menu on ios ([#2071](#2071)) ([6126590](6126590)) * **components:** align dashboard menu item name ([11c7702](11c7702)) * **components:** correct time zone display ([33d1583](33d1583)) * **early-bird:** correct button label ([f980ed3](f980ed3)) * **event:** correct maximum guest count fetching ([95c6528](95c6528)) * **event:** relax ical data requirements ([2ccf25b](2ccf25b)) * **event:** show match only on recommendation ([24a8c3c](24a8c3c)) * **event:** update card style ([69a8170](69a8170)) * **gql:** correct variable computation ([42a5885](42a5885)) * **legal-term:** correct loading ([65e9c80](65e9c80)) * **modules:** correct pwa scope extensions ([3b5d072](3b5d072)) * **notification:** correct enum capitalization for invitation visibility ([6b7c4bd](6b7c4bd)) * **notification:** only attach successfully fetched ical data ([ba52f0b](ba52f0b)) * **notification:** skip empty payloads ([2f34b68](2f34b68)) * **preference:** await asynchronous data at initialization ([bb3422b](bb3422b)) * **pwa:** set id ([cf486d9](cf486d9)) * **recommendation:** don't show section if content is empty ([ce65f88](ce65f88)) * **recommendation:** show just one for now ([77aaacf](77aaacf)) * schedule release ([b695e10](b695e10)) * schedule release ([67695d9](67695d9)) * schedule release ([f8f5a6c](f8f5a6c)) * schedule release ([e273b95](e273b95)) * **security:** apply camera permission globally ([88043a3](88043a3)) * **session:** move developer information button up ([9e1bc5c](9e1bc5c)) * **session:** redirect from root to dashboard when signed in ([41fdbfe](41fdbfe)) * **upload:** unref uppy ([e7dd28e](e7dd28e)) ### Performance Improvements * **aws:** complete migration to ses client v2 ([1ed17ac](1ed17ac)) * **docker:** add volume for development node modules ([9ec8a43](9ec8a43)) * **docker:** cache playwright node modules separately ([7187cf0](7187cf0)) * **docker:** use cache mounts ([ef0a6db](ef0a6db)) * **event:** replace `dayjs` with `Intl` ([44240d7](44240d7)) * **utils:** replace copy dependency ([4eb7c65](4eb7c65))
fix(store)!: remove plain jwt
## [13.0.0-beta.3](13.0.0-beta.2...13.0.0-beta.3) (2025-12-02) ### ⚠ BREAKING CHANGES * **store:** remove plain jwt ### Bug Fixes * **store:** remove plain jwt ([aa03b04](aa03b04))
## [14.0.0-beta.1](13.3.0...14.0.0-beta.1) (2026-01-06) ### ⚠ BREAKING CHANGES * **store:** remove plain jwt * **nuxt:** rename runtime configuration key ### Features * **nuxt:** rename runtime configuration key ([325238f](325238f)) ### Bug Fixes * **store:** remove plain jwt ([aa03b04](aa03b04))
## [14.0.0-beta.2](14.0.0-beta.1...14.0.0-beta.2) (2026-01-17) ### Bug Fixes * schedule release ([c6806e4](c6806e4))
feat(security)!: use http-only cookie
## [14.0.0-beta.3](14.0.0-beta.2...14.0.0-beta.3) (2026-01-26) ### ⚠ BREAKING CHANGES * **security:** use http-only cookie ### Features * **content:** improve error handling ([f441aa0](f441aa0)) * **security:** use http-only cookie ([547df19](547df19)) ### Bug Fixes * **components:** deduplicate icon wrapper ([357a623](357a623)) * **deps:** patch @nuxt/content ([495a671](495a671))
## [14.0.0-beta.4](14.0.0-beta.3...14.0.0-beta.4) (2026-02-20) ### Bug Fixes * **urql:** correct site name ([742aa8c](742aa8c))
|
🎉 This PR is included in version 14.0.0-beta.4 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
perf(guest): allow immediate data display
## [14.0.0-beta.7](14.0.0-beta.6...14.0.0-beta.7) (2026-03-01) ### Features * **cookie:** add `getResponseCookie` utility ([bff2be4](bff2be4)) * **guest:** enable JWT guest mutation endpoint ([88f7b92](88f7b92)) * **guest:** use JWT composable for server-side authentication ([4b6a97a](4b6a97a)) * **jwt:** add JWT and security composables ([cfd74d8](cfd74d8)) ### Reverts * **security:** drop response promise return ([6f719ce](6f719ce))
|
🎉 This PR is included in version 14.0.0-beta.7 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
## [14.0.0-beta.8](14.0.0-beta.7...14.0.0-beta.8) (2026-03-03) ### Bug Fixes * **nuxt:** access headers without h3 imports ([40a9f1c](40a9f1c))
|
🎉 This PR is included in version 14.0.0-beta.8 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
There was a problem hiding this comment.
Pull request overview
This is a major version release (v14) introducing several breaking changes as part of the PostGraphile v5 upgrade and security hardening. The PR refactors JWT handling to use server-side httpOnly cookies exclusively, renames the runtime configuration key from private to vibetype, adds CSRF protection to all routes by default, migrates from nodeId/id to rowId/id throughout the GraphQL layer, and switches to HTTPS for all development and test environments.
Changes:
- Refactored authentication to use server-managed JWT cookies via new
/api/model/jwtendpoints (GET/POST/PUT/DELETE), removing client-side JWT storage and enabling Sentry's Pinia integration - Migrated all GraphQL fragments, queries, and mutations from
nodeId/idtoid/rowIdfor PostGraphile v5 compatibility, and updated all component references accordingly - Added default CSRF protection with
x-csrf-tokenheader, HTTPS-only development certificates, and runtime config key rename fromprivatetovibetype
Reviewed changes
Copilot reviewed 170 out of 178 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/server/api/model/jwt.*.ts |
New server endpoints for JWT CRUD operations |
src/app/app.vue |
JWT initialization and update logic moved from useAuth to inline code |
src/app/composables/jwt.ts |
New JWT composables replacing auth.ts |
src/app/composables/security.ts |
New CSRF composables for token handling |
src/shared/utils/urql.ts |
Updated cache invalidation for rowId/id mapping |
src/shared/utils/jwt.ts |
Refactored JWT utilities (cookie params, public key fetch) |
src/shared/utils/constants.ts |
Moved many constants to node/static |
src/shared/utils/dependencies/urql.ts |
New server-side urql mutation helper |
src/shared/types/model.d.ts |
New Jwt payload type definition |
src/node/static/index.ts |
New central constants including CSRF, JWT cookie names |
src/nuxt.config.ts |
Runtime config rename, CSRF route config |
src/config/modules/security.ts |
CSRF configuration |
src/config/modules/cookieControl.ts |
Added CSRF cookie to consent |
src/config/environments/development.ts |
HTTPS dev server with app.localhost host |
src/gql/documents/** |
All fragments/mutations renamed for rowId |
src/app/components/** |
Updated all component references from id/nodeId to rowId |
src/app/pages/** |
Updated all page references, sign-in flow, event management |
src/server/plugins/urql.ts |
Added SITE_NAME to service href, moved type declaration |
src/server/utils/networking.ts |
New PostGraphile service href helpers (port 5678) |
src/server/utils/notification.ts |
Updated enum comparison, dynamic PostGraphile URL |
tests/** |
Updated imports, snapshots, HTTP→HTTPS, dev→development |
Dockerfile |
Updated base image to PostGraphile 2.0.0-beta.1 |
AGENTS.md |
New project coding guidelines |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)
src/app/components/form/FormSessionCreate.vue:100
- The error messages in
FormSessionCreate.vuehave been simplified from specific PostgreSQL error codes (postgres55000for unverified email,postgresP0002for login failure) to a single generic message. This significantly reduces the ability of users to understand what went wrong during sign-in. The original messages told users whether their email wasn't verified or whether their credentials were wrong — the new generic message doesn't differentiate these cases. Consider preserving the more specific error feedback, possibly by mapping error responses from the server endpoint.
src/app/components/form/FormSessionCreate.vue:83 - The
TURNSTILE_HEADER_KEYused here on line 83 seems like it should beTURNSTILE_HEADER_NAMEto be consistent with the new constant name exported from~~/node/static(line 24 ofsrc/node/static/index.ts). IfTURNSTILE_HEADER_KEYis a different auto-imported constant, this may be fine, but it's worth verifying consistency.
You can also share your feedback on Copilot code review. Take the survey.
| ...('rowId' in args.input | ||
| ? { id: args.input.rowId } | ||
| : { nodeId: args.input.id }), |
There was a problem hiding this comment.
The invalidateCache function's field mapping looks inverted. When the input has rowId, you map it to { id: args.input.rowId } which is correct (since urql cache uses id as the key). But when the input has id (the new GraphQL global node ID), you map it to { nodeId: args.input.id }. After the PostGraphile v5 migration where nodeId has been replaced by id in fragments, shouldn't the fallback also use id instead of nodeId? The cache key name nodeId is no longer present in the fragments (they all now use id and rowId).
| const variables = // TODO: remove with Postgraphile v5 | ||
| typeof query.variables === 'string' | ||
| ? JSON.parse(query.variables) | ||
| : query.variables | ||
|
|
||
| const graphql = await event.$fetch('/graphql', { | ||
| baseURL, | ||
| body: { | ||
| query: query.query, | ||
| operationName: query.operationName, | ||
| variables, | ||
| }, // TODO: remove with Postgraphile v5 | ||
| headers: { | ||
| ...(jwt | ||
| ? { | ||
| authorization: `Bearer ${jwt}`, | ||
| } | ||
| : {}), | ||
| }, | ||
| method: 'GET', | ||
| query, | ||
| method: 'POST', // TODO: 'GET' with Postgraphile v5 | ||
| // query, // TODO: reenable with Postgraphile v5 |
There was a problem hiding this comment.
The TODO comment says "remove with Postgraphile v5", but this PR is the PostGraphile v5 upgrade (as stated in referenced PR #2201). The GET handler now manually parses query.variables from a string and converts the request to POST — this workaround and both TODO comments should be removed or updated since PostGraphile v5 is being adopted in this very PR.
| if (mutationJwtCreate.error) { | ||
| if (mutationJwtCreate.error.networkError) { | ||
| return throwError({ | ||
| status: 500, | ||
| statusText: | ||
| (mutationJwtCreate.error.networkError.cause as { message?: string }) | ||
| ?.message || mutationJwtCreate.error.networkError.message, | ||
| }) | ||
| } | ||
|
|
||
| if (mutationJwtCreate.error.graphQLErrors.length) { | ||
| const messages = mutationJwtCreate.error.graphQLErrors | ||
| .map((error: GraphQLError) => error.message) | ||
| .join('; ') | ||
| return throwError({ | ||
| status: 500, | ||
| statusText: `GraphQL error(s) during JWT creation: ${messages}`, | ||
| }) | ||
| } | ||
|
|
||
| return throwError({ | ||
| status: 500, | ||
| statusText: | ||
| mutationJwtCreate.error.message || | ||
| 'Unexpected error during JWT creation.', | ||
| }) | ||
| } | ||
|
|
||
| if (!mutationJwtCreate.data) { | ||
| return throwError({ | ||
| status: 500, | ||
| statusText: `No data returned from JWT creation mutation.`, | ||
| }) | ||
| } | ||
|
|
||
| const jwt = mutationJwtCreate.data?.jwtCreate?.result | ||
|
|
||
| if (!jwt) { | ||
| return throwError({ | ||
| status: 500, | ||
| statusText: 'No JWT returned from creation mutation.', | ||
| }) | ||
| } |
There was a problem hiding this comment.
jwt.post.ts duplicates the error-handling logic that already exists in getJwtFromResult (from src/shared/utils/api.ts). The jwt.put.ts handler uses getJwtFromResult for the same purpose. This handler should also use getJwtFromResult to avoid maintaining the same error-handling code in two places.
| graphql(` | ||
| mutation DeleteEventFavoriteById($input: DeleteEventFavoriteByIdInput!) { | ||
| deleteEventFavoriteById(input: $input) { | ||
| mutation DeleteEventFavoriteByRowId($input: DeleteEventFavoriteInput!) { |
There was a problem hiding this comment.
The DeleteEventFavoriteByRowId mutation name is inconsistent with the GraphQL operation it calls. The mutation is named DeleteEventFavoriteByRowId but calls deleteEventFavorite (not deleteEventFavoriteByRowId), and the input type is DeleteEventFavoriteInput! (not DeleteEventFavoriteByRowIdInput!). While this may match the actual PostGraphile v5 schema, the mutation operation name DeleteEventFavoriteByRowId is misleading since it doesn't actually delete by row ID — it uses a generic DeleteEventFavoriteInput.
| mutation DeleteEventFavoriteByRowId($input: DeleteEventFavoriteInput!) { | |
| mutation DeleteEventFavorite($input: DeleteEventFavoriteInput!) { |
| const getJwt = async ({ | ||
| event, | ||
| body, | ||
| }: { | ||
| event: H3Event | ||
| body: z.infer<typeof jwtUpdateBodySchema> | ||
| }) => { | ||
| if ('id' in body) { | ||
| const jwtUpdateMutation = await urqlMutate({ | ||
| event, | ||
| urql: { | ||
| mutation: mutationJwtUpdate, | ||
| variables: { | ||
| ...body, | ||
| }, | ||
| }, | ||
| }) | ||
| return getJwtFromResult({ | ||
| context: 'JWT update', | ||
| extract: (data: JwtUpdateMutation) => data.jwtUpdate?.result, | ||
| result: jwtUpdateMutation, | ||
| }) | ||
| } | ||
|
|
||
| if ('attendanceId' in body) { | ||
| const jwtUpdateAttendanceAddMutation = await urqlMutate({ | ||
| event, | ||
| urql: { | ||
| mutation: mutationJwtUpdateAttendanceAdd, | ||
| variables: { | ||
| input: { | ||
| ...body, | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| return getJwtFromResult({ | ||
| context: 'JWT attendance add', | ||
| extract: (data: JwtUpdateAttendanceAddMutation) => | ||
| data.jwtUpdateAttendanceAdd?.result, | ||
| result: jwtUpdateAttendanceAddMutation, | ||
| }) | ||
| } | ||
|
|
||
| if ('guestId' in body) { | ||
| const jwtUpdateGuestAddMutation = await urqlMutate({ | ||
| event, | ||
| urql: { | ||
| mutation: mutationJwtUpdateGuestAdd, | ||
| variables: { | ||
| input: { | ||
| ...body, | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| return getJwtFromResult({ | ||
| context: 'JWT guest add', | ||
| extract: (data: JwtUpdateGuestAddMutation) => | ||
| data.jwtUpdateGuestAdd?.result, | ||
| result: jwtUpdateGuestAddMutation, | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
The getJwt function can return undefined when none of the if conditions match (i.e., the body has none of id, attendanceId, or guestId). While the Zod schema (z.union) should prevent this at runtime, TypeScript won't know that all branches are exhaustive. Consider adding an explicit throw or return throwError(...) at the end of getJwt to make the function's return type non-optional and catch any future schema changes that could introduce a gap.
| const csrfHeaderValue = headers.get(CSRF_HEADER_NAME) | ||
| const turnstileHeaderValue = headers.get(TURNSTILE_HEADER_NAME) |
There was a problem hiding this comment.
The CSRF_COOKIE_NAME and CSRF_HEADER_NAME constants are used in src/shared/utils/dependencies/urql.ts (lines 19, 27) but are not imported. Since this file is under src/shared/utils/ (not a Nuxt auto-import location for server utils), these constants won't be auto-imported. Similarly, TURNSTILE_HEADER_NAME on line 20 is used without import. These need explicit imports from ~~/node/static.
| @@ -1,3 +1,4 @@ | |||
| import { createError } from 'h3' | |||
There was a problem hiding this comment.
The createError import from h3 is added but throwError (which is used throughout the codebase) already exists in this file. If createError is not actually used in this file, this import is unnecessary. If it's meant to be used inside throwError, the function body should be updated accordingly.
feat(jwt)!: use ecdsa key
## [14.0.0-beta.9](14.0.0-beta.8...14.0.0-beta.9) (2026-03-14) ### ⚠ BREAKING CHANGES * **jwt:** use ecdsa key ### Features * **jwt:** use ecdsa key ([80885a8](80885a8))
|
🎉 This PR is included in version 14.0.0-beta.9 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
feat(deps): upgrade @dargmuesli/nuxt-vio to v21
50c9cb2 to
28c9fa5
Compare
📚 Description
Breaking changes:
privatetovibetypeapi/service/traefik/authenticationmoved to/api/internal/service/postgraphile/authenticationx-csrf-tokenConfiguration and Environment Variables:
📝 Checklist