@habityzer/nuxt-symfony-kinde-layer 2.1.2 → 2.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.
@@ -0,0 +1,52 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+
9
+ jobs:
10
+ release:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: write
14
+ issues: write
15
+ pull-requests: write
16
+ id-token: write
17
+
18
+ steps:
19
+ - name: Checkout code
20
+ uses: actions/checkout@v4
21
+ with:
22
+ fetch-depth: 0
23
+ persist-credentials: false
24
+
25
+ - name: Setup pnpm
26
+ uses: pnpm/action-setup@v4
27
+ with:
28
+ version: 9
29
+
30
+ - name: Setup Node.js
31
+ uses: actions/setup-node@v4
32
+ with:
33
+ node-version: '20'
34
+ cache: 'pnpm'
35
+ registry-url: 'https://registry.npmjs.org'
36
+
37
+ - name: Install dependencies
38
+ run: pnpm install --frozen-lockfile
39
+
40
+ - name: Prepare Nuxt
41
+ run: pnpm nuxt prepare
42
+
43
+ - name: Run linter
44
+ run: pnpm lint
45
+
46
+ - name: Release
47
+ env:
48
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
50
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
51
+ run: pnpm release
52
+
package/.releaserc.json CHANGED
@@ -23,7 +23,7 @@
23
23
  "changelogFile": "CHANGELOG.md"
24
24
  }],
25
25
  ["@semantic-release/npm", {
26
- "npmPublish": false
26
+ "npmPublish": true
27
27
  }],
28
28
  ["@semantic-release/git", {
29
29
  "assets": ["package.json", "CHANGELOG.md"],
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## [2.1.4](https://github.com/Habityzer/nuxt-symfony-kinde-layer/compare/v2.1.3...v2.1.4) (2026-01-01)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Add Nuxt prepare step to CI and correct ESLint config import path ([07471f5](https://github.com/Habityzer/nuxt-symfony-kinde-layer/commit/07471f5234e1322a2ccfe5709cb8109badededda))
7
+ * Remove invalid pnpm-workspace.yaml for single package ([7e7d6a1](https://github.com/Habityzer/nuxt-symfony-kinde-layer/commit/7e7d6a19ffd2edccfee80b944c947c364b6b2863))
8
+
9
+ ## [2.1.3](https://github.com/Habityzer/nuxt-symfony-kinde-layer/compare/v2.1.2...v2.1.3) (2026-01-01)
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * Update Kinde configuration to use environment variables and enhance Symfony proxy request handling ([3e0d0bd](https://github.com/Habityzer/nuxt-symfony-kinde-layer/commit/3e0d0bd09ea26a21940f0a0d16cc9aaf5a327be8))
15
+
1
16
  ## [2.1.2](https://github.com/Habityzer/nuxt-symfony-kinde-layer/compare/v2.1.1...v2.1.2) (2025-12-26)
2
17
 
3
18
 
package/README.md CHANGED
@@ -324,3 +324,4 @@ MIT
324
324
  ## Contributing
325
325
 
326
326
  This is a private layer for Habityzer projects. For issues or improvements, contact the team.
327
+
@@ -0,0 +1,134 @@
1
+ # Setting Up Automated NPM Publishing
2
+
3
+ This project uses GitHub Actions with semantic-release to automatically publish to npm when you push to the `master` branch.
4
+
5
+ ## Setup Instructions
6
+
7
+ ### 1. Create NPM Access Token
8
+
9
+ > **Important**: As of December 9, 2025, npm classic tokens have been permanently revoked. You must use **granular access tokens** for CI/CD workflows.
10
+
11
+ **Option A: Using npm CLI (recommended):**
12
+ ```bash
13
+ npm token create --type automation --scope @habityzer
14
+ ```
15
+
16
+ **Option B: Using the web interface:**
17
+ 1. Go to [npmjs.com/settings/~/tokens](https://www.npmjs.com/settings/~/tokens) and log in
18
+ 2. Click **Generate New Token** → **Granular Access Token**
19
+ 3. Configure your token:
20
+ - **Token name**: `github-actions-publish` (or any descriptive name)
21
+ - **Expiration**: Up to 90 days (maximum for publish tokens)
22
+ - **Packages and scopes**: Select your package or `@habityzer` scope
23
+ - **Permissions**: Select **Read and write**
24
+ - ✅ **Enable "Bypass 2FA"** for automated workflows
25
+ 4. Click **Generate Token**
26
+ 5. Copy the token (starts with `npm_...`)
27
+
28
+ > **Note**: Granular tokens for publishing expire after a maximum of 90 days. Set a reminder to regenerate the token before expiration, or consider using [OIDC trusted publishing](https://docs.npmjs.com/generating-provenance-statements) for a more secure, token-free approach.
29
+
30
+ ### 2. Add NPM Token to GitHub
31
+
32
+ 1. Go to your GitHub repository
33
+ 2. Navigate to **Settings** → **Secrets and variables** → **Actions**
34
+ 3. Click **New repository secret**
35
+ 4. Name: `NPM_TOKEN`
36
+ 5. Value: Paste your npm token from step 1
37
+ 6. Click **Add secret**
38
+
39
+ ### 3. Configure GitHub Actions in NPM
40
+
41
+ When setting up your package on npmjs.com for automated publishing:
42
+
43
+ 1. Go to your package page on npmjs.com
44
+ 2. Navigate to **Settings** → **Publishing Access**
45
+ 3. Under **GitHub Actions**, you'll be asked for:
46
+ - **Workflow filename**: Enter `publish.yml`
47
+ - This tells npm which workflow file in `.github/workflows/` will publish your package
48
+
49
+ ### 4. How It Works
50
+
51
+ The workflow (`.github/workflows/publish.yml`) will:
52
+
53
+ 1. **Trigger**: Automatically runs when you push to the `master` branch
54
+ 2. **Analyze**: Semantic-release reads your commit messages (following conventional commits)
55
+ 3. **Version**: Automatically bumps the version based on your commits:
56
+ - `feat:` → minor version (1.0.0 → 1.1.0)
57
+ - `fix:` → patch version (1.0.0 → 1.0.1)
58
+ - `BREAKING CHANGE:` → major version (1.0.0 → 2.0.0)
59
+ 4. **Changelog**: Updates `CHANGELOG.md`
60
+ 5. **Git Tag**: Creates a git tag for the new version
61
+ 6. **Publish**: Publishes to npm
62
+ 7. **Commit**: Commits the changelog and version bump back to your repo
63
+
64
+ ### 5. Commit Message Format
65
+
66
+ Use conventional commits for automatic versioning:
67
+
68
+ ```bash
69
+ # Patch release (1.0.0 → 1.0.1)
70
+ git commit -m "fix: resolve authentication bug"
71
+
72
+ # Minor release (1.0.0 → 1.1.0)
73
+ git commit -m "feat: add new login method"
74
+
75
+ # Major release (1.0.0 → 2.0.0)
76
+ git commit -m "feat: redesign API
77
+
78
+ BREAKING CHANGE: API structure has changed"
79
+
80
+ # No release (documentation, etc.)
81
+ git commit -m "docs: update README"
82
+ git commit -m "chore: update dependencies"
83
+ ```
84
+
85
+ ### 6. Manual Release (Optional)
86
+
87
+ If you prefer to release manually:
88
+
89
+ ```bash
90
+ pnpm release
91
+ ```
92
+
93
+ This runs semantic-release locally. Make sure you have:
94
+ - `NPM_TOKEN` environment variable set
95
+ - Committed all your changes
96
+ - Pushed to the master branch
97
+
98
+ ## Troubleshooting
99
+
100
+ ### "No NPM_TOKEN found"
101
+ - Make sure you've added `NPM_TOKEN` as a GitHub secret
102
+ - Check that the secret name is exactly `NPM_TOKEN` (case-sensitive)
103
+
104
+ ### "No release published"
105
+ - Check your commit messages follow conventional commits format
106
+ - Semantic-release only publishes if there are releasable commits
107
+ - View the GitHub Actions logs for details
108
+
109
+ ### "Permission denied" or "Invalid npm token"
110
+ - Ensure you're using a **granular access token** (not classic token)
111
+ - Check that the token has **Read and write** permissions
112
+ - Ensure **"Bypass 2FA"** is enabled for CI/CD workflows
113
+ - Check that your npm account has publish access to the `@habityzer` scope
114
+ - Verify the token hasn't expired (granular tokens expire after max 90 days)
115
+
116
+ ### "Package already published"
117
+ - Semantic-release automatically handles versions
118
+ - If you manually published the same version, semantic-release will skip it
119
+
120
+ ## Current Configuration
121
+
122
+ - **Branch**: `master` (configured in `.releaserc.json`)
123
+ - **Package**: `@habityzer/nuxt-symfony-kinde-layer`
124
+ - **Workflow**: `.github/workflows/publish.yml`
125
+ - **NPM Publish**: Enabled (set in `.releaserc.json`)
126
+
127
+ ## Additional Resources
128
+
129
+ - [npm Granular Access Tokens Documentation](https://docs.npmjs.com/about-access-tokens#granular-access-tokens)
130
+ - [GitHub Blog: npm Classic Tokens Revoked (Dec 2025)](https://github.blog/changelog/2025-12-09-npm-classic-tokens-revoked-session-based-auth-and-cli-token-management-now-available/)
131
+ - [Semantic Release Documentation](https://semantic-release.gitbook.io/)
132
+ - [Conventional Commits](https://www.conventionalcommits.org/)
133
+ - [OIDC Trusted Publishing for npm](https://docs.npmjs.com/generating-provenance-statements)
134
+
package/eslint.config.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- import withNuxt from './node_modules/.cache/nuxt/.nuxt/eslint.config.mjs'
2
+ import withNuxt from './.nuxt/eslint.config.mjs'
3
3
 
4
4
  export default withNuxt(
5
5
  // Your custom configs here
package/nuxt.config.ts CHANGED
@@ -48,11 +48,11 @@ export default defineNuxtConfig({
48
48
 
49
49
  // Default Kinde configuration (projects MUST override with their credentials)
50
50
  kindeAuth: {
51
- authDomain: '', // Project must provide
52
- clientId: '', // Project must provide
53
- clientSecret: '', // Project must provide
54
- redirectURL: '', // Project must provide
55
- logoutRedirectURL: '', // Project must provide
51
+ authDomain: process.env.KINDE_AUTH_DOMAIN || 'https://dummy.kinde.com', // Project must provide, dummy for build
52
+ clientId: process.env.KINDE_CLIENT_ID || 'dummy-client-id', // Project must provide, dummy for build
53
+ clientSecret: process.env.KINDE_CLIENT_SECRET || 'dummy-secret', // Project must provide, dummy for build
54
+ redirectURL: process.env.KINDE_REDIRECT_URL || 'http://localhost:3000/api/auth/kinde_callback', // Project must provide, dummy for build
55
+ logoutRedirectURL: process.env.KINDE_LOGOUT_REDIRECT_URL || 'http://localhost:3000', // Project must provide, dummy for build
56
56
  postLoginRedirectURL: '/dashboard', // Default, can be overridden
57
57
  cookie: {
58
58
  prefix: 'app_', // Projects MUST override this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@habityzer/nuxt-symfony-kinde-layer",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "Shared Nuxt layer for Symfony + Kinde authentication integration",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -13,95 +13,95 @@
13
13
  */
14
14
 
15
15
  // Auth constants (defined inline to avoid import issues during Nitro bundling)
16
- const E2E_TOKEN_COOKIE_NAME = "kinde_token";
17
- const APP_TOKEN_PREFIX = "app_";
18
- const KINDE_ID_TOKEN_COOKIE_NAME = "id_token";
16
+ const E2E_TOKEN_COOKIE_NAME = 'kinde_token'
17
+ const APP_TOKEN_PREFIX = 'app_'
18
+ const KINDE_ID_TOKEN_COOKIE_NAME = 'id_token'
19
19
 
20
20
  export default defineEventHandler(async (event) => {
21
- const config = useRuntimeConfig();
21
+ const config = useRuntimeConfig()
22
22
 
23
23
  // Get the path (remove /api/symfony prefix)
24
- let path =
25
- event.context.params?.path || event.path.replace("/api/symfony", "");
24
+ let path
25
+ = event.context.params?.path || event.path.replace('/api/symfony', '')
26
26
 
27
27
  // Ensure path starts with / (for catch-all routes it might not)
28
- if (!path.startsWith("/")) {
29
- path = `/${path}`;
28
+ if (!path.startsWith('/')) {
29
+ path = `/${path}`
30
30
  }
31
31
 
32
32
  // Check if this is a public API route (no auth required)
33
- const publicApiRoutes =
34
- config.public.kindeAuth?.middleware?.publicApiRoutes || [];
33
+ const publicApiRoutes
34
+ = config.public.kindeAuth?.middleware?.publicApiRoutes || []
35
35
  const isPublicRoute = publicApiRoutes.some((route: string) => {
36
- if (route.endsWith("/**")) {
37
- const prefix = route.slice(0, -3);
38
- return path.startsWith(prefix);
36
+ if (route.endsWith('/**')) {
37
+ const prefix = route.slice(0, -3)
38
+ return path.startsWith(prefix)
39
39
  }
40
- return path === route;
41
- });
40
+ return path === route
41
+ })
42
42
 
43
- let token: string | undefined;
43
+ let token: string | undefined
44
44
 
45
45
  // Skip authentication for public routes
46
46
  if (isPublicRoute) {
47
- console.log("🔓 [SYMFONY PROXY] Public route, skipping auth:", path);
47
+ console.log('🔓 [SYMFONY PROXY] Public route, skipping auth:', path)
48
48
  // Set token to empty to skip auth headers
49
- token = "";
49
+ token = ''
50
50
  } else {
51
51
  // Check for E2E test token first (from cookie)
52
52
  // Only use E2E token if it's a valid app token (starts with APP_TOKEN_PREFIX)
53
- const e2eToken = getCookie(event, E2E_TOKEN_COOKIE_NAME);
53
+ const e2eToken = getCookie(event, E2E_TOKEN_COOKIE_NAME)
54
54
  if (e2eToken && e2eToken.startsWith(APP_TOKEN_PREFIX)) {
55
- token = e2eToken;
55
+ token = e2eToken
56
56
  } else {
57
57
  // Use Kinde authentication from the module
58
- const kinde = event.context.kinde;
58
+ const kinde = event.context.kinde
59
59
 
60
60
  if (!kinde?.client || !kinde?.sessionManager) {
61
61
  throw createError({
62
62
  statusCode: 500,
63
63
  statusMessage:
64
- "Kinde authentication not initialized. Module may not be loaded correctly.",
65
- });
64
+ 'Kinde authentication not initialized. Module may not be loaded correctly.'
65
+ })
66
66
  }
67
67
 
68
- const { client, sessionManager } = kinde;
68
+ const { client, sessionManager } = kinde
69
69
 
70
70
  try {
71
71
  // Try to get access token first
72
- let accessToken: string | null = null;
72
+ let accessToken: string | null = null
73
73
  try {
74
- accessToken = await client.getToken(sessionManager);
74
+ accessToken = await client.getToken(sessionManager)
75
75
  } catch {
76
76
  // Silent - will try id_token fallback
77
77
  }
78
78
 
79
79
  // If access token is not available, try id_token as fallback
80
- if (!accessToken || accessToken.trim() === "") {
80
+ if (!accessToken || accessToken.trim() === '') {
81
81
  const idToken = (await sessionManager.getSessionItem(
82
82
  KINDE_ID_TOKEN_COOKIE_NAME
83
- )) as string | undefined;
83
+ )) as string | undefined
84
84
 
85
85
  if (idToken) {
86
- token = idToken;
86
+ token = idToken
87
87
  }
88
88
  } else {
89
- token = accessToken;
89
+ token = accessToken
90
90
  }
91
91
 
92
- if (!token || token.trim() === "") {
92
+ if (!token || token.trim() === '') {
93
93
  throw createError({
94
94
  statusCode: 401,
95
- statusMessage: "Unauthorized - Please log in",
96
- });
95
+ statusMessage: 'Unauthorized - Please log in'
96
+ })
97
97
  }
98
98
  } catch (error) {
99
- console.error("❌ [SYMFONY PROXY] Auth error:", error);
99
+ console.error('❌ [SYMFONY PROXY] Auth error:', error)
100
100
  throw createError({
101
101
  statusCode: 401,
102
102
  statusMessage:
103
- error instanceof Error ? error.message : "Authentication failed",
104
- });
103
+ error instanceof Error ? error.message : 'Authentication failed'
104
+ })
105
105
  }
106
106
  }
107
107
  }
@@ -109,53 +109,66 @@ export default defineEventHandler(async (event) => {
109
109
  if (!token && !isPublicRoute) {
110
110
  throw createError({
111
111
  statusCode: 401,
112
- statusMessage: "No authentication token available",
113
- });
112
+ statusMessage: 'No authentication token available'
113
+ })
114
114
  }
115
115
 
116
116
  try {
117
- // Get request method and body
118
- const method = event.method;
119
- const body =
120
- method !== "GET" && method !== "HEAD" ? await readBody(event) : undefined;
117
+ // Get request method
118
+ const method = event.method
119
+
120
+ // Get Content-Type to determine how to handle body
121
+ const contentType = getHeader(event, 'content-type') || ''
122
+
123
+ let body: string | undefined
124
+
125
+ // Handle multipart/form-data specially (preserve binary data and MIME types)
126
+ if (contentType.includes('multipart/form-data')) {
127
+ // For multipart/form-data, read the raw body without parsing
128
+ // This preserves the boundary and binary data including MIME types
129
+ body = await readRawBody(event, false)
130
+ } else if (method !== 'GET' && method !== 'HEAD') {
131
+ // For other content types (JSON, etc.), read and parse the body
132
+ body = await readBody(event)
133
+ }
121
134
 
122
135
  // Get query parameters from original request
123
- const query = getQuery(event);
136
+ const query = getQuery(event)
124
137
 
125
138
  // Prepare headers for Symfony
126
139
  // IMPORTANT: Forward Content-Type and Accept headers for proper API negotiation
127
- const headers: Record<string, string> = {};
140
+ const headers: Record<string, string> = {}
128
141
 
129
142
  // Only add Authorization header if we have a token (not public route)
130
- if (token && token !== "") {
131
- headers.Authorization = `Bearer kinde_${token}`;
143
+ if (token && token !== '') {
144
+ headers.Authorization = `Bearer kinde_${token}`
132
145
  }
133
146
 
134
- // Forward Content-Type header
135
- const contentType = getHeader(event, "content-type");
147
+ // Forward Content-Type header (CRITICAL for multipart/form-data with boundary)
136
148
  if (contentType) {
137
- headers["Content-Type"] = contentType;
149
+ headers['Content-Type'] = contentType
138
150
  }
139
151
 
140
152
  // Forward Accept header (CRITICAL for content negotiation)
141
153
  // Without this, backend returns default format instead of requested format (e.g., JSON vs Hydra)
142
- const accept = getHeader(event, "accept");
154
+ const accept = getHeader(event, 'accept')
143
155
  if (accept) {
144
- headers["Accept"] = accept;
156
+ headers['Accept'] = accept
145
157
  }
146
158
 
147
159
  // Log the request to backend
148
- const backendUrl = `${config.apiBaseUrl}${path}`;
149
- console.log("🔵 [SYMFONY PROXY] Request to backend:", {
160
+ const backendUrl = `${config.apiBaseUrl}${path}`
161
+ console.log('🔵 [SYMFONY PROXY] Request to backend:', {
150
162
  url: backendUrl,
151
163
  method,
152
164
  headers: {
153
165
  ...headers,
154
- Authorization: `Bearer kinde_${token.substring(0, 10)}...`, // Only log first 10 chars of token
166
+ Authorization: token && token !== '' ? `Bearer kinde_${token.substring(0, 10)}...` : 'none'
155
167
  },
156
168
  query,
157
169
  hasBody: !!body,
158
- });
170
+ isMultipart: contentType.includes('multipart/form-data')
171
+ })
159
172
 
160
173
  // Forward request to Symfony with Kinde token
161
174
  const response = await $fetch(path, {
@@ -163,46 +176,47 @@ export default defineEventHandler(async (event) => {
163
176
  method,
164
177
  headers,
165
178
  body,
166
- retry: false, // Disable automatic retries
167
179
  query,
168
- });
180
+ retry: false, // Disable automatic retries
181
+ timeout: 30000 // 30 second timeout
182
+ })
169
183
 
170
- console.log("✅ [SYMFONY PROXY] Backend response received:", {
184
+ console.log('✅ [SYMFONY PROXY] Backend response received:', {
171
185
  url: backendUrl,
172
186
  method,
173
- status: "success",
174
- });
187
+ status: 'success'
188
+ })
175
189
 
176
- return response;
190
+ return response
177
191
  } catch (error) {
178
- console.error("❌ [SYMFONY PROXY] Symfony API error:", {
192
+ console.error('❌ [SYMFONY PROXY] Symfony API error:', {
179
193
  path,
180
194
  statusCode:
181
- error && typeof error === "object" && "statusCode" in error
195
+ error && typeof error === 'object' && 'statusCode' in error
182
196
  ? error.statusCode
183
- : "unknown",
184
- message: error instanceof Error ? error.message : "unknown",
185
- });
197
+ : 'unknown',
198
+ message: error instanceof Error ? error.message : 'unknown'
199
+ })
186
200
  // Handle Symfony API errors
187
- const statusCode =
188
- error && typeof error === "object" && "statusCode" in error
201
+ const statusCode
202
+ = error && typeof error === 'object' && 'statusCode' in error
189
203
  ? (error.statusCode as number)
190
- : 500;
191
- const statusMessage =
192
- error && typeof error === "object" && "statusMessage" in error
204
+ : 500
205
+ const statusMessage
206
+ = error && typeof error === 'object' && 'statusMessage' in error
193
207
  ? (error.statusMessage as string)
194
208
  : error instanceof Error
195
- ? error.message
196
- : "Symfony API error";
197
- const data =
198
- error && typeof error === "object" && "data" in error
209
+ ? error.message
210
+ : 'Symfony API error'
211
+ const data
212
+ = error && typeof error === 'object' && 'data' in error
199
213
  ? error.data
200
- : undefined;
214
+ : undefined
201
215
 
202
216
  throw createError({
203
217
  statusCode,
204
218
  statusMessage,
205
- data,
206
- });
219
+ data
220
+ })
207
221
  }
208
- });
222
+ })
@@ -1,5 +0,0 @@
1
- onlyBuiltDependencies:
2
- - '@tailwindcss/oxide'
3
- - sharp
4
- - unrs-resolver
5
- - vue-demi