@ymys/directus-extension-sso 1.3.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Yusuf M (ymys)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # Directus SSO - Directus Extension
2
+
3
+ This extension allows you to use the web and mobile OAuth callback and logout endpoints directly through your Directus domain.
4
+
5
+ ## Installation
6
+
7
+ 1. **Build the extension:**
8
+ ```bash
9
+ cd extensions/endpoints/directus-extension-sso
10
+ npm install
11
+ npm run build
12
+ ```
13
+
14
+ 2. **The extension will be automatically loaded by Directus** when you restart it.
15
+
16
+ ## Endpoints
17
+
18
+ Once installed, the following endpoints will be available on your Directus domain:
19
+
20
+ - **Health Check:** `GET /sso/health`
21
+ - **Mobile/Browser Callback (Keycloak):** `GET /sso/mobile-callback`
22
+ - **Mobile/Browser Callback (Google):** `GET /sso/google-callback`
23
+ - **Mobile Logout:** `POST /sso/mobile-logout`
24
+
25
+ ## Configuration
26
+
27
+ The extension uses environment variables from your Directus configuration:
28
+
29
+ # Add these to your Directus .env file
30
+ KEYCLOAK_URL=http://keycloak:8080
31
+ KEYCLOAK_REALM=testing
32
+ KEYCLOAK_CLIENT_ID=admin-cli
33
+ KEYCLOAK_ADMIN_USER=admin
34
+ KEYCLOAK_ADMIN_PASSWORD=admin
35
+ PUBLIC_URL=http://localhost:8055
36
+ MOBILE_APP_SCHEME=portalpipq
37
+ MOBILE_APP_CALLBACK_PATH=/auth/callback
38
+ GOOGLE_CALLBACK_PATH=/auth/callback/google
39
+ COOKIE_DOMAIN=.your-domain.com
40
+ COOKIE_SECURE=true
41
+ COOKIE_SAME_SITE=lax
42
+ ```
43
+
44
+ ### Security Note
45
+
46
+ Ensure specific values like `KEYCLOAK_Admin_PASSWORD` are kept secure and not committed to version control. The extension now uses environment variables for all sensitive configuration.
47
+
48
+ ## Usage
49
+
50
+ ### Browser Authentication Flow
51
+
52
+ The extension now supports **both browser and mobile app authentication** with automatic detection. After successful Keycloak login:
53
+
54
+ - **Browser requests**: Session is saved with cookies, allowing SSO across multiple services
55
+ - **Mobile app requests**: Access token is returned via deep link as before
56
+
57
+ #### Browser Login Example:
58
+
59
+ **For Keycloak:**
60
+ ```javascript
61
+ // Simply navigate to the login URL
62
+ window.location.href = 'http://your-directus-domain.com/auth/login/keycloak';
63
+
64
+ // Or with a custom redirect after login
65
+ window.location.href = 'http://your-directus-domain.com/auth/login/keycloak?redirect_uri=/dashboard';
66
+ ```
67
+
68
+ **For Google:**
69
+ ```javascript
70
+ // Simply navigate to the Google login URL
71
+ window.location.href = 'http://your-directus-domain.com/auth/login/google';
72
+
73
+ // Or with a custom redirect after login
74
+ window.location.href = 'http://your-directus-domain.com/auth/login/google?redirect_uri=/dashboard';
75
+ ```
76
+
77
+ After successful login, the user will see a success page and the session cookie will be saved. The user can then access other URLs using the same SSO without logging in again.
78
+
79
+ #### Forcing Browser Mode:
80
+
81
+ If auto-detection doesn't work correctly, you can force browser mode:
82
+
83
+ ```javascript
84
+ window.location.href = 'http://your-directus-domain.com/auth/login/keycloak?type=browser';
85
+ ```
86
+
87
+ #### Query Parameters:
88
+
89
+ - `type`: Force `browser` or `mobile` mode (optional, auto-detected if not provided)
90
+ - `redirect_uri` or `redirect`: URL to redirect to after successful login (optional, defaults to `/admin`)
91
+
92
+ ### Mobile App Authentication Flow
93
+
94
+ Update your mobile app to use the new Directus domain URLs:
95
+
96
+ ### Before (separate proxy server):
97
+ ```javascript
98
+ const PROXY_URL = 'http://your-proxy-domain.com:3000';
99
+ const callbackUrl = `${PROXY_URL}/mobile-callback`;
100
+ ```
101
+
102
+ ### After (Directus extension):
103
+ ```javascript
104
+ const DIRECTUS_URL = 'http://your-directus-domain.com';
105
+ const callbackUrl = `${DIRECTUS_URL}/sso/mobile-callback`;
106
+ ```
107
+
108
+ ### Login Flow:
109
+ ```javascript
110
+ import * as WebBrowser from 'expo-web-browser';
111
+
112
+ const result = await WebBrowser.openAuthSessionAsync(
113
+ `${DIRECTUS_URL}/auth/login/keycloak`,
114
+ 'myapp://auth/callback'
115
+ );
116
+ ```
117
+
118
+ ### Logout:
119
+ ```javascript
120
+ const response = await fetch(`${DIRECTUS_URL}/sso/mobile-logout`, {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Authorization': `Bearer ${accessToken}`,
124
+ },
125
+ });
126
+ ```
127
+
128
+ ## Advantages Over Standalone Proxy
129
+
130
+ 1. **Single Domain:** No need to deploy a separate server
131
+ 2. **Unified Management:** Managed alongside your Directus instance
132
+ 3. **Same Environment:** Uses Directus environment variables and configuration
133
+ 4. **Built-in Logging:** Uses Directus logger for consistent logging
134
+ 5. **Easier Deployment:** Deployed automatically with Directus
135
+
136
+ ## Development
137
+
138
+ To make changes and test locally:
139
+
140
+ ```bash
141
+ cd extensions/endpoints/directus-extension-sso
142
+ npm run dev
143
+ ```
144
+
145
+ This will watch for changes and rebuild automatically.
146
+
147
+ ## Keycloak Client Configuration
148
+
149
+ Update your Keycloak client's redirect URI to use the Directus domain:
150
+
151
+ **Valid Redirect URIs:**
152
+ ```
153
+ http://your-directus-domain.com/auth/login/keycloak/callback
154
+ http://your-directus-domain.com/sso/mobile-callback
155
+ myapp://auth/callback
156
+ ```
157
+
158
+ **Web Origins:**
159
+ ```
160
+ http://your-directus-domain.com
161
+ ```
162
+
163
+ ## Notes
164
+
165
+ - The extension requires Directus 11.0.0 or higher
166
+ - Make sure cookie-parser middleware is available (Directus includes this by default)
167
+ - The extension has access to the same network as Directus, so internal service URLs (like `http://keycloak:8080`) will work if using Docker
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ var t={id:"sso",handler:(t,{env:e,logger:o})=>{const n=e.KEYCLOAK_URL||"http://keycloak:8080",r=e.KEYCLOAK_REALM||"testing",i=e.KEYCLOAK_ADMIN_USER||"admin",a=e.KEYCLOAK_ADMIN_PASSWORD||"admin",s=e.PUBLIC_URL||"http://localhost:8055",c=e.MOBILE_APP_SCHEME||"portalpipq",d=e.MOBILE_APP_CALLBACK_PATH||"/auth/callback",l=e.GOOGLE_CALLBACK_PATH||"/auth/callback/google",u=e.KEYCLOAK_CLIENT_ID||"admin-cli",g=e.COOKIE_DOMAIN||null,f="false"!==e.COOKIE_SECURE,h=e.COOKIE_SAME_SITE||"lax";o.info("🚀 Mobile Auth Proxy Extension loaded"),o.info("🔐 Keycloak URL: "+n),o.info("🌐 Keycloak Realm: "+r),o.info("📱 Mobile App Scheme: "+c+"://"+d),o.info("🔵 Google OAuth enabled"),t.get("/health",(t,e)=>{e.json({status:"ok",service:"directus-mobile-auth-proxy"})}),t.get("/mobile-callback",async(t,e)=>{o.info("📱 Mobile callback received"),o.info("Cookies: "+JSON.stringify(t.cookies)),o.info("Query: "+JSON.stringify(t.query));try{const n=t.cookies.directus_session_token;if(!n)return o.error("❌ No session token found in cookies"),e.send(`\n\t\t\t\t\t<html>\n\t\t\t\t\t\t<body>\n\t\t\t\t\t\t\t<h2>Authentication Failed</h2>\n\t\t\t\t\t\t\t<p>No session token found. Please try logging in again.</p>\n\t\t\t\t\t\t\t<a href="${s}/auth/login/keycloak">Try Again</a>\n\t\t\t\t\t\t</body>\n\t\t\t\t\t</html>\n\t\t\t\t`);o.info("✅ Session token found: "+n.substring(0,20)+"...");const r=await fetch(`${s}/users/me`,{headers:{Cookie:`directus_session_token=${n}`}});if(!r.ok)throw new Error("Failed to get user info: "+await r.text());const i=await r.json(),a=i.data.id,l=i.data.email;o.info("👤 User authenticated: "+a+", "+l);const u=n;if(o.info("🎫 Using session token as access token"),isBrowser){o.info("🌐 Browser request detected - maintaining session"),e.cookie("directus_session_token",n,{httpOnly:!0,secure:f,domain:g,sameSite:h,maxAge:6048e5,path:"/"});const r=t.query.redirect_uri||t.query.redirect||"/";return o.info("🔄 Redirecting browser to: "+r),e.redirect(r)}const p=new URL(`${c}://${d}`);p.searchParams.set("access_token",u),p.searchParams.set("user_id",a),p.searchParams.set("email",l||""),o.info("🔄 Redirecting to app: "+p.toString()),e.redirect(p.toString())}catch(t){o.error("❌ Error in callback:",t),e.status(500).send(`\n\t\t\t\t<html>\n\t\t\t\t\t<body>\n\t\t\t\t\t\t<h2>Error</h2>\n\t\t\t\t\t\t<p>${t.message}</p>\n\t\t\t\t\t\t<a href="${s}/auth/login/keycloak">Try Again</a>\n\t\t\t\t\t</body>\n\t\t\t\t</html>\n\t\t\t`)}}),t.get("/google-callback",async(t,e)=>{const n=function(t){if("browser"===t.query.type)return!0;if("mobile"===t.query.type)return!1;const e=t.headers["user-agent"]||"";return/Mozilla|Chrome|Safari|Firefox|Edge|Opera/i.test(e)&&!/Mobile.*App|ReactNative|Expo/i.test(e)}(t);o.info(`${n?"🌐":"📱"} ${n?"Browser":"Mobile"} Google callback received`),o.info("Cookies: "+JSON.stringify(t.cookies)),o.info("Query: "+JSON.stringify(t.query));try{const r=t.cookies.directus_session_token;if(!r)return o.error("❌ No session token found in cookies"),e.send("\n\t\t\t\t\t\t<html>\n\t\t\t\t\t\t\t<head>\n\t\t\t\t\t\t\t\t<title>Authentication Failed</title>\n\t\t\t\t\t\t\t\t<style>\n\t\t\t\t\t\t\t\t\tbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \n\t\t\t\t\t\t\t\t\t padding: 40px; text-align: center; background: #f5f5f5; }\n\t\t\t\t\t\t\t\t\t.container { max-width: 500px; margin: 0 auto; background: white; \n\t\t\t\t\t\t\t\t\t padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }\n\t\t\t\t\t\t\t\t\th2 { color: #e74c3c; }\n\t\t\t\t\t\t\t\t\ta { display: inline-block; margin-top: 20px; padding: 10px 20px; \n\t\t\t\t\t\t\t\t\t background: #4285f4; color: white; text-decoration: none; border-radius: 4px; }\n\t\t\t\t\t\t\t\t\ta:hover { background: #357ae8; }\n\t\t\t\t\t\t\t\t</style>\n\t\t\t\t\t\t\t</head>\n\t\t\t\t\t\t\t<body>\n\t\t\t\t\t\t\t\t<div class=\"container\">\n\t\t\t\t\t\t\t\t\t<h2>Authentication Failed</h2>\n\t\t\t\t\t\t\t\t\t<p>No session token found. Please close this and try logging in again.</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</body>\n\t\t\t\t\t\t</html>\n\t\t\t\t\t");o.info("✅ Session token found: "+r.substring(0,20)+"...");const i=await fetch(`${s}/users/me`,{headers:{Cookie:`directus_session_token=${r}`}});if(!i.ok)throw new Error("Failed to get user info: "+await i.text());const a=await i.json(),d=a.data.id,u=a.data.email,p=a.data.first_name||a.data.email;o.info("👤 User authenticated via Google: "+d+", "+u);const m=r;if(o.info("🎫 Using session token as access token"),n){o.info("🌐 Browser request detected - maintaining session"),e.cookie("directus_session_token",r,{httpOnly:!0,secure:f,domain:g,sameSite:h,maxAge:6048e5,path:"/"});const n=t.query.redirect_uri||t.query.redirect||"/";return o.info("🔄 Redirecting browser to: "+n),e.send(`\n\t\t\t\t\t\t<html>\n\t\t\t\t\t\t\t<head>\n\t\t\t\t\t\t\t\t<title>Login Successful</title>\n\t\t\t\t\t\t\t\t<meta http-equiv="refresh" content="2;url=${n}">\n\t\t\t\t\t\t\t\t<style>\n\t\t\t\t\t\t\t\t\tbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \n\t\t\t\t\t\t\t\t\t padding: 40px; text-align: center; background: #f5f5f5; }\n\t\t\t\t\t\t\t\t\t.container { max-width: 500px; margin: 0 auto; background: white; \n\t\t\t\t\t\t\t\t\t padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }\n\t\t\t\t\t\t\t\t\th2 { color: #27ae60; }\n\t\t\t\t\t\t\t\t\t.checkmark { font-size: 48px; color: #27ae60; margin-bottom: 20px; }\n\t\t\t\t\t\t\t\t\tp { color: #666; margin: 10px 0; }\n\t\t\t\t\t\t\t\t\t.spinner { border: 3px solid #f3f3f3; border-top: 3px solid #4285f4; \n\t\t\t\t\t\t\t\t\t border-radius: 50%; width: 40px; height: 40px; \n\t\t\t\t\t\t\t\t\t animation: spin 1s linear infinite; margin: 20px auto; }\n\t\t\t\t\t\t\t\t\t@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }\n\t\t\t\t\t\t\t\t\ta { color: #4285f4; text-decoration: none; }\n\t\t\t\t\t\t\t\t\ta:hover { text-decoration: underline; }\n\t\t\t\t\t\t\t\t\t.google-icon { color: #4285f4; margin-right: 5px; }\n\t\t\t\t\t\t\t\t</style>\n\t\t\t\t\t\t\t</head>\n\t\t\t\t\t\t\t<body>\n\t\t\t\t\t\t\t\t<div class="container">\n\t\t\t\t\t\t\t\t\t<div class="checkmark">✓</div>\n\t\t\t\t\t\t\t\t\t<h2><span class="google-icon">🔵</span>Google Login Successful!</h2>\n\t\t\t\t\t\t\t\t\t<p>Welcome, ${p}!</p>\n\t\t\t\t\t\t\t\t\t<p>Your session has been saved. You can now access other services using the same login.</p>\n\t\t\t\t\t\t\t\t\t<div class="spinner"></div>\n\t\t\t\t\t\t\t\t\t<p style="margin-top: 20px;">Redirecting you automatically...</p>\n\t\t\t\t\t\t\t\t\t<p style="font-size: 14px; margin-top: 20px;">\n\t\t\t\t\t\t\t\t\t\t<a href="${n}">Click here if not redirected automatically</a>\n\t\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</body>\n\t\t\t\t\t\t</html>\n\t\t\t\t\t`)}o.info("📱 Mobile app request - redirecting with token");const k=new URL(`${c}://${l}`);k.searchParams.set("access_token",m),k.searchParams.set("user_id",d),k.searchParams.set("email",u||""),k.searchParams.set("provider","google"),o.info("🔄 Redirecting to app (Google): "+k.toString()),e.redirect(k.toString())}catch(t){o.error("❌ Error in Google callback:",t),e.status(500).send(`\n\t\t\t\t\t<html>\n\t\t\t\t\t\t<head>\n\t\t\t\t\t\t\t<title>Error</title>\n\t\t\t\t\t\t\t<style>\n\t\t\t\t\t\t\t\tbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; \n\t\t\t\t\t\t\t\t padding: 40px; text-align: center; background: #f5f5f5; }\n\t\t\t\t\t\t\t\t.container { max-width: 500px; margin: 0 auto; background: white; \n\t\t\t\t\t\t\t\t padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }\n\t\t\t\t\t\t\t\th2 { color: #e74c3c; }\n\t\t\t\t\t\t\t\t.error-icon { font-size: 48px; color: #e74c3c; margin-bottom: 20px; }\n\t\t\t\t\t\t\t\tpre { background: #f8f8f8; padding: 15px; border-radius: 4px; \n\t\t\t\t\t\t\t\t text-align: left; overflow-x: auto; font-size: 12px; }\n\t\t\t\t\t\t\t\ta { display: inline-block; margin-top: 20px; padding: 10px 20px; \n\t\t\t\t\t\t\t\t background: #4285f4; color: white; text-decoration: none; border-radius: 4px; }\n\t\t\t\t\t\t\t\ta:hover { background: #357ae8; }\n\t\t\t\t\t\t\t</style>\n\t\t\t\t\t\t</head>\n\t\t\t\t\t\t<body>\n\t\t\t\t\t\t\t<div class="container">\n\t\t\t\t\t\t\t\t<div class="error-icon">⚠️</div>\n\t\t\t\t\t\t\t\t<h2>Error</h2>\n\t\t\t\t\t\t\t\t<p>An error occurred during Google authentication:</p>\n\t\t\t\t\t\t\t\t<pre>${t.message}</pre>\n\t\t\t\t\t\t\t\t<a href="${s}/auth/login/google">Try Again</a>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</body>\n\t\t\t\t\t</html>\n\t\t\t\t`)}}),t.post("/mobile-logout",async(t,e)=>{o.info("🚪 Logout request received");try{const c=t.headers.authorization,d=c?.replace("Bearer ","");if(!d)return e.status(400).json({error:"No token provided",message:"Authorization header with Bearer token is required"});o.info("🎫 Token received: "+d.substring(0,20)+"...");let l=null;try{const t=await fetch(`${s}/users/me`,{headers:{Authorization:`Bearer ${d}`}});if(t.ok){l=(await t.json()).data.email,o.info("👤 User email: "+l)}}catch(t){o.error("⚠️ Error getting user info: "+t.message)}try{const t=await fetch(`${s}/auth/logout`,{method:"POST",headers:{Authorization:`Bearer ${d}`,"Content-Type":"application/json"},body:JSON.stringify({refresh_token:d})});t.ok?o.info("✅ Directus session invalidated"):o.info("⚠️ Directus logout response: "+t.status)}catch(t){o.error("⚠️ Error logging out from Directus: "+t.message)}if(l)try{o.info("🔐 Getting Keycloak admin token...");const t=await async function(){try{const t=await fetch(`${n}/realms/master/protocol/openid-connect/token`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"password",client_id:u,username:i,password:a}).toString()});if(!t.ok)throw new Error("Failed to get admin token");return(await t.json()).access_token}catch(t){return o.error("Error getting admin token:",t),null}}();if(t){o.info("🔍 Looking up Keycloak user...");const e=await async function(t,e){try{const o=await fetch(`${n}/admin/realms/${r}/users?email=${encodeURIComponent(e)}`,{headers:{Authorization:`Bearer ${t}`}});if(!o.ok)throw new Error("Failed to get user");const i=await o.json();return i.length>0?i[0].id:null}catch(t){return o.error("Error getting user ID:",t),null}}(t,l);if(e){o.info("🚪 Logging out from Keycloak...");const i=await async function(t,e){try{const o=await fetch(`${n}/admin/realms/${r}/users/${e}/logout`,{method:"POST",headers:{Authorization:`Bearer ${t}`}});return o.ok||204===o.status}catch(t){return o.error("Error logging out user from Keycloak:",t),!1}}(t,e);i?o.info("✅ Keycloak sessions terminated"):o.info("⚠️ Failed to logout from Keycloak")}else o.info("⚠️ User not found in Keycloak")}else o.info("⚠️ Failed to get Keycloak admin token")}catch(t){o.error("⚠️ Error logging out from Keycloak: "+t.message)}o.info("🎉 Logout completed"),e.json({success:!0,message:"Logged out successfully from Directus and Keycloak"})}catch(t){o.error("❌ Error in logout:",t),e.status(500).json({error:t.message,message:"Failed to logout"})}})}};export{t as default};
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@ymys/directus-extension-sso",
3
+ "description": "Mobile OAuth proxy endpoints for Directus + Keycloak + Google",
4
+ "icon": "smartphone",
5
+ "version": "1.3.6",
6
+ "keywords": [
7
+ "directus",
8
+ "directus-extension",
9
+ "directus-extension-endpoint",
10
+ "sso",
11
+ "keycloak",
12
+ "google",
13
+ "oauth",
14
+ "mobile",
15
+ "auth",
16
+ "proxy"
17
+ ],
18
+ "author": "Yusuf M <ymys@users.noreply.github.com>",
19
+ "license": "MIT",
20
+ "homepage": "https://github.com/ymys/directus-sso#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/ymys/directus-sso/issues"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "files": [
29
+ "dist",
30
+ "package.json",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "directus:extension": {
35
+ "type": "endpoint",
36
+ "path": "dist/index.js",
37
+ "source": "src/index.js",
38
+ "host": "^11.0.0",
39
+ "id": "sso"
40
+ },
41
+ "scripts": {
42
+ "build": "directus-extension build",
43
+ "dev": "directus-extension build -w --no-minify",
44
+ "link": "directus-extension link"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/ymys/directus-sso.git"
49
+ },
50
+ "devDependencies": {
51
+ "@directus/extensions-sdk": "^12.0.2"
52
+ }
53
+ }