# AGENTS.md — Integrating WME Sync into a userscript

You are adding **WME Sync** to a Waze Map Editor (WME) userscript so its settings and data
follow the editor across browsers and computers. This file is everything you need. Use
`wme-sync-lib` as described here rather than calling the HTTP API yourself.

- Library: `https://sync.wmekit.com/wme-sync-lib.js`
- This file: `https://sync.wmekit.com/AGENTS.md`

## How it works

1. Data is stored per **Waze username** and per **script** (`scriptId`), as JSON values under keys.
2. The first time anything syncs for a username, WME Sync creates that username and generates
   an **8-digit PIN**. Your script must show that PIN to the editor once.
3. In any other browser, or any other script using WME Sync, the editor enters that PIN once.
   After that, a long-lived token is used and refreshed automatically.
4. Editors don't need an account. Optionally, they can link the username at
   sync.wmekit.com (with the PIN) to browse and export their data, change the PIN, and sign
   browsers out.

**Security model, so you set expectations correctly:** WME Sync can't verify Waze account
ownership. A username belongs to whoever synced it first, and the PIN protects its data. It
suits settings and preferences. **Never store passwords, API keys, tokens or anything secret.**

## 1. Add the library to the header

Add these to the userscript metadata block. Keep any existing `@grant`s; `@grant none` won't
work, because the library needs the GM APIs.

```js
// @require      https://sync.wmekit.com/wme-sync-lib.js
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @connect      sync.wmekit.com
```

With any `@grant` other than `none`, the script runs sandboxed, so read the WME SDK globals
(`getWmeSdk`, `SDK_INITIALIZED`) from `unsafeWindow`. The library defines a global `WMESync`. Requests go through `GM_xmlhttpRequest`, so CORS
doesn't apply.

## 2. Initialise after WME is ready

```js
await unsafeWindow.SDK_INITIALIZED
const sdk = unsafeWindow.getWmeSdk({ scriptId: 'my-script', scriptName: 'My Script' }) // or your existing SDK instance
await sdk.Events.once({ eventName: 'wme-ready' })

const sync = await WMESync.init({
  scriptId: 'my-script', // stable, 1–64 chars [A-Za-z0-9._-]; NEVER change it later (it's the data namespace)
  sdk,                   // used to read the signed-in editor's username
  label: 'My Script',    // optional; shown in the editor's dashboard token list
})
```

- `init()` makes **no network request**. The first data call (or `sync.ensureSignedIn()`)
  does the PIN step: for a new username it shows the generated PIN; otherwise it asks for it.
- The username comes from `sdk.State.getUserInfo().userName`. If the editor isn't logged in to
  WME, `init` throws. Catch it and skip syncing.
- Alternatively, pass `username` instead of `sdk` if you already have it.

## 3. API

| Method | Returns / notes |
|---|---|
| `get(key)` | the value, or `undefined` if unset |
| `getEntry(key)` | `{ value, version, size, updated_at }` or `undefined` |
| `set(key, value, { ifVersion? })` | `{ version, updated_at }`. With `ifVersion`, throws status **409** if the stored version differs. `ifVersion: 0` means "only if the key doesn't exist" |
| `setMany({ key: value, … })` | bulk write, max **100** keys per call |
| `getAll()` | `{ key: value }` for this script |
| `keys()` | key names |
| `remove(key)` | `true` if it existed |
| `clear()` | deletes all keys of this script, returns the count |
| `getPin()` | the PIN this browser knows, or `null` (for a "Show my sync PIN" button) |
| `me()` | `{ username, linked, bytes_used, bytes_limit, scripts }` |
| `ensureSignedIn()` | run the PIN step now (e.g. when the editor turns sync on) |
| `signOut()` | revoke this browser's token and forget the PIN |

Keys are 1–128 chars of `[A-Za-z0-9._-]`. Values are any JSON (object, array, string, number,
boolean, null).

| Limit | Value |
|---|---|
| Value size | 256 KB |
| Storage per Waze username | 5 MB, shared by all scripts |
| `setMany` | 100 keys per call |
| Wrong PINs | 5 per username and 20 per IP per 15 min, then `429` |
| New usernames | 5 per IP per day, then `429` |

## 4. Errors

Every failure is a `WMESyncError` with `.status` and `.body` (`{ error, code? }`):

| `status` | Meaning | What to do |
|---|---|---|
| `0` | network error or timeout | keep using local settings; retry later |
| `401` | editor cancelled the PIN prompt (`message: 'WME Sync sign-in cancelled'`) | turn sync off for this session; offer a "Connect WME Sync" button |
| `409` | `ifVersion` mismatch (`body.version` = current version) | re-read, merge, write again |
| `413` | value over 256 KB or quota full | store less, or split it across keys |
| `429` | too many wrong PINs, or too many new usernames from this network today | tell the editor to wait (15 min / until tomorrow) |
| `400` | invalid `scriptId`/key/body | a bug in your integration: fix the key or `scriptId` |
| `403` | `origin_not_allowed`: the call came from a web page other than WME | call from WME, through `wme-sync-lib` / `GM_xmlhttpRequest` |

## 5. PIN UX (do this properly)

The defaults use `window.alert` (new PIN) and `window.prompt` (enter PIN). They work, but a
script with its own UI should replace them:

```js
const sync = await WMESync.init({
  scriptId: 'my-script',
  sdk,
  onPinGenerated: (username, pin) =>
    showMyDialog(`WME Sync is set up for ${username}. Your PIN is ${pin}. Keep it: you'll need it on other browsers.`),
  promptForPin: (username, retry) =>
    askMyDialog(retry ? 'That PIN was wrong. Try again:' : `Enter the WME Sync PIN for ${username}:`), // resolve to string, or null to cancel
})
```

Also add a **"Show my WME Sync PIN"** item to your settings (`sync.getPin()`) and a
**"Disconnect"** item (`sync.signOut()`). Editors lose PINs.

## 6. Recommended integration pattern

Make sync an enhancement on top of local storage, never a hard dependency:

```js
const LOCAL_KEY = 'my-script-settings'
const DEFAULTS = { highlight: true, color: '#33ccff' }

let settings = { ...DEFAULTS, ...JSON.parse(localStorage.getItem(LOCAL_KEY) || '{}') }
let sync = null

async function startSync(sdk) {
  if (localStorage.getItem('my-script-sync') === 'off') return
  try {
    sync = await WMESync.init({ scriptId: 'my-script', sdk, label: 'My Script' })
    const remote = await sync.get('settings') // first call: PIN step happens here
    if (remote) {
      settings = { ...DEFAULTS, ...remote }
      localStorage.setItem(LOCAL_KEY, JSON.stringify(settings))
      applySettings(settings)
    } else {
      await sync.set('settings', settings) // first sync: upload what this browser has
    }
  } catch (e) {
    if (e?.status === 401) localStorage.setItem('my-script-sync', 'off') // editor declined; don't nag on every load
    console.warn('[my-script] WME Sync unavailable, using local settings', e)
    sync = null
  }
}

let saveTimer
function saveSettings(next) {
  settings = next
  localStorage.setItem(LOCAL_KEY, JSON.stringify(settings)) // always save locally first
  clearTimeout(saveTimer)
  saveTimer = setTimeout(() => sync?.set('settings', settings).catch((e) => console.warn('[my-script] sync failed', e)), 1500)
}
```

Guidelines:
- **One `scriptId` per script, never changed.** Renaming it orphans the editor's data.
- **Few, meaningful keys** (`settings`, `favorites`, `layers`), not one key per toggle. Use
  `setMany` when several change together.
- **Debounce writes** (≥ 1 s). Don't write on every keystroke or map move.
- **Read once at startup**, not on every action. There's no push or live updates; another
  browser's changes arrive on the next WME load.
- **Last write wins** by default. If two browsers can edit the same structured data (lists),
  use `getEntry` + `set(key, value, { ifVersion })` and merge on 409.
- **Version your stored shape** (e.g. `{ v: 2, … }`) so later script versions can migrate it.
- Let editors **turn sync off**, and keep working when WME Sync is down.

## Don'ts

- Don't store secrets or other people's personal data.
- Don't hardcode, log, or send the PIN anywhere. Don't read or copy the library's stored tokens
  (`wmesync.*` GM values).
- Don't take the username from anywhere except the WME SDK.
- Don't block your script's startup on WME Sync. Load local settings first, sync after.
- Don't create throwaway usernames for testing; each IP gets 5 per day. Test with the
  editor's own username, and use https://sync.wmekit.com to inspect or delete test data.

## Checklist before you finish

- [ ] Header has `@require`, the GM `@grant`s (+ `unsafeWindow`) and `@connect sync.wmekit.com`.
  (Scripts that still `@connect sync.wazetools.com`, the pre-move host, keep working: the library
  then calls that host. Switch the line to move over.)
- [ ] `init` runs after `wme-ready`, with a fixed `scriptId`, inside `try/catch`.
- [ ] Settings still load and save locally when sync fails or is declined.
- [ ] Writes are debounced; reads happen at startup.
- [ ] A new PIN is shown clearly. "Show my PIN" and "Disconnect" exist in settings.
- [ ] Values are well under 256 KB; the stored shape has a version field.
- [ ] Nothing secret is synced.
