Docs: prefer .env for ASSETS_SYNC_TOKEN; Keychain is optional dev path

The bot runs on a Linux host where macOS Keychain isn't available, so .env
is the default supported storage for the personal PAT. Both paths land in
the same process.env slot, but the previous README framing implied Keychain
was mandatory.

- .env.example: promote ASSETS_SYNC_TOKEN from a comment to a real
  REPLACE_ME field; note chmod 600 and rotation guidance
- README: split the setup section into "Setup A - production/Linux (.env)"
  and "Setup B - local dev on macOS (Keychain)"; clarify that the wrapper
  is a no-op if ASSETS_SYNC_TOKEN is already exported
- bin/load-assets-sync-secret.sh: soften the header comment to match

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jmcqueen 2026-07-07 10:22:40 -04:00
parent fa06538aa4
commit 66255a7b0c
3 changed files with 47 additions and 15 deletions

View file

@ -52,11 +52,18 @@ JIRA_STORE_CUSTOM_FIELD_ID=customfield_10261
# This PAT is used ONLY for reading the Stores schema; nothing that mutates
# Jira state uses it.
#
# ASSETS_SYNC_TOKEN should be exported at runtime from macOS Keychain via
# bin/load-assets-sync-secret.sh, NOT hardcoded here. It's listed only for
# completeness / documentation.
# The .env file is the primary supported storage for the PAT (the bot runs on
# a Linux host; macOS Keychain isn't available there). Because .env stays out
# of source control (.gitignore) and app.log no longer echoes auth headers,
# the cleartext token here is scoped to whoever has filesystem access on the
# deploy host — lock the file down with `chmod 600 .env` and rotate the token
# if that trust changes.
#
# For local dev on macOS you can instead source the token from Keychain via
# bin/load-assets-sync-secret.sh (see README) and leave ASSETS_SYNC_TOKEN out
# of .env entirely.
ASSETS_SYNC_EMAIL=you@ae.com
# ASSETS_SYNC_TOKEN= # loaded from Keychain by bin/load-assets-sync-secret.sh
ASSETS_SYNC_TOKEN=REPLACE_ME
# Where the local cache lives on disk (JSON). Gitignored. Regenerable via
# POST /api/wxccai/admin/storesCache/refresh.

View file

@ -68,27 +68,49 @@ Base path: `/api/wxccai`.
`createSSRequest` needs to translate a store number into an Assets object id. The shared service account is silently filtered out of the Store object type, so the app maintains a local `storeNumber → objectId` cache that's populated from a **personal Atlassian PAT** (a real human account with the right Assets role). The service account is still used for everything else (creating tickets, comments, attachments).
**One-time setup:**
Both storage patterns end up in the same place — `process.env.ASSETS_SYNC_TOKEN` — so the runtime code path is identical. Pick whichever fits the host.
### Setup A — production / Linux host (`.env`)
Put the values directly in `.env` (which is gitignored) and lock the file down:
```bash
cat >> .env <<'EOF'
ASSETS_SYNC_EMAIL=you@ae.com
ASSETS_SYNC_TOKEN=<paste-your-atlassian-api-token>
EOF
chmod 600 .env # only the bot user can read it
```
Then start normally:
```bash
npm start
```
Rotate the token in Atlassian → Account → Security → API tokens whenever the trust boundary on the host changes (new operator, offboarding, suspected leak). The app reloads it on the next process start.
### Setup B — local dev on macOS (Keychain)
If you're running the app on a Mac and would rather not keep the PAT in `.env`, use the wrapper script — it pulls the token from Keychain into `ASSETS_SYNC_TOKEN` before exec'ing the process:
```bash
# Store your PAT in macOS Keychain (never touches disk in cleartext)
security add-generic-password \
-s jira-assets-sync \
-a you@ae.com \
-w '<paste-your-atlassian-api-token>' \
-U
# Export the account name via .env / your shell
echo 'ASSETS_SYNC_EMAIL=you@ae.com' >> .env
```
echo 'ASSETS_SYNC_EMAIL=you@ae.com' >> .env # email in .env, token stays in Keychain
**Run the server:**
```bash
# Wrapper loads the PAT from Keychain into ASSETS_SYNC_TOKEN before exec
./bin/load-assets-sync-secret.sh npm start
```
If `ASSETS_SYNC_TOKEN` is already in the process env (Setup A above), the wrapper is a no-op and skips the Keychain lookup.
### Runtime behavior
On boot the app loads the on-disk cache at `data/stores.json`, kicks off a background refresh if the snapshot is missing or older than `STORES_CACHE_STALE_AFTER_HOURS`, and schedules a periodic full resync every `STORES_CACHE_REFRESH_HOURS`. `resolveStoreAssetReference` then serves lookups from memory (sub-ms) with a live PAT lookup as fallback for brand-new stores.
Force a refresh at any time:

View file

@ -9,8 +9,11 @@
# Rationale:
# The service account is silently filtered out of Object Type 109 (Store
# Address / Hierarchy) — see Forgejo issue #1. The stores cache is populated
# using a personal PAT instead. This PAT authenticates as a real user and so
# should NEVER sit in .env in cleartext; the Keychain is a safer store.
# using a personal PAT instead. On a shared dev Mac, Keychain is a nicer home
# for that PAT than .env (encrypted at rest, per-user access). On the prod
# Linux host where the bot actually runs, put the PAT in .env directly with
# `chmod 600 .env` — this script exits as a no-op if ASSETS_SYNC_TOKEN is
# already exported into the process env.
#
# Setup (one-time, per machine):
#