@unbrained/pm-web 2026.9.23 → 2026.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +64 -6
- package/dist/app.js +8 -5
- package/dist/app.js.map +1 -1
- package/dist/db.d.ts +2 -2
- package/dist/db.js +30 -2
- package/dist/db.js.map +1 -1
- package/dist/idempotency.d.ts +59 -0
- package/dist/idempotency.js +361 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/routes/groups.js +10 -1
- package/dist/routes/groups.js.map +1 -1
- package/dist/routes/pm.js +125 -108
- package/dist/routes/pm.js.map +1 -1
- package/dist/services/package-catalog.js +1 -1
- package/dist/services/package-catalog.js.map +1 -1
- package/dist/services/pm-runner.d.ts +3 -2
- package/dist/services/pm-runner.js +19 -6
- package/dist/services/pm-runner.js.map +1 -1
- package/manifest.json +2 -2
- package/package.json +23 -21
- package/public/src/api.js +79 -0
- package/public/src/api.js.map +1 -1
- package/public/src/api.ts +77 -0
- package/public/src/app.js +3 -1
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +3 -1
- package/public/src/offline-recovery.js +65 -0
- package/public/src/offline-recovery.js.map +1 -0
- package/public/src/offline-recovery.ts +68 -0
- package/public/src/sw.ts +431 -33
- package/public/src/views/auth.js +3 -1
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +3 -1
- package/public/src/views/graph.js +8 -2
- package/public/src/views/graph.js.map +1 -1
- package/public/src/views/graph.ts +9 -2
- package/public/sw.js +379 -26
- package/sql/schema.sql +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.10.5 - 2026-10-05
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Two limiters counted one request twice, halving every published budget on the affected routes ([pm-web-s552](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-s552.toon))
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- The per-IP rate limiter trusted a client-supplied forwarded address by default, so any caller could rotate a header to a fresh bucket ([pm-web-25nv](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-25nv.toon))
|
|
12
|
+
- Offline mutation queue is not bound to the originating account or idempotency key ([pm-web-ccie](https://github.com/unbraind/pm-web/blob/main/.agents/pm/issues/pm-web-ccie.toon))
|
|
13
|
+
|
|
14
|
+
### Other
|
|
15
|
+
|
|
16
|
+
- Auto-merge green Dependabot updates and group the pm toolchain into one daily PR ([pm-web-56no](https://github.com/unbraind/pm-web/blob/main/.agents/pm/tasks/pm-web-56no.toon))
|
|
17
|
+
|
|
18
|
+
## 2026.9.26 - 2026-09-26
|
|
19
|
+
|
|
20
|
+
### Other
|
|
21
|
+
|
|
22
|
+
- Move pm-web's runtime dependency on pm CLI to 2026.9.23 ([pm-web-gbuc](https://github.com/unbraind/pm-web/blob/main/.agents/pm/chores/pm-web-gbuc.toon))
|
|
23
|
+
- Certify pm CLI 2026.9.23 and adopt the guarded pm-ops merge-driver launcher ([pm-web-75mz](https://github.com/unbraind/pm-web/blob/main/.agents/pm/chores/pm-web-75mz.toon))
|
|
24
|
+
|
|
3
25
|
## 2026.9.23 - 2026-09-23
|
|
4
26
|
|
|
5
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -291,12 +291,7 @@ search, `/` focus search, `n`/`c` new item, `t` cycle theme, `a` activity,
|
|
|
291
291
|
This repo tracks its project management in `.agents/pm/` and ships a committed `.gitattributes`
|
|
292
292
|
that maps those tracker artifacts to pm-cli's field-aware Git merge drivers, so concurrent-branch
|
|
293
293
|
tracker edits merge cleanly instead of hard-conflicting. The driver **definitions** live in
|
|
294
|
-
per-clone Git config; `npm install` / `npm ci` from a clone wires them automatically via the
|
|
295
|
-
`prepare` script (`scripts/prepare-merge-driver.ts`, a thin launcher over `pm-ops/merge-driver`, which
|
|
296
|
-
runs `pm merge install` only when the `pm` CLI is resolvable and exits 0 with a one-line notice
|
|
297
|
-
otherwise). Registry installs never run `prepare`, and a production install of a clone
|
|
298
|
-
(`npm ci --omit=dev`) omits `pm-ops` itself, so it must pass `--ignore-scripts`; making that launcher
|
|
299
|
-
guarded is tracked as companion item pm-cli-website-xy19. To (re)run manually: `npm run merge:install`.
|
|
294
|
+
per-clone Git config; `npm install` / `npm ci` from a clone wires them automatically via the `prepare` script, `scripts/prepare-merge-driver.ts`: the launcher template pm-ops ships, copied unchanged, which a test compares byte for byte with the pinned template. It runs pm-ops's installer, which calls `pm merge install` when the `pm` CLI is on `PATH` and skips with a notice when it is not. A production install of a clone (`npm ci --omit=dev`) has no `pm-ops`, so the launcher skips with one notice, while a stale or broken `pm-ops` fails the install. Registry installs of this package never run `prepare`. Being Node-based, it behaves identically on POSIX shells and Windows `cmd.exe`. To (re)run manually: `npm run merge:install`.
|
|
300
295
|
|
|
301
296
|
After merging a branch that touched `.agents/pm/`, reconcile any residual history-hash drift with
|
|
302
297
|
**`pm merge reconcile`** (pm-cli ≥ 2026.7.22): preview with `pm merge reconcile --dry-run`, apply with
|
|
@@ -306,3 +301,66 @@ itself lists each affected stream in its output; `pm history --verify <id>` spot
|
|
|
306
301
|
content, so `reconcile` only re-greens the hash chain (no data loss) — see the authoritative
|
|
307
302
|
[pm-cli merge-safety guide](https://github.com/unbraind/pm-cli/blob/main/docs/MERGE_SAFETY.md). The
|
|
308
303
|
older blunt `pm history-repair --all` remains available as a lower-level primitive.
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
### Offline mutation identity and recovery
|
|
307
|
+
|
|
308
|
+
Each service-worker write outside `/api/auth` gets an `Idempotency-Key` and
|
|
309
|
+
captures its originating account before its first network attempt. If that
|
|
310
|
+
response is lost, the queued record keeps the same key, body and account.
|
|
311
|
+
Every replay sends `X-PM-Expected-Account`; the server refuses a different or
|
|
312
|
+
missing account with 409 before executing or claiming a key, and the worker
|
|
313
|
+
keeps the record. The session is resent when a worker first controls the page or changes;
|
|
314
|
+
session writes and explicit adoption extend the worker event lifetime.
|
|
315
|
+
|
|
316
|
+
The SPA displays blocked offline records. For unknown-owner records, the
|
|
317
|
+
signed-in user may click **These changes are mine — adopt for my signed-in
|
|
318
|
+
account** after reviewing the listed paths. The recovery message carries the
|
|
319
|
+
approving account and adoption verifies it against the current server session.
|
|
320
|
+
Adoption and flush share one operation chain. Adoption reads and updates
|
|
321
|
+
ownership in one IndexedDB transaction, preserves any existing key and assigns
|
|
322
|
+
only missing keys, then retries the queue.
|
|
323
|
+
Records owned by another account remain blocked until that account signs in.
|
|
324
|
+
Auth routes always use live responses and are never keyed or queued; legacy
|
|
325
|
+
auth records remain blocked. Deleted or unknown token accounts bypass keying
|
|
326
|
+
and receive the route's normal authentication outcome. Retention cleanup runs
|
|
327
|
+
before key claims so a cleanup failure leaves the request retryable.
|
|
328
|
+
|
|
329
|
+
API rate limits apply before idempotency, including to replay attempts. Explicit
|
|
330
|
+
pre-mutation refusals (425 and 429) release the intent and are never replayed.
|
|
331
|
+
Handlers may call `markIdempotencyPreCommitFailure` only when they can prove
|
|
332
|
+
no mutation committed. Group creation does so on connection acquisition failure
|
|
333
|
+
or a successful rollback before any COMMIT attempt. A later rollback cannot
|
|
334
|
+
prove that an attempted COMMIT failed. Proven pre-commit 5xx failures release
|
|
335
|
+
the claim, allowing the same key to retry. Every other 5xx and premature response
|
|
336
|
+
close settles into terminal `outcome_unknown`, returning 409 with
|
|
337
|
+
`PM_IDEMPOTENCY_OUTCOME_UNKNOWN` on retries without re-executing. Live duplicates
|
|
338
|
+
wait up to `PM_WEB_IDEMPOTENCY_WAIT_MS` (default 30 seconds), then receive 409
|
|
339
|
+
with `PM_IDEMPOTENCY_IN_FLIGHT`. After `PM_WEB_IDEMPOTENCY_PENDING_TIMEOUT_MS`
|
|
340
|
+
(default five minutes), pending rows settle as unknown on duplicate lookup or
|
|
341
|
+
the next retention sweep, including intents stranded by process termination.
|
|
342
|
+
Outcome persistence failures are caught and logged without request contents.
|
|
343
|
+
|
|
344
|
+
The worker durably marks unknown outcomes and three consecutive 5xx responses
|
|
345
|
+
as **needs attention**, then stops automatic attempts for that record. Replays
|
|
346
|
+
preserve FIFO within each owner's workspace; later records in other workspaces
|
|
347
|
+
may continue. Paths under `/projects/<id>/` define a workspace, including legacy
|
|
348
|
+
records. Account-level paths are barriers: they wait for earlier unresolved
|
|
349
|
+
owned work and block all later owned work until resolved. Other accounts have
|
|
350
|
+
separate ordering. Network or local persistence failures stop the flush.
|
|
351
|
+
|
|
352
|
+
The recovery notice offers the originating owner **Retry as a new request**
|
|
353
|
+
and **Discard**. Retrying requires a confirmation that the original might
|
|
354
|
+
already have committed; it assigns a fresh key at the same queue position,
|
|
355
|
+
then resumes ordered replay. Each new request is deduplicated separately:
|
|
356
|
+
the user's approved retry can duplicate an unknown original commit. Discard
|
|
357
|
+
removes only the queued record and does not undo any server commit. Recovery
|
|
358
|
+
verifies the approving account against the server and updates IndexedDB in a
|
|
359
|
+
single transaction. The keyboard-accessible **Dismiss** control hides the
|
|
360
|
+
notice until the blocked record set changes; dismissing preserves all records.
|
|
361
|
+
|
|
362
|
+
Completed responses and terminal unknown outcomes are retained for
|
|
363
|
+
`PM_WEB_IDEMPOTENCY_RETENTION_MS` (default seven days) from settlement.
|
|
364
|
+
Deduplication is bounded by that window; a retry after expiry may execute again.
|
|
365
|
+
Cleanup uses a partial `updated_at` index and runs at most once per minute per
|
|
366
|
+
guard instance. Pending intents are settled before retention can remove them.
|
package/dist/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { githubRouter } from "./routes/github.js";
|
|
|
15
15
|
import { adminRouter } from "./routes/admin.js";
|
|
16
16
|
import { createTierLimiters, resolveTrustProxy } from "./rate-limit.js";
|
|
17
17
|
import { csrfProtection } from "./csrf.js";
|
|
18
|
+
import { idempotencyGuard } from "./idempotency.js";
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const PUBLIC_DIR = path.resolve(__dirname, "..", "public");
|
|
20
21
|
/**
|
|
@@ -215,18 +216,20 @@ export function createApp(deps) {
|
|
|
215
216
|
// /api/projects/... request matched the /api/projects prefix before its own
|
|
216
217
|
// nested mount. Each shared prefix therefore carries its limiters once, and
|
|
217
218
|
// the routers mount behind them.
|
|
218
|
-
|
|
219
|
+
// Charge every attempt (including replays) before touching the idempotency store.
|
|
220
|
+
const keyedWrites = idempotencyGuard();
|
|
221
|
+
app.use("/api/auth", limiters.auth, keyedWrites);
|
|
219
222
|
app.use("/api/auth", oidcRouter);
|
|
220
223
|
app.use("/api/auth", authRouter);
|
|
221
|
-
app.use("/api/projects", limiters.read, limiters.write);
|
|
224
|
+
app.use("/api/projects", limiters.read, limiters.write, keyedWrites);
|
|
222
225
|
app.use("/api/projects", projectsRouter);
|
|
223
226
|
app.use("/api/projects/:projectId/pm", pmRouter);
|
|
224
227
|
app.use("/api/projects/:projectId/extensions", extensionsRouter);
|
|
225
228
|
app.use("/api/projects/:id/shares", sharesRouter);
|
|
226
229
|
app.use("/api/projects/:id/github", githubRouter);
|
|
227
|
-
app.use("/api/groups", limiters.read, limiters.write, groupsRouter);
|
|
228
|
-
app.use("/api/shared", limiters.read, limiters.write, sharedWithMeRouter);
|
|
229
|
-
app.use("/api/admin", limiters.admin, adminRouter);
|
|
230
|
+
app.use("/api/groups", limiters.read, limiters.write, keyedWrites, groupsRouter);
|
|
231
|
+
app.use("/api/shared", limiters.read, limiters.write, keyedWrites, sharedWithMeRouter);
|
|
232
|
+
app.use("/api/admin", limiters.admin, keyedWrites, adminRouter);
|
|
230
233
|
// Unknown API routes get a JSON 404 instead of falling through to the SPA
|
|
231
234
|
// shell, which would hand HTML to API clients expecting JSON.
|
|
232
235
|
app.all("/api/{*splat}", (_req, res) => {
|
package/dist/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAyB,MAAM,SAAS,CAAC;AAChD,OAAO,YAAY,MAAM,eAAe,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAwB,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAyB,MAAM,SAAS,CAAC;AAChD,OAAO,YAAY,MAAM,eAAe,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAwB,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AAE3D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,cAAc;IACd,gBAAgB;IAChB,OAAO;IACP,iBAAiB;CACT,CAAC;AAEX,gFAAgF;AAChF,MAAM,CAAC,MAAM,eAAe,GAA2B;IACrD,YAAY,EAAE,eAAe;IAC7B,cAAc,EAAE,iBAAiB;IACjC,MAAM,EAAE,QAAQ;IAChB,UAAU,EAAE,kBAAkB;CAC/B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG;IACvE,MAAM,UAAU,GAAG,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAC;IAChD,IAAI,CAAC,UAAU;QAAE,OAAO,UAAU,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;QAClD,IAAI,CAAC;YACH,IAAI,SAAS,CAAC,SAAS,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,oCAAoC,CAAC,CAAC;YACrF,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACvD,MAAM,IAAI,KAAK,CAAC,6CAA6C,IAAI,QAAQ,CAAC,CAAC;YAC7E,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;QACD,MAAM,QAAQ,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;QACzC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YAC/E,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,kDAAkD,CAAC,CAAC;QACnG,CAAC;QACD,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CACpB,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAC5C,CAAC;QAC1B,OAAO,GAAG,CAAC,OAAO,IAAI,SAAS,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAqBD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,SAAS,CAAC,IAAoB;IAC5C,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,MAAM,aAAa,GAAG,oBAAoB,EAAE,CAAC;IAE7C,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,yEAAyE;IACzE,4EAA4E;IAC5E,yEAAyE;IACzE,yEAAyE;IACzE,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAEvD,2EAA2E;IAC3E,mEAAmE;IACnE,wEAAwE;IACxE,WAAW;IACX,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAEjD,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACxC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC;IAExB,GAAG,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACrD,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,uDAAuD,CAAC,CAAC;QACxF,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QACpC,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAC9B,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CACL,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE;QACzB,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;KACzD,CAAC,CACH,CAAC;IAEF,mBAAmB;IACnB,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC1B,GAAG,CAAC,SAAS,CAAC,wBAAwB,EAAE,SAAS,CAAC,CAAC;QACnD,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC;QAC/C,GAAG,CAAC,SAAS,CAAC,iBAAiB,EAAE,iCAAiC,CAAC,CAAC;QACpE,IAAI,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,+EAA+E;IAC/E,MAAM,cAAc,GAAG,kBAAkB,EAAE,CAAC;IAC5C,IAAI,IAAI,EAAE,MAAM,EAAE,CAAC;QACjB,qEAAqE;QACrE,oEAAoE;QACpE,uEAAuE;QACvE,sBAAsB;QACtB,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACxD,CAAC;SAAM,CAAC;QACN,yEAAyE;QACzE,oEAAoE;QACpE,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,qEAAqE;QACrE,sEAAsE;QACtE,0EAA0E;QAC1E,0EAA0E;QAC1E,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAChC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAC7D,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAS,WAAW,CAAC,CAAC;IAEhD,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE;QACrD,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YAC1B,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,iBAAiB,EAAE,QAAQ,EAAE,kBAAkB,CAAC,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC9G,wEAAwE;QACxE,mEAAmE;QACnE,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;QAC3C,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,6EAA6E;IAC7E,4EAA4E;IAC5E,+BAA+B;IAC/B,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC;IAE1B,8EAA8E;IAC9E,iFAAiF;IACjF,kFAAkF;IAClF,yEAAyE;IACzE,wEAAwE;IACxE,wEAAwE;IACxE,0EAA0E;IAC1E,6EAA6E;IAC7E,gEAAgE;IAChE,6EAA6E;IAC7E,6EAA6E;IAC7E,4EAA4E;IAC5E,+DAA+D;IAC/D,4EAA4E;IAC5E,4EAA4E;IAC5E,iCAAiC;IACjC,kFAAkF;IAClF,MAAM,WAAW,GAAG,gBAAgB,EAAE,CAAC;IACvC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACjD,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACjC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACjC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IACrE,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,CAAC,6BAA6B,EAAE,QAAQ,CAAC,CAAC;IACjD,GAAG,CAAC,GAAG,CAAC,qCAAqC,EAAE,gBAAgB,CAAC,CAAC;IACjE,GAAG,CAAC,GAAG,CAAC,0BAA0B,EAAE,YAAY,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,CAAC,0BAA0B,EAAE,YAAY,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;IACjF,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,kBAAkB,CAAC,CAAC;IACvF,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IAEhE,0EAA0E;IAC1E,8DAA8D;IAC9D,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACrC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,yDAAyD;IACzD,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QACxD,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/db.d.ts
CHANGED
|
@@ -22,8 +22,8 @@ export declare const pool: import("pg").Pool;
|
|
|
22
22
|
*
|
|
23
23
|
* Issues `CREATE TABLE IF NOT EXISTS` for users, projects, groups, group
|
|
24
24
|
* members, project shares, external (OIDC) identities, the admin audit log,
|
|
25
|
-
*
|
|
26
|
-
* NOT EXISTS` migrations for later-added columns; and, when
|
|
25
|
+
* GitHub item links and idempotency keys, plus their indexes; runs idempotent
|
|
26
|
+
* `ADD COLUMN IF NOT EXISTS` migrations for later-added columns; and, when
|
|
27
27
|
* `PM_WEB_BOOTSTRAP_ADMIN_EMAIL` is set, promotes that (lower-cased) user to
|
|
28
28
|
* admin. Safe to call on every boot.
|
|
29
29
|
*/
|
package/dist/db.js
CHANGED
|
@@ -67,8 +67,8 @@ const bootstrapAdminEmail = (process.env.PM_WEB_BOOTSTRAP_ADMIN_EMAIL || "")
|
|
|
67
67
|
*
|
|
68
68
|
* Issues `CREATE TABLE IF NOT EXISTS` for users, projects, groups, group
|
|
69
69
|
* members, project shares, external (OIDC) identities, the admin audit log,
|
|
70
|
-
*
|
|
71
|
-
* NOT EXISTS` migrations for later-added columns; and, when
|
|
70
|
+
* GitHub item links and idempotency keys, plus their indexes; runs idempotent
|
|
71
|
+
* `ADD COLUMN IF NOT EXISTS` migrations for later-added columns; and, when
|
|
72
72
|
* `PM_WEB_BOOTSTRAP_ADMIN_EMAIL` is set, promotes that (lower-cased) user to
|
|
73
73
|
* admin. Safe to call on every boot.
|
|
74
74
|
*/
|
|
@@ -173,6 +173,34 @@ export async function initSchema() {
|
|
|
173
173
|
);
|
|
174
174
|
`);
|
|
175
175
|
await pool.query(`CREATE INDEX IF NOT EXISTS pm_github_item_links_project ON pm_github_item_links(project_id)`);
|
|
176
|
+
// Idempotency keys: one row per (account, key) recording the outcome of a
|
|
177
|
+
// mutating request that carried that key, so a retry after an ambiguous
|
|
178
|
+
// (lost) response replays the stored outcome instead of re-executing. A row
|
|
179
|
+
// with a NULL status_code marks an execution still in flight (or crashed
|
|
180
|
+
// before responding); see src/idempotency.ts for the unknown-outcome policy.
|
|
181
|
+
await pool.query(`
|
|
182
|
+
CREATE TABLE IF NOT EXISTS pm_idempotency_keys (
|
|
183
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
184
|
+
user_id UUID NOT NULL REFERENCES pm_users(id) ON DELETE CASCADE,
|
|
185
|
+
idempotency_key TEXT NOT NULL,
|
|
186
|
+
method TEXT NOT NULL,
|
|
187
|
+
path TEXT NOT NULL,
|
|
188
|
+
request_fingerprint TEXT NOT NULL,
|
|
189
|
+
outcome_state TEXT NOT NULL DEFAULT 'pending' CHECK (outcome_state IN ('pending', 'completed', 'outcome_unknown')),
|
|
190
|
+
status_code INTEGER,
|
|
191
|
+
response_body TEXT,
|
|
192
|
+
response_content_type TEXT,
|
|
193
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
194
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
195
|
+
UNIQUE (user_id, idempotency_key)
|
|
196
|
+
);
|
|
197
|
+
`);
|
|
198
|
+
await pool.query("ALTER TABLE pm_idempotency_keys ADD COLUMN IF NOT EXISTS response_content_type TEXT");
|
|
199
|
+
await pool.query("ALTER TABLE pm_idempotency_keys ADD COLUMN IF NOT EXISTS outcome_state TEXT NOT NULL DEFAULT 'pending' CHECK (outcome_state IN ('pending', 'completed', 'outcome_unknown'))");
|
|
200
|
+
await pool.query("UPDATE pm_idempotency_keys SET outcome_state = 'completed' WHERE outcome_state = 'pending' AND status_code IS NOT NULL");
|
|
201
|
+
await pool.query("CREATE INDEX IF NOT EXISTS idx_pm_idempotency_created_at ON pm_idempotency_keys (created_at) WHERE status_code IS NOT NULL");
|
|
202
|
+
await pool.query("CREATE INDEX IF NOT EXISTS idx_pm_idempotency_settled_at ON pm_idempotency_keys (updated_at) WHERE status_code IS NOT NULL");
|
|
203
|
+
await pool.query("CREATE INDEX IF NOT EXISTS idx_pm_idempotency_pending_at ON pm_idempotency_keys (created_at) WHERE status_code IS NULL");
|
|
176
204
|
if (bootstrapAdminEmail) {
|
|
177
205
|
await pool.query(`UPDATE pm_users SET is_admin = TRUE, updated_at = NOW() WHERE lower(email) = lower($1)`, [bootstrapAdminEmail]);
|
|
178
206
|
}
|
package/dist/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAEpB;;;;;;;GAOG;AACH,SAAS,iBAAiB;IACxB,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QAC7B,OAAO,EAAE,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;IACxD,CAAC;IAED,oEAAoE;IACpE,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACzD,OAAO;YACL,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;YAC/B,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,MAAM,EAAE,EAAE,CAAC;YACvD,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;YAC/B,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB;YACvC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW;SAClC,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,UAAU,GACd,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;QACjC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAChE,IAAI,UAAU;QAAE,OAAO;IACvB,MAAM,IAAI,KAAK,CACb,mEAAmE;QACjE,mCAAmC;QACnC,qEAAqE;QACrE,yEAAyE,CAC5E,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC;IAC3B,GAAG,iBAAiB,EAAE;IACtB,2EAA2E;IAC3E,wEAAwE;IACxE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;IAC5E,iBAAiB,EAAE,MAAM;IACzB,uBAAuB,EAAE,KAAK;CAC/B,CAAC,CAAC;AAEH,MAAM,mBAAmB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,EAAE,CAAC;KACzE,IAAI,EAAE;KACN,WAAW,EAAE,CAAC;AAEjB;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDhB,CAAC,CAAC;IAEH,0CAA0C;IAC1C,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;GAMhB,CAAC,CAAC;IAEH,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;;;;;;;;GAahB,CAAC,CAAC;IAEH,MAAM,IAAI,CAAC,KAAK,CACd;;;;;;MAME,CACH,CAAC;IAEF,MAAM,IAAI,CAAC,KAAK,CACd,yFAAyF,CAC1F,CAAC;IAEF,MAAM,IAAI,CAAC,KAAK,CAAC;;;;;;;;;;GAUhB,CAAC,CAAC;IAEH,MAAM,IAAI,CAAC,KAAK,CACd,6FAA6F,CAC9F,CAAC;IAEF,IAAI,mBAAmB,EAAE,CAAC;QACxB,MAAM,IAAI,CAAC,KAAK,CACd,wFAAwF,EACxF,CAAC,mBAAmB,CAAC,CACtB,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { RequestHandler, Response } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Minimum length an `Idempotency-Key` header value may have.
|
|
4
|
+
*
|
|
5
|
+
* Keys are generated by the offline mutation queue as 128-bit UUID-shaped
|
|
6
|
+
* strings, so anything shorter than this is a client bug rather than a key,
|
|
7
|
+
* and accepting it would let a truncated or hand-typed value collide with a
|
|
8
|
+
* future real key.
|
|
9
|
+
*/
|
|
10
|
+
export declare const IDEMPOTENCY_KEY_MIN_LENGTH = 8;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum length an `Idempotency-Key` header value may have.
|
|
13
|
+
*
|
|
14
|
+
* Bounds the stored key so a hostile client cannot stuff multi-kilobyte
|
|
15
|
+
* strings into the unique index; real keys are 36-character UUID strings.
|
|
16
|
+
*/
|
|
17
|
+
export declare const IDEMPOTENCY_KEY_MAX_LENGTH = 200;
|
|
18
|
+
/** Mark a failure only after proving no mutation committed (for example, confirmed rollback before COMMIT was attempted). */
|
|
19
|
+
export declare function markIdempotencyPreCommitFailure(res: Response): void;
|
|
20
|
+
/**
|
|
21
|
+
* Compute the fingerprint that binds one idempotency key to one request.
|
|
22
|
+
*
|
|
23
|
+
* The fingerprint covers the method, the full path with query string, and the
|
|
24
|
+
* parsed JSON body, so a key can only ever be replayed for the identical
|
|
25
|
+
* request it was created for. Re-serializing the parsed body is stable here
|
|
26
|
+
* because a retry sends the same bytes (the offline queue persists the exact
|
|
27
|
+
* body string), which parses to the same property order; a different body —
|
|
28
|
+
* different properties or different values — produces a different fingerprint
|
|
29
|
+
* and is rejected rather than silently deduplicated.
|
|
30
|
+
*
|
|
31
|
+
* @param method - The HTTP method of the request.
|
|
32
|
+
* @param path - The request path including any query string.
|
|
33
|
+
* @param body - The parsed request body, or `null` when the request has none.
|
|
34
|
+
* @returns A hex SHA-256 digest over the request identity.
|
|
35
|
+
*/
|
|
36
|
+
export declare function requestFingerprint(method: string, path: string, body: unknown): string;
|
|
37
|
+
/**
|
|
38
|
+
* Build the middleware that makes keyed mutating requests at-most-once.
|
|
39
|
+
*
|
|
40
|
+
* Scope: keyed mutating `/api` requests outside `/api/auth`. Auth responses
|
|
41
|
+
* always execute live so session cookies are never lost to body-only replay.
|
|
42
|
+
* An expected-account header is checked against an existing authenticated user
|
|
43
|
+
* before execution or claiming, including recovery probes to `/api/auth/me`.
|
|
44
|
+
*
|
|
45
|
+
* Exactly-once per account: the key is stored together with the authenticated
|
|
46
|
+
* user, so two accounts using the same key never interfere. The first
|
|
47
|
+
* execution atomically claims the (account, key) pair, stores its outcome
|
|
48
|
+
* when the response finishes, and any duplicate — a retry after an ambiguous
|
|
49
|
+
* response the client never saw, or a concurrent duplicate of an in-flight
|
|
50
|
+
* request — is answered from that stored outcome without re-executing. A key
|
|
51
|
+
* reused for a *different* request is rejected with 422 rather than silently
|
|
52
|
+
* deduplicated; an unfinished record older than the pending timeout is
|
|
53
|
+
* answered with 409 outcome unknown without re-execution; an in-flight
|
|
54
|
+
* duplicate waits for it to settle and answers 409 only at the
|
|
55
|
+
* deadline.
|
|
56
|
+
*
|
|
57
|
+
* @returns An Express middleware enforcing the contract on its mount.
|
|
58
|
+
*/
|
|
59
|
+
export declare function idempotencyGuard(): RequestHandler;
|