Two-party DocDB upsert flow for apps on *.allenneuraldynamics.org.
Each upsert must be approved by two distinct QC-portal-authenticated users
who submit byte-for-byte identical payloads.
Base URL (prod): https://qc.allenneuraldynamics.org
- Your app redirects the user to
GET /metadata/token(see below). The QC portal verifies the user is logged in and sets a cross-subdomain cookieqc_auth_token(the token) andqc_auth_token_expires_at(unix seconds). - Your app reads the cookie via JS, then
POSTs the metadata to/metadata/v1or/metadata/v2with?auth-token=<token>. - First valid POST returns
{"status":"pending","submissions":1,"required":2,"expires_at":<ts>}. - A second user repeats steps 1–2 with the same payload. The second valid
POST triggers the DocDB upsert and returns
{"status":"submitted", ...}.
Query params:
| Param | Required | Notes |
|---|---|---|
id |
yes | The _id of the metadata record to be approved. |
redirect |
yes | Absolute https:// URL on *.allenneuraldynamics.org(-test). |
Auth: caller must hold a valid QC-portal OAuth session cookie.
Same-site enforcement: the request must include Sec-Fetch-Site: same-origin
or same-site, or an Origin/Referer header on the allowed-host list.
Top-level navigations from *.allenneuraldynamics.org pages satisfy this
automatically.
redirect is sent verbatim in the Location response header. Query strings
and fragments on the supplied redirect URL are preserved as-is — e.g.
redirect=https://data.allenneuraldynamics.org/x?db=foo&id=bar&endpoint=baz
will round-trip those query parameters untouched.
Responses:
302→redirect. Sets two cookies on.allenneuraldynamics.org(Secure, SameSite=None, not HttpOnly):qc_auth_token— the one-time tokenqc_auth_token_expires_at— unix epoch seconds when the token expires
400invalid/missingidorredirect(must be https + allowed host)401user not logged in to the QC portal403request did not come from an allowed AIND subdomain
Routes to DocDB v1 and v2 respectively.
Query params:
| Param | Required | Notes |
|---|---|---|
auth-token |
yes | Token from step 1. |
Body: JSON object representing the full DocDB record (not a partial patch
or fragment). _id and name are required by DocDB itself, and _id must
match the id the token was issued for.
Responses:
200{"status":"pending","submissions":N,"required":2,"expires_at":<ts>,"body_hash":"<sha256>"}— fewer than 2 distinct users have submitted this exact payload. When at least one other pending request exists for the same_idwith a different body, the response additionally includes"other_pending_hashes":[...]so the UI can warn the second actor that its payload doesn't match a pending one (see alsoGET /metadata/v{1,2}/pending).2xx{"status":"submitted","docdb_status":<code>,"docdb_response":<body>}— second distinct user; upsert succeeded. Both tokens are consumed.- Upstream status
{"status":"failed","docdb_status":<code>,...}— upsert failed; tokens are not consumed so the second user can retry. 400missing/invalid body or missing_id401missing/unknown/expired token. Body is JSON:{"status":"error","error":"invalid_token","likely_restart":<bool>,"detail":"..."}and the response carriesWWW-Authenticate: Bearer error="invalid_token".likely_restartistruewhen the portal process has been running for less than the 72h token TTL — i.e. no token issued by this process can have expired naturally yet, so an unknown token is most likely the result of a restart. The UI should prompt the user to re-validate.403token's_iddoes not match the body's_id409same user submitting twice while still waiting for a second approver502DocDB client raised an exception
Returns the in-flight pending entries for a given _id on this DocDB version.
Useful for a second-actor UI to detect payload-hash drift before submitting.
Query params:
| Param | Required | Notes |
|---|---|---|
id |
yes | The _id to look up pending requests for. |
Responses:
200{"id":"<_id>","version":"v1|v2","pending":[<entry>,...]}where each<entry>is{"version":"v1|v2","id":"<_id>","body_hash":"<sha256>","submissions":<n>,"required":2,"body":<full proposed payload>}.400missingid
This endpoint does not require an auth-token. The full proposed body is public so reviewers can diff a pending request against the live DocDB record before approving.
Returns every in-flight pending entry across both DocDB versions in a single call. Powers second-actor reviewer queues.
Responses:
200{"pending":[<entry>, ...]}—<entry>has the same shape as the per-id endpoint above (full body included).
No query params; no auth-token required. Public read-only.
Key behavior:
- Requests are coalesced by
(version, _id, sha256(canonical_json(body))). Key ordering doesn't matter, but any other byte difference produces a separate pending request. - Tokens are bound to
(user, _id)and expire 72 hours after issuance. - Two distinct OAuth user identities are required.
// 1. Send the user off to get a token.
const id = "<record _id>";
const back = location.href;
location.assign(
`https://qc.allenneuraldynamics.org/metadata/token` +
`?id=${encodeURIComponent(id)}&redirect=${encodeURIComponent(back)}`
);
// 2. After redirect, read the cookies.
function readCookie(name) {
return document.cookie.split("; ")
.find(c => c.startsWith(name + "="))?.split("=")[1];
}
const token = readCookie("qc_auth_token");
const expiresAt = Number(readCookie("qc_auth_token_expires_at")) * 1000; // ms
// Use expiresAt to drive a countdown.
// 3. Submit the metadata.
const r = await fetch(
`https://qc.allenneuraldynamics.org/metadata/v2?auth-token=${token}`,
{
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(metadata), // must include _id === id
}
);
const result = await r.json();
// result.status is "pending" or "submitted" (or "failed")The POST /metadata/v{1,2}, GET /metadata/v{1,2}/pending, and
GET /metadata/token endpoints emit CORS headers for browser callers. Cross-
origin requests are accepted only when the Origin header is an https://
URL on *.allenneuraldynamics.org; anything else (including the matching
-test.org domain) is treated as a same-origin / server-side caller and gets
no Access-Control-Allow-Origin header (the browser will block the request).
For browsers, the relevant response headers are:
Access-Control-Allow-Origin: <echoed Origin, when allowed>
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600
Vary: Origin
Content-Type: application/json POSTs trigger a CORS preflight; the handlers
respond to OPTIONS with 204 No Content when the Origin is allowed and
403 otherwise.
- Send the identical payload from both users. The server hashes the
canonical JSON; differing fields produce separate pending requests. Anything
non-deterministic upstream (timestamps, list ordering) will cause silent
divergence — use
GET /metadata/v{1,2}/pending?id=<_id>before submitting, or inspectother_pending_hasheson thependingPOST response, to detect this and warn the user. - The migrate UI must be served from
*.allenneuraldynamics.orgfor the token round-trip to work. The cookies areSecure; SameSite=None; Domain= .allenneuraldynamics.org, sohttp://localhost(and any non-AIND origin) cannot read them. Local-dev against the prod portal is not possible; deploy to a*.allenneuraldynamics.org(-test)host to test end-to-end. qc_auth_tokenis intentionally non-HttpOnly so JS ondata.*can read it. Any XSS on any*.allenneuraldynamics.orgpage can therefore harvest tokens; treat the cookie accordingly.- State is in-memory on the QC portal process. A portal restart invalidates
all outstanding tokens and pending requests. After a restart, the next POST
with a previously-issued token returns
401with{"error":"invalid_token","likely_restart":true,...}— surface this to the user as a re-validation prompt rather than a generic auth failure.