expiring-links
Create and manage time-limited, count-limited, and password-protected share links for uploaded files.
Best use case
expiring-links is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Create and manage time-limited, count-limited, and password-protected share links for uploaded files.
Teams using expiring-links should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/expiring-links/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How expiring-links Compares
| Feature / Agent | expiring-links | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Create and manage time-limited, count-limited, and password-protected share links for uploaded files.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Expiring Links Skill
## When to use
Use this skill when you need to:
- Create links that automatically stop working after a date
- Create links that stop working after N downloads
- Add password protection to a download link
- Check if a link is still valid without consuming a download
- Understand why a link returned 410 or 401
## How expiry works
A share link is checked in this order when accessed via `GET /dl/:token`:
1. Is the link active (`is_active = 1`)? If not, return `410`.
2. Is `expires_at` set and in the past? Return `410`.
3. Is `max_downloads` set and `download_count >= max_downloads`? Return `410`.
4. Does the link have a password (`password_hash != NULL`)? Check `X-Link-Password` header. If missing or wrong, return `401`.
5. Stream the file. After the stream completes, atomically increment `download_count` and insert a `downloads` row.
## Create a link that expires in 7 days
```bash
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{
"label": "7-day access",
"expires_at": "2026-03-27T00:00:00Z"
}'
```
## Create a link limited to 1 download (one-time link)
```bash
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{"max_downloads": 1, "label": "One-time access"}'
```
After the first download, this link returns `410 Gone`.
## Create a link with both expiry and download limit
```bash
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{
"expires_at": "2026-04-20T00:00:00Z",
"max_downloads": 10,
"label": "Client share - limited"
}'
```
## Create a password-protected link
```bash
curl -X POST http://localhost:3000/api/files/ab3kx9mz1qwe/links \
-H "Content-Type: application/json" \
-d '{
"password": "MySecretPassword",
"label": "Secure access"
}'
```
The password is hashed with bcrypt (cost 10) before storage. It is never returned in API responses.
## Check if a link is still valid
Use `HEAD /dl/:token` to check validity without downloading the file and without consuming a download slot:
```bash
curl -I http://localhost:3000/dl/pq7rn4xyz90a
```
Responses:
| Status | Meaning |
|---|---|
| 200 | Link is active and the file is accessible |
| 401 | Link is active but password-protected; provide `X-Link-Password` |
| 404 | Token does not exist |
| 410 | Link is expired, revoked, or download limit reached |
## Check how many downloads are remaining
```bash
curl http://localhost:3000/api/links/pq7rn4xyz90a
```
Response includes:
```json
{
"token": "pq7rn4xyz90a",
"max_downloads": 10,
"download_count": 3,
"expires_at": "2026-04-20T00:00:00Z",
"is_active": true
}
```
Remaining downloads = `max_downloads - download_count` = 7.
If `max_downloads` is null, the link has no download limit.
## Extend the expiry of an existing link
```bash
curl -X PUT http://localhost:3000/api/links/pq7rn4xyz90a \
-H "Content-Type: application/json" \
-d '{"expires_at": "2026-06-01T00:00:00Z"}'
```
## Remove the expiry from a link (make it permanent)
```bash
curl -X PUT http://localhost:3000/api/links/pq7rn4xyz90a \
-H "Content-Type: application/json" \
-d '{"expires_at": null}'
```
## Remove the download limit from a link
```bash
curl -X PUT http://localhost:3000/api/links/pq7rn4xyz90a \
-H "Content-Type: application/json" \
-d '{"max_downloads": null}'
```
## Manually revoke a link
```bash
curl -X DELETE http://localhost:3000/api/links/pq7rn4xyz90a
```
This sets `is_active = 0`. The link immediately returns `410` for all new download attempts. The download history is preserved.
## Reactivate a revoked link
PATCH is not supported. To reactivate, use PUT to update is_active:
```bash
# Note: is_active cannot be directly set via PUT.
# Delete and recreate the link to reactivate, or use the admin UI.
```
The admin UI has a "Reactivate" button that calls an internal endpoint. Via the API, revocation is permanent - create a new link if access needs to be restored.
## Automatic cleanup
The server runs a cleanup job every `CLEANUP_INTERVAL_HOURS` (default: 1 hour). This job:
1. Sets `is_active = 0` for all links where `expires_at < now`
2. Deletes orphan files (files with no active links older than 7 days)
The cleanup job does not delete download history or the file records themselves.
## Troubleshooting
**Link returns 410 but it was just created**
Check that `expires_at` is in the future (UTC). The server compares against UTC time.
```bash
date -u # Check current UTC time
```
**Password returns 401 even with the correct password**
Ensure you are sending the password in the `X-Link-Password` request header, not as a query parameter or body field.
**download_count is not incrementing**
The count only increments after the file stream completes successfully. Cancelled or aborted downloads do not increment the count. Check `GET /api/links/:token/downloads` to see the log of completed downloads.
**Link is active but still returns 410**
Check `max_downloads`. If the count equals the limit, the link is exhausted regardless of `is_active`.
```bash
curl http://localhost:3000/api/links/pq7rn4xyz90a | jq '{max_downloads, download_count, expires_at, is_active}'
```Related Skills
Skill: Uptime Monitoring
## Overview
Skill: Status Page
## Overview
Skill: unit-conversion
## Overview
Skill: recipe-scaler
## Overview
reading-list
Operate the reading-list API to save, manage, tag, search, and export articles.
email-digest
Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.
websocket-realtime
Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".
poll-builder
Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.
Skill: personal-finance
## Overview
Skill: csv-import
## Overview
Skill: Syntax Highlighting
## Purpose
Skill: Pastebin Core
## Purpose