# Hosted Auth preview: two data modes Status: implemented and locally tested; hosted activation must be verified separately with `GET /auth/health`. A 503 or missing route means it is not enabled. Installing the client does not enable the service. This is a bounded pilot, not a production identity platform or an OpenID Connect provider. ## Choose at app creation **Recommended: app-managed** for ordinary apps, settings, dark mode preferences and customer support. **Advanced: customer-private encryption** when the app operator must not read customer content. Private mode offers stronger content privacy, with harder recovery; it is not a blanket security guarantee. Agents should recommend app-managed while explicitly recording the selected mode. - `customer-private`: each customer has a separate device-held encryption key. The browser encrypts record names and values. The service verifies identity and enforces account ownership, but cannot decrypt the records. An account recovery code replaces a lost passkey; a separate data recovery file unlocks existing data. Neither the developer's store encryption key nor the customer's passkey is the customer's data key. - `app-managed`: customers still see only their own records. The service decrypts authorised requests and encrypts record values at rest with a server-held key. The app operator can grant temporary, read-only support access to one customer. The service operator can technically decrypt data. This is not end-to-end encryption. Managed record names and account metadata remain visible to the service. There is no implicit default or in-place mode switch. Existing anonymous stores and Connect links are unchanged. Migrating an existing app requires a deliberate export/re-encryption/import workflow; none is automatic. ## Unattended first build Vault configuration belongs to the VibeCodeStorage operator, once per deployment. A vibecoder does not need a vault account, email provider, manual key handling or an owner-account signup to get the first app running. The agent must already have permission/access to deploy the app to its chosen host. Actual customers perform their own passkey interaction. Download `/auth-setup.mjs` and `/storage-setup.mjs` together into a trusted build directory, install the existing Node SDK, and gitignore `.private/` before running. The helper checks Auth availability before provisioning, defaults to app-managed, saves setup identifiers/owner credentials privately, activates one management store and returns only public app configuration and deployment proof. Retries reuse saved state; there is no background loop. ```js import {prepareAuthApp,verifyAuthApp} from './auth-setup.mjs'; const prepared = await prepareAuthApp({name:'My app',callback:EXACT_HTTPS_CALLBACK}); // Deploy prepared.config in the app, and prepared.proof.content at prepared.proof.path. // Never deploy .private/ or include it in a public artifact. await verifyAuthApp(); ``` After a successful first build, recommend backing up management credentials privately. A convenient “Claim this app” owner-account/recovery interface is planned, not shipped; it must not become a prerequisite for agent setup. Do not claim email-based owner recovery exists. For now the agent/build environment must retain `.private/auth-app.json` and its `.owner` file securely. ## Agent registration The agent needs an existing activated store's owner token and store ID, kept privately in its trusted environment. Complete that store's first encrypted write within four hours using the existing setup helper. Reuse it; do not create stores for health checks or for each customer. The owner's original store encryption key is not used for customer data. Check `/auth/health`, then POST `/auth/management/apps` with `Authorization: Bearer OWNER_TOKEN`, `X-VCS-Store: STORE_ID`, `Content-Type: application/json`, and no browser Origin header: ```json {"name":"My app","mode":"app-managed","callback":"https://example.com/my-app/callback","requestId":"a-saved-32-byte-random-base64url-identifier"} ``` Generate a real 43-character requestId once and save it privately before the request. Repeating the same request returns the same app; changed settings return 409. The response includes public `appId`, mode, `verificationPath`, `verificationToken`, and verified status. Deploy the exact token as a plain-text file at the returned path on the callback's origin. This public deployment proof does not grant management or data access. POST `{ "appId": "..." }` to `/auth/management/verify` with the same owner headers. Verification fetches only HTTPS, pins a public IPv4 address, follows no redirects and limits response length/time. IPv6-only deployments are not yet supported. The callback must be canonical HTTPS without credentials, query, fragment or non-default port. The deployment file sits beside the callback directory, so project subpaths can be used. Only the public app ID, endpoint and exact callback URL go into the app. Do not put an owner token, support token, server data key or shared encryption key in its browser bundle. The developer need not approve each customer's signup. Customers must perform their own passkey interaction. No email account, OAuth console or Resend key is needed for this flow. ## Browser client The client-only npm package adds `vibecodestorage/auth` (browser ES module, Web Crypto, HTTPS). The existing main export remains the Node SDK/CLI. For a static app, vendor `auth-browser.js` from the package locally; do not import the Node SDK into the browser. ```js import { createAuthClient } from 'vibecodestorage/auth'; const client = createAuthClient({ appId: PUBLIC_APP_ID, redirectUri: 'https://example.com/my-app/callback' }); // On the user's Sign in button: await client.signIn(); // On the exact callback page: const identity = await client.finishSignIn(); // On a later page load: await client.resume(); ``` Both modes use hosted passkey registration/login, exact redirect matching, state and S256 PKCE. Sessions expire after an hour; show Sign in again on 401. The client stores the session in sessionStorage and sends it as a bearer token. No background retries or automatic re-provisioning. For app-managed mode, signing in is enough to access records. For customer-private mode, check `identity.locked`. If there is no existing vault: ```js const recovery = await client.prepareRecovery(); // Offer a private download and have the customer confirm they saved it. // Never log this object or send it to analytics, support or the server. await client.unlock(recovery); ``` If a vault already exists, `prepareRecovery()` refuses to replace it. Import the customer's existing recovery file with `client.unlock(file)`, or export it from an unlocked device with `client.exportRecovery()`. Recovery files are bound to the account, app and API endpoint. The key is remembered in that browser's localStorage by default; use `{remember:false}` to hold it only in memory. Browser storage is not a backup. An app-origin script compromise can read browser-held secrets; avoid third-party scripts on sensitive screens and apply a strict CSP. Losing every copy of a private key permanently loses access to the data. ```js await client.set('preferences', {theme:'forest'}, {version:0}); // create only const entry = await client.getEntry('preferences'); await client.set('preferences', {theme:'ocean'}, {version:entry.version}); const entries = await client.list(); await client.delete('preferences', {version:entry.version + 1}); await client.signOut(); // retains this device's saved data key // client.signOut({forgetDevice:true}) also removes this browser's saved data key. ``` Use generic app language: Sign in, Your data, Save, Export. Do not assume a list or offer a share link that gives away an account session or private key. Preserve unsaved drafts on 401, 409 and 429. Conflicts require reload/merge; rate limits require a user-visible wait, not a retry loop. JSON values must be serialisable; do not store secrets or important/sensitive data in the pilot. ## Support access (app-managed only) Owner-authenticated POST `/auth/management/users` with `{appId}` returns up to 100 account IDs, not names/emails/content. POST `/auth/management/support` with `{appId,userId,reason}` issues a 15-minute bearer token limited to read-only records for that account. Use it from a trusted backend tool at GET `/auth/support/records`; browser Origin requests are rejected. Protect the tool with staff authentication before exposing it. There is no staff dashboard or delegated staff identity system in this release. POST `/auth/management/revoke-support` with `{appId,userId}` revokes that customer's support grants. POST `/auth/management/audit` with `{appId}` returns the latest 100 grant/read/revocation events. Logs identify the owner store and target account, not an individual staff member. Reasons must contain a ticket reference, never customer content. Events are retained for 90 days. Revocation cannot retract a downloaded copy. Support tokens cannot write, delete, manage apps or access private-mode content. Customer-approved diagnostic exports for private mode remain an app UI choice; no automatic support upload is provided. ## Pilot limits and recovery 50 apps globally, 3 apps per owner store, 100 accounts per app, 1,000 accounts globally, 100 records and 1 MB of encoded stored data per account, 100 MB aggregate authenticated-record capacity. These preview limits are separate from the existing 200-store / 2 MB anonymous-storage pilot. Sessions last one hour, authorization requests/codes five minutes. Request rate limits also apply. Capacity is not a count of people. Customer passkey recovery replaces existing passkeys, revokes sessions and rotates its recovery code. Private data keys are unchanged. App-managed customers do not need a separate data-key recovery file. App management still relies on the developer's privately backed-up owner token; email does not restore it. Store deletion is blocked while apps are attached. App/account deletion and callback migration tools are not yet shipped; contact the operator for a verified maintenance process. Losing management credentials must never grant someone access to customer data by guessing an email. ## Operator deployment, backup and Linux portability Run Node.js 24+, `npm ci --omit=dev --ignore-scripts`, then `npm start`, or build the private Dockerfile. One process, one persistent SQLite database on local disk, TLS terminated by your reverse proxy, and an exact forwarded Host matching the configured origin. Do not run multiple independent writers or place SQLite on a network filesystem. Set `VCS_AUTH_ORIGIN=https://api.your-domain.example` (origin only). Set `VCS_AUTH_MANAGED_KEY` to a cryptographically random 32-byte base64url value, generated once in a private secret-management workflow. Never print it in deploy logs. Persist it separately from the SQLite database and back it up securely outside the host before admitting managed data. Do not regenerate it on restart. A stored fingerprint makes startup fail if an existing managed key is missing or different. Key rotation requires an explicit re-encryption migration; it is not yet implemented. Leaving `VCS_AUTH_ORIGIN` unset keeps hosted auth disabled. Auth tables share the main SQLite database, so consistent SQLite snapshots include accounts, sessions, encrypted records, vault commitments and support audit events. The managed key is NOT in those backups. Restoring a database without that key does not recover managed data. Private recovery files remain with customers. Keep the API/RP hostname stable when moving hosts; a hostname change breaks existing passkeys and changes the private-key context. Test restores and account recovery before launch. Backup freshness monitoring alone is not proof of an off-site restore. ## App-managed passkey reset links After verifying the customer through an established channel, the agent/backend may POST `/auth/management/reset-passkey` with owner headers and `{appId,userId,customerVerified:true,reason:"ticket reference"}`. The service records that the operator asserted verification; this boolean is not independent identity verification. The returned URL opens the registered callback with a secret fragment. It expires after 15 minutes; issuing a replacement invalidates the older link. Do not log it or send it to an unverified address. No email is sent by this endpoint, and this release has no customer email collection/verification system. Automatic email resets must wait for verified customer email support, or be implemented by the app operator using its own verified identity channel. The app callback must check for `vcs-reset` in the URL fragment BEFORE its usual authorization-code handler. Capture and immediately strip the fragment, show “Replace your passkey and sign out other sessions?”, then call `client.signIn({resetToken})` only after confirmation. The client establishes a normal PKCE/state flow and passes the token to the hosted reset screen in the fragment. The customer must create and verify a new passkey. Successful completion consumes the reset token atomically, replaces passkeys, revokes old sessions/outstanding codes and rotates the account recovery code. Merely opening the link does not change the account. POST `/auth/management/revoke-reset` with `{appId,userId}` cancels an unused link. Issuance, completion and cancellation are audited. The normal sign-in callback then completes with `finishSignIn()`. This is account recovery, not an email magic-link login that skips passkeys. Operator-issued reset links are rejected for customer-private apps; those use the customer's original account recovery code and retain separate data-key recovery. Key custody roadmap: the current preview uses a service-held environment key, not a KMS. Developer owner-account recovery and per-app wrapped keys are not yet implemented. Do not confuse a service-notice email with verified app ownership.