Protect a file so only the right people open it
Protect a file with protect, release it at a set time, share it through getqr.id, see who opened it and revoke it in seconds, all on the local driver first.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will protect a file so it stays ciphertext until its policy allows an open, hold it until a release time, share it with named people, read who opened it, and revoke it. You build it on the local driver, which fakes a TAPS Node in your process, so it needs no account. It takes about 20 minutes.
You need
- A MainSDK app with a
main.yaml(see Thirty-minute path) and a signed-in user (see Sign-in and sessions). - For real links outside tests, a getqr.id key as
secret:GETQR_API_KEY(see Share links and QR codes).
1. Turn on the driver
protect:
driver: local # taps-node in productionlocal is the default, so a missing protect section behaves the same.
2. Protect a file with a release time
import { auth, protect } from "mainsdk";
export async function POST(req: Request) {
await auth.require(req, { role: "exam-admin" });
const form = await req.formData();
const paper = form.get("paper") as File;
const asset = await protect(paper, {
label: "sensitive",
policy: { releaseAt: "2027-03-01T09:00:00Z" }, // no licence before this time, for anyone
});
return Response.json({ asset: asset.id });
}You should get an asset with an id such as urn:taps:asset:01M5… and state: "active". label is one of public, internal, confidential, restricted, or personal, sensitive, child, phi. Policy is default deny; add people, zones, geo, time or purposes to narrow it further.
3. Open it
protect.open() asks for a licence for the current user and returns the content only when the policy allows it.
import { protect } from "mainsdk";
export async function readPaper(id: string) {
const opened = await protect.open(id);
try {
return (await opened.text()).text; // the text layer a screen reader uses
} finally {
await opened.close();
}
}Before releaseAt, the open fails with MAIN-PROTECT-0002 and a retryAfter, and the attempt is written to the decision log. After it, the open succeeds. Pages are available from opened.pages(); read() needs mode: wrapper.
4. Share it
import { protect } from "mainsdk";
export async function sharePaper(assetId: string, email: string) {
const link = await protect.share(assetId, { to: [email], expiresIn: "7d", maxOpens: 3 });
return link.url; // a getqr.id link that opens the TAPS Web Reader
}Recipients open the file in the Web Reader with nothing to install. The TAPS link token is never in the getqr.id path, your logs or channel data.
5. See who opened it, and revoke
import { auth, protect } from "mainsdk";
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
await auth.require(req, { role: "exam-admin" });
const { id } = await params;
return Response.json(await protect.activity(id)); // { items: [{ at, who, action, decision }], next }
}
export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) {
await auth.require(req, { role: "exam-admin", fresh: "5m" });
const { id } = await params;
await protect.revoke(id, { reason: "paper leaked" });
return new Response(null, { status: 204 });
}A revoked asset fails the next open and the next licence renewal, so an open session stops within one renew interval (10 seconds by default). protect.revoke(id, { person }) stops one person and leaves the rest.
6. Test it on the fake clock
import assert from "node:assert/strict";
import { it } from "node:test";
import { createTestApp } from "@mainsdk/testing";
it("protect-taps releases a paper only after releaseAt", async () => {
const app = createTestApp({ yaml: "schema: 1\nproject: papers\n" });
await app.tenancy.as({ user: "u1" }, async () => {
const asset = await app.protect(new TextEncoder().encode("Q1 paper"), {
label: "sensitive",
policy: { releaseAt: "2027-03-01T09:00:00Z" },
});
await assert.rejects(app.protect.open(asset.id), { code: "MAIN-PROTECT-0002" });
app.clock.set(Date.parse("2027-03-01T09:00:01Z"));
const opened = await app.protect.open(asset.id);
assert.equal((await opened.text()).text, "Q1 paper");
await opened.close();
await app.protect.revoke(asset.id, { reason: "test" });
await assert.rejects(app.protect.open(asset.id), { code: "MAIN-PROTECT-0003" });
});
});Verify
npx mainsdk check --env prodA pass has no MAIN-PROTECT finding and no MAIN-CHECK-0026. With driver: local in production the check fails; switch the driver to a TAPS Node with protect.url, protect.share-domain and protect.key: secret:MAINSDK_PROTECT_KEY first (see TAPS Node and SoveDocs).
If it fails
| Code | Cause | Fix |
|---|---|---|
MAIN-PROTECT-0001 | The policy does not allow this open. | Check with protect.policy.check(asset, { as }). |
MAIN-PROTECT-0002 | Opened before releaseAt. | Wait for the release time; retryAfter says how long. |
MAIN-PROTECT-0003 | The asset was revoked or erased. | Protect and share a new copy. |
MAIN-PROTECT-0005 | The change needs a second person. | Pass approvals from another person. |
MAIN-PROTECT-0015 | No user in the context, or an unknown party or agent. | Run inside a signed-in request or tenant.as({ user }, fn); name parties under protect.threshold. |
MAIN-CHECK-0026 | The local driver in production. | Use taps-node, taps-hosted or sovedocs. |
For agents
Goal: protect files this app shares, with a release time and revocation.
Read: docs/specs/protect-taps.md, this page, main.yaml.
Touch only: main.yaml (protect section), files that store or share documents.
Do: replace plain share of sensitive files with protect() then protect.share(); set label and policy; add a test on the fake clock.
Verify: pnpm mainsdk check --env prod --json; node --test on the new test.
Never use the local driver in production, put a TAPS link token in a log, or approve a change on behalf of another person.Next
- Protected files: every call, policy key, the ledger and the driver notes.
- Share links and QR codes: how protected files are shared through getqr.id.
- Show untrusted text safely and protect web responses: headers, CSRF, CORS and sanitized HTML.
Was this page helpful?