Files
anonymous_github/public/maintenance-openapi.json
T
Thomas DurieuxandClaude Opus 5.5 643f4b87e9 feat: let authors leave their bibliography unredacted (#941) (#943)
* feat: let owners leave chosen files unredacted (#941)

Redacting self-citations in a bibliography reveals which papers are the
authors' own. Owners can now pick files whose content is served as
written; file names are still redacted. Patterns follow .gitignore rules.

The form gets a file picker fed by GitHub's tree API, and the terms box
grows with its content and only warns about regex symbols that can stop a
term from matching.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: honour !patterns, keep picked braces literal, preview unredacted README

- Patterns now apply in order and the last match wins, as in .gitignore,
  so "*.bib" then "!private.bib" excludes private.bib instead of
  preserving every other file.
- Picked paths escape glob characters, braces included, with a
  backslash; "[{]" did not survive brace expansion.
- The README preview drops terms when the README is an unredacted file,
  and refreshes when the list changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep rule order when deduplicating unredacted-file patterns

Rules apply in order and the last match wins, so dedupe keeps the last
copy of a repeated pattern instead of the first. The picker also keeps a
plain path in its place when it follows another rule, rather than moving
it to the front as a pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: apply unredacted-file rules to folders, as in .gitignore

A rule that matches a folder now covers every file inside it, so "*.bib"
then "!private" keeps private/refs.bib redacted. A trailing "/" limits a
rule to folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: folder unchecks win over broad rules; accept short commit hashes

- Unchecking a folder that a broader rule such as "*.bib" still covers
  appends a "!/folder/" exclusion, so its files really go back to being
  redacted.
- The tree endpoint accepts the 6+ character hashes the commit field
  allows and resolves them to a full SHA before reading the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: picker counts folder rules exactly as the server does

Rules ending in "/" stay patterns instead of being read as picked files,
and the checkboxes now run the saved rule list (picks, then patterns)
through the server's matcher, so the dialog cannot show files as
redacted that the server leaves unredacted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: insert escaped terms literally in "Match as written"

String.replace expanded "$&", "$'" and "$1" in the term, so the fix
could corrupt the line. Splice by index instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 14:10:54 +01:00

627 lines
18 KiB
JSON

{
"openapi": "3.0.3",
"info": {
"title": "Anonymous GitHub maintenance API",
"version": "1.0.0",
"description": "Maintain existing anonymized snapshots. Session-only token issuance is documented at /llms.txt. No GitHub proxy or file download access."
},
"servers": [
{
"url": "/"
}
],
"security": [
{
"maintenanceToken": []
}
],
"components": {
"securitySchemes": {
"maintenanceToken": {
"type": "http",
"scheme": "bearer"
}
},
"schemas": {
"Error": {
"type": "object",
"required": [
"error"
],
"properties": {
"error": {
"type": "string"
},
"resetAt": {
"type": "number",
"description": "Unix timestamp in milliseconds, when available."
}
}
},
"RepositoryStatus": {
"type": "object",
"required": [
"repoId",
"options"
],
"properties": {
"repoId": {
"type": "string"
},
"status": {
"type": "string"
},
"statusMessage": {
"type": "string"
},
"statusDate": {
"type": "string",
"format": "date-time"
},
"settingsSavedAt": {
"type": "string",
"format": "date-time"
},
"source": {
"type": "object",
"properties": {
"branch": {
"type": "string"
},
"commit": {
"type": "string"
}
}
},
"options": {
"type": "object",
"properties": {
"terms": {
"type": "array",
"maxItems": 1000,
"items": {
"type": "string",
"minLength": 1,
"maxLength": 1000
}
},
"preserveFiles": {
"type": "array",
"maxItems": 200,
"description": "Glob patterns of files whose content is not redacted. Same rules as .gitignore: a pattern without / matches the file name in any folder; one containing / (including a leading /) matches from the repository root; !pattern excludes again and the last match wins. Replaces the whole list.",
"items": {
"type": "string",
"maxLength": 512
}
},
"update": {
"type": "boolean"
},
"image": {
"type": "boolean"
},
"pdf": {
"type": "boolean"
},
"notebook": {
"type": "boolean"
},
"link": {
"type": "boolean"
},
"expirationDate": {
"type": "string",
"format": "date-time"
},
"expirationMode": {
"type": "string",
"enum": [
"never",
"redirect",
"remove"
]
},
"page": {
"type": "boolean"
},
"pageSource": {
"type": "object",
"properties": {
"branch": {
"type": "string"
},
"path": {
"type": "string"
}
}
}
}
}
}
}
}
},
"paths": {
"/api/maintenance/repositories/{repoId}": {
"parameters": [
{
"name": "repoId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-zA-Z0-9_-]+$"
},
"description": "Existing anonymized repository ID selected when issuing the token."
}
],
"get": {
"operationId": "getRepositoryMaintenanceStatus",
"summary": "Read saved status and settings without GitHub calls",
"responses": {
"200": {
"description": "Operation succeeded; refresh or restoration may still be running.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RepositoryStatus"
}
}
}
},
"400": {
"description": "Invalid settings.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Invalid or expired token.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Scope or membership denied.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Repository not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Repository state changed or operation unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Rate limited. Respect Retry-After.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"503": {
"description": "Service unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"patch": {
"operationId": "patchRepositorySettings",
"summary": "Change only supplied settings; terms replaces the entire list",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"terms": {
"type": "array",
"maxItems": 1000,
"items": {
"type": "string",
"minLength": 1,
"maxLength": 1000
}
},
"preserveFiles": {
"type": "array",
"maxItems": 200,
"description": "Glob patterns of files whose content is not redacted. Same rules as .gitignore: a pattern without / matches the file name in any folder; one containing / (including a leading /) matches from the repository root; !pattern excludes again and the last match wins. Replaces the whole list.",
"items": {
"type": "string",
"maxLength": 512
}
},
"update": {
"type": "boolean"
},
"image": {
"type": "boolean"
},
"pdf": {
"type": "boolean"
},
"notebook": {
"type": "boolean"
},
"link": {
"type": "boolean"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Operation succeeded; refresh or restoration may still be running.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"repoId": {
"type": "string"
},
"settingsSavedAt": {
"type": "string",
"format": "date-time"
}
}
}
}
}
},
"400": {
"description": "Invalid settings.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Invalid or expired token.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Scope or membership denied.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Repository not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Repository state changed or operation unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Rate limited. Respect Retry-After.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"503": {
"description": "Service unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/api/repo/{repoId}/refresh": {
"parameters": [
{
"name": "repoId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-zA-Z0-9_-]+$"
},
"description": "Existing anonymized repository ID selected when issuing the token."
}
],
"post": {
"operationId": "refreshRepository",
"summary": "Queue refresh of the saved branch. Poll status, not refresh.",
"responses": {
"200": {
"description": "Operation succeeded; refresh or restoration may still be running.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
}
}
}
},
"400": {
"description": "Invalid settings.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Invalid or expired token.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Scope or membership denied.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Repository not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Repository state changed or operation unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Rate limited. Respect Retry-After.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"503": {
"description": "Service unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
},
"description": "One refresh per repository per minute per account, shared across tokens and IPs. HTTP 429 includes Retry-After."
}
},
"/api/repo/{repoId}/extend": {
"parameters": [
{
"name": "repoId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"pattern": "^[a-zA-Z0-9_-]+$"
},
"description": "Existing anonymized repository ID selected when issuing the token."
}
],
"post": {
"operationId": "extendRepository",
"summary": "Extend expiration by six months. Not idempotent; inspect state before retrying.",
"responses": {
"200": {
"description": "Operation succeeded; refresh or restoration may still be running.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"expirationDate": {
"type": "string",
"format": "date-time"
}
}
}
}
}
},
"400": {
"description": "Invalid settings.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Invalid or expired token.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Scope or membership denied.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Repository not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Repository state changed or operation unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Rate limited. Respect Retry-After.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"503": {
"description": "Service unavailable.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
}
}