Implemented key-based prototype
The public /favicon.ico index and authored document intro remain readable to everyone; the amoosed.com side panel displays its own access controls instead of the site's intro. A bundle config may set "keyId": "edition-1"; bundle then requires a 32-byte key through AMW_CONTENT_KEY_HEX (64 hexadecimal characters). Without a protected list, all app resources are encrypted as in the initial profile. With "protected":["members/"], only matching paths are encrypted and the remaining app resources are public. The key is never placed in the config, favicon, media carriers, extension, or generated site files. The manifest declares access per resource and contains the algorithm, key ID, nonce, ciphertext offset and hash, plaintext length, and final plaintext resource hash for protected chunks.
The optional external index key is a different layer. With "indexDelivery":"external", the site favicon remains ordinary, the full index is AES-GCM encrypted in a data carrier, and a dedicated .amwkey text file or compatible packed ICO is shared privately. Either contains a random index key and a relative locator for the encrypted index. Its holder can open the whole Amoosed app, including resources otherwise labeled public within Amoosed mode. Member resources can still require the independent content key. The index key is stored in .amw/keys/index.hex for reproducible builds, or supplied through AMW_INDEX_KEY_HEX; the exported key files contain it and must be treated as bearer secrets. No payment or individual revocation is provided by either shared-key layer.
For an owner-managed paid edition, the owner may email the exported .amwkey or ICO after payment, for example with a design brief or a question about the project. The recipient imports the intact file on the site's Amoosed boot page; its relative path and byte range locate the encrypted index inside a published carrier. This can make delivery fit an existing customer conversation without adding a key-hosting endpoint. The message or document around the attachment is not part of the cryptographic check, and a misleading filename is not an access-control measure. Anyone who receives or is forwarded either file can unlock that matching release. Email delivery provides no payment verification, customer identity, expiry, or individual revocation.
The two keys support distinct scopes: the external ICO grants entry to the alternative app; the optional members key grants access to selected encrypted resources within it. The owner can send the ICO to one group and the members key to a smaller group. Keep both out of the public site and extension package. If the entire edition is meant for one paid group, the ICO alone can gate it; use the members key when a second tier is needed. This is shared-key access, not DRM, and an authorized recipient can save decrypted content.
The owner can also generate a QR link from either key file with amw key-link. The QR contains the same index key and locator and grants the same entry access; it does not encode or replace the separate members key. A public event poster or business card grants access to anyone who scans or photographs it. For paid access, share the QR only through a controlled channel, and assume recipients can copy it. A printed QR cannot be individually revoked; rotate the underlying index key and publish a matching carrier release for later invitations.
The members tier now has the same delivery choices. build-project writes .amw/keys/<keyId>.amwmember beside the owner-held hex key; a standalone owner can run amw member-file KEY_ID KEY.hex OUT.amwmember. A recipient can select the .amwmember file on the site's member unlock card. To issue a direct link and optional QR, run amw member-link KEY.amwmember https://site.example /amoosed.com/app/members/index.html member.svg. The target must be a protected resource in the verified current index. Its #amw-member= fragment contains both key ID and key bytes and is removed immediately by the boot page. The key ID prevents accidental use for another edition; authentication of a protected chunk rejects a wrong key. A copied complete older release may still open with its old key. The QR, URL, .amwmember, and raw hex key grant the same shared member access and must remain private. They do not replace an external index key: a recipient of an externally indexed app needs that index key first, unless it is already saved for the site.
$env:AMW_CONTENT_KEY_HEX = '<64 hex characters from a secure source>'
node src/cli.js bundle config.json SITE_BUILD APP_BUILD OVERLAY
node src/cli.js verify-bundle OVERLAY
node src/cli.js unpack-bundle OVERLAY RECOVERED_APP
Remove-Item Env:AMW_CONTENT_KEY_HEX
Without a key, verify-bundle verifies the public index, complete data-carrier footers, every ciphertext part, and every complete public app resource. It cannot verify protected plaintext. With the correct key it authenticates every chunk and checks complete resources. A wrong key fails. Encryption is deterministic for identical key, app bytes, carrier bytes, paths, and config: a 96-bit GCM nonce is derived by HMAC-SHA-256 from the key, authenticated resource identity, part index, and plaintext digest. The authenticated data includes the resource path, MIME type, chunk index, and plaintext length. The content key must be random and unique to an edition or key-rotation period; do not use a password directly as this key.
The site owner shares the 64-character access key directly with members. The extension side panel's Member access section accepts, checks, and forgets the key. It passes it to the site's own /amoosed.com/ service worker through a temporary same-origin browser tab and does not store a copy in extension storage. The site-owned boot page also accepts manual entry. The worker authenticates the smallest protected chunk before retaining a nonextractable Web Crypto key in its volatile memory. It does not persist member keys in IndexedDB. The worker can stop at any time, including before the browser session ends, so a member may need to enter the key again. This key check proves that one protected chunk decrypts; it does not claim every carrier is intact. Each complete protected resource is checked when opened. Public app pages load without any key. Only the key ID, not the raw key, appears in the public manifest. The worker checks ciphertext hashes before decrypting and returns only authenticated plaintext resources. The extension's privileged pages do not execute the app code.
The bottom-bar Advanced view can check whether the currently active key still authenticates a protected chunk. It distinguishes no active key, a key rejected by this edition, and a carrier fetch or integrity failure. A damaged carrier is named in the check error and must not be reported as a bad key. It reports key validity for this edition, not a subscription or a verified payment. Future advanced status checks can use the same site-origin tab and worker message pattern, with each operation separately validated and only a narrow result returned to the extension. The website must not supply side-panel markup or scripts. Account or payment status would require a distinct, explicitly designed entitlement source; the current owner-shared-key edition has none.
If an active key fails authentication on a protected resource, the worker removes it. A member-page navigation returns to the key prompt with a validated return path; a protected subresource responds with a key-required error. A carrier hash failure remains a verification error and does not silently turn into a key prompt.
For one-click access to a protected page, the owner may share a link of the form https://customer.example/amoosed.com/boot.html?return=%2Famoosed.com%2Fapp%2Fmembers%2Findex.html#amw-key=<64-character-key>. The return path must name a protected resource in the verified index on the same origin. The fragment is not sent to the website server; the boot page reads it, immediately removes it from the visible URL, checks it with the worker, and opens the member page. Treat the entire link as a bearer secret: anyone who receives or copies it can use the key. Share it privately and do not place it in a public page, analytics event, or support log. The installed site-origin runtime route is still required for the full app; the link adds no entitlement server.
To forget the active key, navigate to /amoosed.com/boot.html?forget=1. The boot page asks the worker to remove the active edition's key before showing the unlock form. App authors can link a sign-out control to this URL. Already open pages may still contain decrypted content until closed or reloaded. Forgetting a local key does not revoke copies already downloaded and does not change a key held on another device. Upgrading from a release that persisted member keys deletes the legacy IndexedDB key store on worker activation.
For a local example, build the ordinary demo first, set a temporary key, then run npm run build:sealed-demo and node tools/serve.js dist-sealed 4174. Visit http://127.0.0.1:4174/, open the amoosed.com panel, and launch the alternative app. Its homepage, style, script, and image are public. Follow Open the members’ night log to get the key prompt; entering the temporary key opens encrypted member HTML, CSS, JavaScript, and image. Keep test keys outside release artifacts. The generated dist-sealed/ retains the ordinary site's index.html, contains the small runtime and two existing image carrier paths with the mixed payload, and contains no key or unbundled app files.
Shared keys and rotation
The starter compiler provides amw rotate-project PROJECT members|site|both. members generates a new random content key and key ID without changing the site-wide key derivation; site rotates the private index key for an external index, or a public encoding salt for a hosted index. A new build and replacement of the complete published carrier release are required. An external-index member-only rebuild changes the index locator, so its new .amwkey or ICO must still reach index-key holders. The old member key cannot decrypt newly published member ciphertext, but old matching carrier snapshots and saved plaintext remain usable. See ROTATION.md for exact commands and deployment steps.
This edition uses an owner-shared key; it needs no membership endpoint, payment service, or account sign-in. The publisher decides who receives the key. A shared key cannot distinguish members or revoke one member alone. To rotate it, generate a new random key and key ID, rebuild the encrypted carrier overlay, publish the replacement carriers together, and share the new key only with continuing members. Existing copies of the old plaintext cannot be recalled. Do not put the key in the extension package, public HTML, a shared page cache, or public media. The ordinary site remains usable for everyone.
Same-origin JavaScript can access the browser's same-origin storage and use a stored CryptoKey to decrypt, even if the key is marked nonextractable. A paying customer can inspect or save the decrypted HTML, JavaScript, images, and responses. Encryption gives access control over downloaded carrier bytes; it is not DRM or source-code secrecy. Once a key has been issued or plaintext downloaded, a server cannot claw it back. Rotation requires publishing new ciphertext under a new key ID and securely delivering that new key only to entitled customers.
For an edition that requires individual membership status, billing, or per-member revocation, a separate customer-specific entitlement design would be required. This shared-key prototype does not claim those features. Keep all protected content in encrypted app resources: the bootstrap and document text in /favicon.ico are intentionally public. SHA-256 detects corrupted carrier chunks without a key but does not conceal their bytes; optional AES-GCM is required when carrier content itself must stay private.