MainSDK
Search⌘K
Interfaces

Security and privacy

Protected files

protect

Protect a file so it opens only under its policy on a TAPS Node, can be released at a set time, revoked in seconds and records every open.

Building for 0.4ships as a experimental APIPhase 1 · Core, public betaSpec protect-tapsFree in your app, Apache-2.0

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.
Stability levels
Updated 2026-10-28

protect keeps a file under your control after you share it: it stays ciphertext until a TAPS Node issues a licence, so you can time-release it, revoke it in seconds and see who opened it.

For a walk-through, see Protect a file so only the right people open it.

At a glance

protect:
  driver: taps-node          # local in dev and tests
  url: https://taps.acme.dev
  share-domain: open.acme.dev
  key: secret:MAINSDK_PROTECT_KEY
  mode: viewer               # viewer | wrapper
  suite: TAPS-CS1            # TAPS-CS1 | TAPS-CS2
  renew: 10s                 # licence renewal, 5s to 5m
  mcp: false                 # true adds the taps_* tools to the MCP server

Calls

  • protect(file, { label, policy })
  • protect.get(asset)
  • protect.list({ cursor })
  • protect.update(asset, { policy, label })
  • protect.versions.create(asset, file)
  • protect.revoke(asset, { reason, person })
  • protect.erase(asset, { reason, approvals })
  • protect.transfer(asset, { to })
  • protect.activity(asset)
  • protect.usage(asset)
  • protect.open(asset, { purpose })
  • protect.policy.check(asset, { as, agent, purpose })
  • protect.policy.simulate(asset, policy)
  • protect.policy.lint(policy)
  • protect.zones.list()
  • protect.zones.update(zones, { approvals })
  • protect.threshold.get(asset)
  • protect.share(asset, { to })
  • protect.share.get(link)
  • protect.share.list(asset)
  • protect.share.update(link, change)
  • protect.share.revoke(link)
  • protect.share.stats(link, { consent })
  • protect.people.create(asset, { person })
  • protect.people.list(asset)
  • protect.people.remove(asset, person)
  • protect.agents(asset, { allowed })
  • protect.delegate.create({ agent, assets, actions, purposes, expiresIn })
  • protect.delegate.get(id)
  • protect.delegate.list()
  • protect.delegate.revoke(id)
  • protect.approvals.list({ state })
  • protect.approvals.get(id)
  • protect.approvals.update(id, { decision })
  • protect.query(assets, { question, purpose })
  • protect.mcp.tools()
  • protect.mcp.manifest()
  • protect.mcp.verify(manifest, tools)
  • protect.attest.agent(agent)
  • protect.attest.verify(evidence, { kind })
  • protect.inspect(asset, { function })
  • protect.environment.get()
  • protect.ledger.list({ asset })
  • protect.ledger.proof(asset)
  • protect.ledger.verify()
  • protect.ledger.export()
  • protect.leaks.create({ asset, evidence })
  • protect.leaks.trace(caseId, { approvals })
  • protect.lockdown.create({ reason, approvals })
  • protect.lockdown.delete(id, { approvals })
  • protect.channels.list()
  • protect.channels.events(pattern)
  • protect.channels.watch(pattern, fn)
  • protect.channels.security()
  • createProtect(deps)
  • new ProtectError(code, message, opts?)
  • PROTECT_CHANNELS(const)
  • PROTECT_DRIVERS(const)
  • localProtect(deps)
  • lintPolicy(policy)
  • parseProtectSettings(raw)
  • protectHosts(settings)
  • routeShare(base, built, { protect })

Guarantees

Whatever the driver.

  • Ciphertext without a licence
  • No licence before releaseAt, or before t of n parties release
  • Revocation takes effect within one renew interval (10 s by default)
  • Opens, denials and revocations in the MainSDK decision log and the TAPS ledger
  • Agents act only inside a delegation from the person they act for
  • Share links and QR codes through getqr.id
  • No post-quantum claim until TAPS-CS3 is registered

Drivers

4 drivers. Select one to see its line in main.yaml.

main.yamlLocal fake TAPS Node (default)
protect:
  driver: local

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-PROTECT-0020Checkmainsdk check, before deployA @taps/* package is imported outside @mainsdk/protect-taps
MAIN-PROTECT-0021Checkmainsdk check, before deployThe protect section is invalid: unknown key or driver, no url or share-domain for a taps-* driver, no key, suite: TAPS-CS3, renew outside 5s to 5m, or a threshold party without a URL
MAIN-PROTECT-0022Checkmainsdk check, before deployA production build resolves @taps/* from a folder (link: or file:) instead of the registry
MAIN-PROTECT-0001RuntimeError responses and logsLicence denied by the policy (403); the TAPS error type is in taps, the failed condition only in the decision log
MAIN-PROTECT-0002RuntimeError responses and logsTime-locked: before releaseAt, or the release round isn't available yet (403, retryAfter)
MAIN-PROTECT-0003RuntimeError responses and logsThe asset, the person's access, the link or the licence was revoked, or the asset was erased (403; 410 when erased)
MAIN-PROTECT-0004RuntimeError responses and logsNo such asset, version, link, delegation or approval in this tenant (404, same body either way)
MAIN-PROTECT-0005RuntimeError responses and logsA person must approve first; the problem carries approvalId and retryAfter (403)
MAIN-PROTECT-0006RuntimeError responses and logsThe approver declined, or the approval timed out (403)
MAIN-PROTECT-0007RuntimeError responses and logsDelegation invalid: wider than the person, expired, revoked, used up or deeper than 3 (403)
MAIN-PROTECT-0008RuntimeError responses and logsA query or play budget, or a delegation's maxUses, is used up (429)
MAIN-PROTECT-0009RuntimeError responses and logsA session, print, device or recipient-verification rate limit was passed (429, retryAfter)
MAIN-PROTECT-0010RuntimeError responses and logsSuite not supported: TAPS-CS3 is refused until it is registered; no post-quantum protection is claimed (400)
MAIN-PROTECT-0011RuntimeError responses and logsThreshold release not reached: fewer than t parties have released (403, retryable)
MAIN-PROTECT-0012RuntimeError responses and logsThe asset or its label is locked down, suspended or quarantined (423)
MAIN-PROTECT-0013RuntimeError responses and logsStep-up required: the policy needs a fresher or stronger sign-in, listed in requirements (401)
MAIN-PROTECT-0014RuntimeError responses and logsThe TAPS Node, a threshold party or the sealing service is unreachable (503, retryable); nothing opens without a licence
MAIN-PROTECT-0015RuntimeError responses and logsInvalid input: unknown policy key, label pan, a zone id that isn't opaque, t outside 2 to n, releaseAt in the past, an unknown party or an undeclared agent (400)
MAIN-PROTECT-0016RuntimeError responses and logsThe file's version is superseded; location names the current version (409)
MAIN-PROTECT-0017RuntimeError responses and logsThe ledger is stale or the registration is pending (503, retryAfter)
MAIN-PROTECT-0018RuntimeError responses and logsA query answer was refused by the output controls: the whole document, a verbatim span over the limit, or a denied field (422)
MAIN-PROTECT-0019RuntimeError responses and logsThe driver's profile lacks the capability the call needs, such as query or web3 (501)

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
Protect, get, list, update, versions, revoke, erase, transfer, activity and usagePhase 10.4protect-taps#local-core
Policy by person, device, network zone, geo, time and purposePhase 10.4protect-taps#policy
Release on time and threshold releasePhase 10.4protect-taps#release
Share through getqr.id into the TAPS Web ReaderPhase 10.4protect-taps#share
Agent delegation and human approvalPhase 10.4protect-taps#agents
Compute-only queryPhase 10.4protect-taps#query
Signed taps_* MCP toolsPhase 10.4protect-taps#mcp
Attestation, inspection, holdings and assistive accessPhase 10.4protect-taps#attest-inspect
Channels, decision log, ledger and security eventsPhase 10.4protect-taps#rails
Post-quantum claim gatePhase 10.4protect-taps#pq-gate
TAPS Node, hosted and SoveDocs drivers in @mainsdk/protect-tapsPhase 10.4protect-taps#driver-package

MainSDK Cloud

Everything above is free in your app. Cloud adds these on top, by plan:

PlanWhat it adds
FleetEstate view of protected files, and revoke every file an app or person shared since a date

Quick start

The local driver needs no setup. Protect a file, then share it. The share link is a getqr.id URL that opens the TAPS Web Reader.

main.yaml
protect:
  driver: local          # taps-node in production

app/api/papers/route.ts

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 examiners = form.getAll("examiner").map(String);

  const asset = await protect(paper, {
    label: "sensitive",
    policy: { releaseAt: "2027-03-01T09:00:00Z" },   // nobody opens it before this time
  });
  const { url } = await protect.share(asset.id, { to: examiners });
  return Response.json({ url, asset: asset.id });
}

Every call reads the tenant, the user and the agent from the request context. Outside a request, run the call as a person with tenant.as({ user }, fn); an explicit user or agent option overrides the context for one call and is audited. With no user, a call fails with MAIN-PROTECT-0015.

Configuration

main.yaml
protect:
  driver: taps-node                 # local | taps-node | taps-hosted | sovedocs
  url: https://taps.acme.dev        # the TAPS Node (not for local)
  share-domain: open.acme.dev       # where the Web Reader runs (not for local)
  key: secret:MAINSDK_PROTECT_KEY   # the app's issuer identity
  mode: viewer                      # viewer | wrapper
  suite: TAPS-CS1                   # TAPS-CS1 | TAPS-CS2
  renew: 10s                        # licence renewal, 5s to 5m
  threshold:
    board: https://taps.board.example
    regulator: https://taps.regulator.example
  mcp: false
  environment: https://seal.acme.dev
KeyTypeWhat it does
driverstringlocal fakes a TAPS Node in process (the default). taps-node is a self-hosted TAPS Node, taps-hosted a hosted one, sovedocs a SoveDocs tenant's Node.
urlURLThe TAPS Node. Required for the taps-* drivers and added to the driver's hosts, so the network allowlist lets it through.
share-domainhostThe tenant's TAPS share domain, where the Web Reader runs. Required for the taps-* drivers.
keysecretThe app's issuer identity: an ed25519 PKCS#8 key, as mainsdk seal keygen writes it. Required for the taps-* drivers; read through secrets, never from code.
modestringThe default delivery. viewer gives pages and a text layer; wrapper also allows read().
suitestringTAPS-CS1 (default) or TAPS-CS2. TAPS-CS3 is refused until it is registered.
renewdurationHow often a licence renews: the bound on revocation. Default 10s.
thresholdmapParty names for policy.threshold.parties; for the taps-* drivers each is the party's Node URL.
mcpbooleantrue adds the taps_* tools to the app's MCP server.
environmentURLThe TAPS sealing service for releaseAt and threshold assets (taps-* drivers).

mainsdk check reports a bad section as MAIN-PROTECT-0021, a @taps/* import outside the driver package as MAIN-PROTECT-0020, a production build that resolves @taps/* from a folder as MAIN-PROTECT-0022, and the local driver in production as MAIN-CHECK-0026.

The policy

Policy is default deny and runs on every licence request and every renew. The owner is never restricted by their own policy.

KeyMeaning
peopleUser ids, team:<id>, role:<name> and email addresses
devices{ managed, attested }
zonesOpaque network zone ids such as z-7f3a
geo{ allow, exclude, confidence }, ISO 3166-1 countries
time{ from, until, hours, weekdays, timeZone }
purposesWhat agents may do; train only when named
releaseAtRFC 3339; no licence before it, for anyone
threshold{ t, parties }: t of the named parties must release
holdingsToken-gated access through a chain driver
offline, watermark, assistiveToolsOffline window, watermark, assistive tools

Labels public, internal, confidential and restricted are the TAPS labels. personal maps to confidential; sensitive, child and phi map to restricted. The label pan is refused: card numbers are never protected or stored.

What you get

  • A protected file carries its own identity, provenance and policy, and stays ciphertext until a TAPS Node issues a licence.
  • Release at a set time, or when enough parties agree.
  • Revoke in seconds, and see who opened it.
  • App agents read it only inside a delegation.
  • Use it for exam papers nobody can open early, tenders, results or any document you may need to pull back.
  • Built on TAPS (Trust Anchor Provenance System), HubXero Labs' open protocol.

Calls

GroupCalls
Assetsprotect(), protect.get(), protect.list(), protect.update(), protect.versions.create(), protect.revoke(), protect.erase(), protect.transfer(), protect.activity(), protect.usage()
Openingprotect.open() returns licence, pages(), page(), text(), read(), print(), renew() and close()
Policy and zonesprotect.policy.check(), .simulate(), .lint(), protect.zones.list(), .update(), protect.threshold.get()
Sharingprotect.share(), .get(), .list(), .update(), .revoke(), .stats(); protect.people.create(), .list(), .remove()
Agentsprotect.agents(), protect.delegate.create(), .get(), .list(), .revoke(), protect.approvals.list(), .get(), .update(), protect.query()
MCPprotect.mcp.tools(), .manifest(), .verify()
Attestation and inspectionprotect.attest.agent(), .verify(), protect.inspect(), protect.environment.get()
Ledger and responseprotect.ledger.list(), .proof(), .verify(), .export(), protect.leaks.create(), .trace(), protect.lockdown.create(), .delete()
Eventsprotect.channels.list(), .events(), .watch(), .security()

Lists return { items, next }. Changes that need a second pair of eyes (protect.erase(), protect.transfer(), lowering a label with protect.update(), protect.zones.update(), protect.lockdown.create() and .delete(), protect.leaks.trace()) take approvals from people other than the caller, and fail with MAIN-PROTECT-0005 without them.

Examples

Release an exam paper at a set time

Before releaseAt, no licence is issued and the attempt is written to the decision log and protect.activity().

app/exams/schedule.ts
import { protect } from "mainsdk";

export async function scheduleRelease(paper: File, examiners: string[]) {
  const asset = await protect(paper, {
    label: "sensitive",
    policy: { people: examiners, releaseAt: "2027-03-01T09:00:00Z" },
  });
  return asset.id;
}

Open a file

protect.open() asks for a licence for the current user or agent and returns nothing without one. A viewer gives pages and the accessible text layer; only mode: "wrapper" allows read(). text() returns { page, blocks, text }.

app/papers/read.ts
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();
  }
}

Revoke a file in seconds

A revoked asset, person or link fails the next open and the next renew, so an open session stops within one renew interval.

app/api/assets/[id]/revoke/route.ts
import { auth, protect } from "mainsdk";

export async function POST(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 });
}

protect.revoke(id, { reason, person }) stops one person and leaves the rest.

See who opened a file

app/api/assets/[id]/activity/route.ts
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;
  const opens = await protect.activity(id);   // { items, next }
  return Response.json(opens);
}

Open only inside a network zone

app/papers/office-only.ts
import { protect } from "mainsdk";

export async function officeOnly(paper: File, approvedBy: string) {
  await protect.zones.update(
    { "z-7f3a": { name: "Head office", cidrs: ["203.0.113.0/24"] } },
    { approvals: [{ by: approvedBy }] },
  );
  return protect(paper, { label: "confidential", policy: { zones: ["z-7f3a"] } });
}

Zone ids are opaque; protect.policy.lint() flags a name such as head-office.

Require several parties to release

Name each party under protect.threshold in main.yaml first; a party that isn't named fails with MAIN-PROTECT-0015. For the local driver a name with no URL is enough:

main.yaml
protect:
  driver: local
  threshold: { board: null, regulator: null, centre: null }
app/papers/board-release.ts
import { protect } from "mainsdk";

export async function boardRelease(minutes: File) {
  const asset = await protect(minutes, {
    label: "restricted",
    policy: {
      threshold: { t: 2, parties: ["board", "regulator", "centre"] },
      releaseAt: "2027-03-01T09:00:00Z",
    },
  });
  return protect.threshold.get(asset.id);   // { t: 2, n: 3, released: [] }
}

No licence is issued until t parties have released their shares, and not before releaseAt when both are set.

Let an agent work within a delegation

An app agent acts only inside a delegation from the person it acts for, never wider than that person. An action in approve.for waits for the person's approval.

Run delegate.create as the person (in a signed-in request, or inside tenant.as({ user }, fn)): a delegation always starts with a person, and an agent can't make one (MAIN-PROTECT-0007).

app/agents/reader.ts
import { protect } from "mainsdk";

export async function letReaderSummarise(assetId: string) {
  const delegation = await protect.delegate.create({
    agent: "reader-bot",              // declared under agents in main.yaml
    assets: [assetId],
    actions: ["view", "print"],
    purposes: ["summarize"],
    expiresIn: "1h",
    approve: { for: ["print"], timeout: "15m" },
  });
  return delegation.id;
}

export async function approvePrint(approvalId: string) {
  return protect.approvals.update(approvalId, { decision: "approve" });
}

protect.query() answers a question over protected assets without returning their bytes: answers are marked generated, carry citations and never hold a verbatim span over 25 tokens. An agent never approves its own request (MAIN-AGENT-0003).

Share through getqr.id

share(asset, { to }) and protect.share() return a getqr.id link and QR whose target is the TAPS link on your share domain. Outside the test kit they use your getqr.id key (secret:GETQR_API_KEY, see Share links and QR codes); the kit makes local links. The TAPS link token is never in the getqr.id path, in logs or in channel data, and every link page shows "Powered by getqr.id".

app/papers/share.ts
import { protect, share } from "mainsdk";

export async function sharePaper(assetId: string, email: string) {
  const link = await protect.share(assetId, { to: [email], expiresIn: "7d", maxOpens: 3 });
  return { url: link.url, id: link.id };
}

export async function stop(linkId: string) {
  await share.revoke(linkId);   // revokes the getqr.id link and the TAPS link
}

Reference

Every call also takes the call options user, agent and signal. Signatures are in mainsdk/protect.

Policy

CallReturnsWhat it does
protect.policy.simulate(asset, policy){ gained, lost }Compares a new policy with the asset's current one without changing anything. An invalid policy fails with MAIN-PROTECT-0015.

A link is { id, url, qr, asset, expiresAt, opens }. The url and qr are getqr.id; the TAPS token is never in them. A link still being registered fails with MAIN-PROTECT-0017.

CallReturnsWhat it does
protect.share.get(link)The linkReads one link.
protect.share.list(asset, { cursor, limit }){ items, next }Lists the links of an asset.
protect.share.update(link, change)The linkChanges expiresIn, maxOpens (a whole number from 1) or actions.
protect.share.revoke(link, { keepPeople })NothingRevokes the link and its getqr.id short link. If getqr.id is unreachable the link is already revoked, and the call fails with MAIN-PROTECT-0014; call it again to finish.
protect.share.stats(link, { consent }){ opens, optedIn }Counts opens. Without consent: true it fails with MAIN-SHARE-0004.

People

CallReturnsWhat it does
protect.people.list(asset, { cursor, limit }){ items, next }Lists the direct grants on an asset.
protect.people.remove(asset, person)NothingRemoves a person's grant.

Delegations and approvals

CallReturnsWhat it does
protect.delegate.get(id)The delegationReads one delegation. Its state is active, revoked, expired or used-up.
protect.delegate.list({ agent, cursor, limit }){ items, next }Lists delegations, optionally for one agent.
protect.delegate.revoke(id, { reason })NothingRevokes a delegation.
protect.approvals.get(id)The approval requestReads one pending or decided approval. Its state is pending, approved, denied or expired.

MCP tools

CallReturnsWhat it does
protect.mcp.manifest(){ tools, keyId, signature }The name and description hash of each taps_* tool, signed by the app's key.
protect.mcp.verify(manifest, tools)booleanChecks the taps_* entries of a tools list against a signed manifest. It returns false when the key, the signature, a tool or a description does not match.

Attestation, ledger and lockdown

CallReturnsWhat it does
protect.attest.verify(evidence, { kind }){ kind, accepted, root, reason? }Appraises evidence bytes of kind device, environment or renderer. The driver must support attestation.
protect.ledger.proof(asset){ proof, verdict }Proves an asset's registration is in the ledger. The verdict is valid, invalid or unanchored.
protect.lockdown.delete(id, { approvals })NothingLifts a lockdown. It needs approvals from other people.

Events

CallReturnsWhat it does
protect.channels.events(pattern)ChannelEvent[]Events seen on the channels, optionally filtered by a pattern.
protect.channels.watch(pattern, fn)A functionCalls fn for each matching event. Call the returned function to stop.

Building it yourself

mainsdk builds protect for you from main.yaml. These are for driver authors, tests and tools.

NameWhat it is
createProtect(deps)Builds the interface on a driver, with the settings and the SDK's services.
localProtect(deps)Builds the local driver.
new ProtectError(code, message, opts?)The error every call throws: a MAIN-PROTECT-* code, with the TAPS type in taps and the approval in approvalId when there is one.
PROTECT_CHANNELS(const)The eight mainsdk:protect.* channel names.
PROTECT_DRIVERS(const)The driver ids a protect section may name: local, taps-node, taps-hosted and sovedocs.
lintPolicy(policy)Checks a policy and returns its issues; protect.policy.lint() calls it.
parseProtectSettings(raw)Reads and checks the protect section, returning { settings, issues }.
protectHosts(settings)The hosts the driver reaches, so the network allowlist can declare them. Empty for local.
routeShare(base, built, { protect })Routes share(asset, { to }) and share.revoke() for a protected asset to protect, and everything else to the plain getqr.id share.

Protecting web requests is a different tool

protect guards files. The headers, CSRF checks, CORS rules and cookie flags that guard your web responses are inbound protection, and showing user or model text as safe HTML is sanitize; both are covered in that guide and have no interface page of their own.

Channels, logs and the ledger

Every decision and side effect goes on one of eight mainsdk:protect.* channels, to the decision log (log.audit, with the actor, on_behalf_of, purpose and policy hash) and to the ledger. Channel data is ids, codes and kinds only: never content, names, emails, tokens or IP addresses.

ChannelFires on
mainsdk:protect.createEvery protect() and protect.versions.create()
mainsdk:protect.licenceEvery licence request and renew, issued or denied
mainsdk:protect.revokeEvery revoke of an asset, person, link or delegation, and every erase
mainsdk:protect.shareEvery link made, changed or revoked, and every grant
mainsdk:protect.delegateEvery delegation made, refused or revoked, and every approval decision
mainsdk:protect.queryEvery query, answered or refused
mainsdk:protect.inspectEvery inspection verdict
mainsdk:protect.policyEvery policy, zone, label, lockdown or owner change

The local ledger is append-only and hash-chained. protect.ledger.verify() finds any edit, deletion or reorder, and protect.ledger.export() returns an in-toto Statement in a DSSE envelope, signed by the app's key. Security events are raised for protect.denied_burst (20 denials of one asset in a minute), protect.leak_reported, protect.lockdown and protect.quarantined; read them with protect.channels.security().

Errors and checks

CaseWhat happensFix
The policy does not allow the openMAIN-PROTECT-0001; the TAPS type (zone-denied, geo-denied, purpose-denied) is in taps, and the failed condition only in the decision logCheck with protect.policy.check()
Opened before releaseAtMAIN-PROTECT-0002 with retryAfterWait for the release time
Opened after a revoke or eraseMAIN-PROTECT-0003 (410 when erased)Protect and share a new copy
Fewer than t parties releasedMAIN-PROTECT-0011Wait for more parties
A change needs a second personMAIN-PROTECT-0005Pass approvals from another person
The TAPS Node is unreachableMAIN-PROTECT-0014; nothing opens, the rest of the app keeps servingCheck protect.url and the network
A call asks for suite TAPS-CS3MAIN-PROTECT-0010; no protection beyond CS1 and CS2 is claimedUse the default suite

See Errors for every MAIN-PROTECT-* code.

Swapping drivers

Change one line of main.yaml; no call changes.

main.yaml
protect:
  driver: taps-hosted   # was taps-node

The local driver is a fake for development and tests, never a Node: mainsdk check refuses it in production (MAIN-CHECK-0026). Assets protected on local stay in development; moving assets between drivers is not specified. A driver that lacks a capability says so in its profile (query, threshold, timelock, inspect, web3, delivery); the call then fails with MAIN-PROTECT-0019.

Testing

createTestApp() from @mainsdk/testing runs the local driver offline with the test clock, so releaseAt, time windows, renewal and expiry move with app.clock. Run each call as a person with app.tenancy.as().

app/papers/protect.test.ts
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";

const app = createTestApp({ yaml: "schema: 1\nproject: papers\n" });
const asset = await app.tenancy.as({ user: "u1" }, () =>
  app.protect(new TextEncoder().encode("Q1 paper"), { label: "sensitive" }),
);
const opened = await app.tenancy.as({ user: "u1" }, () => app.protect.open(asset.id));
assert.equal((await opened.text()).text, "Q1 paper");

The kit adds app.protect.signals({ ip, country, confidence, managed, attested }), app.protect.party(name).release(asset) and .withhold(asset), and app.protect.chain(holdings). The suite in @mainsdk/testing runs every call on any driver and lists the cases a profile lacks as skipped, by name.

Security and privacy

  • A protected file is ciphertext without a licence, wherever a copy ends up. Content keys are wrapped with keys, and every unwrap is an audit record.
  • Each user is a tenant-pairwise principal to TAPS: no user id, email or name reaches a Node or ledger in clear.
  • Every asset belongs to one tenant and opens only in its data region. Another tenant gets the same MAIN-PROTECT-0004 as for an unknown id.
  • Every request to a Node, party or sealing service goes through the network layer, so the allowlist, timeouts and retries apply.
  • MainSDK claims no post-quantum protection for protect. The TAPS-CS3 suite is refused until it is registered.

What Cloud adds

protect is free in your app. The Fleet plan adds an estate view of protected files and a response action to revoke every file an app or person shared since a date. See Free in-app and Cloud.

FAQ

How is this different from a share link? A share link controls the link; once someone downloads the file, the copy is theirs. A protected file stays ciphertext and asks the TAPS Node for a licence every time it opens.

Do readers need to install anything? No. Links open in the TAPS Web Reader.

What does not work offline or on local? Offline licences, the wrapper session's device-sealed keys and DRM key delivery are out of scope for 0.4. A TAPS Node, a threshold party and a sealing service are not run inside MainSDK.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet