@bffless/claude-skills 1.1.1 → 1.2.0

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.md ADDED
@@ -0,0 +1,19 @@
1
+ # O'Saasy License
2
+
3
+ Copyright (c) 2026 BFFless, LLC
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 use,
7
+ copy, modify, and distribute the Software, subject to the following conditions:
8
+
9
+ 1. The above copyright notice and this permission notice shall be included in
10
+ all copies or substantial portions of the Software.
11
+
12
+ 2. The Software is provided "as is", without warranty of any kind, express or
13
+ implied, including but not limited to the warranties of merchantability,
14
+ fitness for a particular purpose and noninfringement.
15
+
16
+ 3. In no event shall the authors or copyright holders be liable for any claim,
17
+ damages or other liability, whether in an action of contract, tort or
18
+ otherwise, arising from, out of or in connection with the Software or the
19
+ use or other dealings in the Software.
package/README.md CHANGED
@@ -91,4 +91,4 @@ These are different from **BFFless pipeline skills**, which are markdown files y
91
91
 
92
92
  ## License
93
93
 
94
- MIT
94
+ [O'Saasy](./LICENSE.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bffless/claude-skills",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Claude Code plugin with BFFless platform skills",
5
5
  "keywords": [
6
6
  "claude-code-plugin",
@@ -10,7 +10,7 @@
10
10
  "static-hosting"
11
11
  ],
12
12
  "author": "BFFless",
13
- "license": "MIT",
13
+ "license": "O'Saasy",
14
14
  "homepage": "https://docs.bffless.app",
15
15
  "repository": {
16
16
  "type": "git",
@@ -0,0 +1,204 @@
1
+ ---
2
+ name: authentication
3
+ description: Cross-domain authentication using the admin login relay pattern, built-in /_bffless/auth endpoints, and cookie-based sessions
4
+ ---
5
+
6
+ # Authentication
7
+
8
+ BFFless uses a cross-domain authentication relay pattern. Users authenticate at the workspace's admin domain (`admin.<workspace>`) and are relayed back to the content domain with auth cookies. Auth endpoints on the content domain are accessed via the **built-in `/_bffless/auth/*` endpoints** — no proxy rules required.
9
+
10
+ ## How Authentication Works
11
+
12
+ ### Workspace Subdomains
13
+
14
+ For workspace subdomains (e.g., `myalias.sandbox.workspace.bffless.app`), SuperTokens session cookies (`sAccessToken`) work directly because they share the parent domain.
15
+
16
+ When a user visits a private deployment and isn't authenticated:
17
+
18
+ 1. Backend redirects to `https://admin.<workspace>/login?redirect=<original-path>&tryRefresh=true`
19
+ 2. The login page attempts a session refresh first (the `tryRefresh` param)
20
+ 3. If refresh fails, the user logs in normally
21
+ 4. After login, the user is redirected back to the original path
22
+ 5. The `sAccessToken` cookie is valid across all subdomains of the workspace
23
+
24
+ ### Custom Domains (customDomainRelay)
25
+
26
+ For custom domains (e.g., `www.bffless.com`), SuperTokens cookies don't work because they're on a completely different domain. BFFless uses a **domain relay** flow:
27
+
28
+ 1. User visits a private page on `www.bffless.com/portal/`
29
+ 2. Frontend detects the user is not authenticated (via `/_bffless/auth/session`)
30
+ 3. Frontend redirects to the admin login with relay params:
31
+ ```
32
+ https://admin.console.bffless.app/login?customDomainRelay=true&targetDomain=www.bffless.com&redirect=%2Fportal%2F
33
+ ```
34
+ 4. User logs in on the admin domain (or is already logged in via SuperTokens session)
35
+ 5. After login, the frontend calls `POST /api/auth/domain-token` with:
36
+ ```json
37
+ { "targetDomain": "www.bffless.com", "redirectPath": "/portal/" }
38
+ ```
39
+ 6. Backend validates that `targetDomain` is a registered domain for this workspace, then creates a short-lived JWT (the "domain token")
40
+ 7. Backend returns a `redirectUrl` pointing to the callback on the custom domain: `https://www.bffless.com/_bffless/auth/callback?token=...&redirect=/portal/`
41
+ 8. The callback endpoint validates the token, sets `bffless_access` and `bffless_refresh` HttpOnly cookies, and redirects to the original path
42
+
43
+ ### Important: Use `/_bffless/auth/*`, NOT `/api/auth/*`
44
+
45
+ The `/_bffless/auth/*` endpoints are **built into BFFless nginx** and handled by a dedicated controller. They are separate from the SuperTokens `/api/auth/*` endpoints. Do NOT use `/api/auth/*` on custom domains — those are SuperTokens endpoints that use different cookies (`sAccessToken`) which are not set by the domain relay flow.
46
+
47
+ The domain relay callback sets `bffless_access` and `bffless_refresh` cookies, which are only recognized by the `/_bffless/auth/*` endpoints. Using `/api/auth/session` instead of `/_bffless/auth/session` will cause a redirect loop because the SuperTokens session check won't find the `bffless_access` cookie.
48
+
49
+ ## Auth Endpoints (Built-in)
50
+
51
+ All auth endpoints are available at `/_bffless/auth/*` on any domain served by BFFless — no proxy rules needed.
52
+
53
+ | Endpoint | Method | Purpose |
54
+ | --------------------------- | ------ | -------------------------------------------------------- |
55
+ | `/_bffless/auth/session` | GET | Check current session (returns user info or 401) |
56
+ | `/_bffless/auth/refresh` | POST | Refresh an expired access token using the refresh cookie |
57
+ | `/_bffless/auth/callback` | GET | Exchange a domain relay token for auth cookies |
58
+ | `/_bffless/auth/logout` | POST | Clear auth cookies |
59
+
60
+ ### Session Check Priority
61
+
62
+ The `/_bffless/auth/session` endpoint checks auth in this order:
63
+
64
+ 1. **`bffless_access` cookie** — custom domain JWT issued by the callback flow
65
+ 2. **`sAccessToken` cookie** — SuperTokens session (fallback for workspace subdomains)
66
+
67
+ If the access token is expired, it returns `401` with `"try refresh token"` to signal the client should call `/_bffless/auth/refresh`.
68
+
69
+ ## Frontend Integration
70
+
71
+ ### Checking Session (with automatic token refresh)
72
+
73
+ Use a shared promise pattern to avoid duplicate session checks across components:
74
+
75
+ ```typescript
76
+ async function checkSession() {
77
+ // Reuse shared session promise so multiple components don't duplicate requests
78
+ if (!(window as any).__bfflessSession) {
79
+ (window as any).__bfflessSession = (async () => {
80
+ const res = await fetch('/_bffless/auth/session', { credentials: 'include' });
81
+ if (res.ok) return res.json();
82
+
83
+ if (res.status === 401) {
84
+ // Token expired — try refreshing
85
+ const refreshRes = await fetch('/_bffless/auth/refresh', {
86
+ method: 'POST',
87
+ credentials: 'include',
88
+ });
89
+ if (refreshRes.ok) {
90
+ // Retry session check with new token
91
+ const retryRes = await fetch('/_bffless/auth/session', { credentials: 'include' });
92
+ if (retryRes.ok) return retryRes.json();
93
+ }
94
+ }
95
+ return null;
96
+ })().catch(() => null);
97
+ }
98
+
99
+ return (window as any).__bfflessSession;
100
+ }
101
+
102
+ // Returns: { authenticated: true, user: { id, email, role } } or null
103
+ ```
104
+
105
+ The flow is: session check → if 401, refresh token → retry session check. This handles the common case where the access token has expired but the refresh token is still valid.
106
+
107
+ ### Redirecting to Login
108
+
109
+ When unauthenticated, redirect the user to the admin login with relay params. Use the **promoted admin domain** (e.g., `admin.console.bffless.app`), not the full workspace subdomain:
110
+
111
+ ```typescript
112
+ function getLoginUrl(adminLoginUrl: string, redirectPath: string): string {
113
+ const targetDomain = window.location.hostname;
114
+ const params = new URLSearchParams({
115
+ customDomainRelay: 'true',
116
+ targetDomain,
117
+ redirect: redirectPath,
118
+ });
119
+ return `${adminLoginUrl}?${params.toString()}`;
120
+ }
121
+
122
+ // Example: redirect to admin login, then relay back to /portal/
123
+ const session = await checkSession();
124
+ if (!session) {
125
+ window.location.href = getLoginUrl(
126
+ 'https://admin.console.bffless.app/login',
127
+ '/portal/',
128
+ );
129
+ }
130
+ ```
131
+
132
+ ### Logout
133
+
134
+ ```typescript
135
+ await fetch('/_bffless/auth/logout', { method: 'POST', credentials: 'include' });
136
+ ```
137
+
138
+ ### Updating UI Based on Auth State (Header example)
139
+
140
+ ```typescript
141
+ // Check auth state and update Login/Portal links
142
+ window.__bfflessSession = window.__bfflessSession || checkBfflessSession().catch(() => null);
143
+
144
+ window.__bfflessSession.then((data) => {
145
+ if (data?.authenticated) {
146
+ // User is logged in — update nav links
147
+ document.querySelectorAll('[data-auth-link]').forEach((el) => {
148
+ el.textContent = 'Portal';
149
+ });
150
+ }
151
+ });
152
+ ```
153
+
154
+ ## Auth Flow Diagram
155
+
156
+ ```
157
+ Custom Domain Flow:
158
+ ┌──────────────────┐ JS redirect ┌──────────────────────────┐
159
+ │ www.bffless.com │ ──────────────────→ │ admin.<workspace>/login │
160
+ │ (private page) │ customDomainRelay= │ ?customDomainRelay=true │
161
+ │ │ true&targetDomain= │ &targetDomain=www... │
162
+ └──────────────────┘ www.bffless.com └────────────┬─────────────┘
163
+ ▲ │
164
+ │ User logs in (SuperTokens)
165
+ │ │
166
+ │ ▼
167
+ │ POST /api/auth/domain-token
168
+ │ → returns { token, redirectUrl }
169
+ │ │
170
+ │ 302 redirect │
171
+ │ ←─────────────────────────────────────────────┘
172
+ │ to: www.bffless.com/_bffless/auth/callback?token=...
173
+
174
+
175
+ ┌──────────────────┐
176
+ │ /_bffless/auth │ Validates token, sets bffless_access
177
+ │ /callback │ + bffless_refresh cookies
178
+ │ (built-in) │ → 302 redirect to /portal/
179
+ └──────────────────┘
180
+ ```
181
+
182
+ ## Troubleshooting
183
+
184
+ **User gets stuck in a redirect loop?**
185
+
186
+ - **Most common cause:** Using `/api/auth/session` instead of `/_bffless/auth/session`. The domain relay callback sets `bffless_access` cookies which are only recognized by `/_bffless/auth/*` endpoints. The `/api/auth/*` endpoints check SuperTokens cookies (`sAccessToken`) which are NOT set by the domain relay flow.
187
+ - Verify the custom domain is registered in `domain_mappings` with `isActive = true`
188
+ - Ensure cookies are being set (requires HTTPS for `Secure` flag)
189
+
190
+ **"Domain not registered" error on domain-token?**
191
+
192
+ - The `targetDomain` must match a `domain_mappings` entry or be a subdomain of `PRIMARY_DOMAIN`
193
+ - Check for www vs non-www mismatch
194
+
195
+ **Session check returns 401 but user just logged in?**
196
+
197
+ - On custom domains: verify the `/_bffless/auth/callback` was reached and cookies were set
198
+ - On workspace subdomains: verify `COOKIE_DOMAIN` is configured for cross-subdomain cookie sharing
199
+ - Check that the `bffless_access` or `sAccessToken` cookie is present in the request
200
+
201
+ **Admin login URL — use promoted domain, not workspace subdomain:**
202
+
203
+ - If the workspace has a promoted domain (e.g., `console.bffless.app`), use `admin.console.bffless.app`, NOT `admin.console.workspace.bffless.app`
204
+ - The workspace subdomain format still works but the promoted domain is cleaner