14 / 15 · Setup & Operations
Build elsewhere, then operate with evidence
A clean-machine path for the new FileLoveNote build, with separate Google sign-in and Drive clients, bounded infrastructure and a recovery plan that protects unsent work.
These examples target a new folder and a new deployment owned by the builder. No account was configured, no server was installed and no production restore was performed for this chapter. service.example is a reserved example name; replace it with a domain you control before a live test. Keep Bun/CommonJS, the custom router, server-rendered HTML and TypeScript/ProseMirror components. This chapter does not introduce React, SQL, a native app, a second file provider or an alternate login.
Read Security, Failure & recovery, API contracts and Acceptance alongside this runbook. The recovery chapter defines the required behavior when a connection or account changes; the acceptance chapter owns the test hardware, datasets and performance budgets.
1 · Freeze the toolchain and dependencies
The source lockfile fixes the package versions below. The source repository does not pin a Bun runtime version. The authoring machine reported Bun 1.4.2, revision 1.4.2+744846f84, on 28 October 2026. Use 1.4.2 as the proposed new-build baseline, record its platform and binary checksum, and run the new build's tests before accepting it. The portable manifest/lock pair was installed in a new isolated temporary directory using bun install --frozen-lockfile --ignore-scripts --prefer-offline: 65 packages installed, exit 0 (28 October 2026 baseline). A second run reported no changes, and both file hashes stayed identical. This checked dependency installation on the authoring machine, with cached packages allowed and lifecycle scripts disabled; it did not start an app or test a clean Linux machine. Do not resolve packages from latest or run an unattended bun upgrade.
Use a supported Linux/macOS machine. The production example assumes a Linux system with systemd and Nginx. Record the OS release, architecture, Git, Nginx, OpenSSL and optional media tool versions in build-status.md. Nginx was not installed on the authoring machine; no Nginx or systemd version is claimed as tested.
# Linux example: install OS tools through your supported distribution.
# Review the package changes before running; these commands require sudo.
sudo apt-get update
sudo apt-get install --yes ca-certificates curl unzip git openssl
# Download and inspect the official installer, then install a specific Bun tag.
curl -fsSLo /tmp/filelovenote-bun-install.sh https://bun.com/install
less /tmp/filelovenote-bun-install.sh
bash /tmp/filelovenote-bun-install.sh bun-v1.4.2
export PATH="$HOME/.bun/bin:$PATH"
test "$(bun --version)" = "1.4.2"
bun --revision
sha256sum "$(command -v bun)" # macOS: shasum -a 256 instead
uname -sm
git --version
The official installer accepts a version tag. Frozen installs use the lockfile instead of silently updating dependencies. See Bun installation and Bun install and frozen lockfiles, checked on 26 October 2026.
Start in a genuinely new folder
mkdir filelovenote-rebuild
cd filelovenote-rebuild
git init --initial-branch=main
mkdir -p prompt tests
# Copy this complete documentation package into prompt/.
# Copy the supplied portable baseline; the complete blocks below are equivalent.
cp prompt/baseline/package.json package.json
cp prompt/baseline/bun.lock bun.lock
# Add the source generated by P00 before trying to start the app.
# Never copy an old .env, account directory, sessions.json or production data.
Download the portable package.json and the matching bun.lock, or copy the complete blocks below. Save the manifest as package.json. Its SHA-256 is 27f7d4323147652a8459518317b4da47c95d47badda28aa3dfdc6972544ebdf7. The versions are resolved direct dependencies from the source lockfile, not guesses about the newest releases. Existing browser test libraries do not change the requirement to use BrowserOS Neo for browser verification.
{
"name": "filelovenote",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "bun server.js",
"dev": "bun --watch --no-clear-screen server.js",
"test": "NODE_ENV=test bun test",
"test:browser": "bunx --bun playwright test --config=playwright/layout.config.js"
},
"license": "ISC",
"type": "commonjs",
"dependencies": {
"dotenv": "18.0.9",
"fflate": "0.8.3",
"google-auth-library": "11.3.1",
"loro-crdt": "1.17.1",
"orderedmap": "2.1.1",
"prosemirror-commands": "1.7.2",
"prosemirror-dropcursor": "1.8.4",
"prosemirror-gapcursor": "1.4.1",
"prosemirror-history": "1.5.1",
"prosemirror-inputrules": "1.5.1",
"prosemirror-keymap": "1.2.3",
"prosemirror-markdown": "1.13.9",
"prosemirror-model": "1.25.12",
"prosemirror-schema-list": "1.5.1",
"prosemirror-state": "1.4.4",
"prosemirror-tables": "1.8.5",
"prosemirror-view": "1.42.8"
},
"devDependencies": {
"@happy-dom/global-registrator": "21.0.4",
"@playwright/test": "1.64.1"
},
"packageManager": "bun@1.4.2"
}
Full baseline bun.lock for a builder without the original checkout
Copy the entire block to bun.lock. Its SHA-256 is 0539f3036bd6f25e4ff79e06ace12c9eec77e36877ffedbed6a4e3b4de1bf867. The portable lockfile normalizes the root workspace name and direct dependency declarations to match the exact manifest above; all 65 resolved package entries are preserved from the source lockfile.
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "filelovenote",
"dependencies": {
"dotenv": "18.0.9",
"fflate": "0.8.3",
"google-auth-library": "11.3.1",
"loro-crdt": "1.17.1",
"orderedmap": "2.1.1",
"prosemirror-commands": "1.7.2",
"prosemirror-dropcursor": "1.8.4",
"prosemirror-gapcursor": "1.4.1",
"prosemirror-history": "1.5.1",
"prosemirror-inputrules": "1.5.1",
"prosemirror-keymap": "1.2.3",
"prosemirror-markdown": "1.13.9",
"prosemirror-model": "1.25.12",
"prosemirror-schema-list": "1.5.1",
"prosemirror-state": "1.4.4",
"prosemirror-tables": "1.8.5",
"prosemirror-view": "1.42.8"
},
"devDependencies": {
"@happy-dom/global-registrator": "21.0.4",
"@playwright/test": "1.64.1"
}
}
},
"packages": {
"@happy-dom/global-registrator": [
"@happy-dom/global-registrator@21.0.4",
"",
{
"dependencies": {
"@types/node": ">=20.0.0",
"happy-dom": "^21.0.4"
}
},
"sha512-Bbr0hW9hhDhBf3Jp0EyDlkCbFgbr0z2jd3BMKYOYW+fOkPM4Nx195GMXyCtRCGEgoNn6i09FabXrSkNFm2ONzg=="
],
"@playwright/test": [
"@playwright/test@1.64.1",
"",
{
"dependencies": {
"playwright": "1.64.1"
},
"bin": {
"playwright": "cli.js"
}
},
"sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg=="
],
"@types/linkify-it": [
"@types/linkify-it@5.0.0",
"",
{},
"sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
],
"@types/markdown-it": [
"@types/markdown-it@14.2.0",
"",
{
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ=="
],
"@types/mdurl": [
"@types/mdurl@2.0.0",
"",
{},
"sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
],
"@types/node": [
"@types/node@26.6.5",
"",
{
"dependencies": {
"undici-types": "~8.9.0"
}
},
"sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg=="
],
"@types/whatwg-mimetype": [
"@types/whatwg-mimetype@3.0.2",
"",
{},
"sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="
],
"@types/ws": [
"@types/ws@8.18.2",
"",
{
"dependencies": {
"@types/node": "*"
}
},
"sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w=="
],
"agent-base": [
"agent-base@7.1.4",
"",
{},
"sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="
],
"argparse": [
"argparse@2.0.1",
"",
{},
"sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
],
"base64-js": [
"base64-js@1.5.1",
"",
{},
"sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
],
"bignumber.js": [
"bignumber.js@9.3.1",
"",
{},
"sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="
],
"buffer-equal-constant-time": [
"buffer-equal-constant-time@1.0.1",
"",
{},
"sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
],
"buffer-image-size": [
"buffer-image-size@0.6.4",
"",
{
"dependencies": {
"@types/node": "*"
}
},
"sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="
],
"data-uri-to-buffer": [
"data-uri-to-buffer@4.0.1",
"",
{},
"sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="
],
"debug": [
"debug@4.4.3",
"",
{
"dependencies": {
"ms": "^2.1.3"
}
},
"sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="
],
"dotenv": [
"dotenv@18.0.9",
"",
{
"bin": {
"dotenv": "dist/index.cjs"
}
},
"sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw=="
],
"ecdsa-sig-formatter": [
"ecdsa-sig-formatter@1.0.11",
"",
{
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="
],
"entities": [
"entities@7.0.1",
"",
{},
"sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="
],
"extend": [
"extend@3.0.2",
"",
{},
"sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
],
"fetch-blob": [
"fetch-blob@3.2.0",
"",
{
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="
],
"fflate": [
"fflate@0.8.3",
"",
{},
"sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="
],
"formdata-polyfill": [
"formdata-polyfill@4.0.10",
"",
{
"dependencies": {
"fetch-blob": "^3.1.2"
}
},
"sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="
],
"gaxios": [
"gaxios@8.3.2",
"",
{
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2"
}
},
"sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw=="
],
"gcp-metadata": [
"gcp-metadata@9.2.2",
"",
{
"dependencies": {
"gaxios": "^8.3.0",
"google-logging-utils": "^2.0.0",
"json-bigint": "^1.0.0"
}
},
"sha512-hlaHpCBoNCzoUjMnBZq0LbUiYqPAhXFaAgyNwQqwWU44TZIYJK3RD3o2wkNdvPs8nnUXqJjTudnodLJkLMBjow=="
],
"google-auth-library": [
"google-auth-library@11.3.1",
"",
{
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^8.3.0",
"gcp-metadata": "^9.0.0",
"google-logging-utils": "^2.0.0",
"jws": "^4.0.0"
}
},
"sha512-mQS0R2Ev/DKkzuWSYtDeq15uhDeaWuT2qzLYqJ41LmooxVKHmn5ut3pc6dlHY6wfHW/D7FBmOYjAtkjaD1A7qA=="
],
"google-logging-utils": [
"google-logging-utils@2.1.0",
"",
{},
"sha512-1R6uiq6XhXW2ZrcGtZpGgVDsYGzSmveVOn8Pp37qe1fbThHZ34e3t3eXj5mVu6PFSVx9fvB1KNVnYgVtq/BRcA=="
],
"happy-dom": [
"happy-dom@21.0.4",
"",
{
"dependencies": {
"@types/node": ">=20.0.0",
"@types/whatwg-mimetype": "^3.0.2",
"@types/ws": "^8.18.1",
"buffer-image-size": "^0.6.4",
"entities": "^7.0.1",
"whatwg-mimetype": "^3.0.0",
"ws": "^8.21.0"
}
},
"sha512-tFm+GaLBgo6o0awHnvHaNoraVo27qJ+2tZEtZ+cIa08hXbeitrxAJajLI05hsUy/2/YTKcUjQYzHe5mCcraLBw=="
],
"https-proxy-agent": [
"https-proxy-agent@7.0.6",
"",
{
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
}
},
"sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="
],
"json-bigint": [
"json-bigint@1.0.0",
"",
{
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="
],
"jwa": [
"jwa@2.0.1",
"",
{
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="
],
"jws": [
"jws@4.0.1",
"",
{
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="
],
"linkify-it": [
"linkify-it@5.0.2",
"",
{
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="
],
"loro-crdt": [
"loro-crdt@1.17.1",
"",
{},
"sha512-83hWRYG6eMMpPezm+yXWlOqCuUsC0BYY1MVCPW9x2zlikgkHhL7GC7Q6JFH+Hjvauyn287Q1MiwTuRW0lUOUag=="
],
"markdown-it": [
"markdown-it@14.3.3",
"",
{
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.5.0",
"linkify-it": "^5.0.2",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"sha512-L//tDAkjTjJCU6pBUSBTgs3Kg4InVaZuUZog/b6V12UKhcDQ7S78877ibbCanRExwPuYP1CZD7Gd2dxSy1GXRg=="
],
"mdurl": [
"mdurl@2.1.0",
"",
{},
"sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="
],
"ms": [
"ms@2.1.3",
"",
{},
"sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
],
"node-domexception": [
"node-domexception@1.0.0",
"",
{},
"sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
],
"node-fetch": [
"node-fetch@3.3.2",
"",
{
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
}
},
"sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="
],
"orderedmap": [
"orderedmap@2.1.1",
"",
{},
"sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
],
"playwright": [
"playwright@1.64.1",
"",
{
"dependencies": {
"playwright-core": "1.64.1"
},
"bin": {
"playwright": "cli.js"
}
},
"sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA=="
],
"playwright-core": [
"playwright-core@1.64.1",
"",
{
"bin": {
"playwright-core": "cli.js"
}
},
"sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ=="
],
"prosemirror-commands": [
"prosemirror-commands@1.7.2",
"",
{
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.10.2"
}
},
"sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw=="
],
"prosemirror-dropcursor": [
"prosemirror-dropcursor@1.8.4",
"",
{
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg=="
],
"prosemirror-gapcursor": [
"prosemirror-gapcursor@1.4.1",
"",
{
"dependencies": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="
],
"prosemirror-history": [
"prosemirror-history@1.5.1",
"",
{
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.0"
}
},
"sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA=="
],
"prosemirror-inputrules": [
"prosemirror-inputrules@1.5.1",
"",
{
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="
],
"prosemirror-keymap": [
"prosemirror-keymap@1.2.3",
"",
{
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="
],
"prosemirror-markdown": [
"prosemirror-markdown@1.13.9",
"",
{
"dependencies": {
"@types/markdown-it": "^14.0.0",
"markdown-it": "^14.0.0",
"prosemirror-model": "^1.25.0"
}
},
"sha512-qAI3I9YdtjWHjTCBErc08ivr24KSrUjcTh5dYncCxFM+JS02kT0iHeWYjO35xeTPwt5t8rhFrelewXhXz16WZQ=="
],
"prosemirror-model": [
"prosemirror-model@1.25.12",
"",
{
"dependencies": {
"orderedmap": "^2.0.0"
}
},
"sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg=="
],
"prosemirror-schema-list": [
"prosemirror-schema-list@1.5.1",
"",
{
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.7.3"
}
},
"sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="
],
"prosemirror-state": [
"prosemirror-state@1.4.4",
"",
{
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="
],
"prosemirror-tables": [
"prosemirror-tables@1.8.5",
"",
{
"dependencies": {
"prosemirror-keymap": "^1.2.3",
"prosemirror-model": "^1.25.4",
"prosemirror-state": "^1.4.4",
"prosemirror-transform": "^1.10.5",
"prosemirror-view": "^1.41.4"
}
},
"sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="
],
"prosemirror-transform": [
"prosemirror-transform@1.12.4",
"",
{
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"sha512-2/WuPum3pn+O/Vi2vzpi8210ICUe2kIPXdLYjQ09Pv5DtT5bcKWMJhAKPBwvpPjpp5iOYUVwVEr0gOM1BY1Mgw=="
],
"prosemirror-view": [
"prosemirror-view@1.42.8",
"",
{
"dependencies": {
"prosemirror-model": "^1.25.12",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"sha512-ftDZjCs9AjAekXmCfJRn2U3IdlRCl0Yc4tZT1/jJWCsz4DiSifEBXtWmpJpw/4f1Rg328uAUQ+u2lCJ0+AVZkw=="
],
"punycode.js": [
"punycode.js@2.3.1",
"",
{},
"sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="
],
"rope-sequence": [
"rope-sequence@1.3.4",
"",
{},
"sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="
],
"safe-buffer": [
"safe-buffer@5.2.1",
"",
{},
"sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
],
"uc.micro": [
"uc.micro@2.1.0",
"",
{},
"sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
],
"undici-types": [
"undici-types@8.9.0",
"",
{},
"sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="
],
"w3c-keyname": [
"w3c-keyname@2.2.8",
"",
{},
"sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
],
"web-streams-polyfill": [
"web-streams-polyfill@3.3.3",
"",
{},
"sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="
],
"whatwg-mimetype": [
"whatwg-mimetype@3.0.0",
"",
{},
"sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="
],
"ws": [
"ws@8.22.0",
"",
{
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"optionalPeers": [
"bufferutil",
"utf-8-validate"
]
},
"sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="
],
"markdown-it/entities": [
"entities@4.5.0",
"",
{},
"sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
]
}
}# Run only after package.json and bun.lock have been saved.
test -f bun.lock
bun install --frozen-lockfile
# Record both hashes with the new build's release; commit the lockfile.
sha256sum package.json bun.lock
# After P00 has created server.js and at least one meaningful test:
bun run test
bun run dev
# Production starts without watch mode:
# bun run start
For DOM unit tests, match the source test setup: bunfig.toml contains [test] and preload = ["./tests/setup-dom.js"]; that file registers @happy-dom/global-registrator. Add the preload file before enabling it. Running the empty skeleton is not proof that the product works. If a frozen install fails, investigate the manifest/lock mismatch; do not delete the lockfile or upgrade packages to hide it. Any deliberate dependency change needs its own lockfile, scan and regression evidence.
Isolated BrowserOS Neo checks
The 28 October source maintenance uses bun run test:browser. In the new build, implement playwright/layout.config.js and a shared test fixture before using that script. Connect Playwright to the running BrowserOS Neo at http://127.0.0.1:9110; do not launch a separate Chrome or headless browser. Start a test-only app with a temporary APP_DATA_DIR, random session secret and unused port. Seed a synthetic Google-provider session only in that isolated data directory, then give each test a fresh browser context. Close test contexts, stop the server and remove its data on teardown. Keep real credentials empty; a synthetic OAuth client may test the initial Google redirect without exchanging a token.
Playwright can apply the test's default storageState to a manual browser.newContext() call. For an anonymous login check, explicitly pass {storageState:{cookies:[],origins:[]}}. Never add a fixed code, password route or runtime login bypass to satisfy a browser test. Wait for the editor's new document ID and committed local content before reloading; typing alone does not prove the save finished.
# After implementing the new build's isolated browser fixture:
pgrep -f "BrowserOS neo" || open -a "BrowserOS neo"
bun run test:browserThe reference app passed 1,843 Bun tests and seven Neo checks on 28 October: default theme tokens at desktop/mobile widths, responsive Notes/Files/Settings layouts, Google-only login and its initial redirect, API-failure persistence and real offline note/folder reloads. These results concern the maintained reference app, not a newly generated app. Real Google token exchange, live Drive sync, A01–A12 performance budgets and clean Linux production setup still require their own evidence.
2 · Configure two Google OAuth clients
Use your own Google Cloud project and synthetic test accounts. Enable the Google Drive API for the Drive client. Configure Google Auth Platform branding, audience and data access; for an external app in testing, add the intended accounts under Test users. Create separate Web application client IDs for sign-in and Drive, preferably separate development and production clients too. Do not use a service account or embed a client secret in browser code. See Google consent configuration and Google web-server OAuth.
| Connection | Development redirect URI | Production redirect URI |
|---|---|---|
| Google sign-in | http://localhost:3000/auth/google/callback | https://service.example/auth/google/callback |
| Drive access | http://localhost:3000/google-drive/callback | https://service.example/google-drive/callback |
Register these as authorized redirect URIs on the matching client, with the exact scheme, hostname, port and path. Localhost HTTP is Google's development exception; use HTTPS for the real deployment. Do not mix localhost and 127.0.0.1 during a flow. An origin is not a callback path. This server-mediated flow does not require adding a browser OAuth SDK; if a later chosen implementation does use one, configure its origins separately rather than putting a secret in it. The source can derive callbacks from request origin; set the explicit environment values below for one canonical production host.
| Purpose | Exact requested scopes | Entry and verification |
|---|---|---|
| Sign-in | openid email profile | POST /auth/google; return to GET /auth/google/callback. Validate OAuth state and the Google ID token for your client ID; rotate the app session. Signup also requires explicit terms consent. |
| Drive | https://www.googleapis.com/auth/drive.readonlyhttps://www.googleapis.com/auth/drive.file | After app sign-in, GET /google-drive/connect; return to GET /google-drive/callback. The source requests offline access with a consent prompt to obtain a refresh token. Verify the connected Drive account before activating sync. |
Sign-in and Drive permission are separate grants. drive.readonly can read beyond the FileLoveNote folder and is a restricted scope; drive.file is narrower per-file access. Do not claim that the combined grant is limited to the app folder. Review Google's verification and, where applicable, security-assessment requirements before public distribution; a test-user grant is not approval for a public launch. See Google's scope definitions. Refresh tokens for an external app in Testing normally expire after seven days when requesting Drive scopes; handle reconnect without losing local work. See Google refresh-token expiration.
Use a fake provider for automated state/token/error tests and a separate real test account for live consent. Never add a fixed code, password bypass or runtime test-login route. If Google requires human approval or MFA, record the pending live check and ask the account owner to complete it.
3 · Environment and local startup
Create the example file below in the new project as .env.example. Make a private .env copy and fill it with your own values. All placeholders must be replaced before live login. Store the actual secret outside Git; generate at least 32 random bytes with a cryptographically secure tool. The production gate below rejects the sample secret rather than running with a fallback.
NODE_ENV=development
PORT=3000
PORT_FALLBACKS=1
APP_DATA_DIR=/absolute/path/outside-the-checkout/filelovenote-data
SESSION_SECRET=REPLACE_WITH_64_RANDOM_HEX_CHARACTERS
SESSION_MAX_AGE_DAYS=90
SESSION_COOKIE_SECURE=false
GOOGLE_CLIENT_ID=REPLACE_WITH_SIGNIN_WEB_CLIENT_ID
GOOGLE_CLIENT_SECRET=REPLACE_WITH_SIGNIN_WEB_CLIENT_SECRET
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback
GOOGLE_DRIVE_CLIENT_ID=REPLACE_WITH_DRIVE_WEB_CLIENT_ID
GOOGLE_DRIVE_CLIENT_SECRET=REPLACE_WITH_DRIVE_WEB_CLIENT_SECRET
GOOGLE_DRIVE_REDIRECT_URI=http://localhost:3000/google-drive/callback
FOLDERS_ENABLED=true
LOCAL_FIRST_ENABLED=true
# Optional features only; empty means unavailable, not invented results.
GEMINI_API_KEY=
GEMINI_MODEL=gemini-2.5-flash
YOUTUBE_API_KEY=
IMAGE_THUMB_MAX_PX=640
IMAGE_THUMB_MAX_BYTES=614400
IMAGE_THUMB_SKIP_TTL_MS=1800000
| Setting | Meaning and required handling |
|---|---|
NODE_ENV, PORT, PORT_FALLBACKS | Use development locally and production on the VPS. Keep one fixed port attempt so an occupied port fails visibly instead of changing OAuth origin. Source default fallback count is 10; explicitly use 1 here. |
APP_DATA_DIR | Absolute, private, outside the code checkout; same owner as the app process. Never expose it as a static directory. It holds technical/account metadata and app-owned media/canvas, not a new readable note archive. |
| Session variables | The source defaults to a 90-day sliding lifetime and insecure cookies unless enabled. Make the duration an explicit operator choice; enable Secure cookies in production. HttpOnly and SameSite=Lax remain required. |
| Two Google credential groups | Sign-in is mandatory. Drive configuration is required for this package's full core acceptance; browser-only persistence before Drive is connected must still work. Client secrets stay on the server. |
| Feature flags and optional APIs | These examples enable folders/local-first UI. They do not permit server persistence of readable note content. AI/YouTube keys are optional. The model string is the source baseline, not a guarantee of provider availability; verify access and pin the accepted model in the new release. |
| Thumbnail settings and tools | Values above match the source defaults. Install ffmpeg only for the chosen media features. Whisper requires a separately installed whisper-cli and the selected model file; the source looks for models/ggml-base.en.bin. Record tool versions and model checksum/licence. Missing tools must give an unavailable/error state, not crash the core. |
| Deliberately absent | No SMTP, fixed test code, native-app, WebDAV or alternate storage variables. The current configuration ignores retired USERNAME, PASSWORD and PASSWORD_HASH values. Password hashing, its helper script and the bcrypt dependency have been removed; do not recreate password login. |
# From the new project, after saving the .env.example above:
umask 077
cp .env.example .env
mkdir -p ../filelovenote-private-data
# Write a fresh secret without printing it; then edit the remaining placeholders.
bun -e 'const fs=require("fs"); const crypto=require("crypto"); const path=require("path"); let env=fs.readFileSync(".env","utf8"); env=env.replace("REPLACE_WITH_64_RANDOM_HEX_CHARACTERS",crypto.randomBytes(32).toString("hex")); env=env.replace("/absolute/path/outside-the-checkout/filelovenote-data",path.resolve("../filelovenote-private-data")); fs.writeFileSync(".env",env,{mode:0o600});'
chmod 600 .env
# Ignore runtime data, secret files, test cookies and local profiles in Git.
# Keep .env.example and bun.lock tracked.
bun run test
bun run dev
# In another terminal; GET is deliberate because HEAD has no promised route.
curl -sS -o /dev/null -w '%{http_code}
' http://localhost:3000/login
# Expected 200: public HTML only, not proof of Google/Drive readiness.
curl -sS -o /dev/null -w '%{http_code} %{redirect_url}
' http://localhost:3000/notes
# Expected unauthenticated redirect to /login; not an authentication bypass.
The current source has no dedicated health endpoint. Do not probe a guessed /health and interpret its 404 as a crash. A new build may add a minimal, content-free liveness/readiness endpoint only if its contract and tests are recorded in API contracts; label that as new behavior. Real readiness still needs synthetic sign-in, note persistence, account isolation and a confirmed Drive round trip. Browser checks use BrowserOS Neo. Keep local, fake-provider, live Google, live Drive and deployment evidence separate.
4 · Production prerequisites that must fail closed
Required new-build protections, not current-source claims: reject missing/placeholder secrets, insecure session cookies, invalid OAuth callback origins, an unsafe data directory and unavailable fixed bind port before opening the listener. Bind Bun to 127.0.0.1:3000 behind Nginx. The current source only warns about a missing session secret and does not specify a listener hostname. Setting an invented HOST environment variable does not fix that. Implement and test the binding and startup validation in the rebuilt runtime, or keep deployment blocked. Bun documents explicit hostname binding in server configuration.
Also require CSRF/origin protection for mutations, trusted proxy handling, account checks, redacted logs, resource limits and the security work selected in Security. Only the local proxy may supply forwarded host/protocol headers. Do not treat the Nginx example or a successful startup as a security audit.
Minimal private layout
/opt/filelovenote/app/ # generated code and frozen dependencies
/opt/filelovenote/runtime/bin/bun # chosen verified Bun binary
/var/lib/filelovenote/ # APP_DATA_DIR; private runtime data
/etc/filelovenote/app.env # production secrets; outside checkout
/var/backups/filelovenote/ # encrypted, access-controlled backups
# Linux operator preparation; populate app/runtime separately from a reviewed release.
sudo useradd --system --home /var/lib/filelovenote --shell /usr/sbin/nologin filelovenote
sudo install -d -o filelovenote -g filelovenote -m 700 /var/lib/filelovenote
sudo install -d -o root -g root -m 700 /etc/filelovenote
sudo install -d -o root -g root -m 700 /var/backups/filelovenote
# Create /etc/filelovenote/app.env privately, mode 600, owned by root.
# Use the environment template above, with these overrides:
# NODE_ENV=production
# APP_DATA_DIR=/var/lib/filelovenote
# SESSION_COOKIE_SECURE=true
# GOOGLE_REDIRECT_URI=https://service.example/auth/google/callback
# GOOGLE_DRIVE_REDIRECT_URI=https://service.example/google-drive/callback
# PORT=3000 and PORT_FALLBACKS=1
# Never copy a real production env file into the project or documentation.
Protect the code and dependency tree from modification by the service user; give that user write access only to its runtime directory and explicitly chosen temporary paths. Keep optional model files read-only in the release. No database server or migration tool is required for this JSON/IndexedDB/Drive architecture. Do not copy a legacy script that blindly archives documents/ or resets the checkout.
Service example: activate only after the rebuilt validation exists
scripts/check-production-config.js below is a required new-build check to implement, not a file supplied by the original app. It must exit nonzero for every rejected configuration, before any service can read a restored queue or talk to Drive. Test missing/weak/placeholder secrets; missing credentials for selected features; non-HTTPS/mismatched production callback URLs; a data path inside the checkout; disabled Secure cookies; and port fallback other than one. Keep this check and the listener's own validation consistent.
[Unit]
Description=FileLoveNote rebuilt web app
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=filelovenote
Group=filelovenote
WorkingDirectory=/opt/filelovenote/app
EnvironmentFile=/etc/filelovenote/app.env
ExecStartPre=/opt/filelovenote/runtime/bin/bun scripts/check-production-config.js
ExecStart=/opt/filelovenote/runtime/bin/bun server.js
Restart=on-failure
RestartSec=5
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/filelovenote
[Install]
WantedBy=multi-user.target
Save as /etc/systemd/system/filelovenote.service and validate on the chosen distribution. These execution controls limit writable paths; they do not replace app authorization or a restore safeguard. See the official systemd execution settings. Record any compatibility change rather than silently dropping a control.
Nginx example with finite request limits
Install Nginx and provision a valid certificate for your actual domain using your chosen certificate manager before enabling this TLS block. The certificate paths below are placeholders. Permit HTTPS publicly and only the access needed to manage the host or renew its certificate; keep port 3000 private. Use an allowed host policy and reject other hosts.
server {
listen 80;
server_name service.example;
return 308 https://service.example$request_uri;
}
server {
listen 443 ssl;
server_name service.example;
ssl_certificate /etc/letsencrypt/live/service.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/service.example/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Finite per-request example; files travel in 8 MiB resumable chunks.
client_max_body_size 9m;
client_body_timeout 60s;
send_timeout 60s;
# Do not retain callback codes, user queries or note payloads in access logs.
access_log off;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host service.example;
proxy_set_header X-Forwarded-Host service.example;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_request_buffering off;
proxy_buffering off;
proxy_max_temp_file_size 0;
proxy_cache off;
proxy_connect_timeout 5s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
add_header Cache-Control "no-store" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header X-Frame-Options "DENY" always;
}
}
This example serves only one configured host; add a default server that rejects unmatched hosts on your chosen Nginx installation, and validate a real CSP against required media/embeds before public exposure. It disables proxy body buffering/caching to avoid creating an incidental note archive and preserve streamed responses; inspect temporary files and inherited logging too. Certificate setup follows Nginx HTTPS guidance; buffering controls are documented in the proxy module. No claim is made that these placeholders have been applied to a live proxy.
The source permits 10 GiB per file and 40 GiB per HTTP request; these are ceilings found in code, not recommended operating budgets. The source resumable upload uses 8 MiB chunks. A 9 MiB proxy request cap permits those chunks while refusing a single multi-GB body. Require the rebuilt client to use bounded resumable chunks; large source files already in Drive need no second upload. Do not use client_max_body_size 0. Enforce the shared new-build limits in the app before parsing or allocating: metadata mutations at most 64 KiB, note sync/search bodies at most 8 MiB, raw upload chunks at most 8 MiB and whole files at most 10 GiB. A body cap includes the entire body, not merely decoded note text; split a batch before encoding if it would exceed the cap. The 9 MiB proxy ceiling is an outer bound, not permission for a 9 MiB chunk or JSON body. Also set per-user/IP concurrency and rate budgets and test them. A 413 must preserve the local queue and allow a smaller retry; never truncate note content to satisfy a limit. Do not add fake MAX_UPLOAD_BYTES environment settings: the source constants are code, not configurable env options. See Nginx body limits and the acceptance datasets and measurements.
# Once app code, private env, startup gate and certificate exist:
sudo systemd-analyze verify /etc/systemd/system/filelovenote.service
sudo nginx -t
sudo systemctl daemon-reload
sudo systemctl enable --now filelovenote
sudo systemctl reload nginx
sudo systemctl status filelovenote --no-pager
sudo journalctl -u filelovenote --since '-5 minutes' --no-pager
# Inspect logs privately; do not paste secrets, cookies, note content or OAuth codes.
curl -sS -o /dev/null -w '%{http_code}
' https://service.example/login
# Expected public GET /login: 200.
# Verify only 127.0.0.1:3000 is listening; test that it is unreachable externally.
sudo ss -ltnp
Keep redacted operational metrics for error rates, queue age/count, Drive 401/429/503 rates, free disk, process memory and certificate expiry. Alert on meaningful failure rather than logging payloads. Synthetic public HTML checks show reachability only; an account-specific end-to-end test must prove a locally saved note survives reload, reaches the correct Drive account and is read back intact. Verify the exact released commit, lockfile and configuration independently. Never call a successful deployment a completed acceptance run.
5 · Backups and restore without a server note archive
Define recovery objectives before live use. Record who may restore, the maximum accepted loss window, restoration time, retention, backup encryption owner and a tested deletion schedule. Cloud sync propagates changes and deletions; it is not an independent backup. The following matrix is a required recovery policy for the new build, not a statement that the source deploy script implements it.
| Asset | Backup/retention policy | Restore boundary |
|---|---|---|
| Code, lockfile and release manifest | Versioned source, reviewed release ID, runtime/package hashes and non-secret configuration schema. | Restore a known release first. Do not restore a checkout bundled with old live data or a private .env. |
| Account registry, technical mappings, settings and folder metadata | Use an explicit content-aware export of allowed account-specific metadata. Encrypt at rest, restrict access and set retention. Include the user-to-storage mapping so accounts cannot be crossed. | Validate the schema/account owner in a disconnected staging copy. Do not blindly tar all JSON, migration documents, debug logs or old sync snapshots; inspect for readable note titles/content and references forbidden by the note privacy contract. |
| App-owned canvas and local media/assets | Include only the app-owned objects selected by policy; canvas may contain private text. Encrypt and label account ownership. Avoid copying remote originals into server storage merely to back them up. | Restore to the same isolated account namespace, scan/validate before rendering and keep external links untrusted. Preserve original Drive files. |
| Sessions, Google credentials and encrypted refresh tokens | Prefer fresh sign-in after restore instead of backing up sessions. Keep secret recovery material in a separate protected key store. Encrypted token records are sensitive and useless without the correct key. | Do not reinstate old sessions automatically. Reconnect through consent and verify the same Drive account before any writes. Never log or export tokens into browser files. |
| Readable note text and unsent browser queue | No persistent readable server note backup. Preserve work in the original browser. Offer an explicit user-controlled export, encrypted and stored outside the appserver, before clearing or replacing a profile. A synchronized export does not include future unsent edits. | Recover into a separate browser profile/workspace while offline. Compare exported/local/Drive versions; import with new recovery IDs when needed. Never clear a queue to solve a restore conflict. |
| Google Drive originals and Markdown | User-owned, independent versioned backup or provider retention policy. Document its limits and permissions. Drive revisions/trash can help in some cases but are not a guaranteed backup. | Inspect the available revision/trash state before editing. Permanent deletion may have no recoverable version. Prefer a recovery copy and explicit reconciliation over overwriting the current object. |
| Temporary encrypted sync station | Exclude from routine long-term backups. It is a short-lived delivery mechanism, not a note archive. Keep the browser's unacknowledged version until exact confirmation. If an outage requires preserving a sealed package, quarantine it under the same temporary lifecycle and access controls rather than extending retention indefinitely. | Never restart old packages automatically from a restored image. Verify owner, matching key, revision tokens, remote state and whether a delete is still intended before an explicitly approved replay. |
| Thumbnails, access-token memory, caches and logs | Regenerate caches; do not back up access tokens. Keep logs redacted, minimal and time-limited. Browser caches contain private data and are not a recovery guarantee. | Rebuild caches after ownership checks. Check old logs/backups for historical content and apply the agreed retention cleanup without silently erasing unsent user work. |
Google documents that revisions can be purged and that trash and permanent deletion differ; do not promise recovery for an app action that permanently deletes. See Drive revision management and Drive trash and deletion.
Disconnected restore drill
- Stop the app service and pause browser sync before restoring. Keep the original profile intact and export its pending work if possible. Do not load an old app page that may immediately replay its queue.
- Restore code and the allowed metadata/media/canvas into an isolated staging copy with no outbound network and no Google credentials. Do not enable the production unit or its auto-restart. Quarantine restored
encrypted-sync/and old deletion jobs before the runtime starts. ARECOVERY_MODEvariable is not an existing safeguard; implement a tested offline recovery workflow in the new build rather than assuming a flag works. - Verify archive checksums, schema version, ownership, file permissions and absence of forbidden readable note content. Restore the matching key only inside a controlled recovery process when it is needed to decrypt existing token/package records.
- Reconnect to Google only after explicit operator/user review. Confirm the same signed-in app account, expected Drive email and app folder IDs. Read the remote revisions without writing, compare them to the preserved local versions and classify every pending upsert/delete/folder operation. An old deletion is never automatically safe to replay.
- Resume only selected, still-valid operations. Use recovery copies for conflicting text, retain the originals and collect exact acknowledgements. Verify content in Drive, then let the original browser retire only the acknowledged revision. Test another account cannot read the restore.
- Run the recovery scenarios and the acceptance checks. Record restored release, key version, data provenance, pending work and what could not be recovered. Reopen production only after this result is reviewed.
Connection and account actions
For the rebuild, disconnect preserves local notes and the queue and deletes no Drive files. Reconnect must verify the same Drive account before replay; a different account requires an explicit safe migration decision. Reset affects only explicitly described app-owned metadata/state and must not delete original cloud files. Account deletion retains Drive files by default, offers export or cancellation for unsent local work, revokes the connection and clears only the confirmed account-specific cache after the user's decision. Treat these as required new contracts and test them; do not infer them from old destructive route names. Failure & recovery is authoritative.
6 · Key rotation and incident response
The source uses SESSION_SECRET for session signatures and as input to Drive-token and sync-package encryption keys. Changing it invalidates sessions and prevents existing encrypted values from being decrypted with the old key. Do not replace it during routine deployment or discard the old key while work is pending.
- Planned rotation: pause sync/writes, preserve unsent browser versions, drain and verify pending packages where safe, and record which ciphertext uses which key. In the new build, use a tested key-version migration/re-encryption path; otherwise keep packages quarantined and plan a fresh Drive consent after the protected recovery/export step. Store old/new keys separately from data backups with a defined retirement date.
- Suspected compromise: isolate the affected deployment and pause writes; preserve minimal protected evidence without collecting extra note content. Revoke app sessions and affected OAuth grants/tokens, rotate the session secret and exposed client/API secrets as appropriate, and use a known clean host/release. Do not reconnect an untrusted host.
- Reconcile before restart: determine which accounts, grants and content may be affected; verify Drive identities and revisions; recover local pending work without replaying stale deletes. Communicate the confirmed impact and open questions without claiming that encryption protected against a server-and-key compromise.
- Prove closure: test that old sessions/tokens no longer work, account isolation holds, new consent works and the correct revision survives an interrupted sync. Inspect redacted logs and backup access; record the incident, selected retention cleanup and follow-up controls.
No zero-data-loss, backup, incident-recovery or security guarantee follows from this runbook alone. Each chosen protection needs implementation and evidence, with a clear open status when missing.
7 · Setup completion record
- A new empty folder can install from the supplied exact manifest/lockfile using the recorded Bun binary; no original checkout, credentials or runtime content is required.
- Both OAuth clients, callbacks, scopes and test accounts are configured by the builder; fake and live results are recorded separately.
- Production rejects unsafe settings, port 3000 is private, HTTPS/headers/request limits are inspected and secrets/data remain outside the checkout.
- The restore drill proves pending browser edits survive and restored jobs cannot automatically delete or overwrite cloud files.
- The release record lists exact source/runtime/dependency/configuration versions, backup policy, recovery outcomes, untested items and acceptance evidence.
Continue with Acceptance. Until these checks pass, describe the environment as configured or partially verified, not ready for production.