diff --git a/.env.example b/.env.example index adb6b2d..33b9c17 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,9 @@ SHARED_LISTS_AUTH_PROVIDER=openai-sites # comma-separated email addresses allowed to create the first list. FIRST_OWNER_EMAILS= ENABLE_FIRST_OWNER_SETUP=true +# Production fail-closed default. Set true only for a deliberate local/demo setup +# where any signed-in user may claim the first list while the database is empty. +ALLOW_ANY_FIRST_OWNER=false # OpenAI Sites optional server features ENABLE_ACCESS_AUDIT=false @@ -18,6 +21,8 @@ ENABLE_PEOPLE_IMPORT=false # Optional generic quick-action integration. Disabled by default. QUICK_ACTION_INTEGRATION_ENABLED=false +# Required when QUICK_ACTION_INTEGRATION_ENABLED=true. Use comma-separated +# browser origins allowed to call the quick-action API. Empty fails closed. QUICK_ACTION_INTEGRATION_ORIGINS= # Optional per-user Google Contacts autocomplete. Disabled by default. diff --git a/AGENTS.md b/AGENTS.md index 38baf89..4c7293f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ You are working in the public Shared Lists Starter repository. ## Normal Commands ```bash -npm install +npm ci npm run dev npm test npm run build diff --git a/AGENT_README.md b/AGENT_README.md index c384f77..935ed58 100644 --- a/AGENT_README.md +++ b/AGENT_README.md @@ -44,6 +44,7 @@ Edit these files for ordinary setup: Do not edit `src/app.js`, `src/worker.js`, or the store layer just to change the first owner, host, feedback email, or auth provider. Do not enable optional features such as access audit, people import, quick-action intake, or private Google Contacts autocomplete unless the user asks for them. +Keep private Google Contacts disabled for real deployments until `docs/PRIVATE_CONTACTS.md` shows the large-sync, OAuth, quota, encryption, disconnect, and recovery gates are complete. ## Pick One Deployment Lane @@ -95,6 +96,8 @@ curl -X POST "$APP_URL/api/setup/first-owner" \ This request must be authenticated by the host. Do not spoof production identity headers. +Production setup fails closed unless `FIRST_OWNER_EMAILS` is nonempty. Set `ALLOW_ANY_FIRST_OWNER=true` only for an intentional local demo or disposable test deployment. + ## Safety Checks Before handing back: diff --git a/CHANGELOG.md b/CHANGELOG.md index c1304a7..a214023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +- Make production auth provider configuration fail closed unless `SHARED_LISTS_AUTH_PROVIDER` is explicitly set to `openai-sites` or `cloudflare-access`. +- Block cross-site state-changing API calls with Origin, Fetch Metadata, and JSON content-type checks. +- Clear private browser cache and session state on sign-out or access loss. +- Make first-owner setup require `FIRST_OWNER_EMAILS` unless `ALLOW_ANY_FIRST_OWNER=true` is explicitly configured. +- Keep Google Contacts and Cloudflare deployment lanes documented as gated until their production readiness work is complete. +- Make Quick Actions fail closed unless explicitly enabled with a nonempty origin allowlist. +- Update `esbuild` past `0.27.3`. + ## 0.1.0 - Initial public starter export. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 92a8aa3..37cd5ce 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -15,7 +15,7 @@ Shared Lists Starter should be a practical place for people to learn, fork, and For ordinary moderation concerns, open a GitHub issue or discussion and keep the report factual. -For a conduct report that includes private personal information, use GitHub private vulnerability reporting and prefix the title with `Conduct report`: +For a conduct report that includes private personal information, use the dedicated confidential conduct-reporting route when the maintainer has configured one. Until then, use GitHub private vulnerability reporting and prefix the title with `Conduct report`: ```text https://github.com/CBruney/shared-lists-starter/security/advisories/new diff --git a/README.md b/README.md index 033d376..9a7e602 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Edit `shared-lists.config.json` for user-visible and client-side settings: } ``` -Use host environment variables for server-side settings. For local development, copy `.env.example` to `.env`; the dev server loads `.env` automatically. Do not commit real secrets or private deployment IDs. Optional admin and integration surfaces, including access audit, people import, quick-action intake, and private Google Contacts autocomplete, are disabled by config until you turn them on. +Use host environment variables for server-side settings. For local development, copy `.env.example` to `.env`; the dev server loads `.env` automatically. Do not commit real secrets or private deployment IDs. Optional admin and integration surfaces, including access audit, people import, quick-action intake, and private Google Contacts autocomplete, are disabled by config and readiness gates until you intentionally turn them on. Quick-action intake also requires an explicit origin allowlist when enabled. Set `publicUrl` and `feedbackEmail` only when you have real values. If `feedbackEmail` is empty, the app hides Feedback and Help/questions. @@ -127,11 +127,16 @@ Set this server-side value before the first production deploy: ```text FIRST_OWNER_EMAILS= +ALLOW_ANY_FIRST_OWNER=false ``` -Set `FIRST_OWNER_EMAILS` to the real email address, or comma-separated real email addresses, allowed to create the first list. Leave it blank only if any signed-in user should be allowed to claim the first list while the database is empty. +Set `FIRST_OWNER_EMAILS` to the real email address, or comma-separated real email addresses, allowed to create the first list. Production setup fails closed when this value is blank. -Then sign in as that email and either create the first list in the app UI or call the setup endpoint. Set `APP_URL` to the deployed app URL and `FIRST_LIST_TITLE` to the real list name before running this command: +Use `ALLOW_ANY_FIRST_OWNER=true` only for a deliberate local demo or disposable test deployment where any signed-in user may claim the first list while the database is empty. + +Then sign in as an allowed email and either create the first list in the app UI or call the setup endpoint through an authenticated host session. + +OpenAI Sites API example: ```bash curl -X POST "$APP_URL/api/setup/first-owner" \ @@ -139,7 +144,9 @@ curl -X POST "$APP_URL/api/setup/first-owner" \ -d "{\"title\":\"$FIRST_LIST_TITLE\"}" ``` -If no `FIRST_OWNER_EMAILS` value is set, any signed-in user may claim first-owner setup while the database has no lists. Once the first list exists, the setup endpoint returns `409`. +Run the command from a session or client authenticated as an email in `FIRST_OWNER_EMAILS`. Do not spoof production identity headers. + +Once the first list exists, the setup endpoint returns `409`. You can also claim setup by signing in as an allowed owner and creating the first list from the app UI. @@ -167,7 +174,9 @@ Open the list, tap Share, add the person's email, then copy or send the list lin ### Can sharing autocomplete use my contacts? -Optionally, yes. Private Google Contacts autocomplete is off by default. If a deployer enables it and a user connects their own Google account in Settings, only that user sees those private suggestions. Everyone can still share by typing a full email address. +Optionally, yes, but it is intentionally gated right now. Private Google Contacts autocomplete is off by default and should stay off until the large-sync, OAuth, quota, encryption, disconnect, and recovery checks in [`docs/PRIVATE_CONTACTS.md`](docs/PRIVATE_CONTACTS.md) pass against real D1. + +Everyone can still share by typing a full email address. The shared people directory is scoped to people who already share at least one list with the signed-in user. The app does not send the full global user table to every browser. @@ -196,6 +205,7 @@ Important follow-ups before a production Cloudflare release: - Make the Cloudflare instructions executable. - Test the production system, not just its components. +- Complete the Cloudflare sign-in/sign-out, security-header, migration, smoke-test, and rollback gates. - Add a hosted demo only after it has dedicated non-production storage and synthetic data. ### Which license does this use? @@ -233,4 +243,6 @@ Never commit `.env`, `wrangler.toml` with real IDs, access tokens, production da For public repository settings, see [`docs/GITHUB_SETUP.md`](docs/GITHUB_SETUP.md). +For a linked documentation map, see [`docs/INDEX.md`](docs/INDEX.md). + For questions, use GitHub Discussions or the Help/questions action when a deployer has configured a support email. diff --git a/db/schema.ts b/db/schema.ts index 3c262ca..8522d6b 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -46,6 +46,7 @@ export const schemaStatements = [ deleted_at TEXT, deleted_by_email TEXT, delete_reason TEXT, + sort_order INTEGER NOT NULL DEFAULT 0, revision INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (list_id) REFERENCES lists(id), FOREIGN KEY (created_by_email) REFERENCES users(email), @@ -75,6 +76,24 @@ export const schemaStatements = [ FOREIGN KEY (requester_email) REFERENCES users(email), FOREIGN KEY (resolved_by_email) REFERENCES users(email) )`, + `CREATE TABLE IF NOT EXISTS idempotency_keys ( + scope TEXT PRIMARY KEY NOT NULL, + status INTEGER NOT NULL, + response_json TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + )`, + `CREATE TABLE IF NOT EXISTS task_external_refs ( + owner_email TEXT NOT NULL, + source TEXT NOT NULL, + external_id TEXT NOT NULL, + task_id TEXT NOT NULL, + list_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (owner_email, source, external_id), + FOREIGN KEY (owner_email) REFERENCES users(email), + FOREIGN KEY (task_id) REFERENCES tasks(id), + FOREIGN KEY (list_id) REFERENCES lists(id) + )`, `CREATE TABLE IF NOT EXISTS user_contact_sources ( owner_email TEXT NOT NULL, provider TEXT NOT NULL, @@ -111,4 +130,32 @@ export const schemaStatements = [ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (owner_email) REFERENCES users(email) )`, + `CREATE INDEX IF NOT EXISTS idx_lists_owner_email ON lists(owner_email)`, + `CREATE INDEX IF NOT EXISTS idx_list_members_email ON list_members(email)`, + `CREATE INDEX IF NOT EXISTS idx_list_members_email_list ON list_members(email, list_id)`, + `CREATE INDEX IF NOT EXISTS idx_tasks_list_status ON tasks(list_id, status)`, + `CREATE INDEX IF NOT EXISTS idx_tasks_due_date ON tasks(due_date)`, + `CREATE INDEX IF NOT EXISTS idx_tasks_list_status_deleted ON tasks(list_id, status, deleted_at)`, + `CREATE INDEX IF NOT EXISTS idx_tasks_open_list_due_created + ON tasks(list_id, due_date, created_at) + WHERE status = 'open' AND deleted_at IS NULL`, + `CREATE INDEX IF NOT EXISTS idx_tasks_completed_list_completed + ON tasks(list_id, completed_at DESC, updated_at DESC) + WHERE status = 'completed' AND deleted_at IS NULL`, + `CREATE INDEX IF NOT EXISTS idx_tasks_open_list_sort_order + ON tasks(list_id, sort_order, created_at) + WHERE status = 'open' AND deleted_at IS NULL`, + `CREATE INDEX IF NOT EXISTS idx_activity_list_created ON activity(list_id, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_activity_list_created_id ON activity(list_id, created_at DESC, id DESC)`, + `CREATE INDEX IF NOT EXISTS idx_idempotency_keys_created_at ON idempotency_keys(created_at)`, + `CREATE INDEX IF NOT EXISTS idx_list_access_requests_list_status + ON list_access_requests(list_id, status, created_at)`, + `CREATE INDEX IF NOT EXISTS idx_list_access_requests_requester_status + ON list_access_requests(requester_email, status)`, + `CREATE INDEX IF NOT EXISTS idx_task_external_refs_task ON task_external_refs(task_id)`, + `CREATE INDEX IF NOT EXISTS idx_task_external_refs_list ON task_external_refs(list_id)`, + `CREATE INDEX IF NOT EXISTS idx_user_contacts_owner_provider_name + ON user_contacts(owner_email, provider, display_name)`, + `CREATE INDEX IF NOT EXISTS idx_contact_oauth_states_expires + ON contact_oauth_states(expires_at)`, ]; diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7af5d79..ef9ad9a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -42,6 +42,7 @@ SHARED_LISTS_AUTH_PROVIDER=openai-sites DEV_DEFAULT_USER_EMAIL= FIRST_OWNER_EMAILS= ENABLE_FIRST_OWNER_SETUP=true +ALLOW_ANY_FIRST_OWNER=false ENABLE_ACCESS_AUDIT=false ACCESS_AUDIT_ADMINS= ENABLE_PEOPLE_IMPORT=false @@ -58,6 +59,10 @@ CLOUDFLARE_ACCESS_AUD= Set blank values in the host environment before production deploy. `DEV_DEFAULT_USER_EMAIL` is for local development only. `FIRST_OWNER_EMAILS` and `ACCESS_AUDIT_ADMINS` should contain real email addresses for the people allowed to use those actions. Cloudflare values should come from the Cloudflare Access application you create for this app. +`SHARED_LISTS_AUTH_PROVIDER` is required outside local development. Missing or unknown values fail closed. + +`FIRST_OWNER_EMAILS` is required before a production first-owner setup. `ALLOW_ANY_FIRST_OWNER=true` is an explicit opt-in for local demos or disposable test deployments where any signed-in user may claim the first list while the database is empty. + For local development, copy `.env.example` to `.env`; `npm run dev` loads `.env` automatically. Production hosts should use their native secret or environment-variable store. ## Auth Provider Values @@ -72,8 +77,8 @@ The configured provider is exclusive in production. OpenAI Sites headers are not - `ENABLE_ACCESS_AUDIT`: exposes the admin access-audit endpoint when set to `true`. - `ENABLE_PEOPLE_IMPORT`: exposes the admin people-import page and API when set to `true`. -- `QUICK_ACTION_INTEGRATION_ENABLED`: exposes the generic quick-action integration when set to `true`. -- `GOOGLE_CONTACTS_ENABLED`: exposes optional per-user Google Contacts autocomplete when set to `true` and when `features.privateGoogleContacts` is also `true`. +- `QUICK_ACTION_INTEGRATION_ENABLED`: exposes the generic quick-action integration when set to `true`. `QUICK_ACTION_INTEGRATION_ORIGINS` must also contain an explicit comma-separated browser origin allowlist. An empty allowlist fails closed. +- `GOOGLE_CONTACTS_ENABLED`: exposes optional per-user Google Contacts autocomplete when set to `true` and when `features.privateGoogleContacts` is also `true`. Keep this disabled until the readiness gates in `PRIVATE_CONTACTS.md` are complete. Keep optional features off until you know you need them. diff --git a/docs/DEPLOY_CLOUDFLARE.md b/docs/DEPLOY_CLOUDFLARE.md index 621aa68..ac820bb 100644 --- a/docs/DEPLOY_CLOUDFLARE.md +++ b/docs/DEPLOY_CLOUDFLARE.md @@ -14,6 +14,8 @@ Not done yet: - Pin and document the supported Wrangler version. - Confirm `wrangler.toml.example` matches current Wrangler migration configuration. - Replace OpenAI Sites sign-in and sign-out paths with provider-correct Cloudflare behavior. +- Confirm `assets.run_worker_first` behavior for API routes and static assets. +- Add security headers for CSP, `frame-ancestors`, `nosniff`, referrer policy, and permissions policy. - Run a disposable production-like Worker + D1 rehearsal that covers migrations, authentication, first-owner setup, list permissions, deployment, and rollback. Use the steps below as a checklist for what the lane should do, not as a guaranteed copy-paste deployment guide. @@ -59,6 +61,7 @@ Use the steps below as a checklist for what the lane should do, not as a guarant CLOUDFLARE_ACCESS_TEAM_DOMAIN = "" CLOUDFLARE_ACCESS_AUD = "" FIRST_OWNER_EMAILS = "" + ALLOW_ANY_FIRST_OWNER = "false" ``` Use the real team domain and audience value from the Cloudflare Access application. Set `FIRST_OWNER_EMAILS` to the real email address, or comma-separated real email addresses, allowed to create the first list. @@ -109,3 +112,14 @@ Use the steps below as a checklist for what the lane should do, not as a guarant The Worker verifies the Cloudflare Access JWT before trusting an email address. Set both the team domain and application audience value. Do not commit `wrangler.toml` if it contains real account, database, domain, or policy values. + +## Release Gates + +Do not advertise Cloudflare support as complete until all of these are true: + +- `wrangler` is added to dev dependencies or otherwise pinned to a documented version. +- `npm` scripts exist for build, deploy, migrations, smoke test, and rollback. +- `wrangler.toml.example` is confirmed against the pinned Wrangler version, including `migrations_dir` and asset routing. +- Cloudflare sign-in and sign-out behavior is provider-correct and covered by tests or a rehearsal. +- The Worker sends production security headers. +- A disposable Worker + D1 rehearsal has passed migrations, auth, first-owner restrictions, list permissions, deploy, smoke, and rollback. diff --git a/docs/DEPLOY_OPENAI_SITES.md b/docs/DEPLOY_OPENAI_SITES.md index ca9bf98..5afa0a3 100644 --- a/docs/DEPLOY_OPENAI_SITES.md +++ b/docs/DEPLOY_OPENAI_SITES.md @@ -26,6 +26,7 @@ Use this lane when you want the shortest path from Codex to a hosted app. ```text FIRST_OWNER_EMAILS= ENABLE_FIRST_OWNER_SETUP=true + ALLOW_ANY_FIRST_OWNER=false SHARED_LISTS_AUTH_PROVIDER=openai-sites ``` @@ -53,11 +54,18 @@ Use this lane when you want the shortest path from Codex to a hosted app. A signed-out request should return `401`. -9. Sign in through the browser as an email listed in `FIRST_OWNER_EMAILS`. +9. Verify that caller-supplied identity headers do not grant access. A signed-out request with an `oai-authenticated-user-email` header should still be rejected or ignored by Sites, not treated as a signed-in user. -10. Create the first list in the app UI. + ```bash + curl -i "$APP_URL/api/session" \ + -H "oai-authenticated-user-email: spoofed@example.test" + ``` + +10. Sign in through the browser as an email listed in `FIRST_OWNER_EMAILS`. - Agents or authenticated API clients can also claim first owner with the setup endpoint. Set `APP_URL` to the deployed app URL and `FIRST_LIST_TITLE` to the real list name before running this command: +11. Create the first list in the app UI. + + Agents or authenticated API clients can also claim first owner with the setup endpoint. Set `APP_URL` to the deployed app URL and `FIRST_LIST_TITLE` to the real list name before running this command from an authenticated host session: ```bash curl -X POST "$APP_URL/api/setup/first-owner" \ @@ -65,6 +73,8 @@ Use this lane when you want the shortest path from Codex to a hosted app. -d "{\"title\":\"$FIRST_LIST_TITLE\"}" ``` + Do not spoof production identity headers. Use the real provider-authenticated browser/session path. + ## Access Model For a family or small group app, the normal Sites setup is: @@ -74,3 +84,32 @@ For a family or small group app, the normal Sites setup is: - List-level permissions in the app. That means a person can open the app URL, but they cannot see a list unless they are a member of that list. + +## Reproducible Sites Procedure + +Before staging: + +- Confirm `.openai/hosting.json` points to the intended Sites project, or has no `project_id` when creating a new reusable starter site. +- Confirm the Sites database binding matches the app's D1-style storage binding expected by the project. +- Confirm all migrations in `drizzle/` are included in the saved version. +- Confirm `SHARED_LISTS_AUTH_PROVIDER=openai-sites`. +- Confirm `FIRST_OWNER_EMAILS` is nonempty for production staging, unless this is a disposable test with `ALLOW_ANY_FIRST_OWNER=true`. +- Keep optional features disabled unless intentionally testing them. + +Staging checklist: + +- Signed-out `/api/session` returns `401`. +- Signed-out `/api/session` with a caller-supplied identity header does not return that supplied identity. +- Signed-in `/api/session` returns the provider identity. +- A signed-in user outside `FIRST_OWNER_EMAILS` cannot create the first list. +- A signed-in user inside `FIRST_OWNER_EMAILS` can create the first list. +- A member can see only lists where they are a member. +- A non-member with a list URL sees no list data and can request access if that flow is enabled. +- Member add/remove and `can_share` changes preserve the share-panel state without a full-page reset. +- Sign-out clears private browser cache and returns to a sign-in-required state with a visible sign-in action. + +Rollback: + +- Keep the previous saved Sites version available until staging passes. +- If verification fails, redeploy the previous known-good saved version. +- Do not run destructive database migrations as part of rollback. Use additive migrations and follow the database backup policy for data recovery. diff --git a/docs/FIRST_OWNER_SETUP.md b/docs/FIRST_OWNER_SETUP.md index 5206bbd..b013d8c 100644 --- a/docs/FIRST_OWNER_SETUP.md +++ b/docs/FIRST_OWNER_SETUP.md @@ -2,7 +2,7 @@ First-owner setup gives a fresh deployment a clear starting point without source edits. -The same guard applies to the normal create-list API while the database is empty. If `FIRST_OWNER_EMAILS` is set, only those signed-in emails can create the first list. +The same guard applies to the normal create-list API while the database is empty. Production deployments fail closed unless `FIRST_OWNER_EMAILS` names the signed-in emails allowed to create the first list, or `ALLOW_ANY_FIRST_OWNER=true` is explicitly set for a deliberate demo/test setup. ## Recommended Setting @@ -10,14 +10,19 @@ Set this before the first production deploy: ```text FIRST_OWNER_EMAILS= +ALLOW_ANY_FIRST_OWNER=false ``` Use the real email address of the person allowed to create the first list. If multiple people may claim setup, use a comma-separated list of real email addresses. -If this value is blank, any signed-in user may claim setup while the database has no lists. +If `FIRST_OWNER_EMAILS` is blank and `ALLOW_ANY_FIRST_OWNER` is not `true`, setup status is not ready and first-list creation returns `503`. + +Use `ALLOW_ANY_FIRST_OWNER=true` only for a local demo or disposable test deployment where any signed-in user may claim setup while the database has no lists. ## Check Setup Status +Use an authenticated host session. A bare unauthenticated request should return `401`. + ```bash curl "$APP_URL/api/setup/status" ``` @@ -30,15 +35,18 @@ Expected fresh-deployment response: "has_lists": false, "ready": true, "can_claim": true, - "allowed_owner_required": true + "allowed_owner_required": true, + "configured": true } ``` +If the deployment is not configured, `ready` and `can_claim` are `false` and `configured` is `false`. + ## Claim First Owner You can claim setup by creating the first list in the app UI while signed in as an allowed owner. -Agents or API clients can also call the setup endpoint while authenticated as an allowed owner. Set `APP_URL` to the deployed app URL and `FIRST_LIST_TITLE` to the real list name before running this command: +Agents or API clients can also call the setup endpoint while authenticated as an allowed owner. Set `APP_URL` to the deployed app URL and `FIRST_LIST_TITLE` to the real list name before running this command from an authenticated host session: ```bash curl -X POST "$APP_URL/api/setup/first-owner" \ @@ -46,6 +54,8 @@ curl -X POST "$APP_URL/api/setup/first-owner" \ -d "{\"title\":\"$FIRST_LIST_TITLE\"}" ``` +Do not spoof production identity headers. Use the provider's real sign-in flow or an authenticated API-capable session. + Key fields in the response: ```json diff --git a/docs/GITHUB_SETUP.md b/docs/GITHUB_SETUP.md index 439b653..ba397f1 100644 --- a/docs/GITHUB_SETUP.md +++ b/docs/GITHUB_SETUP.md @@ -44,6 +44,10 @@ Enable private vulnerability reporting in GitHub repository settings. `SECURITY. Enable GitHub Discussions for open-ended setup questions and community notes. Keep bug reports in Issues and confidential security reports in private vulnerability reporting. +## Confidential Conduct Route + +Before accepting outside contributors, configure a confidential conduct-reporting route. A private maintainer email alias is preferable. If that is not available yet, keep `CODE_OF_CONDUCT.md` pointing to GitHub private vulnerability reporting as the temporary confidential route. + ## First Release Keep the `v0.1.0` release notes aligned with `CHANGELOG.md`, `docs/RELEASES.md`, and the current starter docs. diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 0000000..c94f592 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,28 @@ +# Documentation Index + +Start here when setting up, reviewing, or maintaining Shared Lists Starter. + +## Setup + +- [`../README.md`](../README.md): project overview, quick start, hosting lane chooser, and public-maintainer expectations. +- [`CONFIGURATION.md`](CONFIGURATION.md): client config, server environment variables, optional features, and production fail-closed defaults. +- [`FIRST_OWNER_SETUP.md`](FIRST_OWNER_SETUP.md): first-list ownership setup and the `FIRST_OWNER_EMAILS` guard. +- [`DEPLOY_OPENAI_SITES.md`](DEPLOY_OPENAI_SITES.md): OpenAI Sites deployment procedure, staging checklist, and rollback notes. +- [`DEPLOY_CLOUDFLARE.md`](DEPLOY_CLOUDFLARE.md): Cloudflare Workers + D1 + Access design lane and remaining release gates. + +## Operation + +- [`ARCHITECTURE.md`](ARCHITECTURE.md): app structure, identity adapters, storage, and optional integrations. +- [`PRIVACY_AND_DATA_LIFECYCLE.md`](PRIVACY_AND_DATA_LIFECYCLE.md): stored data, browser caches, logs, retention, deletion, and privacy-notice checklist. +- [`PRIVATE_CONTACTS.md`](PRIVATE_CONTACTS.md): Google Contacts privacy model and disabled-by-default readiness gates. +- [`ACCESSIBILITY.md`](ACCESSIBILITY.md): accessibility target and known testing gaps. + +## Project Health + +- [`PILOT_READINESS_BACKLOG.md`](PILOT_READINESS_BACKLOG.md): P1/P2 follow-up gates before broader production or Cloudflare use. +- [`ROADMAP.md`](ROADMAP.md): project direction. +- [`RELEASES.md`](RELEASES.md): release expectations and current pre-1.0 status. +- [`PUBLICATION_CHECKLIST.md`](PUBLICATION_CHECKLIST.md): public-release checks. +- [`GITHUB_SETUP.md`](GITHUB_SETUP.md): repository settings to configure manually. +- [`OPEN_SOURCE_PLAN.md`](OPEN_SOURCE_PLAN.md): long-term public-project plan. + diff --git a/docs/OPEN_SOURCE_PLAN.md b/docs/OPEN_SOURCE_PLAN.md index b98b6e9..903920c 100644 --- a/docs/OPEN_SOURCE_PLAN.md +++ b/docs/OPEN_SOURCE_PLAN.md @@ -21,7 +21,7 @@ Goal: make the repo usable without an agent. Included: -- `npm install` +- `npm ci` - `npm run dev` - `npm test` - `npm run build` diff --git a/docs/PILOT_READINESS_BACKLOG.md b/docs/PILOT_READINESS_BACKLOG.md new file mode 100644 index 0000000..da84860 --- /dev/null +++ b/docs/PILOT_READINESS_BACKLOG.md @@ -0,0 +1,57 @@ +# Pilot Readiness Backlog + +This backlog tracks follow-up work that should not be hidden by the existence of starter docs or config flags. + +## P1 Gates + +### Google Contacts stays disabled + +Status: blocked for real deployments. + +Needed before enabling: + +- Replace large one-contact-at-a-time D1 writes with resumable staged writes that stay inside Worker and D1 limits. +- Add OAuth tests for state ownership, replay prevention, expiry, PKCE, token exchange, token refresh, callback recovery, and disconnect cleanup. +- Add failure tests for encryption errors, D1 disconnects, Google quota/rate limits, and partial sync recovery. +- Add per-user sync cooldowns. +- Revoke Google grants on disconnect where supported. +- Add no-store and no-referrer callback headers. +- Validate token-secret strength and document key rotation. +- Pass a real D1 large-sync rehearsal. + +### Cloudflare lane stays incomplete + +Status: documented design lane, not production-ready. + +Needed before advertising complete support: + +- Pin a supported Wrangler version. +- Add working deploy, migrate, smoke-test, and rollback commands. +- Confirm `migrations_dir` and asset routing with the pinned Wrangler version. +- Implement Cloudflare-correct sign-in and sign-out behavior. +- Add `assets.run_worker_first` or equivalent verified routing for API and static assets. +- Add production security headers: CSP, `frame-ancestors`, `nosniff`, referrer policy, and permissions policy. +- Pass a disposable Worker + D1 rehearsal covering migrations, authentication, first-owner restrictions, list permissions, deploy, smoke, and rollback. + +### Release and repository controls + +Status: partially manual. + +- Do not retag, delete, or recreate `v0.1.0`. +- Use `v0.1.1` or later for the next immutable release. +- Keep `CHANGELOG.md` updated with an `Unreleased` section before tagging. +- Protect `main` before accepting outside contributions. This requires a maintainer action in GitHub settings. + +## P2 Quality + +- Add keyboard alternatives for task reordering and open-task deletion. +- Complete dialog focus management and combobox semantics. +- Add browser plus axe-style accessibility checks for the WCAG 2.2 AA target. +- Keep `npm run dev` aligned with production metadata and manifest rendering. +- Keep `db/schema.ts` in parity with migrations. +- Keep the root package private unless the project intentionally publishes an npm package. +- Add a dedicated confidential conduct-reporting route when repo settings or a private reporting mailbox are available. +- Keep release-process details out of `NOTICE`. +- Use `npm ci` in agent setup instructions. +- Keep build tooling current. + diff --git a/docs/PRIVACY_AND_DATA_LIFECYCLE.md b/docs/PRIVACY_AND_DATA_LIFECYCLE.md index adcf9d2..4333e0d 100644 --- a/docs/PRIVACY_AND_DATA_LIFECYCLE.md +++ b/docs/PRIVACY_AND_DATA_LIFECYCLE.md @@ -28,11 +28,24 @@ The browser may store: - Theme preference. - Sidebar and onboarding dismissal state. -- A per-user people autocomplete index in `localStorage`. -- In-progress UI state while the page is open. +- List summary surface data in `localStorage`, keyed by signed-in email. +- List detail data in `sessionStorage`, keyed by signed-in email and list ID. +- A per-user people autocomplete index in `localStorage`, keyed by signed-in email. +- In-progress UI state while the page is open, including pending requests and optimistic edits. The people index is keyed by signed-in email and should contain only people who share a list with that user plus that user's private contacts, when enabled. +Suggested browser-cache lifetimes: + +- List summary surface: up to seven days. +- List detail/session cache: current browser session, with a short freshness window for faster navigation. +- Shared people autocomplete index: up to 24 hours. +- Private contacts autocomplete entries: up to 24 hours. + +On sign-out or access loss, the app clears private browser state before redirecting or repainting the signed-out screen. That includes list surface keys, list detail keys, people-index keys, pending list-delete timers, in-flight list/detail/people requests, Google Contacts status in memory, and optimistic mutation queues. + +The app intentionally keeps non-private UI preferences such as theme choice, sidebar state, and onboarding/tutorial dismissal. + ## Logs The Worker logs structured API events. Logs should avoid task text and raw actor email. Operators should configure their host log retention and access policy before production use. diff --git a/docs/PRIVATE_CONTACTS.md b/docs/PRIVATE_CONTACTS.md index d483fbb..efee041 100644 --- a/docs/PRIVATE_CONTACTS.md +++ b/docs/PRIVATE_CONTACTS.md @@ -4,6 +4,23 @@ Shared Lists can optionally let each signed-in user connect Google Contacts for This is not required for the app to work. Users can always type a full email address into Share and add that person directly. +## Current Status + +Keep Google Contacts disabled for real deployments. + +The OAuth and storage foundations are present, but the sync path is not production-ready. The current large-sync approach can exceed Cloudflare Worker D1 query limits when importing a large contact set, and the failure/recovery coverage is not complete enough for real users yet. + +Required gates before enabling: + +- Replace one-contact-at-a-time D1 writes with resumable staged writes that stay inside Worker and D1 limits. +- Add tests for OAuth state ownership, replay prevention, expiry, PKCE, token exchange, token refresh, encryption failure, D1 disconnect, quota handling, callback recovery, and disconnect cleanup. +- Add per-user sync cooldowns and retry behavior that does not make autocomplete slow. +- Revoke the Google grant on disconnect where the provider supports it. +- Send no-store and no-referrer headers on OAuth callback responses. +- Validate token-secret strength before accepting encrypted refresh-token storage. +- Document and test token key rotation. +- Pass a real D1 large-sync rehearsal before changing the default from disabled. + ## Privacy Boundary - Contacts are scoped by the signed-in user email. @@ -23,9 +40,9 @@ The low-latency path is: Google is used during connect/sync, not during each keystroke. -## Enable It +## Future Enablement -Turn on the client-visible setting: +After the gates above are complete, a deployer can turn on the client-visible setting: ```json { @@ -35,7 +52,7 @@ Turn on the client-visible setting: } ``` -Set server environment values in the host secret store: +And set server environment values in the host secret store: ```text GOOGLE_CONTACTS_ENABLED=true diff --git a/docs/PUBLICATION_CHECKLIST.md b/docs/PUBLICATION_CHECKLIST.md index 5977816..cf6dd9e 100644 --- a/docs/PUBLICATION_CHECKLIST.md +++ b/docs/PUBLICATION_CHECKLIST.md @@ -12,13 +12,13 @@ git diff --check ## Sanitization Checks -Search for private markers. Replace the terms below with the private project IDs, domains, email domains, and database IDs used by the private source app: +Search for private markers. Replace the terms below with any project IDs, domains, email domains, and database IDs used in your non-public deployment: ```bash rg -i "private-project-id|private-version-id|private-domain|private-email-domain|real-cloudflare-aud|real-database-id" ``` -The only acceptable matches should be in the private source repo, not in the generated public export. +The only acceptable matches should be documentation examples that are clearly synthetic. ## Public Repo Setup @@ -34,7 +34,7 @@ The only acceptable matches should be in the private source repo, not in the gen ## Release -- Create an initial tag such as `v0.1.0`. +- Create an immutable release tag such as `v0.1.1` for the next release. Do not retag, delete, or recreate `v0.1.0`. - Keep public commits free of private material. - Do not copy private deployment files into the starter. - Record whether each release changes runtime code, docs only, or repository settings. diff --git a/docs/RELEASES.md b/docs/RELEASES.md index 74bf73b..b7f697d 100644 --- a/docs/RELEASES.md +++ b/docs/RELEASES.md @@ -29,6 +29,8 @@ Included: Known limits: - No hosted public demo yet; use the local demo. +- Do not retag, delete, or recreate `v0.1.0`; use `v0.1.1` or later for the next immutable release. +- Keep an `Unreleased` section in `CHANGELOG.md` until the next tag is cut. - Cloudflare deploy/migrate/smoke/rollback commands are not implemented yet. - No automated production-system deploy smoke test yet. - No published package for the shared core yet. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 969d56c..5477f2b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -16,9 +16,13 @@ Shared Lists Starter is pre-1.0. The goal is to stay small, useful, and easy to ## Next +- Complete the OpenAI Sites staging pilot checklist in `DEPLOY_OPENAI_SITES.md`. +- Keep Google Contacts disabled until the readiness gates in `PRIVATE_CONTACTS.md` pass. - Make the Cloudflare instructions executable. - Add a complete Cloudflare smoke test with a disposable D1 database. - Test the production system, not just its components. +- Add browser and accessibility automation for keyboard, focus, and combobox behavior. +- Add a dedicated confidential conduct-reporting route before accepting outside contributors. - Split the ACL, schema, and store layer into a published package. - Add starter templates for grocery, packing, chores, and trip lists. - Add import/export helpers for text and CSV. diff --git a/examples/cloudflare-workers-d1/README.md b/examples/cloudflare-workers-d1/README.md index 75b0261..65d08e1 100644 --- a/examples/cloudflare-workers-d1/README.md +++ b/examples/cloudflare-workers-d1/README.md @@ -23,6 +23,7 @@ SHARED_LISTS_AUTH_PROVIDER=cloudflare-access CLOUDFLARE_ACCESS_TEAM_DOMAIN= CLOUDFLARE_ACCESS_AUD= FIRST_OWNER_EMAILS= +ALLOW_ANY_FIRST_OWNER=false ENABLE_FIRST_OWNER_SETUP=true ``` diff --git a/examples/openai-sites/README.md b/examples/openai-sites/README.md index 4d09db6..c856135 100644 --- a/examples/openai-sites/README.md +++ b/examples/openai-sites/README.md @@ -21,6 +21,7 @@ Required server values: ```text SHARED_LISTS_AUTH_PROVIDER=openai-sites FIRST_OWNER_EMAILS= +ALLOW_ANY_FIRST_OWNER=false ENABLE_FIRST_OWNER_SETUP=true ``` diff --git a/package-lock.json b/package-lock.json index ce234e7..cb5cfa3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "Apache-2.0", "devDependencies": { - "esbuild": "0.27.3" + "esbuild": "^0.28.1" }, "engines": { "node": ">=24 <27", @@ -17,9 +17,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -34,9 +34,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -51,9 +51,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -68,9 +68,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -85,9 +85,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -102,9 +102,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -119,9 +119,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -136,9 +136,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -170,9 +170,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -187,9 +187,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -204,9 +204,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -221,9 +221,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -238,9 +238,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -255,9 +255,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -289,9 +289,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -306,9 +306,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -323,9 +323,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -340,9 +340,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -357,9 +357,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -391,9 +391,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -408,9 +408,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -425,9 +425,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -442,9 +442,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -459,9 +459,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -472,32 +472,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } } } diff --git a/package.json b/package.json index e0ba502..2181644 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "description": "Reusable starter for private shared lists with D1-style storage and list-level permissions.", "license": "Apache-2.0", - "private": false, + "private": true, "type": "module", "packageManager": "npm@11.12.1", "engines": { @@ -25,6 +25,6 @@ "test": "node --test tests/*.test.mjs" }, "devDependencies": { - "esbuild": "0.27.3" + "esbuild": "^0.28.1" } } diff --git a/scripts/build.mjs b/scripts/build.mjs index 25b2e9b..ca1f115 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -5,6 +5,7 @@ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; +import { applyDocumentMetadata, injectSharedListsConfig, renderManifest } from "./render-client-config.mjs"; const root = dirname(fileURLToPath(new URL("../package.json", import.meta.url))); const dist = join(root, "dist"); @@ -53,7 +54,7 @@ const builtIndex = markGenericDocument( await writeFile(join(dist, "client", "index.html"), builtIndex); await writeFile(join(dist, "client", "shell.html"), builtIndex); const manifest = await readFile(join(dist, "client", "manifest.webmanifest"), "utf8"); -await writeFile(join(dist, "client", "manifest.webmanifest"), renderManifest(manifest, installIconDataUri, sharedListsConfig)); +await writeFile(join(dist, "client", "manifest.webmanifest"), renderManifest(manifest, sharedListsConfig, { installIconDataUri })); const serviceWorker = await readFile(join(dist, "client", "service-worker.js"), "utf8"); await writeFile(join(dist, "client", "service-worker.js"), versionServiceWorker(serviceWorker, assetVersion)); @@ -90,66 +91,12 @@ async function readSharedListsConfig() { } } -function injectSharedListsConfig(html, config) { - return html.replace("__SHARED_LISTS_CONFIG__", escapeScriptJson(JSON.stringify(config))); -} - -function escapeScriptJson(value) { - return value.replace(/[^<]*<\/title>/, `${escapeHtml(appName)}`) - .replace(/
Shared Lists<\/h1>/, `

${escapeHtml(appName)}

`) - .replace(/ 12 ? "Shared Lists" : parsed.name; - parsed.description = String(config.manifestDescription || parsed.description || "Private shared lists with list-level permissions.").trim(); - parsed.icons = parsed.icons.map((icon) => - icon.src === "/apple-touch-icon.png" ? { ...icon, src: dataUri } : icon, - ); - return `${JSON.stringify(parsed, null, 2)}\n`; -} - -function normalizeAppName(value) { - return String(value || "Shared Lists").trim().slice(0, 80) || "Shared Lists"; -} - -function escapeHtml(value) { - return String(value || "") - .replace(/&/g, "&") - .replace(//g, ">"); -} - -function escapeAttr(value) { - return escapeHtml(value).replace(/"/g, """); -} - function stripDevUserOptions(html) { return html.replace(/(