showpane 0.4.0 → 0.4.2
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 +22 -1
- package/bundle/meta/scaffold-manifest.json +73 -0
- package/bundle/scaffold/VERSION +1 -0
- package/bundle/scaffold/__dot__env.example +24 -0
- package/bundle/scaffold/__dot__gitignore +41 -0
- package/bundle/scaffold/docker/Caddyfile +3 -0
- package/bundle/scaffold/docker/Dockerfile +30 -0
- package/bundle/scaffold/docker-compose.yml +53 -0
- package/bundle/scaffold/next.config.ts +20 -0
- package/bundle/scaffold/package-lock.json +5843 -0
- package/bundle/scaffold/package.json +42 -0
- package/bundle/scaffold/postcss.config.js +6 -0
- package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
- package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
- package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
- package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
- package/bundle/scaffold/prisma/schema.local.prisma +131 -0
- package/bundle/scaffold/prisma/schema.prisma +128 -0
- package/bundle/scaffold/prisma/seed.ts +49 -0
- package/bundle/scaffold/public/example-avatar.svg +4 -0
- package/bundle/scaffold/public/example-logo.svg +4 -0
- package/bundle/scaffold/public/robots.txt +2 -0
- package/bundle/scaffold/scripts/backup.sh +19 -0
- package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
- package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
- package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
- package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
- package/bundle/scaffold/scripts/restore.sh +31 -0
- package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
- package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
- package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
- package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
- package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
- package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
- package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
- package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
- package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
- package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
- package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
- package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
- package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
- package/bundle/scaffold/src/app/api/health/route.ts +19 -0
- package/bundle/scaffold/src/app/globals.css +7 -0
- package/bundle/scaffold/src/app/layout.tsx +25 -0
- package/bundle/scaffold/src/app/page.tsx +171 -0
- package/bundle/scaffold/src/components/portal-login.tsx +169 -0
- package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
- package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
- package/bundle/scaffold/src/lib/branding.ts +50 -0
- package/bundle/scaffold/src/lib/client-auth.ts +98 -0
- package/bundle/scaffold/src/lib/client-portals.ts +134 -0
- package/bundle/scaffold/src/lib/control-plane.ts +100 -0
- package/bundle/scaffold/src/lib/db.ts +7 -0
- package/bundle/scaffold/src/lib/files.ts +124 -0
- package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
- package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
- package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
- package/bundle/scaffold/src/lib/storage.ts +204 -0
- package/bundle/scaffold/src/lib/token.ts +186 -0
- package/bundle/scaffold/src/lib/utils.ts +6 -0
- package/bundle/scaffold/src/middleware.ts +61 -0
- package/bundle/scaffold/tailwind.config.ts +15 -0
- package/bundle/scaffold/tests/__dot__gitkeep +0 -0
- package/bundle/scaffold/tsconfig.json +23 -0
- package/bundle/scaffold/vitest.config.ts +13 -0
- package/bundle/toolchain/VERSION +1 -0
- package/bundle/toolchain/bin/check-slug.ts +59 -0
- package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
- package/bundle/toolchain/bin/create-portal.ts +71 -0
- package/bundle/toolchain/bin/delete-portal.ts +48 -0
- package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
- package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
- package/bundle/toolchain/bin/generate-share-link.ts +68 -0
- package/bundle/toolchain/bin/list-portals.ts +53 -0
- package/bundle/toolchain/bin/materialize-file.ts +35 -0
- package/bundle/toolchain/bin/query-analytics.ts +88 -0
- package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
- package/bundle/toolchain/bin/showpane-config +63 -0
- package/bundle/toolchain/bin/tsconfig.json +13 -0
- package/bundle/toolchain/skills/VERSION +1 -0
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
- package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
- package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
- package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
- package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
- package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
- package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
- package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
- package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
- package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
- package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
- package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
- package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
- package/bundle/toolchain/skills/shared/preamble.md +137 -0
- package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
- package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
- package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
- package/dist/index.js +875 -159
- package/package.json +4 -2
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: portal-credentials
|
|
3
|
+
description: |
|
|
4
|
+
Create or rotate login credentials for a client portal. Generates username and password, hashes and stores in DB.
|
|
5
|
+
Trigger phrases: "portal credentials", "create login", "rotate password", "reset credentials", "portal password". (showpane)
|
|
6
|
+
allowed-tools: [Bash, Read, Write, Edit]
|
|
7
|
+
hooks:
|
|
8
|
+
PreToolUse:
|
|
9
|
+
- matcher: "Bash"
|
|
10
|
+
hooks:
|
|
11
|
+
- type: command
|
|
12
|
+
command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
|
|
13
|
+
- matcher: "Edit"
|
|
14
|
+
hooks:
|
|
15
|
+
- type: command
|
|
16
|
+
command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Preamble (run first)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Read config
|
|
23
|
+
CONFIG="$HOME/.showpane/config.json"
|
|
24
|
+
if [ ! -f "$CONFIG" ]; then
|
|
25
|
+
echo "Showpane not configured. Run /portal setup first."
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
APP_PATH=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
|
|
29
|
+
DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','docker'))" 2>/dev/null)
|
|
30
|
+
ORG_SLUG=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
|
|
31
|
+
APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
|
|
32
|
+
if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
|
|
33
|
+
DATABASE_URL="${DATABASE_URL:-}"
|
|
34
|
+
if [ ! -d "$APP_PATH/node_modules/.prisma" ]; then
|
|
35
|
+
echo "App dependencies not installed. Run: cd $APP_PATH && npm install"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
SKILL_DIR="${SHOWPANE_TOOLCHAIN_DIR:-$HOME/.showpane/current}"
|
|
39
|
+
SKILL_VERSION=$(cat "$SKILL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
|
40
|
+
echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
|
|
41
|
+
LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
42
|
+
[ -f "$LEARN_FILE" ] && echo "LEARNINGS: $(wc -l < "$LEARN_FILE" | tr -d ' ') loaded" || echo "LEARNINGS: 0"
|
|
43
|
+
|
|
44
|
+
# Predictive next-skill suggestion
|
|
45
|
+
if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
|
|
46
|
+
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
|
|
47
|
+
[ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Search relevant learnings
|
|
51
|
+
LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
52
|
+
if [ -f "$LEARN_FILE" ]; then
|
|
53
|
+
_LEARN_COUNT=$(wc -l < "$LEARN_FILE" 2>/dev/null | tr -d ' ')
|
|
54
|
+
echo "LEARNINGS: $_LEARN_COUNT entries"
|
|
55
|
+
if [ "$_LEARN_COUNT" -gt 0 ] 2>/dev/null; then
|
|
56
|
+
echo "RECENT_LEARNINGS:"
|
|
57
|
+
tail -5 "$LEARN_FILE" 2>/dev/null
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Track skill execution
|
|
62
|
+
SHOWPANE_TIMELINE="$HOME/.showpane/timeline.jsonl"
|
|
63
|
+
mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
|
|
64
|
+
echo '{"skill":"portal-credentials","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If RECENT_SKILLS is shown, suggest the likely next skill:
|
|
68
|
+
- After portal-create → suggest /portal-preview
|
|
69
|
+
- After portal-preview → suggest /portal-deploy or /portal-share
|
|
70
|
+
- After portal-deploy → suggest /portal-status or /portal-verify
|
|
71
|
+
- After portal-setup → suggest /portal-create
|
|
72
|
+
- After portal-credentials → suggest /portal-share
|
|
73
|
+
- After portal-update → suggest /portal-deploy
|
|
74
|
+
|
|
75
|
+
If RECENT_LEARNINGS is shown, review them before proceeding. Past learnings may contain
|
|
76
|
+
relevant warnings or tips for this operation. Apply them where relevant but don't
|
|
77
|
+
mention them unless they directly affect the current task.
|
|
78
|
+
|
|
79
|
+
## Steps
|
|
80
|
+
|
|
81
|
+
### Step 1: Identify the portal
|
|
82
|
+
|
|
83
|
+
If the user provided a slug (e.g., `/portal credentials acme-health`), use it. Otherwise, list available portals to help the user choose:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/list-portals.ts" --org-id <org_id>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Present the list and ask which portal needs credentials. If there is only one portal, confirm it rather than asking.
|
|
90
|
+
|
|
91
|
+
Verify the portal exists by checking the database. The `create-portal.ts` script should have been run during `/portal create` to register the portal. If no DB record exists, inform the user:
|
|
92
|
+
|
|
93
|
+
> "No portal record found for '<slug>'. Run `/portal create <slug>` first to register it."
|
|
94
|
+
|
|
95
|
+
Also check the portal's current credential status from the list output. If credentials already exist, inform the user before proceeding:
|
|
96
|
+
|
|
97
|
+
> "Portal '<slug>' already has credentials (username: <username>). Running this will rotate to a new password and invalidate all existing sessions. Continue? (y/N)"
|
|
98
|
+
|
|
99
|
+
This confirmation is important because credential rotation has an immediate impact on any clients currently logged in. Default to "no" to prevent accidental rotation.
|
|
100
|
+
|
|
101
|
+
### Step 2: Run the rotate-credentials script
|
|
102
|
+
|
|
103
|
+
This script handles both initial credential creation and rotation:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/rotate-credentials.ts" --slug <slug> --org-id <org_id>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The script returns JSON on stdout:
|
|
110
|
+
|
|
111
|
+
**For initial creation:**
|
|
112
|
+
```json
|
|
113
|
+
{"ok": true, "username": "acme-health", "password": "xK9mP2vL8nQr", "rotated": false}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**For rotation (credentials already existed):**
|
|
117
|
+
```json
|
|
118
|
+
{"ok": true, "username": "acme-health", "password": "bT4wN7jF3hYs", "rotated": true}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**On error:**
|
|
122
|
+
```json
|
|
123
|
+
{"error": "portal_not_found", "message": "No portal with slug 'acme-health' in org 'demo'"}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Step 3: Handle script errors
|
|
127
|
+
|
|
128
|
+
If the script returns an error, handle it based on the error type:
|
|
129
|
+
|
|
130
|
+
- `portal_not_found` — The portal slug doesn't exist in the database for this org. Suggest running `/portal create <slug>` first.
|
|
131
|
+
- `database_error` — Connection or query failure. Check DATABASE_URL, verify the database is running, and check that migrations are up to date (`cd $APP_PATH && npx prisma migrate deploy`).
|
|
132
|
+
- `auth_secret_missing` — The AUTH_SECRET environment variable is not set in `$APP_PATH/.env`. The rotate-credentials script needs this to function. Suggest adding `AUTH_SECRET=<random-string>` to the .env file.
|
|
133
|
+
|
|
134
|
+
If the script exits with a non-zero code but produces no JSON output, check stderr for Prisma or Node.js errors and relay them to the user.
|
|
135
|
+
|
|
136
|
+
### Step 4: Display credentials
|
|
137
|
+
|
|
138
|
+
Present the credentials clearly and prominently. Use an ASCII box for visibility:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
┌─────────────────────────────────────────┐
|
|
142
|
+
│ Portal credentials for: acme-health │
|
|
143
|
+
│ │
|
|
144
|
+
│ Username: acme-health │
|
|
145
|
+
│ Password: xK9mP2vL8nQr │
|
|
146
|
+
│ │
|
|
147
|
+
│ Login URL: /client │
|
|
148
|
+
└─────────────────────────────────────────┘
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The ASCII box ensures the credentials stand out clearly in terminal output. Do not use markdown formatting (bold, code blocks) as these may not render in all terminal environments.
|
|
152
|
+
|
|
153
|
+
### Step 5: Security warning
|
|
154
|
+
|
|
155
|
+
Immediately after displaying credentials, show this warning:
|
|
156
|
+
|
|
157
|
+
> **Save these credentials now.** They will not be shown again. The password is hashed in the database and cannot be retrieved. If lost, run `/portal credentials <slug>` again to rotate to a new password.
|
|
158
|
+
|
|
159
|
+
If this was a rotation (`"rotated": true`), add:
|
|
160
|
+
|
|
161
|
+
> **Credential rotation complete.** All existing sessions for this portal have been invalidated immediately. Anyone currently logged in will need to re-authenticate with the new password.
|
|
162
|
+
|
|
163
|
+
### Step 6: Suggest sharing method
|
|
164
|
+
|
|
165
|
+
Provide guidance on how to share the credentials with the client:
|
|
166
|
+
|
|
167
|
+
> **How to share with your client:**
|
|
168
|
+
> - Send the username and password via a secure channel (encrypted email, Signal, etc.)
|
|
169
|
+
> - The login page is at `/client` — the client enters the portal slug as the "company" field, then the username and password
|
|
170
|
+
> - For a direct link without credentials, use `/portal share <slug>` to generate a reusable share URL that bypasses the login entirely
|
|
171
|
+
|
|
172
|
+
Recommend against sharing credentials via:
|
|
173
|
+
- Unencrypted email (can be intercepted)
|
|
174
|
+
- Slack or Teams messages (persistent and searchable by admins)
|
|
175
|
+
- Shared documents or spreadsheets
|
|
176
|
+
|
|
177
|
+
Good sharing channels:
|
|
178
|
+
- Signal or WhatsApp (encrypted, ephemeral)
|
|
179
|
+
- A phone call (verbal)
|
|
180
|
+
- An encrypted email service
|
|
181
|
+
- A password manager's secure sharing feature
|
|
182
|
+
|
|
183
|
+
### Step 7: Verify login works (if dev server running)
|
|
184
|
+
|
|
185
|
+
If the dev server is running, suggest testing the credentials:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
lsof -i :3000 -sTCP:LISTEN -t 2>/dev/null
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If running, inform the user:
|
|
192
|
+
|
|
193
|
+
> "Test the login at http://localhost:3000/client — enter slug '<slug>', username '<username>', and the password shown above."
|
|
194
|
+
|
|
195
|
+
This step is optional but helps catch configuration issues (like AUTH_SECRET not being set) before sharing credentials with a client.
|
|
196
|
+
|
|
197
|
+
### Step 8: Summary
|
|
198
|
+
|
|
199
|
+
Print a brief summary:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
Credentials created for: acme-health
|
|
203
|
+
Action: new credentials (or: rotated — old sessions invalidated)
|
|
204
|
+
Username: acme-health
|
|
205
|
+
Login: /client
|
|
206
|
+
|
|
207
|
+
Next steps:
|
|
208
|
+
1. Send credentials to the client via a secure channel
|
|
209
|
+
2. Generate a share link: /portal share <slug>
|
|
210
|
+
3. Deploy if not already live: /portal deploy
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Step 9: Record credential event
|
|
214
|
+
|
|
215
|
+
Record that credentials were created or rotated (but NEVER record the actual credentials):
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
echo '{"skill":"portal-credentials","key":"credentials-created","insight":"Created credentials for <slug>. Rotated: <true|false>.","confidence":10,"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/learnings.jsonl"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Security Conventions
|
|
222
|
+
|
|
223
|
+
- **Credentials are shown exactly once.** Never log them, never write them to a file, never include them in learnings or telemetry.
|
|
224
|
+
- **Rotation is immediate.** When credentials are rotated, the `credentialVersion` is bumped in the database. All existing session tokens for that portal are invalidated instantly because they reference the old version.
|
|
225
|
+
- **Passwords are generated server-side** by the `rotate-credentials.ts` script using cryptographically secure random bytes. Do not generate passwords in the skill — always use the script.
|
|
226
|
+
- **Usernames are derived from the slug.** The username is typically the same as the portal slug (e.g., slug `acme-health` gets username `acme-health`).
|
|
227
|
+
- **No password in config or learnings.** The `~/.showpane/config.json` and `~/.showpane/learnings.jsonl` files must never contain passwords or password hashes.
|
|
228
|
+
- **AUTH_SECRET stays in .env.** The secret used for token signing lives in `$APP_PATH/.env` and is read at runtime by the app. It is never copied to config.json.
|
|
229
|
+
|
|
230
|
+
## Error Handling
|
|
231
|
+
|
|
232
|
+
- **Script not found**: If `$SKILL_DIR/bin/rotate-credentials.ts` does not exist, the skill pack may not be fully installed. Suggest running `/portal upgrade` or checking the Showpane installation.
|
|
233
|
+
- **Prisma connection error**: DATABASE_URL may be wrong or the database may be down. Check the .env file and database status.
|
|
234
|
+
- **Permission denied on .env**: The preamble sources `$APP_PATH/.env`. If this file is not readable, the DATABASE_URL will not be set. Check file permissions.
|
|
235
|
+
- **Script timeout**: If the script takes more than 30 seconds, something is wrong. The most common cause is a database connection timeout — check network connectivity to the database host.
|
|
236
|
+
|
|
237
|
+
## Bulk credential creation
|
|
238
|
+
|
|
239
|
+
If the user asks to create credentials for multiple portals at once, run the rotate-credentials script for each portal sequentially. Display all credential sets together in a single output block:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
┌─────────────────────────────────────────┐
|
|
243
|
+
│ 1. acme-health │
|
|
244
|
+
│ Username: acme-health │
|
|
245
|
+
│ Password: xK9mP2vL8nQr │
|
|
246
|
+
│ │
|
|
247
|
+
│ 2. beta-corp │
|
|
248
|
+
│ Username: beta-corp │
|
|
249
|
+
│ Password: mT7kR4wQ9nFs │
|
|
250
|
+
└─────────────────────────────────────────┘
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Even in bulk mode, show the security warning once after all credentials are displayed.
|
|
254
|
+
|
|
255
|
+
## Completion
|
|
256
|
+
|
|
257
|
+
As a final step, log skill completion:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
echo '{"skill":"portal-credentials","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/timeline.jsonl" 2>/dev/null
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Conventions
|
|
264
|
+
|
|
265
|
+
- Always run the bin script rather than manipulating the database directly — never generate passwords, hash them, or write to the database from the skill
|
|
266
|
+
- Show credentials in an ASCII box for clear visibility in the terminal
|
|
267
|
+
- Warn about credential impermanence immediately after displaying them
|
|
268
|
+
- If the portal has no DB record, direct the user to `/portal create` first
|
|
269
|
+
- Never store, log, or record the plaintext password anywhere — not in learnings, not in telemetry, not in config
|
|
270
|
+
- Confirm before rotating existing credentials (default to "no") since rotation invalidates sessions
|
|
271
|
+
- The username is always the same as the portal slug — this is by design for simplicity
|
|
272
|
+
- If the user asks "what is the password for X portal", explain that passwords are one-way hashed and cannot be retrieved — offer to rotate to a new one instead
|
|
273
|
+
- After creating credentials, always suggest the next step: either share with the client or deploy if not already live
|
|
274
|
+
- If the app is not deployed yet, credentials still work — they are stored in the database regardless of whether the app is running in production
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: portal-delete
|
|
3
|
+
description: |
|
|
4
|
+
Deactivate a client portal. Use when asked to "delete portal", "remove portal",
|
|
5
|
+
"deactivate portal", "archive portal", or "shut down portal". (showpane)
|
|
6
|
+
allowed-tools: [Bash, Read, Write, Edit]
|
|
7
|
+
hooks:
|
|
8
|
+
PreToolUse:
|
|
9
|
+
- matcher: "Bash"
|
|
10
|
+
hooks:
|
|
11
|
+
- type: command
|
|
12
|
+
command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
|
|
13
|
+
- matcher: "Edit"
|
|
14
|
+
hooks:
|
|
15
|
+
- type: command
|
|
16
|
+
command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Preamble (run first)
|
|
20
|
+
|
|
21
|
+
Before doing anything else, execute this block in a Bash tool call:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
CONFIG="$HOME/.showpane/config.json"
|
|
25
|
+
if [ ! -f "$CONFIG" ]; then
|
|
26
|
+
echo "Showpane not configured. Run /portal setup first."
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
APP_PATH=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('app_path',''))" 2>/dev/null)
|
|
30
|
+
DEPLOY_MODE=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('deploy_mode','docker'))" 2>/dev/null)
|
|
31
|
+
ORG_SLUG=$(cat "$CONFIG" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
|
|
32
|
+
APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
|
|
33
|
+
if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
|
|
34
|
+
DATABASE_URL="${DATABASE_URL:-}"
|
|
35
|
+
if [ ! -d "$APP_PATH/node_modules/.prisma" ]; then
|
|
36
|
+
echo "App dependencies not installed. Run: cd $APP_PATH && npm install"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
SKILL_DIR="${SHOWPANE_TOOLCHAIN_DIR:-$HOME/.showpane/current}"
|
|
40
|
+
SKILL_VERSION=$(cat "$SKILL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
|
41
|
+
echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
|
|
42
|
+
LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
43
|
+
[ -f "$LEARN_FILE" ] && echo "LEARNINGS: $(wc -l < "$LEARN_FILE" | tr -d ' ') loaded" || echo "LEARNINGS: 0"
|
|
44
|
+
|
|
45
|
+
# Predictive next-skill suggestion
|
|
46
|
+
if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
|
|
47
|
+
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
|
|
48
|
+
[ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Search relevant learnings
|
|
52
|
+
LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
53
|
+
if [ -f "$LEARN_FILE" ]; then
|
|
54
|
+
_LEARN_COUNT=$(wc -l < "$LEARN_FILE" 2>/dev/null | tr -d ' ')
|
|
55
|
+
echo "LEARNINGS: $_LEARN_COUNT entries"
|
|
56
|
+
if [ "$_LEARN_COUNT" -gt 0 ] 2>/dev/null; then
|
|
57
|
+
echo "RECENT_LEARNINGS:"
|
|
58
|
+
tail -5 "$LEARN_FILE" 2>/dev/null
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Track skill execution
|
|
63
|
+
SHOWPANE_TIMELINE="$HOME/.showpane/timeline.jsonl"
|
|
64
|
+
mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
|
|
65
|
+
echo '{"skill":"portal-delete","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If RECENT_SKILLS is shown, suggest the likely next skill:
|
|
69
|
+
- After portal-create → suggest /portal-preview
|
|
70
|
+
- After portal-preview → suggest /portal-deploy or /portal-share
|
|
71
|
+
- After portal-deploy → suggest /portal-status or /portal-verify
|
|
72
|
+
- After portal-setup → suggest /portal-create
|
|
73
|
+
- After portal-credentials → suggest /portal-share
|
|
74
|
+
- After portal-update → suggest /portal-deploy
|
|
75
|
+
|
|
76
|
+
If RECENT_LEARNINGS is shown, review them before proceeding. Past learnings may contain
|
|
77
|
+
relevant warnings or tips for this operation. Apply them where relevant but don't
|
|
78
|
+
mention them unless they directly affect the current task.
|
|
79
|
+
|
|
80
|
+
## Safety Guard
|
|
81
|
+
|
|
82
|
+
This skill has a PreToolUse guard that warns before destructive operations
|
|
83
|
+
(database resets, file deletion, Vercel project removal). If the guard
|
|
84
|
+
triggers, confirm the action is intentional before proceeding.
|
|
85
|
+
|
|
86
|
+
## Overview
|
|
87
|
+
|
|
88
|
+
This skill deactivates a client portal by setting `isActive` to `false` in the database. It is a soft delete -- the portal page files remain in the git repository and the database record is preserved, but the portal becomes inaccessible to clients. Existing authenticated sessions and share links stop working because token validation requires an active portal record.
|
|
89
|
+
|
|
90
|
+
Soft delete is intentional. Nothing is truly destroyed. If the user changes their mind, the portal can be reactivated by updating the database record directly. This design avoids the need for a separate "undo" mechanism.
|
|
91
|
+
|
|
92
|
+
## Steps
|
|
93
|
+
|
|
94
|
+
### Step 1: Identify the target portal
|
|
95
|
+
|
|
96
|
+
The user must specify which portal to deactivate. If no slug is provided, ask: "Which portal do you want to deactivate? Run /portal list to see your portals."
|
|
97
|
+
|
|
98
|
+
Do not proceed without a confirmed slug.
|
|
99
|
+
|
|
100
|
+
### Step 2: Confirm with the user
|
|
101
|
+
|
|
102
|
+
Before executing the delete, present a confirmation prompt. This is the one moment where the skill pauses for human input:
|
|
103
|
+
|
|
104
|
+
"This will deactivate portal '<slug>'. Existing sessions will expire within 7 days. The page files will remain in git but won't be accessible. Continue?"
|
|
105
|
+
|
|
106
|
+
Wait for the user to confirm. Accept "yes", "y", "continue", "go ahead", or similar affirmatives. If the user says "no", "cancel", "wait", or anything non-affirmative, abort: "Cancelled. Portal '<slug>' remains active."
|
|
107
|
+
|
|
108
|
+
Do NOT skip this confirmation step. Even though the operation is reversible, deactivating a portal affects live client access. The user should consciously decide.
|
|
109
|
+
|
|
110
|
+
### Step 3: Execute the deactivation
|
|
111
|
+
|
|
112
|
+
Run the delete script:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd $APP_PATH && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig $APP_PATH/tsconfig.json $SKILL_DIR/bin/delete-portal.ts --slug <slug> --org-id <org_id>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Expected success response:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"ok": true,
|
|
123
|
+
"slug": "whzan",
|
|
124
|
+
"previousStatus": "active",
|
|
125
|
+
"newStatus": "inactive",
|
|
126
|
+
"sessionsAffected": 3,
|
|
127
|
+
"shareLinksAffected": 1
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Expected error responses:
|
|
132
|
+
|
|
133
|
+
- `portal_not_found`: The slug does not exist in this organization. Suggest `/portal list`.
|
|
134
|
+
- `already_inactive`: The portal is already deactivated. Inform the user: "Portal '<slug>' is already inactive. No changes made."
|
|
135
|
+
|
|
136
|
+
### Step 4: Print confirmation
|
|
137
|
+
|
|
138
|
+
Display a clear confirmation of what happened:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
════════════════════════════════════════
|
|
142
|
+
Portal deactivated: whzan
|
|
143
|
+
|
|
144
|
+
Status: Active -> Inactive
|
|
145
|
+
Sessions affected: 3 (will expire within 7 days)
|
|
146
|
+
Share links affected: 1 (invalidated by deactivation)
|
|
147
|
+
|
|
148
|
+
Page files remain in:
|
|
149
|
+
src/app/(portal)/client/whzan/
|
|
150
|
+
════════════════════════════════════════
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Step 5: Explain what happens next
|
|
154
|
+
|
|
155
|
+
After the confirmation box, provide a brief explanation of the consequences:
|
|
156
|
+
|
|
157
|
+
- "The portal URL will return a 'not found' page for any new visitors."
|
|
158
|
+
- "Clients with active sessions can still access the portal until their session expires (up to 7 days)."
|
|
159
|
+
- "The page files (`page.tsx`, `*-client.tsx`) remain in the codebase. You can delete them from git manually if you want to clean up, or leave them for potential reactivation."
|
|
160
|
+
|
|
161
|
+
If the deploy mode is `vercel` or `docker`, note: "The deactivation takes effect immediately -- no redeployment needed. The app checks the `isActive` flag at request time."
|
|
162
|
+
|
|
163
|
+
### Step 6: Suggest next actions
|
|
164
|
+
|
|
165
|
+
Depending on context:
|
|
166
|
+
|
|
167
|
+
- "To reactivate this portal later, update the database record: set `isActive` to `true` on the ClientPortal with slug '<slug>'."
|
|
168
|
+
- "To permanently remove the page files: `git rm -r src/app/(portal)/client/<slug>/` and commit."
|
|
169
|
+
- "To see your remaining portals: /portal list"
|
|
170
|
+
|
|
171
|
+
## What Deactivation Does NOT Do
|
|
172
|
+
|
|
173
|
+
Be explicit about what this skill does not touch:
|
|
174
|
+
|
|
175
|
+
1. **Does not delete database records.** The `ClientPortal` row, credentials, and analytics events are all preserved.
|
|
176
|
+
2. **Does not delete files.** The React components in `src/app/(portal)/client/<slug>/` remain in the codebase.
|
|
177
|
+
3. **Does not immediately terminate sessions.** Existing sessions expire on their natural schedule. The `isActive` check happens on new requests, but session tokens may still be valid for their remaining TTL.
|
|
178
|
+
4. **Does not notify the client.** There is no email or notification sent to the client. If the client needs to know, the user should communicate that separately.
|
|
179
|
+
|
|
180
|
+
## Edge Cases
|
|
181
|
+
|
|
182
|
+
- **Portal with active share link**: Deactivation invalidates the link because the portal is no longer active. If the portal should stay active but all tokens must be revoked, rotate credentials with `/portal credentials <slug>`.
|
|
183
|
+
- **Portal already inactive**: The script returns `already_inactive`. Do not treat this as an error -- just inform the user.
|
|
184
|
+
- **Last remaining portal**: No special handling. The user can deactivate their last portal. The organization continues to exist with zero active portals.
|
|
185
|
+
|
|
186
|
+
## Conventions
|
|
187
|
+
|
|
188
|
+
- Always confirm before executing. No exceptions.
|
|
189
|
+
- Use double-line box drawing (`═`) for the confirmation box.
|
|
190
|
+
- Show the file path where page files remain so the user knows where to look if they want to clean up.
|
|
191
|
+
- Do not suggest automatic file deletion. Let the user decide whether to keep or remove the source files.
|
|
192
|
+
- If learnings indicate the user has previously reactivated a portal, emphasize the reversibility more prominently.
|
|
193
|
+
|
|
194
|
+
## Error Handling
|
|
195
|
+
|
|
196
|
+
- If the preamble fails, stop and display the error.
|
|
197
|
+
- If the portal is not found, suggest `/portal list` to verify the slug.
|
|
198
|
+
- If the database operation fails, show the error from stderr. Common cause: database connection issues.
|
|
199
|
+
- If the user cancels, acknowledge cleanly and stop. Do not ask again.
|
|
200
|
+
|
|
201
|
+
## Reactivation
|
|
202
|
+
|
|
203
|
+
Deactivation is reversible. To reactivate a portal, you have two options:
|
|
204
|
+
|
|
205
|
+
**Option 1: Direct database update.** Run a query against the database to set `isActive` back to `true`:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cd $APP_PATH && npx tsx -e "
|
|
209
|
+
const { PrismaClient } = require('@prisma/client');
|
|
210
|
+
const prisma = new PrismaClient();
|
|
211
|
+
prisma.clientPortal.update({
|
|
212
|
+
where: { slug_organizationId: { slug: '<slug>', organizationId: '<org_id>' } },
|
|
213
|
+
data: { isActive: true }
|
|
214
|
+
}).then(console.log).finally(() => prisma.\$disconnect());
|
|
215
|
+
"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This is intentionally manual. There is no `/portal reactivate` skill in v1. Reactivation should be a deliberate choice, not a casual undo.
|
|
219
|
+
|
|
220
|
+
**Option 2: Re-create the portal.** If the page files were deleted from git after deactivation, the simplest path is to run `/portal create <slug>` again. This creates new page files and a new database record. The old analytics data is tied to the old record and will not carry over.
|
|
221
|
+
|
|
222
|
+
## Audit Trail
|
|
223
|
+
|
|
224
|
+
The delete operation updates the database record but does not create an explicit audit log entry. However, the state change is visible through:
|
|
225
|
+
|
|
226
|
+
- The `isActive` field on the `ClientPortal` record (false after deactivation)
|
|
227
|
+
- The `updatedAt` timestamp on the record (set to the deactivation time)
|
|
228
|
+
- Git history if the user subsequently removes the page files
|
|
229
|
+
|
|
230
|
+
If learnings are enabled, the skill records the deactivation event:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{"skill":"portal-delete","key":"deactivated","insight":"whzan deactivated on 2026-04-07","confidence":10,"ts":"2026-04-07T15:00:00Z"}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This helps other skills provide context. For example, `/portal status` can note when a portal was deactivated if the user asks about it.
|
|
237
|
+
|
|
238
|
+
## Bulk Deactivation
|
|
239
|
+
|
|
240
|
+
If the user wants to deactivate multiple portals at once, handle each one sequentially with a confirmation for each. Do not batch-deactivate without individual confirmation. Each portal represents a client relationship, and the user should consciously decide about each one.
|
|
241
|
+
|
|
242
|
+
If the user explicitly asks to "deactivate all portals" or "shut everything down", confirm once with the full list: "This will deactivate all 5 portals: whzan, acme, example, client-a, client-b. Are you sure?" Then execute them sequentially.
|
|
243
|
+
|
|
244
|
+
## Deactivation vs Credential Rotation
|
|
245
|
+
|
|
246
|
+
Two operations can revoke client access, and they serve different purposes:
|
|
247
|
+
|
|
248
|
+
- **Deactivation** (this skill): Removes the portal entirely from client view. New visitors see "not found". Existing sessions and share links stop working because the portal is inactive. Use when the client relationship has ended or the portal is no longer needed.
|
|
249
|
+
- **Credential rotation** (`/portal credentials`): Changes the password and invalidates all tokens (sessions + share links). The portal remains active and accessible. Use when credentials may be compromised but the portal should stay live.
|
|
250
|
+
|
|
251
|
+
If the user wants to immediately revoke all access AND deactivate, the sequence is: rotate credentials first (to kill existing sessions immediately), then deactivate (to prevent new logins).
|
|
252
|
+
|
|
253
|
+
## Completion
|
|
254
|
+
|
|
255
|
+
As a final step, log skill completion:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
echo '{"skill":"portal-delete","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/timeline.jsonl" 2>/dev/null
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Related Skills
|
|
262
|
+
|
|
263
|
+
- `/portal list` -- see all portals before deciding which to deactivate
|
|
264
|
+
- `/portal credentials` -- rotate credentials to immediately revoke all access (stronger than deactivate)
|
|
265
|
+
- `/portal status` -- check portal health to identify candidates for deactivation
|