From 6a751bc51d027b993c55b705553fab17543d96b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 26 Sep 2026 14:28:46 +0200 Subject: [PATCH] docs(api): publish sourceInstance on the upload and lookup routes, and the picker's review changes --- docs/api/openapi.yaml | 118 ++++++++++++++------- src/lib/openapi/routes/document-sources.ts | 10 +- src/lib/openapi/routes/documents.ts | 27 ++++- 3 files changed, 111 insertions(+), 44 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 91f77e5c8..5be68b466 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -15721,21 +15721,23 @@ paths: **Importing from another system (v1.39.2).** Optional `sourceSystem` (`PAPERLESS`, `PAPRA`, `OTHER`) and - `sourceId` (its id there, printable, up to 128 characters; needs `sourceSystem`) key the upload. A re-send with - a known key is a duplicate: 200 with the live row (`meta.duplicate: true`), or, when the owner DELETED that - document, 200 with `data.deleted: true` and `meta.deleted: true` and nothing stored; this holds after the 30-day - purge too. The key may also ride the query string (`?sourceSystem=…&sourceId=…`): a key the vault already holds - is then answered at once, the file is not stored or processed, and the request counts only toward the lookup - allowance shared with `GET /api/documents/inbound/source` (5000 an hour per token, per person for a session; 429 - past it), not toward the hourly upload limit. A key in both places must agree (else 422). Every other upload - counts toward the hourly upload limit, duplicates included. A key that is answered with an existing document by - its bytes is remembered for that document, so deleting it keeps that key deleted too; one document takes at most - 20 such keys, beyond which the upload is refused 409 (`documents.inbound.sourceAliasLimit`). `OTHER` is one - shared namespace per account: two different systems both sent as `OTHER` must not reuse each other's ids. - `aiRead=defer` holds back automatic AI reading for this upload: the thumbnail and a local text index still run, - the summary and lab staging do not, and the document stays out of every automatic AI path (the summary catch-up, - the automatic index, Index all for search) until the person reads it with AI or generates its summary. Absent, - behaviour is unchanged. + `sourceId` (its id there, printable, up to 128 characters, never `.` or `..`; needs `sourceSystem`) key the + upload, optionally with `sourceInstance` (v1.39.3: the address of the Paperless-ngx or Papra it came from, + normalised to its origin; without it the key matches any instance of the system). A re-send with a known key is + a duplicate: 200 with the live row (`meta.duplicate: true`), or, when the owner DELETED that document, 200 with + `data.deleted: true` and `meta.deleted: true` and nothing stored; this holds after the 30-day purge too. The key + may also ride the query string (`?sourceSystem=…&sourceId=…`): a key the vault already holds is then answered at + once, the file is not stored or processed, and the request counts only toward the lookup allowance shared with + `GET /api/documents/inbound/source` (5000 an hour per token, per person for a session; 429 past it), not toward + the hourly upload limit. A key in both places must agree (else 422). Every other upload counts toward the hourly + upload limit, duplicates included. A key that is answered with an existing document by its bytes is remembered + for that document, so deleting it keeps that key deleted too; one document takes at most 20 such keys, beyond + which the upload is refused 409 (`documents.inbound.sourceAliasLimit`). `OTHER` is one shared namespace per + account: two different systems both sent as `OTHER` must not reuse each other's ids. `aiRead=defer` holds back + automatic AI reading for this upload: the thumbnail and a local text index still run, the summary and lab + staging do not, and the document stays out of every automatic AI path (the summary catch-up, the automatic + index, Index all for search) until the person reads it with AI or generates its summary. Absent, behaviour is + unchanged. **Narrow token.** Also reachable with a `documents:write` Bearer (minted at `POST /api/tokens/documents`), which @@ -15767,6 +15769,16 @@ paths: type: string maxLength: 128 description: Import source key, second half. Both halves or neither; half a key is 422. + - name: sourceInstance + in: query + required: false + schema: + type: string + maxLength: 512 + description: "Optional, with a key. The instance of `sourceSystem` the id belongs to, as its http(s) address; normalised + to the origin (`scheme://host[:port]`). The same id in two instances is two documents. Optional: a key + stored without an instance (every upload before v1.39.3) matches any instance of its system, and a key sent + without one matches any stored instance. If also sent in the form it must agree." requestBody: required: true content: @@ -15821,6 +15833,13 @@ paths: documents. type: string maxLength: 128 + sourceInstance: + description: "Optional; requires `sourceId`. The instance of `sourceSystem` the id belongs to, as its http(s) address; + normalised to the origin (`scheme://host[:port]`). The same id in two instances is two documents. + Optional: a key stored without an instance (every upload before v1.39.3) matches any instance of its + system, and a key sent without one matches any stored instance." + type: string + maxLength: 512 aiRead: description: Optional. `defer` holds back automatic AI reading for this upload (local index and thumbnail only). type: string @@ -15896,6 +15915,16 @@ paths: schema: type: string maxLength: 128 + - name: sourceInstance + in: query + required: false + schema: + type: string + maxLength: 512 + description: "The instance of `sourceSystem` the id belongs to, as its http(s) address; normalised to the origin + (`scheme://host[:port]`). The same id in two instances is two documents. Optional: a key stored without an + instance (every upload before v1.39.3) matches any instance of its system, and a key sent without one + matches any stored instance." responses: "200": description: The answer for this key. @@ -20975,7 +21004,9 @@ paths: at least one origin), the listed origins, and the caller's own saved connections to Paperless-ngx or Papra. A connection never carries its token: `hasToken` is always true for a saved one, and no route returns the token in whole or in part. `originAllowed` is false once the operator removed the connection's origin from the list. No - request leaves the server. Cookie session only; not delegable." + request leaves the server. Needs neither the Documents module nor the list: with the list unset, `available` is + false and any connection still stored is listed (with `originAllowed: false`) so it can be removed. Cookie + session only; not delegable." security: &a52 - cookieAuth: [] responses: @@ -21004,9 +21035,11 @@ paths: description: "Saves or replaces the caller's connection to Paperless-ngx (`paperless`: base address and API token) or Papra (`papra`: base address, organization id, API key with `documents:read` and `tags:read`). The base address may carry a path; its origin must be listed in `DOCUMENT_SOURCE_ORIGINS`. The connection is tested live before - anything is stored. Leaving out `token` keeps the stored one (the first save must carry it). The token is stored - encrypted (AES-256-GCM) and never returned. Ten saves and tests a minute per person. Cookie session only; not - delegable." + anything is stored. Leaving out `token` keeps the stored one, but only when the new address is on the same + origin as the saved one: the first save and a move to another origin must carry it (422 + `documents.sources.tokenRequired`), so a stored token is never sent to an address it was not saved for. A Papra + organization id is letters, digits, `_` and `-`. The token is stored encrypted (AES-256-GCM) and never returned. + Ten saves and tests a minute per person. Cookie session only; not delegable." security: *a52 parameters: - &a54 @@ -21032,9 +21065,7 @@ paths: maxLength: 2048 organizationId: type: string - minLength: 1 - maxLength: 128 - pattern: ^[\x21-\x7e]+$ + pattern: ^[A-Za-z0-9_-]{1,128}$ token: type: string minLength: 1 @@ -21084,8 +21115,8 @@ paths: - Documents summary: Remove a connection to a document archive description: Deletes the caller's connection for this system, and its token with it. Works even after the operator - removed the origin from the list. `disconnected` is false when there was nothing to delete. Cookie session only; - not delegable. + removed the origin from the list, and with the Documents module switched off. `disconnected` is false when there + was nothing to delete. Cookie session only; not delegable. security: *a52 parameters: - *a54 @@ -21160,11 +21191,13 @@ paths: - Documents summary: Search a connected archive by name, tag and date description: "Searches the source 25 documents at a time. `q` matches the document's name (Paperless-ngx title, Papra - name); `tag` narrows to one of the source's tags by id; `from` / `to` to a date range, inclusive (Papra's range - is applied to each page after it arrives, so a page can hold fewer than 25). Each result carries what the vault - already holds under that source key: `new`, `imported` with the vault document's `documentId`, or `deleted` - (deleted in HealthLog; importing it again stores nothing). Nothing is stored. Sixty searches a minute, shared - with the tag read. Cookie session only; not delegable." + name); `tag` narrows to one of the source's tags by id; `from` / `to` to a date range, inclusive (Paperless-ngx + filters on `created__gte` / `created__lte`; Papra's range is applied to each page after it arrives). When a + filter applied after arrival empties a page, the route reads on, up to five source pages, and `page` in the + answer is the last source page read; ask for `page + 1` next. Each result carries what the vault already holds + under that source key: `new`, `imported` with the vault document's `documentId`, or `deleted` (deleted in + HealthLog; importing it again stores nothing). Nothing is stored. Sixty searches a minute, shared with the tag + read. Cookie session only; not delegable." security: *a52 parameters: - *a54 @@ -21218,15 +21251,19 @@ paths: tags: - Documents summary: Import one picked document - description: "Fetches one document from the source and stores it in the vault through the same path as an upload, with - `sourceSystem` / `sourceId` provenance. A source key the vault already holds is answered without a download: - `duplicate` for a live document (same id), `deleted` for one the person deleted in HealthLog, which is never - stored again. Bytes already in the vault under another key are `duplicate` too, and the key is remembered for - that document. `link` files the document against the record the picker was opened from (a condition, a visit or - a vaccination, which must be the caller's own and live; checked before anything is fetched), including a - duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the person's own setting. - Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new document, 200 - otherwise. Cookie session only; not delegable." + description: "`sourceId` must be an id the system issues (Paperless-ngx: digits; Papra: letters, digits, `_`, `-`), else + 422 before anything is fetched. Fetches one document from the source and stores it in the vault through the same + path as an upload, with `sourceSystem` / `sourceId` / `sourceInstance` (the connection's origin) provenance; the + same id from another instance is another document. One import per key runs at a time; a concurrent second one + waits and then answers `duplicate`. The key check counts toward the lookup allowance, the fetch toward the + search allowance, and only a file that arrived toward the upload allowance. A source key the vault already holds + is answered without a download: `duplicate` for a live document (same id), `deleted` for one the person deleted + in HealthLog, which is never stored again. Bytes already in the vault under another key are `duplicate` too, and + the key is remembered for that document. `link` files the document against the record the picker was opened from + (a condition, a visit or a vaccination, which must be the caller's own and live; checked before anything is + fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the + person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new + document, 200 otherwise. Cookie session only; not delegable." security: *a52 parameters: - *a54 @@ -35769,6 +35806,11 @@ components: - type: string maxLength: 128 - type: "null" + sourceInstance: + anyOf: + - type: string + maxLength: 512 + - type: "null" aiReadDeferred: type: boolean summary: diff --git a/src/lib/openapi/routes/document-sources.ts b/src/lib/openapi/routes/document-sources.ts index e25c0b813..53f4cfe6f 100644 --- a/src/lib/openapi/routes/document-sources.ts +++ b/src/lib/openapi/routes/document-sources.ts @@ -117,7 +117,7 @@ export const documentSourcePaths: NonNullable = { tags: ["Documents"], summary: "Document archives: availability and connections", description: - "Whether the operator enabled the document picker (`available`, true when `DOCUMENT_SOURCE_ORIGINS` lists at least one origin), the listed origins, and the caller's own saved connections to Paperless-ngx or Papra. A connection never carries its token: `hasToken` is always true for a saved one, and no route returns the token in whole or in part. `originAllowed` is false once the operator removed the connection's origin from the list. No request leaves the server. Cookie session only; not delegable.", + "Whether the operator enabled the document picker (`available`, true when `DOCUMENT_SOURCE_ORIGINS` lists at least one origin), the listed origins, and the caller's own saved connections to Paperless-ngx or Papra. A connection never carries its token: `hasToken` is always true for a saved one, and no route returns the token in whole or in part. `originAllowed` is false once the operator removed the connection's origin from the list. No request leaves the server. Needs neither the Documents module nor the list: with the list unset, `available` is false and any connection still stored is listed (with `originAllowed: false`) so it can be removed. Cookie session only; not delegable.", security: cookieOnly, responses: { "200": { @@ -141,7 +141,7 @@ export const documentSourcePaths: NonNullable = { tags: ["Documents"], summary: "Save a connection to a document archive", description: - "Saves or replaces the caller's connection to Paperless-ngx (`paperless`: base address and API token) or Papra (`papra`: base address, organization id, API key with `documents:read` and `tags:read`). The base address may carry a path; its origin must be listed in `DOCUMENT_SOURCE_ORIGINS`. The connection is tested live before anything is stored. Leaving out `token` keeps the stored one (the first save must carry it). The token is stored encrypted (AES-256-GCM) and never returned. Ten saves and tests a minute per person. Cookie session only; not delegable.", + "Saves or replaces the caller's connection to Paperless-ngx (`paperless`: base address and API token) or Papra (`papra`: base address, organization id, API key with `documents:read` and `tags:read`). The base address may carry a path; its origin must be listed in `DOCUMENT_SOURCE_ORIGINS`. The connection is tested live before anything is stored. Leaving out `token` keeps the stored one, but only when the new address is on the same origin as the saved one: the first save and a move to another origin must carry it (422 `documents.sources.tokenRequired`), so a stored token is never sent to an address it was not saved for. A Papra organization id is letters, digits, `_` and `-`. The token is stored encrypted (AES-256-GCM) and never returned. Ten saves and tests a minute per person. Cookie session only; not delegable.", security: cookieOnly, parameters: [systemParameter], requestBody: { @@ -176,7 +176,7 @@ export const documentSourcePaths: NonNullable = { tags: ["Documents"], summary: "Remove a connection to a document archive", description: - "Deletes the caller's connection for this system, and its token with it. Works even after the operator removed the origin from the list. `disconnected` is false when there was nothing to delete. Cookie session only; not delegable.", + "Deletes the caller's connection for this system, and its token with it. Works even after the operator removed the origin from the list, and with the Documents module switched off. `disconnected` is false when there was nothing to delete. Cookie session only; not delegable.", security: cookieOnly, parameters: [systemParameter], responses: { @@ -269,7 +269,7 @@ export const documentSourcePaths: NonNullable = { tags: ["Documents"], summary: "Search a connected archive by name, tag and date", description: - "Searches the source 25 documents at a time. `q` matches the document's name (Paperless-ngx title, Papra name); `tag` narrows to one of the source's tags by id; `from` / `to` to a date range, inclusive (Papra's range is applied to each page after it arrives, so a page can hold fewer than 25). Each result carries what the vault already holds under that source key: `new`, `imported` with the vault document's `documentId`, or `deleted` (deleted in HealthLog; importing it again stores nothing). Nothing is stored. Sixty searches a minute, shared with the tag read. Cookie session only; not delegable.", + "Searches the source 25 documents at a time. `q` matches the document's name (Paperless-ngx title, Papra name); `tag` narrows to one of the source's tags by id; `from` / `to` to a date range, inclusive (Paperless-ngx filters on `created__gte` / `created__lte`; Papra's range is applied to each page after it arrives). When a filter applied after arrival empties a page, the route reads on, up to five source pages, and `page` in the answer is the last source page read; ask for `page + 1` next. Each result carries what the vault already holds under that source key: `new`, `imported` with the vault document's `documentId`, or `deleted` (deleted in HealthLog; importing it again stores nothing). Nothing is stored. Sixty searches a minute, shared with the tag read. Cookie session only; not delegable.", security: cookieOnly, parameters: [ systemParameter, @@ -335,7 +335,7 @@ export const documentSourcePaths: NonNullable = { tags: ["Documents"], summary: "Import one picked document", description: - "Fetches one document from the source and stores it in the vault through the same path as an upload, with `sourceSystem` / `sourceId` provenance. A source key the vault already holds is answered without a download: `duplicate` for a live document (same id), `deleted` for one the person deleted in HealthLog, which is never stored again. Bytes already in the vault under another key are `duplicate` too, and the key is remembered for that document. `link` files the document against the record the picker was opened from (a condition, a visit or a vaccination, which must be the caller's own and live; checked before anything is fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new document, 200 otherwise. Cookie session only; not delegable.", + "`sourceId` must be an id the system issues (Paperless-ngx: digits; Papra: letters, digits, `_`, `-`), else 422 before anything is fetched. Fetches one document from the source and stores it in the vault through the same path as an upload, with `sourceSystem` / `sourceId` / `sourceInstance` (the connection's origin) provenance; the same id from another instance is another document. One import per key runs at a time; a concurrent second one waits and then answers `duplicate`. The key check counts toward the lookup allowance, the fetch toward the search allowance, and only a file that arrived toward the upload allowance. A source key the vault already holds is answered without a download: `duplicate` for a live document (same id), `deleted` for one the person deleted in HealthLog, which is never stored again. Bytes already in the vault under another key are `duplicate` too, and the key is remembered for that document. `link` files the document against the record the picker was opened from (a condition, a visit or a vaccination, which must be the caller's own and live; checked before anything is fetched), including a duplicate. `kind` sets the document type (default OTHER). Automatic AI reading follows the person's own setting. Counts against the person's upload allowance (60 an hour) and storage quota. 201 for a new document, 200 otherwise. Cookie session only; not delegable.", security: cookieOnly, parameters: [systemParameter], requestBody: { diff --git a/src/lib/openapi/routes/documents.ts b/src/lib/openapi/routes/documents.ts index d31ad375d..9bdbcf89e 100644 --- a/src/lib/openapi/routes/documents.ts +++ b/src/lib/openapi/routes/documents.ts @@ -24,6 +24,7 @@ import { inboundConfirmSchema, inboundFactEditSchema, DOCUMENT_SOURCE_ID_MAX, + DOCUMENT_SOURCE_INSTANCE_MAX, DOCUMENT_SOURCE_SYSTEMS, DOCUMENT_SUMMARY_STATES, INBOUND_DOCUMENT_KINDS, @@ -417,12 +418,20 @@ export const inboundDocumentPaths: NonNullable = { description: "Import source key, second half. Both halves or neither; half a key is 422.", }, + { + name: "sourceInstance", + in: "query", + required: false, + schema: { type: "string", maxLength: DOCUMENT_SOURCE_INSTANCE_MAX }, + description: + "Optional, with a key. The instance of `sourceSystem` the id belongs to, as its http(s) address; normalised to the origin (`scheme://host[:port]`). The same id in two instances is two documents. Optional: a key stored without an instance (every upload before v1.39.3) matches any instance of its system, and a key sent without one matches any stored instance. If also sent in the form it must agree.", + }, ], tags: ["Documents"], summary: "Store a document (no extraction)", description: 'STORE-ONLY upload. Stores the raw document ENCRYPTED at rest with `status: STORED` and runs NO extraction — provider-free, no AI consent / budget / egress. A file is always filable, even with no document-scan provider configured. `multipart/form-data`: a `file` plus optional `title`, `kind`, `documentDate` (YYYY-MM-DD), and repeated `episodeIds` form fields (pre-link to the caller\'s illness/condition episodes). Accepted types (magic-byte sniffed, never the wire Content-Type): PDF/JPEG/PNG/WebP/GIF render inline; Office (docx/xlsx/pptx/doc/xls/ppt), text/CSV/Markdown/RTF, TIFF, HEIC/HEIF, XML/JSON are stored verbatim and served download-only. HEIC is stored as-is but attachment-only — prefer transcoding to JPEG client-side for inline preview parity. Error contract: `413` with `meta.reason = "fileTooLarge"` (+ `maxFileBytes`) or `"quotaExceeded"` (+ `quotaBytes`, `usedBytes`); `415` with `meta.reason = "unsupportedType"`. A same-user duplicate (same bytes) returns 200 + `meta.duplicate: true` with the existing row — not an error. `Idempotency-Key` honoured. Read `GET /api/documents/inbound/usage` for the effective limits before offering an upload. AI extraction is a separate opt-in action — see `POST /api/documents/inbound/{id}/extract`.\n\n' + - "**Importing from another system (v1.39.2).** Optional `sourceSystem` (`PAPERLESS`, `PAPRA`, `OTHER`) and `sourceId` (its id there, printable, up to 128 characters; needs `sourceSystem`) key the upload. A re-send with a known key is a duplicate: 200 with the live row (`meta.duplicate: true`), or, when the owner DELETED that document, 200 with `data.deleted: true` and `meta.deleted: true` and nothing stored; this holds after the 30-day purge too. The key may also ride the query string (`?sourceSystem=…&sourceId=…`): a key the vault already holds is then answered at once, the file is not stored or processed, and the request counts only toward the lookup allowance shared with `GET /api/documents/inbound/source` (5000 an hour per token, per person for a session; 429 past it), not toward the hourly upload limit. A key in both places must agree (else 422). Every other upload counts toward the hourly upload limit, duplicates included. A key that is answered with an existing document by its bytes is remembered for that document, so deleting it keeps that key deleted too; one document takes at most 20 such keys, beyond which the upload is refused 409 (`documents.inbound.sourceAliasLimit`). `OTHER` is one shared namespace per account: two different systems both sent as `OTHER` must not reuse each other\'s ids. `aiRead=defer` holds back automatic AI reading for this upload: the thumbnail and a local text index still run, the summary and lab staging do not, and the document stays out of every automatic AI path (the summary catch-up, the automatic index, Index all for search) until the person reads it with AI or generates its summary. Absent, behaviour is unchanged.\n\n" + + "**Importing from another system (v1.39.2).** Optional `sourceSystem` (`PAPERLESS`, `PAPRA`, `OTHER`) and `sourceId` (its id there, printable, up to 128 characters, never `.` or `..`; needs `sourceSystem`) key the upload, optionally with `sourceInstance` (v1.39.3: the address of the Paperless-ngx or Papra it came from, normalised to its origin; without it the key matches any instance of the system). A re-send with a known key is a duplicate: 200 with the live row (`meta.duplicate: true`), or, when the owner DELETED that document, 200 with `data.deleted: true` and `meta.deleted: true` and nothing stored; this holds after the 30-day purge too. The key may also ride the query string (`?sourceSystem=…&sourceId=…`): a key the vault already holds is then answered at once, the file is not stored or processed, and the request counts only toward the lookup allowance shared with `GET /api/documents/inbound/source` (5000 an hour per token, per person for a session; 429 past it), not toward the hourly upload limit. A key in both places must agree (else 422). Every other upload counts toward the hourly upload limit, duplicates included. A key that is answered with an existing document by its bytes is remembered for that document, so deleting it keeps that key deleted too; one document takes at most 20 such keys, beyond which the upload is refused 409 (`documents.inbound.sourceAliasLimit`). `OTHER` is one shared namespace per account: two different systems both sent as `OTHER` must not reuse each other\'s ids. `aiRead=defer` holds back automatic AI reading for this upload: the thumbnail and a local text index still run, the summary and lab staging do not, and the document stays out of every automatic AI path (the summary catch-up, the automatic index, Index all for search) until the person reads it with AI or generates its summary. Absent, behaviour is unchanged.\n\n" + "**Narrow token.** Also reachable with a `documents:write` Bearer (minted at `POST /api/tokens/documents`), which reaches only this route and the source-key lookup `GET /api/documents/inbound/source`. Such a caller draws on a bucket of its own, keyed on the token (default 120 an hour, `DOCUMENT_UPLOAD_LIMIT_PER_HOUR`, clamped 1-1000; the 429 carries `Retry-After` and `X-RateLimit-*`), and gets a `DocumentUploadReceipt` instead of the stored row; its 413 `quotaExceeded` carries no `quotaBytes` / `usedBytes`. The response is an untagged union (row or receipt) on purpose: a discriminator would need a new field on the row every existing client decodes. A cookie or wildcard caller keeps the 60-an-hour per-user bucket and the full row. With the vault module off the answer is 403 `module.disabled` for every caller.", requestBody: { required: true, @@ -461,6 +470,14 @@ export const inboundDocumentPaths: NonNullable = { description: "Optional. The document's id in `sourceSystem`; requires it. Unique per account across live and deleted documents.", }), + sourceInstance: z + .string() + .max(DOCUMENT_SOURCE_INSTANCE_MAX) + .optional() + .meta({ + description: + "Optional; requires `sourceId`. The instance of `sourceSystem` the id belongs to, as its http(s) address; normalised to the origin (`scheme://host[:port]`). The same id in two instances is two documents. Optional: a key stored without an instance (every upload before v1.39.3) matches any instance of its system, and a key sent without one matches any stored instance.", + }), aiRead: z.enum(["defer"]).optional().meta({ description: "Optional. `defer` holds back automatic AI reading for this upload (local index and thumbnail only).", @@ -527,6 +544,14 @@ export const inboundDocumentPaths: NonNullable = { required: true, schema: { type: "string", maxLength: DOCUMENT_SOURCE_ID_MAX }, }, + { + name: "sourceInstance", + in: "query", + required: false, + schema: { type: "string", maxLength: DOCUMENT_SOURCE_INSTANCE_MAX }, + description: + "The instance of `sourceSystem` the id belongs to, as its http(s) address; normalised to the origin (`scheme://host[:port]`). The same id in two instances is two documents. Optional: a key stored without an instance (every upload before v1.39.3) matches any instance of its system, and a key sent without one matches any stored instance.", + }, ], responses: { "200": {