Skip to main content

documents-app-integration

Implement the separately maintained Documents React client for AuthCrunch encrypted storage, recovery keys, chunk upload/download, recipient sharing, and safe session handling. Does not implement manager/profile APIs or provision the backend.

Source facts

Repository
greenpau/go-authcrunch
Last source activity
October 11, 2026 at 10:55
Detected SKILL.md language
English
Stars
60
Forks
66

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
documents-app-integration
description
Implement the separately maintained Documents React client for AuthCrunch encrypted storage, recovery keys, chunk upload/download, recipient sharing, and safe session handling. Does not implement manager/profile APIs or provision the backend.
# Documents React Integration This skill is intended for the external React application's agent to read from its AuthCrunch library checkout. Keep implementation in the task's authorized repository; reading this skill does not authorize edits to sibling checkouts. For an app-local AGENTS.md, add a route pointing to this skill at the actual library checkout location. Do not copy a machine-specific path into the app's source or pretend the compiled app already ships in AuthCrunch. Read the [crypto/wire contract](../authentication-portal-documents/references/protocol.md) and use the portable [crypto.mjs module](../../../pkg/authn/documents/client/crypto.mjs). Vendor a reviewed version into the frontend's source or bundle it via an explicit build dependency. It is plain ESM using native Web Crypto with no third-party runtime dependencies. It is not currently served by a production asset route. Preserve the protocol and test against the [interoperability vector](../../../pkg/authn/documents/testdata/protocol.json). Use the shared [Mermaid sequences and design rationale](../authentication-portal-documents/references/sequences.md) to follow enrollment/unlock, encrypted upload/download, sharing, revocation and reset. They distinguish local crypto and required React UX from implemented server behavior, and explain retries, key context and account-switch protection. Keep these sequences and the protocol aligned when changing a client workflow. The [OpenAPI source](../../../assets/openapi/content/openapi.yaml) describes the API. Its rendered `/#tag/documents` catalogue documents every operation's fields, request/response messages, state transitions and retry behavior. Every operation has its own method/resource path, expected response schema, parameter/body examples and native Test Request console. No plaintext-upload endpoint, username-directory endpoint or public share link exists. The manager application and profile application are separate products. ## Session and capability discovery The host must explicitly enable `APIConfig.DocumentsEnabled`; the feature is disabled by default and independent of Admin/Profile. A configured storage path alone does not enable it. Configure the exact portal origin/base path via the embedding app's trusted bootstrap; API base is `<basePath>/api/documents`. Every operation has its own method/resource path; use the complete [operation table](../authentication-portal-documents/references/protocol.md#http-messages). GET reads; PUT registers keys and writes immutable manifests, chunks or grants; DELETE removes documents or grants; POST allocates, commits and resets. Paths use `fetch`, never `get`. For example, fetch a chunk with `GET /api/documents/file/{id}/chunk/{idx}/fetch` and delete an owned file with `DELETE /api/documents/file/{id}/delete`. Select from the fixed catalogue; never accept a complete operation URL from untrusted file data. Validate and encode individual path parameters. Use canonical decimal chunk indices without leading zeros. List files with `GET /api/documents/files` and allocate uploads with `POST /api/documents/files/allocate`. Own-key operations use `/keys/fetch`, `/keys/register` and `/keys/reset` beneath the API base; recipient lookup remains `/key/{user_id}/lookup`. The only query is file listing's optional `after` cursor. Use relative same-origin fetch URLs and `credentials: "same-origin"`. GET, DELETE and commit have no body. Other writes require `Content-Type: application/json` and only their documented direct fields. Do not send `operation`/`payload` wrappers or repeat resource path parameters in a body. Every success is HTTP 200 with resource fields directly in the response. Validate them against the selected method/path schema. There is no `operation` or `result` wrapper. Mutations without resource data return `{"success":true}`; require the literal boolean true. Failures have an HTTP error status and `{status,message}`. See the linked protocol's operation table for exact payloads. Bound response reads to 2 MiB; reject oversized or malformed JSON. For example, capability discovery sends: ```js const response = await fetch(`${apiBase}/bootstrap`, { method: "GET", credentials: "same-origin", signal, }); // Read a bounded response, check HTTP status, then validate its direct fields. ``` Do not store JWTs in JavaScript storage or introduce a token in URLs. The API needs both a valid portal cookie and server-cached local browser login evidence. Do not use access-only JSON login as a substitute. A document recovery key is an additional client-only secret, independent of the portal password and MFA. GET `/bootstrap`, verify `protocol === "authcrunch-documents"`, obtain the portal ID, exact local user ID and limits, then `key.fetch`. Distinguish its 404 (not enrolled) from the bootstrap's 404 (feature unavailable). Handle 401 through a fresh portal browser login. When the host already provides refresh/session coordination, reuse it for expiry; do not invent a competing refresh loop or repeatedly refresh revoked identity evidence. Follow the host's validated return-navigation contract. 403 is a permission/origin/realm failure; 503 is temporary storage unavailability. Never persist decrypted documents or keys in localStorage, service-worker caches, analytics, error monitoring payloads, logs, query strings or React debug tools. Keep secrets out of React component state where a dedicated worker can own them. On account/portal change, logout or lock, abort requests, clear decrypted state, terminate the crypto worker, and revoke object URLs. Bootstrap user ID/portal must match the unlocked vault before every operation; transformed display names are not ownership identifiers. ## Enrollment, unlock, and key loss `enroll(bootstrap)` returns `{registration,recoveryKey}`. The recovery key is a random 256-bit secret encoded as 43-character base64url. Require the user to save it and confirm the backup before `key.register` with **`{key:registration}` only**. Show the key locally; do not send it in telemetry or application API state. Explain that anyone with this key and the encrypted vault can decrypt the user's files and received shares. Login-password reset cannot recover document keys. On later devices, fetch the registration and call `unlock(bootstrap, registration, recoveryKey)`. Retain the non-exportable private CryptoKey in memory. Wrong keys and tampered vaults must stop before showing files. Never derive a recovery key from the portal password or accept a weak human passphrase in its place; the client has no passphrase KDF. On lost recovery key, present a deliberate destructive-reset flow. Explain that `key.reset` with `{user_id:confirmedUserID,key_id:confirmedKeyID}` removes all owned documents, their shares, and received grants, then allows fresh enrollment; it does not recover plaintext or affect other owners' stored files. Require an explicit user confirmation in the UI before sending that deletion. Capture the user ID and fingerprint from the registration shown in the confirmation UI; do not replace them with a later bootstrap/key value before sending. A cookie account switch or changed registration causes 409 and must invalidate the confirmation. Never automatically retarget or reconfirm a reset after a conflict. This server check closes the gap between a UI check and another tab switching cookies. A different `key.register` registration returns 409. If an enrollment response is lost, `key.fetch` and reconcile the fingerprint and vault before retry. Never auto-reset after an unlock or network error. ## Upload workflow 1. Read `File.size` locally; call `describeSize(size)` and compare its ciphertext `size` against the server limit. Send `file.allocate` with only `{size,chunks}`. Keep the returned ID/owner/size/chunks immutable. Allocation is not idempotent; after a lost response, inspect the owner's pending list. Concurrent tabs can allocate indistinguishable descriptors: never guess ownership of an operation from size/time or automatically delete a matching candidate. Present uncertain pending entries for deliberate abandonment; fresh attempts use a fresh ID/key. 2. Call `prepareUpload(bootstrap, descriptor, ownRegistration, {name:file.name,type:file.type,size:file.size})`. It returns `manifest`, `encryptChunk(idx,bytes)` and `dispose()`. 3. PUT `{manifest}` to `/file/{id}/manifest`. Read each Blob slice of at most 1048576 plaintext bytes, encrypt once, then PUT `{ciphertext:encode(encryptedBytes)}` to `/file/{id}/chunk/{idx}/put`. No FormData, plaintext metadata or data URLs go to the API. Limit concurrent encryption/network work to bound memory. 4. Retry uncertain writes using the exact buffered ciphertext. The module rejects encrypting the same `idx` twice. Do not mix chunks from different prepareUpload calls. If exact upload state is lost on reload, delete/abandon that allocation and allocate a fresh document/key. `chunk.fetch` does not support pending files. 5. POST `/file/{id}/commit` **without a body** after every chunk succeeds. Identical commits/chunk and manifest writes are idempotent; conflicting ciphertext returns 409. Check the descriptor after an uncertain commit. Finally dispose the upload key buffer. Crypto calls snapshot input buffers and the context fields they use before their first await. You may clear or reuse your own Uint8Array after starting a call; the helper clears only its owned copies. This includes Node Buffers. Keep concurrency bounded because each pending call owns a copy. This snapshot is not cancellation: dispose prevents new chunk encryption but already started work may finish. After logout or an account switch, discard late results and stop network writes even if the cryptographic promise succeeds. Cancel aborts requests and may DELETE `/file/{id}/delete` without a body for the pending owner file. Pending uploads have an absolute TTL; activity does not extend it. Expired uploads return 404. Handle 507 as quota exhaustion, retaining the user's original local file. Empty files still upload one 16-byte ciphertext/tag chunk. Files and metadata are immutable; editing means allocating a new file/key. Revocation-sensitive new versions must use a new key and only wrap it for current recipients. ## List and download workflow GET `/files` without a body or query for the first page. Follow `next` using `/files?after=<next>`. Lists omit metadata/envelopes, so GET `/file/{id}/fetch` with bounded concurrency for display. After unlock, call `unwrapKey(bootstrap, detail, ownRegistration, privateKey)` and `decryptMetadata(...)`. Render filenames as text; never use decrypted metadata as HTML or a navigation URL. This API supports flat documents, not server-side folders or plaintext search; any name search happens locally after decryption. GET every expected `/file/{id}/chunk/{idx}/fetch` without a body, decode its `ciphertext` using the module's canonical base64url decoder, and call `decryptChunk(bootstrap, detail, documentKey, idx, ciphertext)`. Verify all tags, indices, lengths and the metadata plaintext size before marking the download complete. Fail closed on corruption/truncation; never offer partial data as a successful file. A bounded Blob fallback can use `URL.createObjectURL`, an anchor with a sanitized basename in `download`, and timely `URL.revokeObjectURL` after the browser accepts the save. Use application/octet-stream for the download; avoid inline execution or preview of uploaded HTML/SVG. For large files, use a supported local streaming save flow with explicit partial-file cleanup on failure. The client cannot guarantee secure erasure of strings or browser-managed memory. Clear owned Uint8Arrays with fill(0), release CryptoKeys, and stop workers when finished. Avoid buffering an entire large file solely to make an object URL. ## Sharing Use a JSON share code with exactly these string fields: ```json {"protocol":"authcrunch-documents","portal_id":"<bootstrap.portal_id>","user_id":"<bootstrap.user_id>","key_id":"<registration.key_id>"} ``` The portal ID and key fingerprint are canonical 43-character base64url values. `user_id` is the exact canonical lowercase non-nil UUID (36 characters) from the local user database. Never substitute email, username, a hash or an external provider subject. Future federation-to-local mapping is not implemented here. Bound pasted codes to 1 KiB and reject unknown fields, invalid identifiers and unsupported protocols. This is public identity data, not a secret link or a bearer grant. Display the portal and recipient fingerprint for confirmation. Never include recovery/document keys or session credentials. Compare the code's portal ID against the current bootstrap, then GET `/key/{user_id}/lookup` and verify its user_id and full fingerprint match the code. The public-key response has no portal ID field. Refuse changed keys and require an explicitly confirmed new code. Owner unwraps the existing document key, calls `wrapKey(bootstrap, detail, recipientPublicRegistration, documentKey)` and sends PUT `/file/{id}/share/{user_id}/put` with only `{envelope}`. Only owners can share. Recipients receive read access and use their own vault/key; no private key is exchanged. GET `/file/{id}/share/list` returns recipient UUIDs only. DELETE `/file/{id}/share/{user_id}/delete` without a body stops future downloads. Clearly explain that recipients can keep or redistribute plaintext already read. ## Product and integration limits Present encryption as protection against passive traffic/content inspection, with a server that stores ciphertext. Operation paths, resource IDs and list cursors are visible in URL logs. Decrypted TLS messages also reveal sharing metadata and ciphertext sizes; do not promise otherwise. Do not promise protection against modified JavaScript, compromised browsers/devices, a malicious recipient, traffic analysis, or file recovery without the key. The protocol is public; its security relies on keys rather than obscurity. Recovery-key compromise exposes historical documents; there is no forward-secrecy claim. The Go repository currently embeds only the existing Profile SPA. This work adds the Documents API and reusable crypto module, not an embedded Documents build or new page route. App build packaging, hosting, route reservation, CSP and frontend links must be integrated explicitly when the external React app is ready; do not place a source tree into the compiled `pkg/authn/ui/profile` directory or run the profile-only asset refresh script for Documents. ## Acceptance tests Use actual browser Web Crypto and real TLS portal login with two distinct users. Assert actual network requests use each documented method and resource path, with only the optional list cursor in queries. GET, DELETE and commit send no body; other writes send direct JSON without repeated path parameters. Cover rejection of obsolete `get` paths, operation/payload wrappers and fields that do not belong to the selected operation. Cover enrollment backup/unlock on another device, zero-byte and multichunk files, owner and recipient decryption, outsider rejection, pagination, retry of identical ciphertext, interrupted uploads, stale key fingerprints, reset, revocation, logout, 401 recovery and account switches. Check URLs and decoded TLS-visible request bodies for plaintext filenames, file content, recovery keys and unwrapped keys. Tamper with ciphertext, metadata, portal/owner/document IDs, sizes and chunk ordering and require decryption failure. Verify safe filename rendering and completed download contents. Start encryption/wrapping, immediately clear the input buffers, and verify the original bytes still decrypt; mutate caller-owned context during a pending operation and verify that it uses the call-entry snapshot. Run the independent protocol vector along with the frontend tests. Delay an upload while its login evidence is revoked, and delay a share while the recipient resets and reenrolls. The first request must return 401 without writing a chunk; the second must return 409 without creating a grant for the old fingerprint. Retain the user's source file and require fresh login or explicitly confirmed recipient identity before starting new work. The backend's reference Chrome journey is [`documents_browser_e2e.cjs`](../../../pkg/authn/ui/testdata/documents_browser_e2e.cjs). It checks an HttpOnly cookie bootstrap using a session established by real HTML login, then uses bearer transport for isolated multi-user crypto/API assertions. The external app must additionally exercise its own login navigation, cookie-only journey, account switching and session-expiry UI; the reference is not a React UI.
View on GitHub